Home Reference Source

src/modules/popupFilter.js

import {Feature} from '../feature';
import {isUndef, EMPTY_FN} from '../types';
import {createElm, removeElm} from '../dom';
import {addEvt, cancelEvt, stopEvt, targetEvt, removeEvt} from '../event';
import {INPUT, NONE, CHECKLIST, MULTIPLE} from '../const';
import {root} from '../root';
import {defaultsStr, defaultsBool, defaultsArr, defaultsFn} from '../settings';

/**
 * Pop-up filter component
 * @export
 * @class PopupFilter
 * @extends {Feature}
 */
export class PopupFilter extends Feature {

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

        // Configuration object
        let f = this.config.popup_filters || {};

        /**
         * Close active popup filter upon filtering, enabled by default
         * @type {Boolean}
         */
        this.closeOnFiltering = defaultsBool(f.close_on_filtering, true);

        /**
         * Filter icon path
         * @type {String}
         */
        this.iconPath = defaultsStr(f.image, tf.themesPath + 'icn_filter.gif');

        /**
         * Active filter icon path
         * @type {string}
         */
        this.activeIconPath = defaultsStr(f.image_active,
            tf.themesPath + 'icn_filterActive.gif');

        /**
         * HTML for the filter icon
         * @type {string}
         */
        this.iconHtml = defaultsStr(f.image_html,
            '<img src="' + this.iconPath + '" alt="Column filter" />');

        /**
         * Css class assigned to the popup container element
         * @type {String}
         */
        this.placeholderCssClass = defaultsStr(f.placeholder_css_class,
            'popUpPlaceholder');

        /**
         * Css class assigned to filter container element
         * @type {String}
         */
        this.containerCssClass = defaultsStr(f.div_css_class, 'popUpFilter');

        /**
         * Ensure filter's container element width matches column width, enabled
         * by default
         * @type {Boolean}
         */
        this.adjustToContainer = defaultsBool(f.adjust_to_container, true);

        /**
         * Callback fired before a popup filter is opened
         * @type {Function}
         */
        this.onBeforeOpen = defaultsFn(f.on_before_popup_filter_open, EMPTY_FN);

        /**
         * Callback fired after a popup filter is opened
         * @type {Function}
         */
        this.onAfterOpen = defaultsFn(f.on_after_popup_filter_open, EMPTY_FN);

        /**
         * Callback fired before a popup filter is closed
         * @type {Function}
         */
        this.onBeforeClose = defaultsFn(f.on_before_popup_filter_close,
            EMPTY_FN);

        /**
         * Callback fired after a popup filter is closed
         * @type {Function}
         */
        this.onAfterClose = defaultsFn(f.on_after_popup_filter_close, EMPTY_FN);

        /**
         * Collection of filters spans
         * @type {Array}
         * @private
         */
        this.fltSpans = [];

        /**
         * Collection of filters icons
         * @type {Array}
         * @private
         */
        this.fltIcons = [];

        /**
         * Collection of filters icons cached after pop-up filters are removed
         * @type {Array}
         * @private
         */
        this.filtersCache = null;

        /**
         * Collection of filters containers
         * @type {Array}
         * @private
         */
        this.fltElms = defaultsArr(this.filtersCache, []);

        /**
         * Prefix for pop-up filter container ID
         * @type {String}
         * @private
         */
        this.prfxDiv = 'popup_';

        /**
         * Column index of popup filter currently active
         * @type {Number}
         * @private
         */
        this.activeFilterIdx = -1;
    }

    /**
     * Click event handler for pop-up filter icon
     * @private
     */
    onClick(evt) {
        let elm = targetEvt(evt).parentNode;
        let colIndex = parseInt(elm.getAttribute('ci'), 10);

        this.closeAll(colIndex);
        this.toggle(colIndex);

        if (this.adjustToContainer) {
            let cont = this.fltElms[colIndex],
                header = this.tf.getHeaderElement(colIndex),
                headerWidth = header.clientWidth * 0.95;
            cont.style.width = parseInt(headerWidth, 10) + 'px';
        }
        cancelEvt(evt);
        stopEvt(evt);
    }

    /**
     * Mouse-up event handler handling popup filter auto-close behaviour
     * @private
     */
    onMouseup(evt) {
        if (this.activeFilterIdx === -1) {
            return;
        }
        let targetElm = targetEvt(evt);
        let activeFlt = this.fltElms[this.activeFilterIdx];
        let icon = this.fltIcons[this.activeFilterIdx];

        if (icon === targetElm) {
            return;
        }

        while (targetElm && targetElm !== activeFlt) {
            targetElm = targetElm.parentNode;
        }

        if (targetElm !== activeFlt) {
            this.close(this.activeFilterIdx);
        }

        return;
    }

    /**
     * Initialize DOM elements
     */
    init() {
        if (this.initialized) {
            return;
        }

        let tf = this.tf;

        // Enable external filters
        tf.externalFltIds = [''];

        // Override filters row index supplied by configuration
        tf.filtersRowIndex = 0;

        // Override headers row index if no grouped headers
        // TODO: Because of the filters row generation, headers row index needs
        // adjusting: prevent useless row generation
        if (tf.headersRow <= 1 && isNaN(tf.config().headers_row_index)) {
            tf.headersRow = 0;
        }

        // Adjust headers row index for grid-layout mode
        // TODO: Because of the filters row generation, headers row index needs
        // adjusting: prevent useless row generation
        if (tf.gridLayout) {
            tf.headersRow--;
            this.buildIcons();
        }

        // subscribe to events
        this.emitter.on(['before-filtering'], () => this.setIconsState());
        this.emitter.on(['after-filtering'], () => this.closeAll());
        this.emitter.on(['cell-processed'],
            (tf, cellIndex) => this.changeState(cellIndex, true));
        this.emitter.on(['filters-row-inserted'], () => this.buildIcons());
        this.emitter.on(['before-filter-init'],
            (tf, colIndex) => this.build(colIndex));

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

    /**
     * Reset previously destroyed feature
     */
    reset() {
        this.enable();
        this.init();
        this.buildIcons();
        this.buildAll();
    }

    /**
     * Build all filters icons
     */
    buildIcons() {
        let tf = this.tf;

        // TODO: Because of the filters row generation, headers row index needs
        // adjusting: prevent useless row generation
        tf.headersRow++;

        tf.eachCol(
            (i) => {
                let icon = createElm('span', ['ci', i]);
                icon.innerHTML = this.iconHtml;
                let header = tf.getHeaderElement(i);
                header.appendChild(icon);
                addEvt(icon, 'click', (evt) => this.onClick(evt));
                this.fltSpans[i] = icon;
                this.fltIcons[i] = icon.firstChild;
            },
            // continue condition function
            (i) => tf.getFilterType(i) === NONE
        );
    }

    /**
     * Build all pop-up filters elements
     */
    buildAll() {
        for (let i = 0; i < this.filtersCache.length; i++) {
            this.build(i, this.filtersCache[i]);
        }
    }

    /**
     * Build a specified pop-up filter elements
     * @param  {Number} colIndex Column index
     * @param  {Object} div      Optional container DOM element
     */
    build(colIndex, div) {
        let tf = this.tf;
        let contId = `${this.prfxDiv}${tf.id}_${colIndex}`;
        let placeholder = createElm('div', ['class', this.placeholderCssClass]);
        let cont = div ||
            createElm('div', ['id', contId], ['class', this.containerCssClass]);
        tf.externalFltIds[colIndex] = cont.id;
        placeholder.appendChild(cont);

        let header = tf.getHeaderElement(colIndex);
        header.insertBefore(placeholder, header.firstChild);
        addEvt(cont, 'click', (evt) => stopEvt(evt));
        this.fltElms[colIndex] = cont;
    }

    /**
     * Toggle visibility of specified filter
     * @param  {Number} colIndex Column index
     */
    toggle(colIndex) {
        if (!this.isOpen(colIndex)) {
            this.open(colIndex);
        } else {
            this.close(colIndex);
        }
    }

    /**
     * Open popup filter of specified column
     * @param {Number} colIndex Column index
     */
    open(colIndex) {
        let tf = this.tf,
            container = this.fltElms[colIndex];

        this.onBeforeOpen(this, container, colIndex);

        container.style.display = 'block';
        this.activeFilterIdx = colIndex;
        addEvt(root, 'mouseup', (evt) => this.onMouseup(evt));

        if (tf.getFilterType(colIndex) === INPUT) {
            let flt = tf.getFilterElement(colIndex);
            if (flt) {
                flt.focus();
            }
        }

        this.onAfterOpen(this, container, colIndex);
    }

    /**
     * Close popup filter of specified column
     * @param {Number} colIndex Column index
     */
    close(colIndex) {
        let container = this.fltElms[colIndex];

        this.onBeforeClose(this, container, colIndex);

        container.style.display = NONE;
        if (this.activeFilterIdx === colIndex) {
            this.activeFilterIdx = -1;
        }
        removeEvt(root, 'mouseup', (evt) => this.onMouseup(evt));

        this.onAfterClose(this, container, colIndex);
    }

    /**
     * Check if popup filter for specified column is open
     * @param {Number} colIndex Column index
     * @returns {Boolean}
     */
    isOpen(colIndex) {
        return this.fltElms[colIndex].style.display === 'block';
    }

    /**
     * Close all filters excepted for the specified one if any
     * @param  {Number} exceptIdx Column index of the filter to not close
     */
    closeAll(exceptIdx) {
        // Do not close filters only if argument is undefined and close on
        // filtering option is disabled
        if (isUndef(exceptIdx) && !this.closeOnFiltering) {
            return;
        }
        for (let i = 0; i < this.fltElms.length; i++) {
            if (i === exceptIdx) {
                continue;
            }
            let fltType = this.tf.getFilterType(i);
            let isMultipleFilter =
                (fltType === CHECKLIST || fltType === MULTIPLE);

            // Always hide all single selection filter types but hide multiple
            // selection filter types only if index set
            if (!isMultipleFilter || !isUndef(exceptIdx)) {
                this.close(i);
            }
        }
    }

    /**
     * Build all the icons representing the pop-up filters
     */
    setIconsState() {
        for (let i = 0; i < this.fltIcons.length; i++) {
            this.changeState(i, false);
        }
    }

    /**
     * Apply specified icon state
     * @param  {Number} colIndex Column index
     * @param  {Boolean} active   Apply active state
     */
    changeState(colIndex, active) {
        let icon = this.fltIcons[colIndex];
        if (icon) {
            icon.src = active ? this.activeIconPath : this.iconPath;
        }
    }

    /**
     * Remove pop-up filters
     */
    destroy() {
        if (!this.initialized) {
            return;
        }

        this.filtersCache = [];
        for (let i = 0; i < this.fltElms.length; i++) {
            let container = this.fltElms[i],
                placeholder = container.parentNode,
                icon = this.fltSpans[i],
                iconImg = this.fltIcons[i];
            if (container) {
                removeElm(container);
                this.filtersCache[i] = container;
            }
            container = null;
            if (placeholder) {
                removeElm(placeholder);
            }
            placeholder = null;
            if (icon) {
                removeElm(icon);
            }
            icon = null;
            if (iconImg) {
                removeElm(iconImg);
            }
            iconImg = null;
        }
        this.fltElms = [];
        this.fltSpans = [];
        this.fltIcons = [];

        // TODO: expose an API to handle external filter IDs
        this.tf.externalFltIds = [];

        // unsubscribe to events
        this.emitter.off(['before-filtering'], () => this.setIconsState());
        this.emitter.off(['after-filtering'], () => this.closeAll());
        this.emitter.off(['cell-processed'],
            (tf, cellIndex) => this.changeState(cellIndex, true));
        this.emitter.off(['filters-row-inserted'], () => this.buildIcons());
        this.emitter.off(['before-filter-init'],
            (tf, colIndex) => this.build(colIndex));

        this.initialized = false;
    }

}

// TODO: remove as soon as feature name is fixed
PopupFilter.meta = {altName: 'popupFilters'};