Home Reference Source

src/modules/gridLayout.js

import {Feature} from '../feature';
import {createElm, removeElm, elm, tag} from '../dom';
import {addEvt, targetEvt} from '../event';
import {contains} from '../string';
import {NONE} from '../const';
import {
    defaultsBool, defaultsStr, defaultsNb, defaultsArr
} from '../settings';

/**
 * Grid layout, table with fixed headers
 */
export class GridLayout extends Feature {

    /**
     * Creates an instance of GridLayout
     * @param {TableFilter} tf TableFilter instance
     */
    constructor(tf) {
        super(tf, 'gridLayout');

        let f = this.config.grid_layout || {};

        /**
         * Grid-layout container width as CSS string
         * @type {String}
         */
        this.width = defaultsStr(f.width, null);

        /**
         * Grid-layout container height as CSS string
         * @type {String}
         */
        this.height = defaultsStr(f.height, null);

        /**
         * Css class for main container element
         * @type {String}
         */
        this.mainContCssClass = defaultsStr(f.cont_css_class, 'grd_Cont');

        /**
         * Css class for body table container element
         * @type {String}
         */
        this.contCssClass = defaultsStr(f.tbl_cont_css_class, 'grd_tblCont');

        /**
         * Css class for headers table container element
         * @type {String}
         */
        this.headContCssClass = defaultsStr(f.tbl_head_css_class,
            'grd_headTblCont');

        /**
         * Css class for toolbar container element (rows counter, paging etc.)
         * @type {String}
         */
        this.infDivCssClass = defaultsStr(f.inf_grid_css_class, 'grd_inf');

        /**
         * Index of the headers row, default: 0
         * @type {Number}
         */
        this.headRowIndex = defaultsNb(f.headers_row_index, 0);

        /**
         * Collection of the header row indexes to be moved into headers table
         * @type {Array}
         */
        this.headRows = defaultsArr(f.headers_rows, [0]);

        /**
         * Enable or disable column filters generation, default: true
         * @type {Boolean}
         */
        this.filters = defaultsBool(f.filters, true);

        /**
         * Enable or disable column headers, default: false
         * @type {Boolean}
         */
        this.noHeaders = Boolean(f.no_headers);

        /**
         * Grid-layout default column widht as CSS string
         * @type {String}
         */
        this.defaultColWidth = defaultsStr(f.default_col_width, '100px');

        /**
         * List of column elements
         * @type {Array}
         * @private
         */
        this.colElms = [];

        /**
         * Prefix for grid-layout filter's cell ID
         * @type {String}
         * @private
         */
        this.prfxGridFltTd = '_td_';

        /**
         * Prefix for grid-layout header's cell ID
         * @type {String}
         * @private
         */
        this.prfxGridTh = 'tblHeadTh_';

        /**
         * Mark-up of original HTML table
         * @type {String}
         * @private
         */
        this.sourceTblHtml = tf.dom().outerHTML;

        /**
         * Indicates if working table has column elements
         * @type {Boolean}
         * @private
         */
        this.tblHasColTag = tag(tf.dom(), 'col').length > 0 ? true : false;

        /**
         * Main container element
         * @private
         */
        this.tblMainCont = null;

        /**
         * Table container element
         * @private
         */
        this.tblCont = null;

        /**
         * Headers' table container element
         * @private
         */
        this.headTblCont = null;

        /**
         * Headers' table element
         * @private
         */
        this.headTbl = null;

        // filters flag at TF level
        tf.fltGrid = this.filters;
    }

    /**
     * Generates a grid with fixed headers
     * TODO: reduce size of init by extracting single purposed methods
     */
    init() {
        let tf = this.tf;
        let tbl = tf.dom();

        if (this.initialized) {
            return;
        }

        // Override relevant TableFilter properties
        this.setOverrides();

        // Assign default column widths
        this.setDefaultColWidths();

        //Main container: it will contain all the elements
        this.tblMainCont = this.createContainer(
            'div', this.mainContCssClass);
        if (this.width) {
            this.tblMainCont.style.width = this.width;
        }
        tbl.parentNode.insertBefore(this.tblMainCont, tbl);

        //Table container: div wrapping content table
        this.tblCont = this.createContainer('div', this.contCssClass);
        this.setConfigWidth(this.tblCont);
        if (this.height) {
            this.tblCont.style.height = this.height;
        }
        tbl.parentNode.insertBefore(this.tblCont, tbl);
        let t = removeElm(tbl);
        this.tblCont.appendChild(t);

        //In case table width is expressed in %
        if (tbl.style.width === '') {
            let tblW = this.initialTableWidth();
            tbl.style.width = (contains('%', tblW) ?
                tbl.clientWidth : tblW) + 'px';
        }

        let d = removeElm(this.tblCont);
        this.tblMainCont.appendChild(d);

        //Headers table container: div wrapping headers table
        this.headTblCont = this.createContainer(
            'div', this.headContCssClass);

        //Headers table
        this.headTbl = createElm('table');
        let tH = createElm('tHead');

        //1st row should be headers row, ids are added if not set
        //Those ids are used by the sort feature
        let hRow = tbl.rows[this.headRowIndex];
        let sortTriggers = this.getSortTriggerIds(hRow);

        //Filters row is created
        let filtersRow = this.createFiltersRow();

        //Headers row are moved from content table to headers table
        this.setHeadersRow(tH);

        this.headTbl.appendChild(tH);
        if (tf.filtersRowIndex === 0) {
            tH.insertBefore(filtersRow, hRow);
        } else {
            tH.appendChild(filtersRow);
        }

        this.headTblCont.appendChild(this.headTbl);
        this.tblCont.parentNode.insertBefore(this.headTblCont, this.tblCont);

        //THead needs to be removed in content table for sort feature
        let thead = tag(tbl, 'thead');
        if (thead.length > 0) {
            tbl.removeChild(thead[0]);
        }

        // ensure table layout is always set even if already set in css
        // definitions, potentially with custom css class this could be lost
        this.headTbl.style.tableLayout = 'fixed';
        tbl.style.tableLayout = 'fixed';

        //content table without headers needs col widths to be reset
        tf.setColWidths(this.headTbl);

        //Headers container width
        this.headTbl.style.width = tbl.style.width;
        //

        //scroll synchronisation
        addEvt(this.tblCont, 'scroll', (evt) => {
            let elm = targetEvt(evt);
            let scrollLeft = elm.scrollLeft;
            this.headTblCont.scrollLeft = scrollLeft;
            //New pointerX calc taking into account scrollLeft
            // if(!o.isPointerXOverwritten){
            //     try{
            //         o.Evt.pointerX = function(evt){
            //             let e = evt || global.event;
            //             let bdScrollLeft = tf_StandardBody().scrollLeft +
            //                 scrollLeft;
            //             return (e.pageX + scrollLeft) ||
            //                 (e.clientX + bdScrollLeft);
            //         };
            //         o.isPointerXOverwritten = true;
            //     } catch(err) {
            //         o.isPointerXOverwritten = false;
            //     }
            // }
        });

        // TODO: Trigger a custom event handled by sort extension
        let sort = tf.extension('sort');
        if (sort) {
            sort.asyncSort = true;
            sort.triggerIds = sortTriggers;
        }

        //Col elements are enough to keep column widths after sorting and
        //filtering
        this.setColumnElements();

        if (tf.popupFilters) {
            filtersRow.style.display = NONE;
        }

        /** @inherited */
        this.initialized = true;
    }

    /**
     * Overrides TableFilter instance properties to adjust to grid layout mode
     * @private
     */
    setOverrides() {
        let tf = this.tf;
        tf.refRow = 0;
        tf.headersRow = 0;
        tf.filtersRowIndex = 1;
    }

    /**
     * Set grid-layout default column widths if column widths are not defined
     * @private
     */
    setDefaultColWidths() {
        let tf = this.tf;
        if (tf.colWidths.length > 0) {
            return;
        }

        tf.eachCol((k) => {
            let colW;
            let cell = tf.dom().rows[tf.getHeadersRowIndex()].cells[k];
            if (cell.width !== '') {
                colW = cell.width;
            } else if (cell.style.width !== '') {
                colW = parseInt(cell.style.width, 10);
            } else {
                colW = this.defaultColWidth;
            }
            tf.colWidths[k] = colW;
        });

        tf.setColWidths();
    }

    /**
     * Initial table width
     * @returns {Number}
     * @private
     */
    initialTableWidth() {
        let tbl = this.tf.dom();
        let width; //initial table width

        if (tbl.width !== '') {
            width = tbl.width;
        }
        else if (tbl.style.width !== '') {
            width = tbl.style.width;
        } else {
            width = tbl.clientWidth;
        }
        return parseInt(width, 10);
    }

    /**
     * Creates container element
     * @param {String} tag Tag name
     * @param {String} className Css class to assign to element
     * @returns {DOMElement}
     * @private
     */
    createContainer(tag, className) {
        let element = createElm(tag);
        element.className = className;
        return element;
    }

    /**
     * Creates filters row with cells
     * @returns {HTMLTableRowElement}
     * @private
     */
    createFiltersRow() {
        let tf = this.tf;
        let filtersRow = createElm('tr');
        if (this.filters && tf.fltGrid) {
            tf.externalFltIds = [];
            tf.eachCol((j) => {
                let fltTdId = `${tf.prfxFlt + j + this.prfxGridFltTd + tf.id}`;
                let cl = createElm(tf.fltCellTag, ['id', fltTdId]);
                filtersRow.appendChild(cl);
                tf.externalFltIds[j] = fltTdId;
            });
        }
        return filtersRow;
    }

    /**
     * Generates column elements if necessary and assigns their widths
     * @private
     */
    setColumnElements() {
        let tf = this.tf;
        let cols = tag(tf.dom(), 'col');
        this.tblHasColTag = cols.length > 0;

        for (let k = (tf.getCellsNb() - 1); k >= 0; k--) {
            let col;

            if (!this.tblHasColTag) {
                col = createElm('col');
                tf.dom().insertBefore(col, tf.dom().firstChild);
            } else {
                col = cols[k];
            }
            col.style.width = tf.colWidths[k];
            this.colElms[k] = col;
        }
        this.tblHasColTag = true;
    }

    /**
     * Sets headers row in headers table
     * @param {HTMLHeadElement} tableHead Table head element
     * @private
     */
    setHeadersRow(tableHead) {
        if (this.noHeaders) {
            // Handle table with no headers, assuming here headers do not
            // exist
            tableHead.appendChild(createElm('tr'));
        } else {
            // Headers row are moved from content table to headers table
            for (let i = 0; i < this.headRows.length; i++) {
                let row = this.tf.dom().rows[this.headRows[i]];
                tableHead.appendChild(row);
            }
        }
    }

    /**
     * Sets width defined in configuration to passed element
     * @param {DOMElement} element DOM element
     * @private
     */
    setConfigWidth(element) {
        if (!this.width) {
            return;
        }
        if (this.width.indexOf('%') !== -1) {
            element.style.width = '100%';
        } else {
            element.style.width = this.width;
        }
    }

    /**
     * Returns a list of header IDs used for specifing external sort triggers
     * @param {HTMLTableRowElement} row DOM row element
     * @returns {Array} List of IDs
     * @private
     */
    getSortTriggerIds(row) {
        let tf = this.tf;
        let sortTriggers = [];
        tf.eachCol((n) => {
            let c = row.cells[n];
            let thId = c.getAttribute('id');
            if (!thId || thId === '') {
                thId = `${this.prfxGridTh + n}_${tf.id}`;
                c.setAttribute('id', thId);
            }
            sortTriggers.push(thId);
        });
        return sortTriggers;
    }

    /**
     * Removes the grid layout
     */
    destroy() {
        let tf = this.tf;
        let tbl = tf.dom();

        if (!this.initialized) {
            return;
        }
        let t = removeElm(tbl);
        this.tblMainCont.parentNode.insertBefore(t, this.tblMainCont);
        removeElm(this.tblMainCont);

        this.tblMainCont = null;
        this.headTblCont = null;
        this.headTbl = null;
        this.tblCont = null;

        tbl.outerHTML = this.sourceTblHtml;
        //needed to keep reference of table element for future usage
        this.tf.tbl = elm(tf.id);

        this.initialized = false;
    }
}