mirror of
https://github.com/koalyptus/TableFilter.git
synced 2024-06-20 14:45:15 +02:00
3066 lines
90 KiB
JavaScript
3066 lines
90 KiB
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 filtering
|
|
* @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 = 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 :
|
|
isArray(f.sort_select) ? f.sort_select : Boolean(f.sort_select);
|
|
|
|
/**
|
|
* Indicate whether options in drop-down filter types are sorted in a
|
|
* ascending numeric manner
|
|
* @type {Boolean}
|
|
* @private
|
|
*/
|
|
this.isSortNumAsc = Boolean(f.sort_num_asc);
|
|
|
|
/**
|
|
* List of columns implementing options sorting in a ascending numeric
|
|
* manner
|
|
* @type {Array}
|
|
*/
|
|
this.sortNumAsc = this.isSortNumAsc ? f.sort_num_asc : [];
|
|
|
|
/**
|
|
* Indicate whether options in drop-down filter types are sorted in a
|
|
* descending numeric manner
|
|
* @type {Boolean}
|
|
* @private
|
|
*/
|
|
this.isSortNumDesc = Boolean(f.sort_num_desc);
|
|
|
|
/**
|
|
* List of columns implementing options sorting in a descending numeric
|
|
* manner
|
|
* @type {Array}
|
|
*/
|
|
this.sortNumDesc = this.isSortNumDesc ? f.sort_num_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 = hiddenRows;
|
|
|
|
//fire onafterfilter callback
|
|
this.onAfterFilter(this);
|
|
|
|
emitter.emit('after-filtering', this, searchArgs);
|
|
}
|
|
|
|
/**
|
|
* Match search term in cell data
|
|
* @param {String} term Search term
|
|
* @param {String} cellValue Cell data
|
|
* @param {DOMElement} cell Current cell
|
|
* @return {Boolean}
|
|
* @private
|
|
*/
|
|
_match(term, cellValue, cell) {
|
|
let numData;
|
|
let colIdx = cell.cellIndex;
|
|
let decimal = this.getDecimal(colIdx);
|
|
let reLe = new RegExp(this.leOperator),
|
|
reGe = new RegExp(this.geOperator),
|
|
reL = new RegExp(this.lwOperator),
|
|
reG = new RegExp(this.grOperator),
|
|
reD = new RegExp(this.dfOperator),
|
|
reLk = new RegExp(rgxEsc(this.lkOperator)),
|
|
reEq = new RegExp(this.eqOperator),
|
|
reSt = new RegExp(this.stOperator),
|
|
reEn = new RegExp(this.enOperator),
|
|
// re_an = new RegExp(this.anOperator),
|
|
// re_cr = new RegExp(this.curExp),
|
|
reEm = this.emOperator,
|
|
reNm = this.nmOperator,
|
|
reRe = new RegExp(rgxEsc(this.rgxOperator));
|
|
|
|
term = matchCase(term, this.caseSensitive);
|
|
|
|
let occurence = false;
|
|
|
|
//Search arg operator tests
|
|
let hasLO = reL.test(term),
|
|
hasLE = reLe.test(term),
|
|
hasGR = reG.test(term),
|
|
hasGE = reGe.test(term),
|
|
hasDF = reD.test(term),
|
|
hasEQ = reEq.test(term),
|
|
hasLK = reLk.test(term),
|
|
// hatermN = re_an.test(term),
|
|
hasST = reSt.test(term),
|
|
hasEN = reEn.test(term),
|
|
hasEM = (reEm === term),
|
|
hasNM = (reNm === term),
|
|
hasRE = reRe.test(term);
|
|
|
|
// Check for dates or resolve date type
|
|
if (this.hasType(colIdx, [DATE])) {
|
|
let dte1, dte2;
|
|
|
|
let dateType = this.Mod.dateType;
|
|
let isValidDate = dateType.isValid.bind(dateType);
|
|
let parseDate = dateType.parse.bind(dateType);
|
|
let locale = dateType.getLocale(colIdx);
|
|
|
|
// Search arg dates tests
|
|
let isLDate = hasLO &&
|
|
isValidDate(term.replace(reL, ''), locale);
|
|
let isLEDate = hasLE &&
|
|
isValidDate(term.replace(reLe, ''), locale);
|
|
let isGDate = hasGR &&
|
|
isValidDate(term.replace(reG, ''), locale);
|
|
let isGEDate = hasGE &&
|
|
isValidDate(term.replace(reGe, ''), locale);
|
|
let isDFDate = hasDF &&
|
|
isValidDate(term.replace(reD, ''), locale);
|
|
let isEQDate = hasEQ &&
|
|
isValidDate(term.replace(reEq, ''), locale);
|
|
|
|
dte1 = parseDate(cellValue, locale);
|
|
|
|
// lower equal date
|
|
if (isLEDate) {
|
|
dte2 = parseDate(term.replace(reLe, ''), locale);
|
|
occurence = dte1 <= dte2;
|
|
}
|
|
// lower date
|
|
else if (isLDate) {
|
|
dte2 = parseDate(term.replace(reL, ''), locale);
|
|
occurence = dte1 < dte2;
|
|
}
|
|
// greater equal date
|
|
else if (isGEDate) {
|
|
dte2 = parseDate(term.replace(reGe, ''), locale);
|
|
occurence = dte1 >= dte2;
|
|
}
|
|
// greater date
|
|
else if (isGDate) {
|
|
dte2 = parseDate(term.replace(reG, ''), locale);
|
|
occurence = dte1 > dte2;
|
|
}
|
|
// different date
|
|
else if (isDFDate) {
|
|
dte2 = parseDate(term.replace(reD, ''), locale);
|
|
occurence = dte1.toString() !== dte2.toString();
|
|
}
|
|
// equal date
|
|
else if (isEQDate) {
|
|
dte2 = parseDate(term.replace(reEq, ''), locale);
|
|
occurence = dte1.toString() === dte2.toString();
|
|
}
|
|
// searched keyword with * operator doesn't have to be a date
|
|
else if (reLk.test(term)) {// like date
|
|
occurence = contains(term.replace(reLk, ''), cellValue,
|
|
false, this.caseSensitive);
|
|
}
|
|
else if (isValidDate(term)) {
|
|
dte2 = parseDate(term, locale);
|
|
occurence = dte1.toString() === dte2.toString();
|
|
}
|
|
//empty
|
|
else if (hasEM) {
|
|
occurence = !cell.hasChildNodes();
|
|
}
|
|
//non-empty
|
|
else if (hasNM) {
|
|
occurence = cell.hasChildNodes();
|
|
} else {
|
|
occurence = contains(term, cellValue,
|
|
this.isExactMatch(colIdx), this.caseSensitive);
|
|
}
|
|
} else {
|
|
// Convert to number anyways to auto-resolve type in case not
|
|
// defined by configuration. Order is important first try to
|
|
// parse formatted number then fallback to Number coercion
|
|
// to avoid false positives with Number
|
|
numData = parseNb(cellValue, decimal) || Number(cellValue);
|
|
|
|
// first checks if there is any operator (<,>,<=,>=,!,*,=,{,},
|
|
// rgx:)
|
|
|
|
//regexp
|
|
if (hasRE) {
|
|
//in case regexp throws
|
|
try {
|
|
//operator is removed
|
|
let srchArg = term.replace(reRe, '');
|
|
let rgx = new RegExp(srchArg);
|
|
occurence = rgx.test(cellValue);
|
|
} catch (ex) {
|
|
occurence = false;
|
|
}
|
|
}
|
|
// lower equal
|
|
else if (hasLE) {
|
|
occurence = numData <= parseNb(
|
|
term.replace(reLe, ''),
|
|
decimal
|
|
);
|
|
}
|
|
//greater equal
|
|
else if (hasGE) {
|
|
occurence = numData >= parseNb(
|
|
term.replace(reGe, ''),
|
|
decimal
|
|
);
|
|
}
|
|
//lower
|
|
else if (hasLO) {
|
|
occurence = numData < parseNb(
|
|
term.replace(reL, ''),
|
|
decimal
|
|
);
|
|
}
|
|
//greater
|
|
else if (hasGR) {
|
|
occurence = numData > parseNb(
|
|
term.replace(reG, ''),
|
|
decimal
|
|
);
|
|
}
|
|
//different
|
|
else if (hasDF) {
|
|
occurence = contains(term.replace(reD, ''), cellValue,
|
|
false, this.caseSensitive) ? false : true;
|
|
}
|
|
//like
|
|
else if (hasLK) {
|
|
occurence = contains(term.replace(reLk, ''), cellValue,
|
|
false, this.caseSensitive);
|
|
}
|
|
//equal
|
|
else if (hasEQ) {
|
|
occurence = contains(term.replace(reEq, ''), cellValue,
|
|
true, this.caseSensitive);
|
|
}
|
|
//starts with
|
|
else if (hasST) {
|
|
occurence = cellValue.indexOf(term.replace(reSt, '')) === 0 ?
|
|
true : false;
|
|
}
|
|
//ends with
|
|
else if (hasEN) {
|
|
let searchArg = term.replace(reEn, '');
|
|
occurence =
|
|
cellValue.lastIndexOf(searchArg, cellValue.length - 1) ===
|
|
(cellValue.length - 1) - (searchArg.length - 1) &&
|
|
cellValue.lastIndexOf(searchArg, cellValue.length - 1)
|
|
> -1 ? true : false;
|
|
}
|
|
//empty
|
|
else if (hasEM) {
|
|
occurence = !cell.hasChildNodes();
|
|
}
|
|
//non-empty
|
|
else if (hasNM) {
|
|
occurence = cell.hasChildNodes();
|
|
} else {
|
|
// If numeric type data, perform a strict equality test and
|
|
// fallback to unformatted number string comparison
|
|
if (numData &&
|
|
this.hasType(colIdx, [NUMBER, FORMATTED_NUMBER]) &&
|
|
!this.singleFlt) {
|
|
// parseNb can return 0 for strings which are not
|
|
// formatted numbers, in that case return the original
|
|
// string. TODO: handle this in parseNb
|
|
term = parseNb(term, decimal) || term;
|
|
occurence = numData === term ||
|
|
contains(term.toString(), numData.toString(),
|
|
this.isExactMatch(colIdx), this.caseSensitive);
|
|
} else {
|
|
// Finally test search term is contained in cell data
|
|
occurence = contains(
|
|
term,
|
|
cellValue,
|
|
this.isExactMatch(colIdx),
|
|
this.caseSensitive,
|
|
this.ignoresDiacritics(colIdx)
|
|
);
|
|
}
|
|
}
|
|
|
|
}//else
|
|
|
|
return occurence;
|
|
}
|
|
|
|
/**
|
|
* Return the data of a specified column
|
|
* @param {Number} colIndex Column index
|
|
* @param {Boolean} [includeHeaders=false] Include headers row
|
|
* @param {Array} [exclude=[]] List of row indexes to be excluded
|
|
* @return Flat list of data for a column
|
|
*/
|
|
getColumnData(colIndex, includeHeaders = false, exclude = []) {
|
|
return this.getColValues(colIndex, includeHeaders, true, exclude);
|
|
}
|
|
|
|
/**
|
|
* Return the values of a specified column
|
|
* @param {Number} colIndex Column index
|
|
* @param {Boolean} [includeHeaders=false] Include headers row
|
|
* @param {Array} [exclude=[]] List of row indexes to be excluded
|
|
* @return Flat list of values for a column
|
|
*/
|
|
getColumnValues(colIndex, includeHeaders = false, exclude = []) {
|
|
return this.getColValues(colIndex, includeHeaders, false, exclude);
|
|
}
|
|
|
|
/**
|
|
* Return the data of a specified column
|
|
* @param {Number} colIndex Column index
|
|
* @param {Boolean} [includeHeaders=false] Include headers row
|
|
* @param {Boolean} [typed=false] Return a typed value
|
|
* @param {Array} [exclude=[]] List of row indexes to be excluded
|
|
* @return {Array} Flat list of data for a column
|
|
* @private
|
|
*/
|
|
getColValues(
|
|
colIndex,
|
|
includeHeaders = false,
|
|
typed = false,
|
|
exclude = []
|
|
) {
|
|
let colValues = [];
|
|
let getContent = typed ? this.getCellData.bind(this) :
|
|
this.getCellValue.bind(this);
|
|
|
|
if (includeHeaders) {
|
|
colValues.push(this.getHeadersText()[colIndex]);
|
|
}
|
|
|
|
let eachRow = this.eachRow();
|
|
eachRow((row, i) => {
|
|
// checks if current row index appears in exclude array
|
|
let isExludedRow = exclude.indexOf(i) !== -1;
|
|
let cells = row.cells;
|
|
|
|
// checks if row has exact cell # and is not excluded
|
|
if (cells.length === this.nbCells && !isExludedRow) {
|
|
let data = getContent(cells[colIndex]);
|
|
colValues.push(data);
|
|
}
|
|
});
|
|
return colValues;
|
|
}
|
|
|
|
/**
|
|
* Return the filter's value of a specified column
|
|
* @param {Number} index Column index
|
|
* @return {String} Filter value
|
|
*/
|
|
getFilterValue(index) {
|
|
if (!this.fltGrid) {
|
|
return;
|
|
}
|
|
let fltValue = '';
|
|
let flt = this.getFilterElement(index);
|
|
if (!flt) {
|
|
return fltValue;
|
|
}
|
|
|
|
let fltColType = this.getFilterType(index);
|
|
if (fltColType !== MULTIPLE && fltColType !== CHECKLIST) {
|
|
fltValue = flt.value;
|
|
}
|
|
//mutiple select
|
|
else if (fltColType === MULTIPLE) {
|
|
fltValue = this.feature('dropdown').getValues(index);
|
|
}
|
|
//checklist
|
|
else if (fltColType === CHECKLIST) {
|
|
fltValue = this.feature('checkList').getValues(index);
|
|
}
|
|
//return an empty string if collection is empty or contains a single
|
|
//empty string
|
|
if (isArray(fltValue) && fltValue.length === 0 ||
|
|
(fltValue.length === 1 && fltValue[0] === '')) {
|
|
fltValue = '';
|
|
}
|
|
|
|
return fltValue;
|
|
}
|
|
|
|
/**
|
|
* Return the filters' values
|
|
* @return {Array} List of filters' values
|
|
*/
|
|
getFiltersValue() {
|
|
if (!this.fltGrid) {
|
|
return;
|
|
}
|
|
let searchArgs = [];
|
|
|
|
this.fltIds.forEach((id, i) => {
|
|
let fltValue = this.getFilterValue(i);
|
|
if (isArray(fltValue)) {
|
|
searchArgs.push(fltValue);
|
|
} else {
|
|
searchArgs.push(trim(fltValue));
|
|
}
|
|
});
|
|
return searchArgs;
|
|
}
|
|
|
|
/**
|
|
* Return the ID of a specified column's filter
|
|
* @param {Number} index Column's index
|
|
* @return {String} ID of the filter element
|
|
*/
|
|
getFilterId(index) {
|
|
if (!this.fltGrid) {
|
|
return;
|
|
}
|
|
return this.fltIds[index];
|
|
}
|
|
|
|
/**
|
|
* Return the list of ids of filters matching a specified type.
|
|
* Note: hidden filters are also returned
|
|
*
|
|
* @param {String} type Filter type string ('input', 'select', 'multiple',
|
|
* 'checklist')
|
|
* @param {Boolean} bool If true returns columns indexes instead of IDs
|
|
* @return {[type]} List of element IDs or column indexes
|
|
*/
|
|
getFiltersByType(type, bool) {
|
|
if (!this.fltGrid) {
|
|
return;
|
|
}
|
|
let arr = [];
|
|
for (let i = 0, len = this.fltIds.length; i < len; i++) {
|
|
let fltType = this.getFilterType(i);
|
|
if (fltType === type.toLowerCase()) {
|
|
let a = bool ? i : this.fltIds[i];
|
|
arr.push(a);
|
|
}
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
/**
|
|
* Return the filter's DOM element for a given column
|
|
* @param {Number} index Column's index
|
|
* @return {DOMElement}
|
|
*/
|
|
getFilterElement(index) {
|
|
return elm(this.fltIds[index]);
|
|
}
|
|
|
|
/**
|
|
* Return the number of cells for a given row index
|
|
* @param {Number} rowIndex Index of the row
|
|
* @return {Number} Number of cells
|
|
*/
|
|
getCellsNb(rowIndex = 0) {
|
|
let tr = this.dom().rows[rowIndex >= 0 ? rowIndex : 0];
|
|
return tr ? tr.cells.length : 0;
|
|
}
|
|
|
|
/**
|
|
* Return the number of working rows starting from reference row if
|
|
* defined
|
|
* @param {Boolean} includeHeaders Include the headers row(s)
|
|
* @return {Number} Number of working rows
|
|
*/
|
|
getRowsNb(includeHeaders) {
|
|
let nbRows = this.getWorkingRows().length;
|
|
if (this.dom().tHead) {
|
|
return includeHeaders ?
|
|
nbRows + this.dom().querySelectorAll('thead > tr').length :
|
|
nbRows;
|
|
}
|
|
return includeHeaders ? nbRows : nbRows - this.refRow;
|
|
}
|
|
|
|
/**
|
|
* Return the collection of the working rows, that is, the rows belonging
|
|
* to the tbody section(s)
|
|
* @returns {Array}
|
|
*/
|
|
getWorkingRows() {
|
|
return doc.querySelectorAll(`table#${this.id} > tbody > tr`);
|
|
}
|
|
|
|
/**
|
|
* Return the text content of a given cell
|
|
* @param {DOMElement} Cell's DOM element
|
|
* @return {String}
|
|
*/
|
|
getCellValue(cell) {
|
|
let idx = cell.cellIndex;
|
|
let cellParser = this.cellParser;
|
|
// Invoke cellParser for this column if any
|
|
if (cellParser.cols.indexOf(idx) !== -1) {
|
|
return cellParser.parse(this, cell, idx);
|
|
} else {
|
|
return getText(cell);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the typed data of a given cell based on the column type definition
|
|
* @param {DOMElement} cell Cell's DOM element
|
|
* @return {String|Number|Date}
|
|
*/
|
|
getCellData(cell) {
|
|
let colIndex = cell.cellIndex;
|
|
let value = this.getCellValue(cell);
|
|
|
|
if (this.hasType(colIndex, [FORMATTED_NUMBER])) {
|
|
return parseNb(value, this.getDecimal(colIndex));
|
|
}
|
|
else if (this.hasType(colIndex, [NUMBER])) {
|
|
return Number(value);
|
|
}
|
|
else if (this.hasType(colIndex, [DATE])){
|
|
let dateType = this.Mod.dateType;
|
|
return dateType.parse(value, dateType.getLocale(colIndex));
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Return the table data based on its columns data type definitions
|
|
* with following structure:
|
|
* [
|
|
* [rowIndex, [data0, data1...]],
|
|
* [rowIndex, [data0, data1...]]
|
|
* ]
|
|
* @param {Boolean} [includeHeaders=false] Include headers row
|
|
* @param {Boolean} [excludeHiddenCols=false] Exclude hidden columns
|
|
* @return {Array}
|
|
*/
|
|
getData(includeHeaders = false, excludeHiddenCols = false) {
|
|
return this.getTableData(includeHeaders, excludeHiddenCols, true);
|
|
}
|
|
|
|
/**
|
|
* Return the table values with following structure:
|
|
* [
|
|
* [rowIndex, [value0, value1...]],
|
|
* [rowIndex, [value0, value1...]]
|
|
* ]
|
|
* @param {Boolean} [includeHeaders=false] Include headers row
|
|
* @param {Boolean} [excludeHiddenCols=false] Exclude hidden columns
|
|
* @return {Array}
|
|
*/
|
|
getValues(includeHeaders = false, excludeHiddenCols = false) {
|
|
return this.getTableData(includeHeaders, excludeHiddenCols, false);
|
|
}
|
|
|
|
/**
|
|
* Return the table data with following structure:
|
|
* [
|
|
* [rowIndex, [value0, value1...]],
|
|
* [rowIndex, [value0, value1...]]
|
|
* ]
|
|
* @param {Boolean} [includeHeaders=false] Include headers row
|
|
* @param {Boolean} [excludeHiddenCols=false] Exclude hidden columns
|
|
* @param {Boolean} [typed=false] Return typed value
|
|
* @return {Array}
|
|
* @private
|
|
*
|
|
* TODO: provide an API returning data in JSON format
|
|
*/
|
|
getTableData(
|
|
includeHeaders = false,
|
|
excludeHiddenCols = false,
|
|
typed = false
|
|
) {
|
|
let tblData = [];
|
|
let getContent = typed ? this.getCellData.bind(this) :
|
|
this.getCellValue.bind(this);
|
|
|
|
if (includeHeaders) {
|
|
let headers = this.getHeadersText(excludeHiddenCols);
|
|
tblData.push([this.getHeadersRowIndex(), headers]);
|
|
}
|
|
|
|
let eachRow = this.eachRow();
|
|
eachRow((row, k) => {
|
|
let rowData = [k, []];
|
|
let cells = row.cells;
|
|
for (let j = 0, len = cells.length; j < len; j++) {
|
|
if (excludeHiddenCols && this.hasExtension('colsVisibility')) {
|
|
if (this.extension('colsVisibility').isColHidden(j)) {
|
|
continue;
|
|
}
|
|
}
|
|
let cellContent = getContent(cells[j]);
|
|
rowData[1].push(cellContent);
|
|
}
|
|
tblData.push(rowData);
|
|
});
|
|
return tblData;
|
|
}
|
|
|
|
/**
|
|
* Return the filtered table data based on its columns data type definitions
|
|
* with following structure:
|
|
* [
|
|
* [rowIndex, [data0, data1...]],
|
|
* [rowIndex, [data0, data1...]]
|
|
* ]
|
|
* @param {Boolean} [includeHeaders=false] Include headers row
|
|
* @param {Boolean} [excludeHiddenCols=false] Exclude hidden columns
|
|
* @return {Array}
|
|
*
|
|
* TODO: provide an API returning data in JSON format
|
|
*/
|
|
getFilteredData(includeHeaders = false, excludeHiddenCols = false) {
|
|
return this.filteredData(includeHeaders, excludeHiddenCols, true);
|
|
}
|
|
|
|
/**
|
|
* Return the filtered table values with following structure:
|
|
* [
|
|
* [rowIndex, [value0, value1...]],
|
|
* [rowIndex, [value0, value1...]]
|
|
* ]
|
|
* @param {Boolean} [includeHeaders=false] Include headers row
|
|
* @param {Boolean} [excludeHiddenCols=false] Exclude hidden columns
|
|
* @return {Array}
|
|
*
|
|
* TODO: provide an API returning data in JSON format
|
|
*/
|
|
getFilteredValues(includeHeaders = false, excludeHiddenCols = false) {
|
|
return this.filteredData(includeHeaders, excludeHiddenCols, false);
|
|
}
|
|
|
|
/**
|
|
* Return the filtered data with following structure:
|
|
* [
|
|
* [rowIndex, [value0, value1...]],
|
|
* [rowIndex, [value0, value1...]]
|
|
* ]
|
|
* @param {Boolean} [includeHeaders=false] Include headers row
|
|
* @param {Boolean} [excludeHiddenCols=false] Exclude hidden columns
|
|
* @param {Boolean} [typed=false] Return typed value
|
|
* @return {Array}
|
|
* @private
|
|
*
|
|
* TODO: provide an API returning data in JSON format
|
|
*/
|
|
filteredData(
|
|
includeHeaders = false,
|
|
excludeHiddenCols = false,
|
|
typed = false
|
|
) {
|
|
if (this.validRowsIndex.length === 0) {
|
|
return [];
|
|
}
|
|
let rows = this.dom().rows,
|
|
filteredData = [];
|
|
let getContent = typed ? this.getCellData.bind(this) :
|
|
this.getCellValue.bind(this);
|
|
|
|
if (includeHeaders) {
|
|
let headers = this.getHeadersText(excludeHiddenCols);
|
|
filteredData.push([this.getHeadersRowIndex(), headers]);
|
|
}
|
|
|
|
let validRows = this.getValidRows(true);
|
|
for (let i = 0; i < validRows.length; i++) {
|
|
let rData = [this.validRowsIndex[i], []],
|
|
cells = rows[this.validRowsIndex[i]].cells;
|
|
for (let k = 0; k < cells.length; k++) {
|
|
if (excludeHiddenCols && this.hasExtension('colsVisibility')) {
|
|
if (this.extension('colsVisibility').isColHidden(k)) {
|
|
continue;
|
|
}
|
|
}
|
|
let cellValue = getContent(cells[k]);
|
|
rData[1].push(cellValue);
|
|
}
|
|
filteredData.push(rData);
|
|
}
|
|
return filteredData;
|
|
}
|
|
|
|
/**
|
|
* Return the filtered data for a given column index
|
|
* @param {any} colIndex Colmun's index
|
|
* @param {boolean} [includeHeaders=false] Optional Include headers row
|
|
* @param {any} [exclude=[]] Optional List of row indexes to be excluded
|
|
* @return {Array} Flat list of typed values [data0, data1, data2...]
|
|
*
|
|
* TODO: provide an API returning data in JSON format
|
|
*/
|
|
getFilteredColumnData(colIndex, includeHeaders = false, exclude = []) {
|
|
return this.getFilteredDataCol(
|
|
colIndex, includeHeaders, true, exclude, false);
|
|
}
|
|
|
|
/**
|
|
* Return the filtered and visible data for a given column index
|
|
* @param {any} colIndex Colmun's index
|
|
* @param {boolean} [includeHeaders=false] Optional Include headers row
|
|
* @param {any} [exclude=[]] Optional List of row indexes to be excluded
|
|
* @return {Array} Flat list of typed values [data0, data1, data2...]
|
|
*
|
|
* TODO: provide an API returning data in JSON format
|
|
*/
|
|
getVisibleColumnData(colIndex, includeHeaders = false, exclude = []) {
|
|
return this.getFilteredDataCol(
|
|
colIndex, includeHeaders, true, exclude, true);
|
|
}
|
|
|
|
/**
|
|
* Return the filtered values for a given column index
|
|
* @param {any} colIndex Colmun's index
|
|
* @param {boolean} [includeHeaders=false] Optional Include headers row
|
|
* @param {any} [exclude=[]] Optional List of row indexes to be excluded
|
|
* @return {Array} Flat list of values ['value0', 'value1', 'value2'...]
|
|
*
|
|
* TODO: provide an API returning data in JSON format
|
|
*/
|
|
getFilteredColumnValues(colIndex, includeHeaders = false, exclude = []) {
|
|
return this.getFilteredDataCol(
|
|
colIndex, includeHeaders, false, exclude, false);
|
|
}
|
|
|
|
/**
|
|
* Return the filtered and visible values for a given column index
|
|
* @param {any} colIndex Colmun's index
|
|
* @param {boolean} [includeHeaders=false] Optional Include headers row
|
|
* @param {any} [exclude=[]] Optional List of row indexes to be excluded
|
|
* @return {Array} Flat list of values ['value0', 'value1', 'value2'...]
|
|
*
|
|
* TODO: provide an API returning data in JSON format
|
|
*/
|
|
getVisibleColumnValues(colIndex, includeHeaders = false, exclude = []) {
|
|
return this.getFilteredDataCol(
|
|
colIndex, includeHeaders, false, exclude, true);
|
|
}
|
|
|
|
/**
|
|
* Return the filtered data for a given column index
|
|
* @param {Number} colIndex Colmun's index
|
|
* @param {Boolean} [includeHeaders=false] Include headers row
|
|
* @param {Boolean} [typed=false] Return typed value
|
|
* @param {Array} [exclude=[]] List of row indexes to be excluded
|
|
* @param {Boolean} [visible=true] Return only filtered and visible data
|
|
* (relevant for paging)
|
|
* @return {Array} Flat list of values ['val0','val1','val2'...]
|
|
* @private
|
|
*
|
|
* TODO: provide an API returning data in JSON format
|
|
*/
|
|
getFilteredDataCol(
|
|
colIndex,
|
|
includeHeaders = false,
|
|
typed = false,
|
|
exclude = [],
|
|
visible = true
|
|
) {
|
|
if (isUndef(colIndex)) {
|
|
return [];
|
|
}
|
|
|
|
let rows = this.dom().rows;
|
|
let getContent = typed ? this.getCellData.bind(this) :
|
|
this.getCellValue.bind(this);
|
|
|
|
// ensure valid rows index do not contain excluded rows and row is
|
|
// displayed
|
|
let validRows = this.getValidRows(true).filter((rowIdx) => {
|
|
return exclude.indexOf(rowIdx) === -1 &&
|
|
(visible ?
|
|
this.getRowDisplay(rows[rowIdx]) !== 'none' :
|
|
true);
|
|
});
|
|
|
|
// convert column value to expected type if necessary
|
|
let validColValues = validRows.map((rowIdx) => {
|
|
return getContent(rows[rowIdx].cells[colIndex]);
|
|
});
|
|
|
|
if (includeHeaders) {
|
|
validColValues.unshift(this.getHeadersText()[colIndex]);
|
|
}
|
|
|
|
return validColValues;
|
|
}
|
|
|
|
/**
|
|
* Get the display value of a row
|
|
* @param {HTMLTableRowElement} row DOM element of the row
|
|
* @return {String} Usually 'none' or ''
|
|
*/
|
|
getRowDisplay(row) {
|
|
return row.style.display;
|
|
}
|
|
|
|
/**
|
|
* Validate/invalidate row by setting the 'validRow' attribute on the row
|
|
* @param {Number} rowIndex Index of the row
|
|
* @param {Boolean} isValid
|
|
*/
|
|
validateRow(rowIndex, isValid) {
|
|
let row = this.dom().rows[rowIndex];
|
|
if (!row || !isBoolean(isValid)) {
|
|
return;
|
|
}
|
|
|
|
// always visible rows are valid
|
|
if (this.excludeRows.indexOf(rowIndex) !== -1) {
|
|
isValid = true;
|
|
}
|
|
|
|
let displayFlag = isValid ? '' : NONE,
|
|
validFlag = isValid ? 'true' : 'false';
|
|
row.style.display = displayFlag;
|
|
|
|
if (this.paging) {
|
|
row.setAttribute('validRow', validFlag);
|
|
}
|
|
|
|
if (isValid) {
|
|
if (this.validRowsIndex.indexOf(rowIndex) === -1) {
|
|
this.validRowsIndex.push(rowIndex);
|
|
}
|
|
|
|
this.onRowValidated(this, rowIndex);
|
|
this.emitter.emit('row-validated', this, rowIndex);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate all filterable rows
|
|
*/
|
|
validateAllRows() {
|
|
if (!this.initialized) {
|
|
return;
|
|
}
|
|
this.validRowsIndex = [];
|
|
for (let k = this.refRow; k < this.nbFilterableRows; k++) {
|
|
this.validateRow(k, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set search value to a given filter
|
|
* @param {Number} index Column's index
|
|
* @param {String or Array} query searcharg Search term
|
|
*/
|
|
setFilterValue(index, query = '') {
|
|
if (!this.fltGrid) {
|
|
return;
|
|
}
|
|
let slc = this.getFilterElement(index),
|
|
fltColType = this.getFilterType(index);
|
|
|
|
if (!slc) {
|
|
return;
|
|
}
|
|
|
|
//multiple selects
|
|
if (fltColType === MULTIPLE) {
|
|
let values = isArray(query) ? query :
|
|
query.split(' ' + this.orOperator + ' ');
|
|
|
|
if (this.loadFltOnDemand && !this.initialized) {
|
|
this.emitter.emit('build-select-filter', this, index,
|
|
this.linkedFilters, this.isExternalFlt());
|
|
}
|
|
|
|
this.emitter.emit('select-options', this, index, values);
|
|
}
|
|
//checklist
|
|
else if (fltColType === CHECKLIST) {
|
|
let values = [];
|
|
if (this.loadFltOnDemand && !this.initialized) {
|
|
this.emitter.emit('build-checklist-filter', this, index,
|
|
this.linkedFilters);
|
|
}
|
|
if (isArray(query)) {
|
|
values = query;
|
|
} else {
|
|
query = matchCase(query, this.caseSensitive);
|
|
values = query.split(' ' + this.orOperator + ' ');
|
|
}
|
|
|
|
this.emitter.emit('select-checklist-options', this, index, values);
|
|
}
|
|
else {
|
|
if (this.loadFltOnDemand && !this.initialized) {
|
|
this.emitter.emit('build-select-filter', this, index,
|
|
this.linkedFilters, this.isExternalFlt());
|
|
}
|
|
slc.value = query;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make passed or default working table element width fixed
|
|
* @param {TableElement} tbl optional table DOM element
|
|
*/
|
|
setFixedLayout(tbl = this.dom()) {
|
|
let colWidths = this.colWidths;
|
|
let tableWidth = tbl.clientWidth;
|
|
|
|
if (colWidths.length > 0) {
|
|
let defaultWidth = this.defaultColWidth;
|
|
tableWidth = colWidths
|
|
.reduce((x, y) =>
|
|
parseInt((x || defaultWidth), 10) +
|
|
parseInt((y || defaultWidth), 10)
|
|
);
|
|
}
|
|
|
|
tbl.style.width = `${tableWidth}px`;
|
|
tbl.style.tableLayout = 'fixed';
|
|
}
|
|
|
|
/**
|
|
* Set passed or default working table columns' widths with configuration
|
|
* values
|
|
* @param {TableElement} tbl optional table DOM element
|
|
*/
|
|
setColWidths(tbl = this.dom()) {
|
|
let colWidths = this.colWidths;
|
|
if (colWidths.length === 0) {
|
|
return;
|
|
}
|
|
|
|
let colTags = tag(tbl, 'col');
|
|
let tblHasColTag = colTags.length > 0;
|
|
let frag = !tblHasColTag ? doc.createDocumentFragment() : null;
|
|
|
|
this.eachCol((k) => {
|
|
let col;
|
|
if (tblHasColTag) {
|
|
col = colTags[k];
|
|
} else {
|
|
col = createElm('col');
|
|
frag.appendChild(col);
|
|
}
|
|
col.style.width = colWidths[k];
|
|
});
|
|
|
|
if (!tblHasColTag) {
|
|
tbl.insertBefore(frag, tbl.firstChild);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exclude rows from actions
|
|
*/
|
|
setExcludeRows() {
|
|
if (!this.hasExcludedRows) {
|
|
return;
|
|
}
|
|
this.excludeRows.forEach((rowIdx) => this.validateRow(rowIdx, true));
|
|
}
|
|
|
|
/**
|
|
* Clear all the filters' values
|
|
*/
|
|
clearFilters() {
|
|
if (!this.fltGrid) {
|
|
return;
|
|
}
|
|
|
|
this.emitter.emit('before-clearing-filters', this);
|
|
this.onBeforeReset(this, this.getFiltersValue());
|
|
|
|
for (let i = 0, len = this.fltIds.length; i < len; i++) {
|
|
this.setFilterValue(i, '');
|
|
}
|
|
|
|
this.filter();
|
|
|
|
this.onAfterReset(this);
|
|
this.emitter.emit('after-clearing-filters', this);
|
|
}
|
|
|
|
/**
|
|
* Return the ID of the current active filter
|
|
* @return {String}
|
|
*/
|
|
getActiveFilterId() {
|
|
return this.activeFilterId;
|
|
}
|
|
|
|
/**
|
|
* Set the ID of the current active filter
|
|
* @param {String} filterId Element ID
|
|
*/
|
|
setActiveFilterId(filterId) {
|
|
this.activeFilterId = filterId;
|
|
}
|
|
|
|
/**
|
|
* Return the column index for a given filter ID
|
|
* @param {string} [filterId=''] Filter ID
|
|
* @return {Number} Column index
|
|
*/
|
|
getColumnIndexFromFilterId(filterId = '') {
|
|
let idx = filterId.split('_')[0];
|
|
idx = idx.split(this.prfxFlt)[1];
|
|
return parseInt(idx, 10);
|
|
}
|
|
|
|
/**
|
|
* Build filter element ID for a given column index
|
|
* @param {any} colIndex
|
|
* @return {String} Filter element ID string
|
|
* @private
|
|
*/
|
|
buildFilterId(colIndex) {
|
|
return `${this.prfxFlt}${colIndex}_${this.id}`;
|
|
}
|
|
|
|
/**
|
|
* Check if has external filters
|
|
* @returns {Boolean}
|
|
* @private
|
|
*/
|
|
isExternalFlt() {
|
|
return this.externalFltIds.length > 0;
|
|
}
|
|
|
|
/**
|
|
* Returns styles path
|
|
* @returns {String}
|
|
* @private
|
|
*/
|
|
getStylePath() {
|
|
return defaultsStr(this.config.style_path, this.basePath + 'style/');
|
|
}
|
|
|
|
/**
|
|
* Returns main stylesheet path
|
|
* @returns {String}
|
|
* @private
|
|
*/
|
|
getStylesheetPath() {
|
|
return defaultsStr(this.config.stylesheet,
|
|
this.getStylePath() + 'tablefilter.css');
|
|
}
|
|
|
|
/**
|
|
* Returns themes path
|
|
* @returns {String}
|
|
* @private
|
|
*/
|
|
getThemesPath() {
|
|
return defaultsStr(this.config.themes_path,
|
|
this.getStylePath() + 'themes/');
|
|
}
|
|
|
|
/**
|
|
* Make specified column's filter active
|
|
* @param colIndex Index of a column
|
|
*/
|
|
activateFilter(colIndex) {
|
|
if (isUndef(colIndex)) {
|
|
return;
|
|
}
|
|
this.setActiveFilterId(this.getFilterId(colIndex));
|
|
}
|
|
|
|
/**
|
|
* Determine if passed filter column implements exact query match
|
|
* @param {Number} colIndex Column index
|
|
* @return {Boolean}
|
|
*/
|
|
isExactMatch(colIndex) {
|
|
let fltType = this.getFilterType(colIndex);
|
|
return this.exactMatchByCol[colIndex] || this.exactMatch ||
|
|
fltType !== INPUT;
|
|
}
|
|
|
|
/**
|
|
* Check if passed row is valid
|
|
* @param {Number} rowIndex Row index
|
|
* @return {Boolean}
|
|
*/
|
|
isRowValid(rowIndex) {
|
|
return this.getValidRows().indexOf(rowIndex) !== -1;
|
|
}
|
|
|
|
/**
|
|
* Check if passed row is visible
|
|
* @param {Number} rowIndex Row index
|
|
* @return {Boolean}
|
|
*/
|
|
isRowDisplayed(rowIndex) {
|
|
let row = this.dom().rows[rowIndex];
|
|
return this.getRowDisplay(row) === '';
|
|
}
|
|
|
|
/**
|
|
* Check if specified column filter ignores diacritics.
|
|
* Note this is only applicable to input filter types.
|
|
* @param {Number} colIndex Column index
|
|
* @return {Boolean}
|
|
*/
|
|
ignoresDiacritics(colIndex) {
|
|
let ignoreDiac = this.ignoreDiacritics;
|
|
if (isArray(ignoreDiac)) {
|
|
return ignoreDiac[colIndex];
|
|
}
|
|
return Boolean(ignoreDiac);
|
|
}
|
|
|
|
/**
|
|
* Return clear all text for specified filter column
|
|
* @param {Number} colIndex Column index
|
|
* @return {String}
|
|
*/
|
|
getClearFilterText(colIndex) {
|
|
let clearText = this.clearFilterText;
|
|
if (isArray(clearText)) {
|
|
return clearText[colIndex];
|
|
}
|
|
return clearText;
|
|
}
|
|
|
|
/**
|
|
* Column iterator invoking continue and break condition callbacks if any
|
|
* then calling supplied callback for each item
|
|
* @param {Function} [fn=EMPTY_FN] callback
|
|
* @param {Function} [continueFn=EMPTY_FN] continue condition callback
|
|
* @param {Function} [breakFn=EMPTY_FN] break condition callback
|
|
*/
|
|
eachCol(fn = EMPTY_FN, continueFn = EMPTY_FN, breakFn = EMPTY_FN) {
|
|
let len = this.getCellsNb(this.refRow);
|
|
for (let i = 0; i < len; i++) {
|
|
if (continueFn(i) === true) {
|
|
continue;
|
|
}
|
|
if (breakFn(i) === true) {
|
|
break;
|
|
}
|
|
fn(i);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rows iterator starting from supplied row index or defaulting to reference
|
|
* row index. Closure function accepts a callback function and optional
|
|
* continue and break callbacks.
|
|
* @param {Number} startIdx Row index from which filtering starts
|
|
*/
|
|
eachRow(startIdx = this.refRow) {
|
|
return (fn = EMPTY_FN, continueFn = EMPTY_FN, breakFn = EMPTY_FN) => {
|
|
let rows = this.dom().rows;
|
|
let len = this.getRowsNb(true);
|
|
for (let i = startIdx; i < len; i++) {
|
|
if (continueFn(rows[i], i) === true) {
|
|
continue;
|
|
}
|
|
if (breakFn(rows[i], i) === true) {
|
|
break;
|
|
}
|
|
fn(rows[i], i);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if passed script or stylesheet is already imported
|
|
* @param {String} filePath Ressource path
|
|
* @param {String} type Possible values: 'script' or 'link'
|
|
* @return {Boolean}
|
|
*/
|
|
isImported(filePath, type = 'script') {
|
|
let imported = false,
|
|
attr = type === 'script' ? 'src' : 'href',
|
|
files = tag(doc, type);
|
|
for (let i = 0, len = files.length; i < len; i++) {
|
|
if (isUndef(files[i][attr])) {
|
|
continue;
|
|
}
|
|
if (files[i][attr].match(filePath)) {
|
|
imported = true;
|
|
break;
|
|
}
|
|
}
|
|
return imported;
|
|
}
|
|
|
|
/**
|
|
* Import script or stylesheet
|
|
* @param {String} fileId Ressource ID
|
|
* @param {String} filePath Ressource path
|
|
* @param {Function} callback Callback
|
|
* @param {String} type Possible values: 'script' or 'link'
|
|
*/
|
|
import(fileId, filePath, callback, type = 'script') {
|
|
if (this.isImported(filePath, type)) {
|
|
return;
|
|
}
|
|
let o = this,
|
|
isLoaded = false,
|
|
file,
|
|
head = tag(doc, 'head')[0];
|
|
|
|
if (type.toLowerCase() === 'link') {
|
|
file = createElm('link',
|
|
['id', fileId], ['type', 'text/css'],
|
|
['rel', 'stylesheet'], ['href', filePath]
|
|
);
|
|
} else {
|
|
file = createElm('script',
|
|
['id', fileId],
|
|
['type', 'text/javascript'], ['src', filePath]
|
|
);
|
|
}
|
|
|
|
//Browser <> IE onload event works only for scripts, not for stylesheets
|
|
file.onload = file.onreadystatechange = () => {
|
|
if (!isLoaded &&
|
|
(!this.readyState || this.readyState === 'loaded' ||
|
|
this.readyState === 'complete')) {
|
|
isLoaded = true;
|
|
if (typeof callback === 'function') {
|
|
callback.call(null, o);
|
|
}
|
|
}
|
|
};
|
|
file.onerror = () => {
|
|
throw new Error(`TableFilter could not load: ${filePath}`);
|
|
};
|
|
head.appendChild(file);
|
|
}
|
|
|
|
/**
|
|
* Check if table has filters grid
|
|
* @return {Boolean}
|
|
*/
|
|
isInitialized() {
|
|
return this.initialized;
|
|
}
|
|
|
|
/**
|
|
* Get list of filter IDs
|
|
* @return {Array} List of filters ids
|
|
*/
|
|
getFiltersId() {
|
|
return this.fltIds || [];
|
|
}
|
|
|
|
/**
|
|
* Get filtered (valid) rows indexes
|
|
* @param {Boolean} reCalc Force calculation of filtered rows list
|
|
* @return {Array} List of row indexes
|
|
*/
|
|
getValidRows(reCalc) {
|
|
if (!reCalc) {
|
|
return this.validRowsIndex;
|
|
}
|
|
|
|
this.validRowsIndex = [];
|
|
|
|
let eachRow = this.eachRow();
|
|
eachRow((row) => {
|
|
if (!this.paging) {
|
|
if (this.getRowDisplay(row) !== NONE) {
|
|
this.validRowsIndex.push(row.rowIndex);
|
|
}
|
|
} else {
|
|
if (row.getAttribute('validRow') === 'true' ||
|
|
row.getAttribute('validRow') === null) {
|
|
this.validRowsIndex.push(row.rowIndex);
|
|
}
|
|
}
|
|
});
|
|
return this.validRowsIndex;
|
|
}
|
|
|
|
/**
|
|
* Get the index of the row containing the filters
|
|
* @return {Number}
|
|
*/
|
|
getFiltersRowIndex() {
|
|
return this.filtersRowIndex;
|
|
}
|
|
|
|
/**
|
|
* Get the index of the headers row
|
|
* @return {Number}
|
|
*/
|
|
getHeadersRowIndex() {
|
|
return this.headersRow;
|
|
}
|
|
|
|
/**
|
|
* Get the row index from where the filtering process start (1st filterable
|
|
* row)
|
|
* @return {Number}
|
|
*/
|
|
getStartRowIndex() {
|
|
return this.refRow;
|
|
}
|
|
|
|
/**
|
|
* Get the index of the last row
|
|
* @return {Number}
|
|
*/
|
|
getLastRowIndex() {
|
|
let nbRows = this.getRowsNb(true);
|
|
return (nbRows - 1);
|
|
}
|
|
|
|
/**
|
|
* Determine whether the specified column has one of the passed types
|
|
* @param {Number} colIndex Column index
|
|
* @param {Array} [types=[]] List of column types
|
|
* @return {Boolean}
|
|
*/
|
|
hasType(colIndex, types = []) {
|
|
if (this.colTypes.length === 0) {
|
|
return false;
|
|
}
|
|
let colType = this.colTypes[colIndex];
|
|
if (isObj(colType)) {
|
|
colType = colType.type;
|
|
}
|
|
return types.indexOf(colType) !== -1;
|
|
}
|
|
|
|
/**
|
|
* Get the header DOM element for a given column index
|
|
* @param {Number} colIndex Column index
|
|
* @return {Element}
|
|
*/
|
|
getHeaderElement(colIndex) {
|
|
let table = this.gridLayout ? this.Mod.gridLayout.headTbl : this.dom();
|
|
let tHead = tag(table, 'thead');
|
|
let rowIdx = this.getHeadersRowIndex();
|
|
let header;
|
|
if (tHead.length === 0) {
|
|
header = table.rows[rowIdx].cells[colIndex];
|
|
}
|
|
if (tHead.length === 1) {
|
|
header = tHead[0].rows[rowIdx].cells[colIndex];
|
|
}
|
|
return header;
|
|
}
|
|
|
|
/**
|
|
* Return the list of headers' text
|
|
* @param {Boolean} excludeHiddenCols Optional: exclude hidden columns
|
|
* @return {Array} list of headers' text
|
|
*/
|
|
getHeadersText(excludeHiddenCols = false) {
|
|
let headers = [];
|
|
this.eachCol(
|
|
(j) => {
|
|
let header = this.getHeaderElement(j);
|
|
let headerText = getFirstTextNode(header);
|
|
headers.push(headerText);
|
|
},
|
|
// continue condition function
|
|
(j) => {
|
|
if (excludeHiddenCols && this.hasExtension('colsVisibility')) {
|
|
return this.extension('colsVisibility').isColHidden(j);
|
|
}
|
|
return false;
|
|
}
|
|
);
|
|
return headers;
|
|
}
|
|
|
|
/**
|
|
* Return the filter type for a specified column
|
|
* @param {Number} colIndex Column's index
|
|
* @return {String}
|
|
*/
|
|
getFilterType(colIndex) {
|
|
return this.filterTypes[colIndex];
|
|
}
|
|
|
|
/**
|
|
* Get the total number of filterable rows
|
|
* @return {Number}
|
|
*/
|
|
getFilterableRowsNb() {
|
|
return this.getRowsNb(false);
|
|
}
|
|
|
|
/**
|
|
* Return the total number of valid rows
|
|
* @param {Boolean} [reCalc=false] Forces calculation of filtered rows
|
|
* @return {Number}
|
|
*/
|
|
getValidRowsNb(reCalc = false) {
|
|
return this.getValidRows(reCalc).length;
|
|
}
|
|
|
|
/**
|
|
* Return the working DOM element
|
|
* @return {HTMLTableElement}
|
|
*/
|
|
dom() {
|
|
return this.tbl;
|
|
}
|
|
|
|
/**
|
|
* Return the decimal separator for supplied column as per column type
|
|
* configuration or global setting
|
|
* @param {Number} colIndex Column index
|
|
* @returns {String} '.' or ','
|
|
*/
|
|
getDecimal(colIndex) {
|
|
let decimal = this.decimalSeparator;
|
|
if (this.hasType(colIndex, [FORMATTED_NUMBER])) {
|
|
let colType = this.colTypes[colIndex];
|
|
if (colType.hasOwnProperty('decimal')) {
|
|
decimal = colType.decimal;
|
|
}
|
|
}
|
|
return decimal;
|
|
}
|
|
|
|
/**
|
|
* Get the configuration object (literal object)
|
|
* @return {Object}
|
|
*/
|
|
config() {
|
|
return this.cfg;
|
|
}
|
|
}
|