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, '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'};