table-columns.js

/**
 * Created by IvanP on 09.09.2016.
 */

class TableColumns{
  /**
   * Creates an array of objects corresponding to the cells of `defaultHeaderRow`, that contain `sortable` property, denoting the column is sortable,
   * `index` of the column and reference to the `cell`. Adds `.sortable` to a sortable cell
   * @param {Object} options - options passed to configure the Sorting
   * @param {HTMLTableElement} options.source - source table sorting will be applied to
   * @param {HTMLTableElement} options.refSource - floating header if any
   * @param {Number|Object} [options.defaultHeaderRow=-1] - index of the row in `thead` (incremented from 0) that will have sorting enabled for columns. If `-1` then last row.
   * @return {{index:Number, title:String, colSpan:Number, cell: HTMLTableCellElement, ?refCell:HTMLTableCellElement}} - an array of objects that have this structure
   * */
  constructor(options){
    let {source,refSource,defaultHeaderRow=-1} = options;
    let thead,refThead;
    if(source){thead=TableColumns.getHeader(source)} else {throw new TypeError('`source` table is not specified, cannot create TableColumns')}
    if(refSource){refThead=TableColumns.getHeader(refSource)}
    return TableColumns.computeColumns(thead,refThead,defaultHeaderRow);
  }

  /**
   * Gets a header
   * @param {HTMLTableElement} source - source table headers are created for
   * */
  static getHeader(source){
    if(source && source.tagName == 'TABLE'){
      let header = source.querySelector("thead");
      if(header && header.children.length>0) {
        return header;
      } else {
        throw new TypeError('`source` table has no header or rows');
      }
    } else {
      throw new TypeError('`source` is not specified or is not a table');
    }
  }

  /**
   * Calculates defaultHeaderRow for a passed `thead`
   * @param {!HTMLTableElement} thead - source table header
   * @param {!Number} defaultHeaderRowIndex - index of the row in `thead` (incremented from 0) that will be considered default to have actions executed upon.
   * @return {{index:Number, row: HTMLTableRowElement}}
   * */
  static getDefaultHeaderRow(thead,defaultHeaderRowIndex){
    // calculate default header row
    let headerRows = thead.children,
      headerRowIndex = defaultHeaderRowIndex==-1 ? headerRows.length + defaultHeaderRowIndex : defaultHeaderRowIndex;
    return {
      index:headerRowIndex,
      row:headerRows.item(headerRowIndex)
    };
  }

  /**
   * Gets an array of header cell nodes from default header row
   * @param {?HTMLTableElement} thead - source table header
   * @param {!Number} defaultHeaderRowIndex - index of the row in `thead` (incremented from 0) that will be considered default to have actions executed upon.
   * @return {?Array} Returns an array of header cell nodes or null if `thead` is not specified
   * */
  static getHeaderCells(thead,defaultHeaderRowIndex){
    if(thead){
      if(defaultHeaderRowIndex!=null){
        let defaultHeaderRow = TableColumns.getDefaultHeaderRow(thead,defaultHeaderRowIndex);
        let headerRows = thead.children;
        let rowsLength = headerRows.length;
        let abstr = {};
        for(let r=0;r<rowsLength;r++){
          let row = headerRows.item(r);
          let augmentIndex=0; // index that will account for colSpan of upper rows' cells
          [].slice.call(row.children).forEach((cell,index)=>{ //iterate through cells
            for(let rs=0; rs<=cell.rowSpan-1;rs++){ //spread cell across its rowspan
              let rowA = abstr[r+rs] = abstr[r+rs] || {}; //create row if not exists
              if(!rowA[augmentIndex]){ //insert cell into slot if not filled
                rowA[augmentIndex]=cell;
              } else { //if filled look for the next empty because rowspanned columns fill them in a linear way
                let i=0;
                while(true){
                  if(!rowA[i]){
                    rowA[i]=cell;
                    augmentIndex=i;
                    break;
                  }
                  i++;
                }
              }
            }
            augmentIndex+=cell.colSpan;
          })
        }
        return Object.keys(abstr[defaultHeaderRow.index]).map(k => abstr[defaultHeaderRow.index][k])
      } else {
        throw new TypeError('TableColumns.getHeaderCells: defaultHeaderRowIndex is not specified or is not a Number')
      }
    }
    return null
  }

  /**
   * Gets an array of columns from the table
   * @param {!HTMLTableElement} thead - source table header
   * @param {!HTMLTableElement} refThead - reference table header from floating header if any
   * @param {Number} defaultHeaderRowIndex - index of the row in `thead` (incremented from 0) that will be considered default to have actions executed upon.
   * @return {?Array} Returns an array of header cell nodes or null if `thead` is not specified
   * */
  static computeColumns(thead,refThead,defaultHeaderRowIndex){
    let theadCells = TableColumns.getHeaderCells(thead,defaultHeaderRowIndex);
    let refTheadCells = TableColumns.getHeaderCells(refThead,defaultHeaderRowIndex);
    let realColumnIndex=0;
    return theadCells.map((cell,index)=>{
      let obj = {
        index: realColumnIndex,
        title: cell.textContent,
        cell,
        colSpan:cell.colSpan
      };
      if(refTheadCells!=null){obj.refCell = refTheadCells[index]}
      // we need to increment the colspan only for columns that follow rowheader because the block is not in data.
      realColumnIndex= realColumnIndex>0?(realColumnIndex + cell.colSpan):realColumnIndex+1;
      return obj;
    });
  }
}
export default TableColumns;