UNPKG

tablefilter

Version:

A Javascript library making HTML tables filterable and a bit more

1,668 lines (1,417 loc) 95.4 kB
import {addEvt, cancelEvt, stopEvt, targetEvt, isKeyPressed} from './event'; import { addClass, createElm, elm, getText, getFirstTextNode, removeClass, tag } from './dom'; import {contains, matchCase, rgxEsc, trim, toCamelCase, uuid} from './string'; import { isArray, isEmpty, isFn, isNumber, isObj, isString, isUndef, EMPTY_FN, isBoolean } from './types'; import {parse as parseNb} from './number'; import { defaultsBool, defaultsStr, defaultsFn, defaultsNb, defaultsArr } from './settings'; import {root} from './root'; import {Emitter} from './emitter'; import {Dropdown} from './modules/dropdown'; import {CheckList} from './modules/checkList'; import {DateType} from './modules/dateType'; import {Help} from './modules/help'; import {State} from './modules/state'; import {GridLayout} from './modules/gridLayout'; import {Loader} from './modules/loader'; import {HighlightKeyword} from './modules/highlightKeywords'; import {PopupFilter} from './modules/popupFilter'; import {MarkActiveColumns} from './modules/markActiveColumns'; import {RowsCounter} from './modules/rowsCounter'; import {StatusBar} from './modules/statusBar'; import {ClearButton} from './modules/clearButton'; import {AlternateRows} from './modules/alternateRows'; import {NoResults} from './modules/noResults'; import {Paging} from './modules/paging'; import {Toolbar} from './modules/toolbar'; import { INPUT, SELECT, MULTIPLE, CHECKLIST, NONE, ENTER_KEY, TAB_KEY, ESC_KEY, UP_ARROW_KEY, DOWN_ARROW_KEY, CELL_TAG, AUTO_FILTER_DELAY, NUMBER, DATE, FORMATTED_NUMBER } from './const'; let doc = root.document; const FEATURES = [ DateType, Help, State, MarkActiveColumns, GridLayout, Loader, HighlightKeyword, PopupFilter, RowsCounter, StatusBar, ClearButton, AlternateRows, NoResults, Paging, Toolbar ]; /** * Makes HTML tables filterable and a bit more :) * * @export * @class TableFilter */ export class TableFilter { /** * Creates an instance of TableFilter * requires `table` or `id` arguments, `row` and `configuration` optional * @param {HTMLTableElement} table Table DOM element * @param {String} id Table id * @param {Number} row index indicating the 1st row * @param {Object} configuration object */ constructor(...args) { /** * ID of current instance * @type {String} * @private */ this.id = null; /** * Current version * @type {String} */ this.version = '{VERSION}'; /** * Current year * @type {Number} * @private */ this.year = new Date().getFullYear(); /** * HTML Table DOM element * @type {DOMElement} * @private */ this.tbl = null; /** * Calculated row's index from which starts filtering once filters * are generated * @type {Number} */ this.refRow = null; /** * Index of the headers row * @type {Number} * @private */ this.headersRow = null; /** * Configuration object * @type {Object} * @private */ this.cfg = {}; /** * Number of rows that can be filtered * @type {Number} * @private */ this.nbFilterableRows = 0; /** * Number of cells in the reference row * @type {Number} * @private */ this.nbCells = null; /** * Has a configuration object * @type {Object} * @private */ this.hasConfig = false; /** @private */ this.initialized = false; let startRow; // TODO: use for-of args.forEach((arg) => { if (typeof arg === 'object' && arg.nodeName === 'TABLE') { this.tbl = arg; this.id = arg.id || `tf_${uuid()}`; this.tbl.id = this.id; } else if (isString(arg)) { this.id = arg; this.tbl = elm(arg); } else if (isNumber(arg)) { startRow = arg; } else if (isObj(arg)) { this.cfg = arg; this.hasConfig = true; } }); if (!this.tbl || this.tbl.nodeName !== 'TABLE') { throw new Error(`Could not instantiate TableFilter: HTML table DOM element not found.`); } if (this.getRowsNb(true) === 0) { throw new Error(`Could not instantiate TableFilter: HTML table requires at least 1 row.`); } // configuration object let f = this.cfg; /** * Event emitter instance * @type {Emitter} */ this.emitter = new Emitter(); // start row this.refRow = isUndef(startRow) ? 2 : (startRow + 1); /** * Collection of filter type by column * @type {Array} * @private */ this.filterTypes = [].map.call( (this.dom().rows[this.refRow] || this.dom().rows[0]).cells, (cell, idx) => { let colType = this.cfg[`col_${idx}`]; return !colType ? INPUT : colType.toLowerCase(); }); /** * Base path for static assets * @type {String} */ this.basePath = defaultsStr(f.base_path, 'tablefilter/'); /*** filters' grid properties ***/ /** * Enable/disable filters * @type {Boolean} */ this.fltGrid = defaultsBool(f.grid, true); /** * Enable/disable grid layout (fixed headers) * @type {Object|Boolean} */ this.gridLayout = isObj(f.grid_layout) || Boolean(f.grid_layout); /** * Filters row index * @type {Number} */ this.filtersRowIndex = defaultsNb(f.filters_row_index, 0); /** * Headers row index * @type {Number} */ this.headersRow = defaultsNb(f.headers_row_index, (this.filtersRowIndex === 0 ? 1 : 0)); /** * Define the type of cell containing a filter (td/th) * @type {String} */ this.fltCellTag = defaultsStr(f.filters_cell_tag, CELL_TAG); /** * List of filters IDs * @type {Array} * @private */ this.fltIds = []; /** * List of valid rows indexes (rows visible upon filtering) * @type {Array} * @private */ this.validRowsIndex = []; /*** filters' grid appearance ***/ /** * Path for stylesheets * @type {String} */ this.stylePath = this.getStylePath(); /** * Main stylesheet path * @type {String} */ this.stylesheet = this.getStylesheetPath(); /** * Main stylesheet ID * @type {String} * @private */ this.stylesheetId = this.id + '_style'; /** * Css class for the filters row * @type {String} */ this.fltsRowCssClass = defaultsStr(f.flts_row_css_class, 'fltrow'); /** * Enable/disable icons (paging, reset button) * @type {Boolean} */ this.enableIcons = defaultsBool(f.enable_icons, true); /** * Enable/disable alternating rows * @type {Boolean} */ this.alternateRows = Boolean(f.alternate_rows); /** * Columns widths array * @type {Array} */ this.colWidths = defaultsArr(f.col_widths, []); /** * Default column width when column widths are defined */ this.defaultColWidth = defaultsNb(f.default_col_width, 100); /** * Css class for a filter element * @type {String} */ this.fltCssClass = defaultsStr(f.flt_css_class, 'flt'); /** * Css class for multiple select filters * @type {String} */ this.fltMultiCssClass = defaultsStr(f.flt_multi_css_class, 'flt_multi'); /** * Css class for small filter (when submit button is active) * @type {String} */ this.fltSmallCssClass = defaultsStr(f.flt_small_css_class, 'flt_s'); /** * Css class for single filter type * @type {String} */ this.singleFltCssClass = defaultsStr((f.single_filter || {}).css_class, 'single_flt'); /*** filters' grid behaviours ***/ /** * Enable/disable enter key for input type filters * @type {Boolean} */ this.enterKey = defaultsBool(f.enter_key, true); /** * Callback fired before filtering process starts * @type {Function} */ this.onBeforeFilter = defaultsFn(f.on_before_filter, EMPTY_FN); /** * Callback fired after filtering process is completed * @type {Function} */ this.onAfterFilter = defaultsFn(f.on_after_filter, EMPTY_FN); /** * Enable/disable case sensitivity for filtering, default false * @type {Boolean} */ this.caseSensitive = Boolean(f.case_sensitive); /** * Indicate whether exact match filtering is enabled on a per column * basis * @type {Boolean} * @private */ this.hasExactMatchByCol = isArray(f.columns_exact_match); /** * Exact match filtering per column array * @type {Array} */ this.exactMatchByCol = this.hasExactMatchByCol ? f.columns_exact_match : []; /** * Globally enable/disable exact match filtering * @type {Boolean} */ this.exactMatch = Boolean(f.exact_match); /** * Ignore diacritics globally or on a column basis * @type {Boolean|Array} */ this.ignoreDiacritics = f.ignore_diacritics; /** * Enable/disable linked filters filtering mode * @type {Boolean} */ this.linkedFilters = Boolean(f.linked_filters); /** * Enable/disable readonly state for excluded options when * linked filters filtering mode is on * @type {Boolean} */ this.disableExcludedOptions = Boolean(f.disable_excluded_options); /** * Active filter ID * @type {String} * @private */ this.activeFilterId = null; /** * Determine if there are excluded rows from filtering * @type {Boolean} * @private */ this.hasExcludedRows = Boolean(isArray(f.exclude_rows) && f.exclude_rows.length > 0); /** * List of row indexes to be excluded from filtering * @type {Array} */ this.excludeRows = defaultsArr(f.exclude_rows, []); /** * List of containers IDs where external filters will be generated * @type {Array} */ this.externalFltIds = defaultsArr(f.external_flt_ids, []); /** * Callback fired after filters are generated * @type {Function} */ this.onFiltersLoaded = defaultsFn(f.on_filters_loaded, EMPTY_FN); /** * Enable/disable single filter mode * @type {Boolean|Object} */ this.singleFlt = isObj(f.single_filter) || Boolean(f.single_filter); /** * Specify columns to be excluded from single filter search, by default * searching in all columns: * single_filter: { * exclude_cols: [2, 7] * } */ this.singleFltExcludeCols = isObj(f.single_filter) && isArray(f.single_filter.exclude_cols) ? f.single_filter.exclude_cols : []; /** * Callback fired after a row is validated during filtering * @type {Function} */ this.onRowValidated = defaultsFn(f.on_row_validated, EMPTY_FN); /** * Specify which column implements a custom cell parser to retrieve the * cell value: * cell_parser: { * cols: [0, 2], * parse: function(tf, cell, colIndex) { * // custom cell parser logic here * return cellValue; * } * } * @type {Object} */ this.cellParser = isObj(f.cell_parser) && isFn(f.cell_parser.parse) && isArray(f.cell_parser.cols) ? f.cell_parser : { cols: [], parse: EMPTY_FN }; /** * Global watermark text for input filter type or watermark for each * filter if an array is supplied * @type {String|Array} */ this.watermark = f.watermark || ''; /** * Indicate whether watermark is on a per column basis * @type {Boolean} * @private */ this.isWatermarkArray = isArray(this.watermark); /** * Indicate whether help UI component is disabled * @type {Boolean} */ this.help = isUndef(f.help_instructions) ? undefined : (isObj(f.help_instructions) || Boolean(f.help_instructions)); /** * Indicate whether pop-up filters UI is enabled * @type {Boolean|Object} */ this.popupFilters = isObj(f.popup_filters) || Boolean(f.popup_filters); /** * Indicate whether filtered (active) columns indicator is enabled * @type {Boolean} */ this.markActiveColumns = isObj(f.mark_active_columns) || Boolean(f.mark_active_columns); /*** select filter's customisation and behaviours ***/ /** * Text for clear option in drop-down filter types (1st option) * @type {String|Array} */ this.clearFilterText = isArray(f.clear_filter_text) ? f.clear_filter_text : defaultsStr(f.clear_filter_text, 'Clear'); /** * Indicate whether empty option is enabled in drop-down filter types * @type {Boolean} */ this.enableEmptyOption = Boolean(f.enable_empty_option); /** * Text for empty option in drop-down filter types * @type {String} */ this.emptyText = defaultsStr(f.empty_text, '(Empty)'); /** * Indicate whether non-empty option is enabled in drop-down filter * types * @type {Boolean} */ this.enableNonEmptyOption = Boolean(f.enable_non_empty_option); /** * Text for non-empty option in drop-down filter types * @type {String} */ this.nonEmptyText = defaultsStr(f.non_empty_text, '(Non empty)'); /** * Indicate whether drop-down filter types filter the table by default * on change event * @type {Boolean} */ this.onSlcChange = defaultsBool(f.on_change, true); /** * Make drop-down filter types options sorted in alpha-numeric manner * by default globally or on a column basis * @type {Boolean|Array} */ this.sortSlc = isUndef(f.sort_select) ? true : defaultsArr(f.sort_select, Boolean(f.sort_select)); /** * List of columns implementing filter options sorting in ascending * manner based on column data type * @type {Array} */ this.sortFilterOptionsAsc = defaultsArr(f.sort_filter_options_asc, []); /** * List of columns implementing filter options sorting in descending * manner based on column data type * @type {Array} */ this.sortFilterOptionsDesc = defaultsArr(f.sort_filter_options_desc, []); /** * Indicate whether drop-down filter types are populated on demand at * first usage * @type {Boolean} */ this.loadFltOnDemand = Boolean(f.load_filters_on_demand); /** * Indicate whether custom drop-down filter options are implemented * @type {Boolean} */ this.hasCustomOptions = isObj(f.custom_options); /** * Custom options definition of a per column basis, ie: * custom_options: { * cols:[0, 1], * texts: [ * ['a0', 'b0', 'c0'], * ['a1', 'b1', 'c1'] * ], * values: [ * ['a0', 'b0', 'c0'], * ['a1', 'b1', 'c1'] * ], * sorts: [false, true] * } * * @type {Object} */ this.customOptions = f.custom_options; /*** Filter operators ***/ /** * Regular expression operator for input filter. Defaults to 'rgx:' * @type {String} */ this.rgxOperator = defaultsStr(f.regexp_operator, 'rgx:'); /** * Empty cells operator for input filter. Defaults to '[empty]' * @type {String} */ this.emOperator = defaultsStr(f.empty_operator, '[empty]'); /** * Non-empty cells operator for input filter. Defaults to '[nonempty]' * @type {String} */ this.nmOperator = defaultsStr(f.nonempty_operator, '[nonempty]'); /** * Logical OR operator for input filter. Defaults to '||' * @type {String} */ this.orOperator = defaultsStr(f.or_operator, '||'); /** * Logical AND operator for input filter. Defaults to '&&' * @type {String} */ this.anOperator = defaultsStr(f.and_operator, '&&'); /** * Greater than operator for input filter. Defaults to '>' * @type {String} */ this.grOperator = defaultsStr(f.greater_operator, '>'); /** * Lower than operator for input filter. Defaults to '<' * @type {String} */ this.lwOperator = defaultsStr(f.lower_operator, '<'); /** * Lower than or equal operator for input filter. Defaults to '<=' * @type {String} */ this.leOperator = defaultsStr(f.lower_equal_operator, '<='); /** * Greater than or equal operator for input filter. Defaults to '>=' * @type {String} */ this.geOperator = defaultsStr(f.greater_equal_operator, '>='); /** * Inequality operator for input filter. Defaults to '!' * @type {String} */ this.dfOperator = defaultsStr(f.different_operator, '!'); /** * Like operator for input filter. Defaults to '*' * @type {String} */ this.lkOperator = defaultsStr(f.like_operator, '*'); /** * Strict equality operator for input filter. Defaults to '=' * @type {String} */ this.eqOperator = defaultsStr(f.equal_operator, '='); /** * Starts with operator for input filter. Defaults to '=' * @type {String} */ this.stOperator = defaultsStr(f.start_with_operator, '{'); /** * Ends with operator for input filter. Defaults to '=' * @type {String} */ this.enOperator = defaultsStr(f.end_with_operator, '}'); // this.curExp = f.cur_exp || '^[¥£€$]'; /** * Stored values separator * @type {String} */ this.separator = defaultsStr(f.separator, ','); /** * Enable rows counter UI component * @type {Boolean|Object} */ this.rowsCounter = isObj(f.rows_counter) || Boolean(f.rows_counter); /** * Enable status bar UI component * @type {Boolean|Object} */ this.statusBar = isObj(f.status_bar) || Boolean(f.status_bar); /** * Enable activity/spinner indicator UI component * @type {Boolean|Object} */ this.loader = isObj(f.loader) || Boolean(f.loader); /*** validation - reset buttons/links ***/ /** * Enable filters submission button * @type {Boolean} */ this.displayBtn = Boolean(f.btn); /** * Define filters submission button text * @type {String} */ this.btnText = defaultsStr(f.btn_text, (!this.enableIcons ? 'Go' : '')); /** * Css class for filters submission button * @type {String} */ this.btnCssClass = defaultsStr(f.btn_css_class, (!this.enableIcons ? 'btnflt' : 'btnflt_icon')); /** * Enable clear button * @type {Object|Boolean} */ this.btnReset = isObj(f.btn_reset) || Boolean(f.btn_reset); /** * Callback fired before filters are cleared * @type {Function} */ this.onBeforeReset = defaultsFn(f.on_before_reset, EMPTY_FN); /** * Callback fired after filters are cleared * @type {Function} */ this.onAfterReset = defaultsFn(f.on_after_reset, EMPTY_FN); /** * Enable paging component * @type {Object|Boolean} */ this.paging = isObj(f.paging) || Boolean(f.paging); /** * Number of hidden rows * @type {Number} * @private */ this.nbHiddenRows = 0; /** * Enable auto-filter behaviour, table is filtered when a user * stops typing * @type {Object|Boolean} */ this.autoFilter = isObj(f.auto_filter) || Boolean(f.auto_filter); /** * Auto-filter delay in milliseconds * @type {Number} */ this.autoFilterDelay = isObj(f.auto_filter) && isNumber(f.auto_filter.delay) ? f.auto_filter.delay : AUTO_FILTER_DELAY; /** * Indicate whether user is typing * @type {Boolean} * @private */ this.isUserTyping = null; /** * Auto-filter interval ID * @type {String} * @private */ this.autoFilterTimer = null; /** * Enable keyword highlighting behaviour * @type {Boolean} */ this.highlightKeywords = Boolean(f.highlight_keywords); /** * Enable no results message UI component * @type {Object|Boolean} */ this.noResults = isObj(f.no_results_message) || Boolean(f.no_results_message); /** * Enable state persistence * @type {Object|Boolean} */ this.state = isObj(f.state) || Boolean(f.state); /*** data types ***/ /** * Enable date type module * @type {Boolean} * @private */ this.dateType = true; /** * Define default locale, default to 'en' as per Sugar Date module: * https://sugarjs.com/docs/#/DateLocales * @type {String} */ this.locale = defaultsStr(f.locale, 'en'); /** * Define thousands separator ',' or '.', defaults to ',' * @type {String} */ this.thousandsSeparator = defaultsStr(f.thousands_separator, ','); /** * Define decimal separator ',' or '.', defaults to '.' * @type {String} */ this.decimalSeparator = defaultsStr(f.decimal_separator, '.'); /** * Define data types on a column basis, possible values 'string', * 'number', 'formatted-number', 'date', 'ipaddress' ie: * col_types : [ * 'string', 'date', 'number', * { type: 'formatted-number', decimal: ',', thousands: '.' }, * { type: 'date', locale: 'en-gb' }, * { type: 'date', format: ['{dd}-{months}-{yyyy|yy}'] } * ] * * Refer to https://sugarjs.com/docs/#/DateParsing for exhaustive * information on date parsing formats supported by Sugar Date * @type {Array} */ this.colTypes = isArray(f.col_types) ? f.col_types : []; /*** ids prefixes ***/ /** * Main prefix * @private */ this.prfxTf = 'TF'; /** * Filter's ID prefix (inputs - selects) * @private */ this.prfxFlt = 'flt'; /** * Button's ID prefix * @private */ this.prfxValButton = 'btn'; /** * Responsive Css class * @private */ this.prfxResponsive = 'resp'; /** @private */ this.stickyCssClass = 'sticky'; /*** extensions ***/ /** * List of loaded extensions * @type {Array} */ this.extensions = defaultsArr(f.extensions, []); /*** themes ***/ /** * Enable default theme * @type {Boolean} */ this.enableDefaultTheme = Boolean(f.enable_default_theme); /** * Determine whether themes are enables * @type {Boolean} * @private */ this.hasThemes = (this.enableDefaultTheme || isArray(f.themes)); /** * List of themes, ie: * themes: [{ name: 'skyblue' }] * @type {Array} */ this.themes = defaultsArr(f.themes, []); /** * Define path to themes assets, defaults to * 'tablefilter/style/themes/'. Usage: * themes: [{ name: 'skyblue' }] * @type {Array} */ this.themesPath = this.getThemesPath(); /** * Enable responsive layout * @type {Boolean} */ this.responsive = Boolean(f.responsive); /** * Enable toolbar component * @type {Object|Boolean} */ this.toolbar = isObj(f.toolbar) || Boolean(f.toolbar); /** * Enable sticky headers * @type {Boolean} */ this.stickyHeaders = Boolean(f.sticky_headers); /** * Features registry * @private */ this.Mod = {}; /** * Extensions registry * @private */ this.ExtRegistry = {}; // instantiate features if needed this.instantiateFeatures(FEATURES); } /** * Initialise features and layout */ init() { if (this.initialized) { return; } // import main stylesheet this.import(this.stylesheetId, this.getStylesheetPath(), null, 'link'); let Mod = this.Mod; let inpclass; //loads theme this.loadThemes(); //explicitly initialise features in given order this.initFeatures([ DateType, Help, State, MarkActiveColumns, GridLayout, Loader, HighlightKeyword, PopupFilter ]); //filters grid is not generated if (!this.fltGrid) { this._initNoFilters(); } else { let fltrow = this._insertFiltersRow(); this.nbCells = this.getCellsNb(this.refRow); this.nbFilterableRows = this.getRowsNb(); let n = this.singleFlt ? 1 : this.nbCells; //build filters for (let i = 0; i < n; i++) { this.emitter.emit('before-filter-init', this, i); let fltCell = createElm(this.fltCellTag), col = this.getFilterType(i); if (this.singleFlt) { fltCell.colSpan = this.nbCells; } if (!this.gridLayout) { fltrow.appendChild(fltCell); } inpclass = (i === n - 1 && this.displayBtn) ? this.fltSmallCssClass : this.fltCssClass; //only 1 input for single search if (this.singleFlt) { col = INPUT; inpclass = this.singleFltCssClass; } //drop-down filters if (col === SELECT || col === MULTIPLE) { Mod.dropdown = Mod.dropdown || new Dropdown(this); Mod.dropdown.init(i, this.isExternalFlt(), fltCell); } // checklist else if (col === CHECKLIST) { Mod.checkList = Mod.checkList || new CheckList(this); Mod.checkList.init(i, this.isExternalFlt(), fltCell); } else { this._buildInputFilter(i, inpclass, fltCell); } // this adds submit button if (i === n - 1 && this.displayBtn) { this._buildSubmitButton( this.isExternalFlt() ? elm(this.externalFltIds[i]) : fltCell ); } this.emitter.emit('after-filter-init', this, i); } this.emitter.on(['filter-focus'], (tf, filter) => this.setActiveFilterId(filter.id)); }//if this.fltGrid /* Features */ if (this.hasExcludedRows) { this.emitter.on(['after-filtering'], () => this.setExcludeRows()); this.setExcludeRows(); } this.initFeatures([ RowsCounter, StatusBar, ClearButton, AlternateRows, NoResults, Paging, Toolbar ]); this.setColWidths(); //TF css class is added to table if (!this.gridLayout) { addClass(this.dom(), this.prfxTf); if (this.responsive) { addClass(this.dom(), this.prfxResponsive); } if (this.colWidths.length > 0) { this.setFixedLayout(); } if (this.stickyHeaders && this.dom().tHead) { addClass(this.dom(), this.stickyCssClass); } } /* Load extensions */ this.initExtensions(); this.initialized = true; this.onFiltersLoaded(this); this.emitter.emit('initialized', this); } /** * Detect <enter> key * @param {Event} evt */ detectKey(evt) { if (!this.enterKey) { return; } if (isKeyPressed(evt, [ENTER_KEY])) { this.filter(); cancelEvt(evt); stopEvt(evt); } else { this.isUserTyping = true; root.clearInterval(this.autoFilterTimer); this.autoFilterTimer = null; } } /** * Filter's keyup event: if auto-filter on, detect user is typing and filter * columns * @param {Event} evt */ onKeyUp(evt) { if (!this.autoFilter) { return; } this.isUserTyping = false; function filter() { root.clearInterval(this.autoFilterTimer); this.autoFilterTimer = null; if (!this.isUserTyping) { this.filter(); this.isUserTyping = null; } } if (isKeyPressed(evt, [ENTER_KEY, TAB_KEY, ESC_KEY, UP_ARROW_KEY, DOWN_ARROW_KEY])) { root.clearInterval(this.autoFilterTimer); this.autoFilterTimer = null; } else { if (this.autoFilterTimer !== null) { return; } this.autoFilterTimer = root.setInterval( filter.bind(this), this.autoFilterDelay); } } /** * Filter's keydown event: if auto-filter on, detect user is typing */ onKeyDown() { if (this.autoFilter) { this.isUserTyping = true; } } /** * Filter's focus event * @param {Event} evt */ onInpFocus(evt) { let elm = targetEvt(evt); this.emitter.emit('filter-focus', this, elm); } /** * Filter's blur event: if auto-filter on, clear interval on filter blur */ onInpBlur() { if (this.autoFilter) { this.isUserTyping = false; root.clearInterval(this.autoFilterTimer); } this.emitter.emit('filter-blur', this); } /** * Insert filters row at initialization */ _insertFiltersRow() { // TODO: prevent filters row generation for popup filters too, // to reduce and simplify headers row index adjusting across lib modules // (GridLayout, PopupFilter etc) if (this.gridLayout) { return; } let fltrow; let thead = tag(this.dom(), 'thead'); if (thead.length > 0) { fltrow = thead[0].insertRow(this.filtersRowIndex); } else { fltrow = this.dom().insertRow(this.filtersRowIndex); } fltrow.className = this.fltsRowCssClass; if (this.isExternalFlt()) { fltrow.style.display = NONE; } this.emitter.emit('filters-row-inserted', this, fltrow); return fltrow; } /** * Initialize filtersless table */ _initNoFilters() { if (this.fltGrid) { return; } this.refRow = this.refRow > 0 ? this.refRow - 1 : 0; this.nbFilterableRows = this.getRowsNb(); } /** * Build input filter type * @param {Number} colIndex Column index * @param {String} cssClass Css class applied to filter * @param {DOMElement} container Container DOM element */ _buildInputFilter(colIndex, cssClass, container) { let col = this.getFilterType(colIndex); let externalFltTgtId = this.isExternalFlt() ? this.externalFltIds[colIndex] : null; let inpType = col === INPUT ? 'text' : 'hidden'; let inp = createElm(INPUT, ['id', this.buildFilterId(colIndex)], ['type', inpType], ['ct', colIndex]); if (inpType !== 'hidden' && this.watermark) { inp.setAttribute('placeholder', this.isWatermarkArray ? (this.watermark[colIndex] || '') : this.watermark ); } inp.className = cssClass || this.fltCssClass; addEvt(inp, 'focus', (evt) => this.onInpFocus(evt)); //filter is appended in custom element if (externalFltTgtId) { elm(externalFltTgtId).appendChild(inp); } else { container.appendChild(inp); } this.fltIds.push(inp.id); addEvt(inp, 'keypress', (evt) => this.detectKey(evt)); addEvt(inp, 'keydown', () => this.onKeyDown()); addEvt(inp, 'keyup', (evt) => this.onKeyUp(evt)); addEvt(inp, 'blur', () => this.onInpBlur()); } /** * Build submit button * @param {DOMElement} container Container DOM element */ _buildSubmitButton(container) { let btn = createElm(INPUT, ['type', 'button'], ['value', this.btnText] ); btn.className = this.btnCssClass; //filter is appended in container element container.appendChild(btn); addEvt(btn, 'click', () => this.filter()); } /** * Conditionally istantiate each feature class in passed collection if * required by configuration and add it to the features registry. A feature * class meta information contains a `name` field and optional `altName` and * `alwaysInstantiate` fields * @param {Array} [features=[]] * @private */ instantiateFeatures(features = []) { features.forEach(featureCls => { let Cls = featureCls; // assign meta info if not present Cls.meta = Cls.meta || {name: null, altName: null}; Cls.meta.name = toCamelCase(Cls.name); let {name, altName, alwaysInstantiate} = Cls.meta; let prop = altName || name; if (!this.hasConfig || this[prop] === true || Boolean(alwaysInstantiate)) { this.Mod[name] = this.Mod[name] || new Cls(this); } }); } /** * Initialise each feature class in passed collection. * @param {Array} [features=[]] * @private */ initFeatures(features = []) { features.forEach(featureCls => { let {name, altName} = featureCls.meta; let prop = altName || name; if (this[prop] === true && this.Mod[name]) { this.Mod[name].init(); } }); } /** * Return a feature instance for a given name * @param {String} name Name of the feature * @return {Object} */ feature(name) { return this.Mod[name]; } /** * Initialise all the extensions defined in the configuration object */ initExtensions() { let exts = this.extensions; if (exts.length === 0) { return; } // Set config's publicPath dynamically for Webpack... __webpack_public_path__ = this.basePath; this.emitter.emit('before-loading-extensions', this); exts.forEach((ext) => { this.loadExtension(ext); }); this.emitter.emit('after-loading-extensions', this); } /** * Load an extension module * @param {Object} ext Extension config object */ loadExtension(ext) { if (!ext || !ext.name || this.hasExtension(ext.name)) { return; } let {name, path} = ext; let modulePath; if (name && path) { modulePath = ext.path + name; } else { name = name.replace('.js', ''); modulePath = 'extensions/{}/{}'.replace(/{}/g, name); } // Require pattern for Webpack require(['./' + modulePath], (mod) => { /* eslint-disable */ let inst = new mod.default(this, ext); /* eslint-enable */ inst.init(); this.ExtRegistry[name] = inst; }); } /** * Get an extension instance * @param {String} name Name of the extension * @return {Object} Extension instance */ extension(name) { return this.ExtRegistry[name]; } /** * Check passed extension name exists * @param {String} name Name of the extension * @return {Boolean} */ hasExtension(name) { return !isEmpty(this.ExtRegistry[name]); } /** * Register the passed extension instance with associated name * @param {Object} inst Extension instance * @param {String} name Name of the extension */ registerExtension(inst, name) { this.ExtRegistry[name] = inst; } /** * Destroy all the extensions store in extensions registry */ destroyExtensions() { let reg = this.ExtRegistry; Object.keys(reg).forEach((key) => { reg[key].destroy(); reg[key] = undefined; }); } /** * Load themes defined in the configuration object */ loadThemes() { if (!this.hasThemes) { return; } let themes = this.themes; this.emitter.emit('before-loading-themes', this); //Default theme config if (this.enableDefaultTheme) { let defaultTheme = { name: 'default' }; this.themes.push(defaultTheme); } themes.forEach((theme, i) => { let {name, path} = theme; let styleId = this.prfxTf + name; if (name && !path) { path = this.themesPath + name + '/' + name + '.css'; } else if (!name && theme.path) { name = 'theme{0}'.replace('{0}', i); } if (!this.isImported(path, 'link')) { this.import(styleId, path, null, 'link'); } }); // Enable loader indicator this.loader = true; this.emitter.emit('after-loading-themes', this); } /** * Return stylesheet DOM element for a given theme name * @return {DOMElement} stylesheet element */ getStylesheet(name = 'default') { return elm(this.prfxTf + name); } /** * Destroy filter grid */ destroy() { if (!this.initialized) { return; } let emitter = this.emitter; if (this.isExternalFlt() && !this.popupFilters) { this.removeExternalFlts(); } this.destroyExtensions(); this.validateAllRows(); // broadcast destroy event modules and extensions are subscribed to emitter.emit('destroy', this); if (this.fltGrid && !this.gridLayout) { this.dom().deleteRow(this.filtersRowIndex); } // unsubscribe to events if (this.hasExcludedRows) { emitter.off(['after-filtering'], () => this.setExcludeRows()); } this.emitter.off(['filter-focus'], (tf, filter) => this.setActiveFilterId(filter.id)); removeClass(this.dom(), this.prfxTf); removeClass(this.dom(), this.prfxResponsive); if (this.dom().tHead) { removeClass(this.dom().tHead, this.stickyCssClass); } this.nbHiddenRows = 0; this.validRowsIndex = []; this.fltIds = []; this.initialized = false; } /** * Remove all the external column filters */ removeExternalFlts() { if (!this.isExternalFlt()) { return; } let ids = this.externalFltIds; ids.forEach((id) => { let externalFlt = elm(id); if (externalFlt) { externalFlt.innerHTML = ''; } }); } /** * Check if given column implements a filter with custom options * @param {Number} colIndex Column's index * @return {Boolean} */ isCustomOptions(colIndex) { return this.hasCustomOptions && this.customOptions.cols.indexOf(colIndex) !== -1; } /** * Returns an array [[value0, value1 ...],[text0, text1 ...]] with the * custom options values and texts * @param {Number} colIndex Column's index * @return {Array} */ getCustomOptions(colIndex) { if (isEmpty(colIndex) || !this.isCustomOptions(colIndex)) { return; } let customOptions = this.customOptions; let cols = customOptions.cols; let optTxt = [], optArray = []; let index = cols.indexOf(colIndex); let slcValues = customOptions.values[index]; let slcTexts = customOptions.texts[index]; let slcSort = customOptions.sorts[index]; for (let r = 0, len = slcValues.length; r < len; r++) { optArray.push(slcValues[r]); if (slcTexts[r]) { optTxt.push(slcTexts[r]); } else { optTxt.push(slcValues[r]); } } if (slcSort) { optArray.sort(); optTxt.sort(); } return [optArray, optTxt]; } /** * Filter the table by retrieving the data from each cell in every single * row and comparing it to the search term for current column. A row is * hidden when all the search terms are not found in inspected row. */ filter() { if (!this.fltGrid || !this.initialized) { return; } let emitter = this.emitter; //fire onbefore callback this.onBeforeFilter(this); emitter.emit('before-filtering', this); let hiddenRows = 0; this.validRowsIndex = []; // search args let searchArgs = this.getFiltersValue(); let eachRow = this.eachRow(); eachRow( (row, k) => { // already filtered rows display re-init row.style.display = ''; let cells = row.cells; let nbCells = cells.length; let occurence = [], isMatch = true, //only for single filter search isSingleFltMatch = false; // this loop retrieves cell data for (let j = 0; j < nbCells; j++) { //searched keyword let sA = searchArgs[this.singleFlt ? 0 : j]; if (sA === '') { continue; } let cellValue = matchCase(this.getCellValue(cells[j]), this.caseSensitive); //multiple search parameter operator || let sAOrSplit = sA.toString().split(this.orOperator), //multiple search || parameter boolean hasMultiOrSA = sAOrSplit.length > 1, //multiple search parameter operator && sAAndSplit = sA.toString().split(this.anOperator), //multiple search && parameter boolean hasMultiAndSA = sAAndSplit.length > 1; //detect operators or array query if (isArray(sA) || hasMultiOrSA || hasMultiAndSA) { let cS, s; let found = false; if (isArray(sA)) { s = sA; } else { s = hasMultiOrSA ? sAOrSplit : sAAndSplit; } // isolate search term and check occurence in cell data for (let w = 0, len = s.length; w < len; w++) { cS = trim(s[w]); found = this._match(cS, cellValue, cells[j]); if (found) { emitter.emit('highlight-keyword', this, cells[j], cS); } if ((hasMultiOrSA && found) || (hasMultiAndSA && !found)) { break; } if (isArray(sA) && found) { break; } } occurence[j] = found; } //single search parameter else { occurence[j] = this._match(trim(sA), cellValue, cells[j]); if (occurence[j]) { emitter.emit('highlight-keyword', this, cells[j], sA); } } if (!occurence[j]) { isMatch = false; } if (this.singleFlt && this.singleFltExcludeCols.indexOf(j) === -1 && occurence[j]) { isSingleFltMatch = true; } emitter.emit('cell-processed', this, j, cells[j]); }//for j if (isSingleFltMatch) { isMatch = true; } this.validateRow(k, isMatch); if (!isMatch) { hiddenRows++; } emitter.emit('row-processed', this, k, this.validRowsIndex.length - 1, isMatch); }, // continue condition (row) => row.cells.length !== this.nbCells ); this.nbHiddenRows = hid