tablefilter
Version:
A Javascript library making HTML tables filterable and a bit more
1,668 lines (1,417 loc) • 95.4 kB
JavaScript
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