Home Reference Source

src/modules/checkList.js

import {BaseDropdown} from './baseDropdown';
import {
    addClass, createCheckItem, createText, createElm, elm, removeClass, tag
} from '../dom';
import {has} from '../array';
import {matchCase, trim, rgxEsc} from '../string';
import {addEvt, removeEvt, targetEvt} from '../event';
import {isEmpty} from '../types';
import {CHECKLIST, NONE} from '../const';
import {defaultsStr, defaultsBool} from '../settings';

/**
 * Checklist filter UI component
 * @export
 * @class CheckList
 * @extends {BaseDropdown}
 */
export class CheckList extends BaseDropdown {

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

        let f = this.config;

        /**
         * List of container DOM elements
         * @type {Array}
         */
        this.containers = [];

        /**
         * Css class for the container of the checklist filter (div)
         * @type {String}
         */
        this.containerCssClass = defaultsStr(f.div_checklist_css_class,
            'div_checklist');

        /**
         * Css class for the checklist filter element (ul)
         * @type {String}
         */
        this.filterCssClass = defaultsStr(f.checklist_css_class,
            'flt_checklist');

        /**
         * Css class for the item of a checklist (li)
         * @type {String}
         */
        this.itemCssClass = defaultsStr(f.checklist_item_css_class,
            'flt_checklist_item');

        /**
         * Css class for a selected item of a checklist (li)
         * @type {String}
         */
        this.selectedItemCssClass = defaultsStr(
            f.checklist_selected_item_css_class,
            'flt_checklist_slc_item'
        );

        /**
         * Text placed in the filter's container when load filter on demand
         * feature is enabled
         * @type {String}
         */
        this.activateText = defaultsStr(
            f.activate_checklist_text,
            'Click to load filter data'
        );

        /**
         * Css class for a disabled item of a checklist (li)
         * @type {String}
         */
        this.disabledItemCssClass = defaultsStr(
            f.checklist_item_disabled_css_class,
            'flt_checklist_item_disabled'
        );

        /**
         * Enable the reset filter option as first item
         * @type {Boolean}
         */
        this.enableResetOption = defaultsBool(f.enable_checklist_reset_filter,
            true);

        /**
         * Prefix for container element ID
         * @type {String}
         * @private
         */
        this.prfx = 'chkdiv_';
    }

    /**
     * Checklist option click event handler
     * @param {Event} evt
     * @private
     */
    optionClick(evt) {
        let elm = targetEvt(evt);
        let tf = this.tf;

        this.emitter.emit('filter-focus', tf, elm);
        this.setItemOption(elm);
        tf.filter();
    }

    /**
     * Checklist container click event handler for load-on-demand feature
     * @param {Event} evt
     * @private
     */
    onCheckListClick(evt) {
        let elm = targetEvt(evt);
        if (this.tf.loadFltOnDemand && elm.getAttribute('filled') === '0') {
            let ct = elm.getAttribute('ct');
            let div = this.containers[ct];
            this.build(ct);
            removeEvt(div, 'click', (evt) => this.onCheckListClick(evt));
        }
    }

    /**
     * Refresh all checklist filters
     */
    refreshAll() {
        let colIdxs = this.tf.getFiltersByType(CHECKLIST, true);
        this.refreshFilters(colIdxs);
    }

    /**
     * Initialize checklist filter
     * @param  {Number}     colIndex   Column index
     * @param  {Boolean}    isExternal External filter flag
     * @param  {DOMElement} container  Dom element containing the filter
     */
    init(colIndex, isExternal, container) {
        let tf = this.tf;
        let externalFltTgtId = isExternal ?
            tf.externalFltIds[colIndex] : null;

        let divCont = createElm('div',
            ['id', `${this.prfx}${colIndex}_${tf.id}`],
            ['ct', colIndex], ['filled', '0']);
        divCont.className = this.containerCssClass;

        //filter is appended in desired element
        if (externalFltTgtId) {
            elm(externalFltTgtId).appendChild(divCont);
        } else {
            container.appendChild(divCont);
        }

        this.containers[colIndex] = divCont;
        tf.fltIds.push(tf.buildFilterId(colIndex));

        if (!tf.loadFltOnDemand) {
            this.build(colIndex);
        } else {
            addEvt(divCont, 'click', (evt) => this.onCheckListClick(evt));
            divCont.appendChild(createText(this.activateText));
        }

        this.emitter.on(
            ['build-checklist-filter'],
            (tf, colIndex, isLinked) => this.build(colIndex, isLinked)
        );

        this.emitter.on(
            ['select-checklist-options'],
            (tf, colIndex, values) => this.selectOptions(colIndex, values)
        );

        this.emitter.on(['rows-changed'], () => this.refreshAll());

        this.emitter.on(['after-filtering'], () => this.linkFilters());

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

    /**
     * Build checklist UI
     * @param  {Number}  colIndex   Column index
     * @param  {Boolean} isLinked    Enable linked filters behaviour
     */
    build(colIndex, isLinked = false) {
        let tf = this.tf;
        colIndex = Number(colIndex);

        this.emitter.emit('before-populating-filter', tf, colIndex);

        /** @inherited */
        this.opts = [];
        /** @inherited */
        this.optsTxt = [];

        let flt = this.containers[colIndex];
        let ul = createElm('ul',
            ['id', tf.fltIds[colIndex]],
            ['colIndex', colIndex]);
        ul.className = this.filterCssClass;

        let caseSensitive = tf.caseSensitive;
        /** @inherited */
        this.isCustom = tf.isCustomOptions(colIndex);

        //Retrieves custom values
        if (this.isCustom) {
            let customValues = tf.getCustomOptions(colIndex);
            this.opts = customValues[0];
            this.optsTxt = customValues[1];
        }

        let activeIdx;
        let activeFilterId = tf.getActiveFilterId();

        if (isLinked && activeFilterId) {
            activeIdx = tf.getColumnIndexFromFilterId(activeFilterId);
        }

        let filteredDataCol = [];
        if (isLinked && tf.disableExcludedOptions) {
            /** @inherited */
            this.excludedOpts = [];
        }

        flt.innerHTML = '';

        let eachRow = tf.eachRow();
        eachRow(
            (row) => {
                let cellValue = tf.getCellValue(row.cells[colIndex]);
                //Vary Peter's patch
                let cellString = matchCase(cellValue, caseSensitive);
                // checks if celldata is already in array
                if (!has(this.opts, cellString, caseSensitive)) {
                    this.opts.push(cellValue);
                }
                let filteredCol = filteredDataCol[colIndex];
                if (isLinked && tf.disableExcludedOptions) {
                    if (!filteredCol) {
                        filteredCol = tf.getVisibleColumnValues(colIndex);
                    }
                    if (!has(filteredCol, cellString, caseSensitive) &&
                        !has(this.excludedOpts, cellString, caseSensitive)) {
                        this.excludedOpts.push(cellValue);
                    }
                }
            },
            // continue conditions function
            (row, k) => {
                // excluded rows don't need to appear on selects as always valid
                if (tf.excludeRows.indexOf(k) !== -1) {
                    return true;
                }

                // checks if row has expected number of cells
                if (row.cells.length !== tf.nbCells || this.isCustom) {
                    return true;
                }

                if (isLinked && !this.isValidLinkedValue(k, activeIdx)) {
                    return true;
                }
            }
        );

        //sort options
        this.opts = this.sortOptions(colIndex, this.opts);
        if (this.excludedOpts) {
            this.excludedOpts = this.sortOptions(colIndex, this.excludedOpts);
        }

        this.addChecks(colIndex, ul);

        if (tf.loadFltOnDemand) {
            flt.innerHTML = '';
        }
        flt.appendChild(ul);
        flt.setAttribute('filled', '1');

        this.emitter.emit('after-populating-filter', tf, colIndex, flt);
    }

    /**
     * Add checklist options
     * @param {Number} colIndex  Column index
     * @param {Object} ul        Ul element
     * @private
     */
    addChecks(colIndex, ul) {
        let tf = this.tf;
        let chkCt = this.addTChecks(colIndex, ul);

        for (let y = 0; y < this.opts.length; y++) {
            let val = this.opts[y]; //item value
            let lbl = this.isCustom ? this.optsTxt[y] : val; //item text
            let fltId = tf.fltIds[colIndex];
            let lblIdx = y + chkCt;
            let li = createCheckItem(`${fltId}_${lblIdx}`, val, lbl,
                ['data-idx', lblIdx]);
            li.className = this.itemCssClass;

            if (tf.linkedFilters && tf.disableExcludedOptions &&
                has(this.excludedOpts, matchCase(val, tf.caseSensitive),
                    tf.caseSensitive)) {
                addClass(li, this.disabledItemCssClass);
                li.check.disabled = true;
                li.disabled = true;
            } else {
                addEvt(li.check, 'click', evt => this.optionClick(evt));
            }
            ul.appendChild(li);

            if (val === '') {
                //item is hidden
                li.style.display = NONE;
            }
        }
    }

    /**
     * Add checklist header option
     * @param {Number} colIndex Column index
     * @param {Object} ul       Ul element
     * @private
     */
    addTChecks(colIndex, ul) {
        let tf = this.tf;
        let chkCt = 1;
        let fltId = tf.fltIds[colIndex];
        let li0 = createCheckItem(`${fltId}_0`, '',
            tf.getClearFilterText(colIndex), ['data-idx', 0]);
        li0.className = this.itemCssClass;
        ul.appendChild(li0);

        addEvt(li0.check, 'click', evt => this.optionClick(evt));

        if (!this.enableResetOption) {
            li0.style.display = NONE;
        }

        if (tf.enableEmptyOption) {
            let li1 = createCheckItem(`${fltId}_1`, tf.emOperator,
                tf.emptyText, ['data-idx', 1]);
            li1.className = this.itemCssClass;
            ul.appendChild(li1);
            addEvt(li1.check, 'click', evt => this.optionClick(evt));
            chkCt++;
        }

        if (tf.enableNonEmptyOption) {
            let li2 = createCheckItem(`${fltId}_2`, tf.nmOperator,
                tf.nonEmptyText, ['data-idx', 2]);
            li2.className = this.itemCssClass;
            ul.appendChild(li2);
            addEvt(li2.check, 'click', evt => this.optionClick(evt));
            chkCt++;
        }
        return chkCt;
    }

    /**
     * Set/unset value of passed item option in filter's DOM element attribute
     * @param {Object} o checklist option DOM element
     * @private
     */
    setItemOption(o) {
        if (!o) {
            return;
        }

        let tf = this.tf;
        let chkValue = o.value; //checked item value
        let chkIndex = o.dataset.idx;
        let colIdx = tf.getColumnIndexFromFilterId(o.id);
        let n = tf.getFilterElement(parseInt(colIdx, 10));
        let items = n.childNodes;
        let li = items[chkIndex];
        //selected values (ul tag)
        let slcValues = n.getAttribute('value') || '';
        //selected items indexes (ul tag)
        let slcIndexes = n.getAttribute('indexes') || '';

        if (o.checked) {
            //show all item
            if (chkValue === '') {
                //items indexes
                let indexes = slcIndexes.split(tf.separator);
                indexes.forEach(idx => {
                    idx = Number(idx);
                    let li = items[idx];
                    let chx = tag(li, 'input')[0];
                    if (chx && idx > 0) {
                        chx.checked = false;
                        removeClass(li, this.selectedItemCssClass);
                    }
                });

                n.setAttribute('value', '');
                n.setAttribute('indexes', '');

            } else {
                let indexes = slcIndexes + chkIndex + tf.separator;
                let values =
                    trim(slcValues + ' ' + chkValue + ' ' + tf.orOperator);

                n.setAttribute('value', values);
                n.setAttribute('indexes', indexes);

                //uncheck first option
                let chx0 = tag(items[0], 'input')[0];
                if (chx0) {
                    chx0.checked = false;
                }
            }

            removeClass(items[0], this.selectedItemCssClass);
            addClass(li, this.selectedItemCssClass);
        } else { //removes values and indexes
            let replaceValue =
                new RegExp(rgxEsc(chkValue + ' ' + tf.orOperator));
            let values = slcValues.replace(replaceValue, '');
            let replaceIndex = new RegExp(rgxEsc(chkIndex + tf.separator));
            let indexes = slcIndexes.replace(replaceIndex, '');

            n.setAttribute('value', trim(values));
            n.setAttribute('indexes', indexes);

            removeClass(li, this.selectedItemCssClass);
        }
    }

    /**
     * Select filter options programmatically
     * @param  {Number} colIndex Column index
     * @param  {Array}  values   Array of option values to select
     */
    selectOptions(colIndex, values = []) {
        let tf = this.tf;
        let flt = tf.getFilterElement(colIndex);
        if (!flt || values.length === 0) {
            return;
        }

        let lis = tag(flt, 'li');

        flt.setAttribute('value', '');
        flt.setAttribute('indexes', '');

        [].forEach.call(lis, (li) => {
            let chk = tag(li, 'input')[0];
            let chkVal = matchCase(chk.value, tf.caseSensitive);

            if (chkVal !== '' && has(values, chkVal, tf.caseSensitive)) {
                chk.checked = true;
            } else {
                // Check non-empty-text or empty-text option
                if (values.indexOf(tf.nmOperator) !== -1 &&
                    chkVal === matchCase(tf.nonEmptyText, tf.caseSensitive)) {
                    chk.checked = true;
                }
                else if (values.indexOf(tf.emOperator) !== -1 &&
                    chkVal === matchCase(tf.emptyText, tf.caseSensitive)) {
                    chk.checked = true;
                } else {
                    chk.checked = false;
                }
            }
            this.setItemOption(chk);
        });
    }

    /**
     * Get filter values for a given column index
     * @param {Number} colIndex Column index
     * @returns {Array} values Collection of selected values
     */
    getValues(colIndex) {
        let tf = this.tf;
        let flt = tf.getFilterElement(colIndex);
        if (!flt) {
            return [];
        }

        let fltAttr = flt.getAttribute('value');
        let values = isEmpty(fltAttr) ? '' : fltAttr;
        //removes last operator ||
        values = values.substr(0, values.length - 3);
        //turn || separated values into array
        values = values.split(' ' + tf.orOperator + ' ');

        return values;
    }

    /**
     * Destroy CheckList instance
     */
    destroy() {
        this.emitter.off(
            ['build-checklist-filter'],
            (tf, colIndex, isLinked) => this.build(colIndex, isLinked)
        );
        this.emitter.off(
            ['select-checklist-options'],
            (tf, colIndex, values) => this.selectOptions(colIndex, values)
        );
        this.emitter.off(['rows-changed'], () => this.refreshAll());
        this.emitter.off(['after-filtering'], () => this.linkFilters());

        this.initialized = false;
    }
}