UNPKG

95.4 kBJavaScriptView Raw
1import {addEvt, cancelEvt, stopEvt, targetEvt, isKeyPressed} from './event';
2import {
3 addClass, createElm, elm, getText, getFirstTextNode, removeClass, tag
4} from './dom';
5import {contains, matchCase, rgxEsc, trim, toCamelCase, uuid} from './string';
6import {
7 isArray, isEmpty, isFn, isNumber, isObj, isString, isUndef, EMPTY_FN,
8 isBoolean
9} from './types';
10import {parse as parseNb} from './number';
11import {
12 defaultsBool, defaultsStr, defaultsFn,
13 defaultsNb, defaultsArr
14} from './settings';
15
16import {root} from './root';
17import {Emitter} from './emitter';
18import {Dropdown} from './modules/dropdown';
19import {CheckList} from './modules/checkList';
20import {DateType} from './modules/dateType';
21import {Help} from './modules/help';
22import {State} from './modules/state';
23import {GridLayout} from './modules/gridLayout';
24import {Loader} from './modules/loader';
25import {HighlightKeyword} from './modules/highlightKeywords';
26import {PopupFilter} from './modules/popupFilter';
27import {MarkActiveColumns} from './modules/markActiveColumns';
28import {RowsCounter} from './modules/rowsCounter';
29import {StatusBar} from './modules/statusBar';
30import {ClearButton} from './modules/clearButton';
31import {AlternateRows} from './modules/alternateRows';
32import {NoResults} from './modules/noResults';
33import {Paging} from './modules/paging';
34import {Toolbar} from './modules/toolbar';
35
36import {
37 INPUT, SELECT, MULTIPLE, CHECKLIST, NONE,
38 ENTER_KEY, TAB_KEY, ESC_KEY, UP_ARROW_KEY, DOWN_ARROW_KEY,
39 CELL_TAG, AUTO_FILTER_DELAY, NUMBER, DATE, FORMATTED_NUMBER
40} from './const';
41
42let doc = root.document;
43
44const FEATURES = [
45 DateType, Help, State, MarkActiveColumns, GridLayout, Loader,
46 HighlightKeyword, PopupFilter, RowsCounter, StatusBar, ClearButton,
47 AlternateRows, NoResults, Paging, Toolbar
48];
49
50/**
51 * Makes HTML tables filterable and a bit more :)
52 *
53 * @export
54 * @class TableFilter
55 */
56export class TableFilter {
57
58 /**
59 * Creates an instance of TableFilter
60 * requires `table` or `id` arguments, `row` and `configuration` optional
61 * @param {HTMLTableElement} table Table DOM element
62 * @param {String} id Table id
63 * @param {Number} row index indicating the 1st row
64 * @param {Object} configuration object
65 */
66 constructor(...args) {
67 /**
68 * ID of current instance
69 * @type {String}
70 * @private
71 */
72 this.id = null;
73
74 /**
75 * Current version
76 * @type {String}
77 */
78 this.version = '{VERSION}';
79
80 /**
81 * Current year
82 * @type {Number}
83 * @private
84 */
85 this.year = new Date().getFullYear();
86
87 /**
88 * HTML Table DOM element
89 * @type {DOMElement}
90 * @private
91 */
92 this.tbl = null;
93
94 /**
95 * Calculated row's index from which starts filtering once filters
96 * are generated
97 * @type {Number}
98 */
99 this.refRow = null;
100
101 /**
102 * Index of the headers row
103 * @type {Number}
104 * @private
105 */
106 this.headersRow = null;
107
108 /**
109 * Configuration object
110 * @type {Object}
111 * @private
112 */
113 this.cfg = {};
114
115 /**
116 * Number of rows that can be filtered
117 * @type {Number}
118 * @private
119 */
120 this.nbFilterableRows = 0;
121
122 /**
123 * Number of cells in the reference row
124 * @type {Number}
125 * @private
126 */
127 this.nbCells = null;
128
129 /**
130 * Has a configuration object
131 * @type {Object}
132 * @private
133 */
134 this.hasConfig = false;
135
136 /** @private */
137 this.initialized = false;
138
139 let startRow;
140
141 // TODO: use for-of
142 args.forEach((arg) => {
143 if (typeof arg === 'object' && arg.nodeName === 'TABLE') {
144 this.tbl = arg;
145 this.id = arg.id || `tf_${uuid()}`;
146 this.tbl.id = this.id;
147 } else if (isString(arg)) {
148 this.id = arg;
149 this.tbl = elm(arg);
150 } else if (isNumber(arg)) {
151 startRow = arg;
152 } else if (isObj(arg)) {
153 this.cfg = arg;
154 this.hasConfig = true;
155 }
156 });
157
158 if (!this.tbl || this.tbl.nodeName !== 'TABLE') {
159 throw new Error(`Could not instantiate TableFilter: HTML table
160 DOM element not found.`);
161 }
162
163 if (this.getRowsNb(true) === 0) {
164 throw new Error(`Could not instantiate TableFilter: HTML table
165 requires at least 1 row.`);
166 }
167
168 // configuration object
169 let f = this.cfg;
170
171 /**
172 * Event emitter instance
173 * @type {Emitter}
174 */
175 this.emitter = new Emitter();
176
177 // start row
178 this.refRow = isUndef(startRow) ? 2 : (startRow + 1);
179
180 /**
181 * Collection of filter type by column
182 * @type {Array}
183 * @private
184 */
185 this.filterTypes = [].map.call(
186 (this.dom().rows[this.refRow] || this.dom().rows[0]).cells,
187 (cell, idx) => {
188 let colType = this.cfg[`col_${idx}`];
189 return !colType ? INPUT : colType.toLowerCase();
190 });
191
192 /**
193 * Base path for static assets
194 * @type {String}
195 */
196 this.basePath = defaultsStr(f.base_path, 'tablefilter/');
197
198 /*** filters' grid properties ***/
199
200 /**
201 * Enable/disable filters
202 * @type {Boolean}
203 */
204 this.fltGrid = defaultsBool(f.grid, true);
205
206 /**
207 * Enable/disable grid layout (fixed headers)
208 * @type {Object|Boolean}
209 */
210 this.gridLayout = isObj(f.grid_layout) || Boolean(f.grid_layout);
211
212 /**
213 * Filters row index
214 * @type {Number}
215 */
216 this.filtersRowIndex = defaultsNb(f.filters_row_index, 0);
217
218 /**
219 * Headers row index
220 * @type {Number}
221 */
222 this.headersRow = defaultsNb(f.headers_row_index,
223 (this.filtersRowIndex === 0 ? 1 : 0));
224
225 /**
226 * Define the type of cell containing a filter (td/th)
227 * @type {String}
228 */
229 this.fltCellTag = defaultsStr(f.filters_cell_tag, CELL_TAG);
230
231 /**
232 * List of filters IDs
233 * @type {Array}
234 * @private
235 */
236 this.fltIds = [];
237
238 /**
239 * List of valid rows indexes (rows visible upon filtering)
240 * @type {Array}
241 * @private
242 */
243 this.validRowsIndex = [];
244
245 /*** filters' grid appearance ***/
246 /**
247 * Path for stylesheets
248 * @type {String}
249 */
250 this.stylePath = this.getStylePath();
251
252 /**
253 * Main stylesheet path
254 * @type {String}
255 */
256 this.stylesheet = this.getStylesheetPath();
257
258 /**
259 * Main stylesheet ID
260 * @type {String}
261 * @private
262 */
263 this.stylesheetId = this.id + '_style';
264
265 /**
266 * Css class for the filters row
267 * @type {String}
268 */
269 this.fltsRowCssClass = defaultsStr(f.flts_row_css_class, 'fltrow');
270
271 /**
272 * Enable/disable icons (paging, reset button)
273 * @type {Boolean}
274 */
275 this.enableIcons = defaultsBool(f.enable_icons, true);
276
277 /**
278 * Enable/disable alternating rows
279 * @type {Boolean}
280 */
281 this.alternateRows = Boolean(f.alternate_rows);
282
283 /**
284 * Columns widths array
285 * @type {Array}
286 */
287 this.colWidths = defaultsArr(f.col_widths, []);
288
289 /**
290 * Default column width when column widths are defined
291 */
292 this.defaultColWidth = defaultsNb(f.default_col_width, 100);
293
294 /**
295 * Css class for a filter element
296 * @type {String}
297 */
298 this.fltCssClass = defaultsStr(f.flt_css_class, 'flt');
299
300 /**
301 * Css class for multiple select filters
302 * @type {String}
303 */
304 this.fltMultiCssClass = defaultsStr(f.flt_multi_css_class, 'flt_multi');
305
306 /**
307 * Css class for small filter (when submit button is active)
308 * @type {String}
309 */
310 this.fltSmallCssClass = defaultsStr(f.flt_small_css_class, 'flt_s');
311
312 /**
313 * Css class for single filter type
314 * @type {String}
315 */
316 this.singleFltCssClass = defaultsStr((f.single_filter || {}).css_class,
317 'single_flt');
318
319 /*** filters' grid behaviours ***/
320
321 /**
322 * Enable/disable enter key for input type filters
323 * @type {Boolean}
324 */
325 this.enterKey = defaultsBool(f.enter_key, true);
326
327 /**
328 * Callback fired before filtering process starts
329 * @type {Function}
330 */
331 this.onBeforeFilter = defaultsFn(f.on_before_filter, EMPTY_FN);
332
333 /**
334 * Callback fired after filtering process is completed
335 * @type {Function}
336 */
337 this.onAfterFilter = defaultsFn(f.on_after_filter, EMPTY_FN);
338
339 /**
340 * Enable/disable case sensitivity for filtering, default false
341 * @type {Boolean}
342 */
343 this.caseSensitive = Boolean(f.case_sensitive);
344
345 /**
346 * Indicate whether exact match filtering is enabled on a per column
347 * basis
348 * @type {Boolean}
349 * @private
350 */
351 this.hasExactMatchByCol = isArray(f.columns_exact_match);
352
353 /**
354 * Exact match filtering per column array
355 * @type {Array}
356 */
357 this.exactMatchByCol = this.hasExactMatchByCol ?
358 f.columns_exact_match : [];
359
360 /**
361 * Globally enable/disable exact match filtering
362 * @type {Boolean}
363 */
364 this.exactMatch = Boolean(f.exact_match);
365
366 /**
367 * Ignore diacritics globally or on a column basis
368 * @type {Boolean|Array}
369 */
370 this.ignoreDiacritics = f.ignore_diacritics;
371
372 /**
373 * Enable/disable linked filters filtering mode
374 * @type {Boolean}
375 */
376 this.linkedFilters = Boolean(f.linked_filters);
377
378 /**
379 * Enable/disable readonly state for excluded options when
380 * linked filters filtering mode is on
381 * @type {Boolean}
382 */
383 this.disableExcludedOptions = Boolean(f.disable_excluded_options);
384
385 /**
386 * Active filter ID
387 * @type {String}
388 * @private
389 */
390 this.activeFilterId = null;
391
392 /**
393 * Determine if there are excluded rows from filtering
394 * @type {Boolean}
395 * @private
396 */
397 this.hasExcludedRows = Boolean(isArray(f.exclude_rows) &&
398 f.exclude_rows.length > 0);
399
400 /**
401 * List of row indexes to be excluded from filtering
402 * @type {Array}
403 */
404 this.excludeRows = defaultsArr(f.exclude_rows, []);
405
406 /**
407 * List of containers IDs where external filters will be generated
408 * @type {Array}
409 */
410 this.externalFltIds = defaultsArr(f.external_flt_ids, []);
411
412 /**
413 * Callback fired after filters are generated
414 * @type {Function}
415 */
416 this.onFiltersLoaded = defaultsFn(f.on_filters_loaded, EMPTY_FN);
417
418 /**
419 * Enable/disable single filter mode
420 * @type {Boolean|Object}
421 */
422 this.singleFlt = isObj(f.single_filter) || Boolean(f.single_filter);
423
424 /**
425 * Specify columns to be excluded from single filter search, by default
426 * searching in all columns:
427 * single_filter: {
428 * exclude_cols: [2, 7]
429 * }
430 */
431 this.singleFltExcludeCols = isObj(f.single_filter) &&
432 isArray(f.single_filter.exclude_cols) ?
433 f.single_filter.exclude_cols : [];
434
435 /**
436 * Callback fired after a row is validated during filtering
437 * @type {Function}
438 */
439 this.onRowValidated = defaultsFn(f.on_row_validated, EMPTY_FN);
440
441 /**
442 * Specify which column implements a custom cell parser to retrieve the
443 * cell value:
444 * cell_parser: {
445 * cols: [0, 2],
446 * parse: function(tf, cell, colIndex) {
447 * // custom cell parser logic here
448 * return cellValue;
449 * }
450 * }
451 * @type {Object}
452 */
453 this.cellParser = isObj(f.cell_parser) && isFn(f.cell_parser.parse) &&
454 isArray(f.cell_parser.cols) ?
455 f.cell_parser : { cols: [], parse: EMPTY_FN };
456
457 /**
458 * Global watermark text for input filter type or watermark for each
459 * filter if an array is supplied
460 * @type {String|Array}
461 */
462 this.watermark = f.watermark || '';
463
464 /**
465 * Indicate whether watermark is on a per column basis
466 * @type {Boolean}
467 * @private
468 */
469 this.isWatermarkArray = isArray(this.watermark);
470
471 /**
472 * Indicate whether help UI component is disabled
473 * @type {Boolean}
474 */
475 this.help = isUndef(f.help_instructions) ? undefined :
476 (isObj(f.help_instructions) || Boolean(f.help_instructions));
477
478 /**
479 * Indicate whether pop-up filters UI is enabled
480 * @type {Boolean|Object}
481 */
482 this.popupFilters = isObj(f.popup_filters) || Boolean(f.popup_filters);
483
484 /**
485 * Indicate whether filtered (active) columns indicator is enabled
486 * @type {Boolean}
487 */
488 this.markActiveColumns = isObj(f.mark_active_columns) ||
489 Boolean(f.mark_active_columns);
490
491 /*** select filter's customisation and behaviours ***/
492 /**
493 * Text for clear option in drop-down filter types (1st option)
494 * @type {String|Array}
495 */
496 this.clearFilterText = isArray(f.clear_filter_text)
497 ? f.clear_filter_text
498 : defaultsStr(f.clear_filter_text, 'Clear');
499
500 /**
501 * Indicate whether empty option is enabled in drop-down filter types
502 * @type {Boolean}
503 */
504 this.enableEmptyOption = Boolean(f.enable_empty_option);
505
506 /**
507 * Text for empty option in drop-down filter types
508 * @type {String}
509 */
510 this.emptyText = defaultsStr(f.empty_text, '(Empty)');
511
512 /**
513 * Indicate whether non-empty option is enabled in drop-down filter
514 * types
515 * @type {Boolean}
516 */
517 this.enableNonEmptyOption = Boolean(f.enable_non_empty_option);
518
519 /**
520 * Text for non-empty option in drop-down filter types
521 * @type {String}
522 */
523 this.nonEmptyText = defaultsStr(f.non_empty_text, '(Non empty)');
524
525 /**
526 * Indicate whether drop-down filter types filter the table by default
527 * on change event
528 * @type {Boolean}
529 */
530 this.onSlcChange = defaultsBool(f.on_change, true);
531
532 /**
533 * Make drop-down filter types options sorted in alpha-numeric manner
534 * by default globally or on a column basis
535 * @type {Boolean|Array}
536 */
537 this.sortSlc = isUndef(f.sort_select)
538 ? true
539 : defaultsArr(f.sort_select, Boolean(f.sort_select));
540
541 /**
542 * List of columns implementing filter options sorting in ascending
543 * manner based on column data type
544 * @type {Array}
545 */
546 this.sortFilterOptionsAsc = defaultsArr(f.sort_filter_options_asc, []);
547
548 /**
549 * List of columns implementing filter options sorting in descending
550 * manner based on column data type
551 * @type {Array}
552 */
553 this.sortFilterOptionsDesc =
554 defaultsArr(f.sort_filter_options_desc, []);
555
556 /**
557 * Indicate whether drop-down filter types are populated on demand at
558 * first usage
559 * @type {Boolean}
560 */
561 this.loadFltOnDemand = Boolean(f.load_filters_on_demand);
562
563 /**
564 * Indicate whether custom drop-down filter options are implemented
565 * @type {Boolean}
566 */
567 this.hasCustomOptions = isObj(f.custom_options);
568
569 /**
570 * Custom options definition of a per column basis, ie:
571 * custom_options: {
572 * cols:[0, 1],
573 * texts: [
574 * ['a0', 'b0', 'c0'],
575 * ['a1', 'b1', 'c1']
576 * ],
577 * values: [
578 * ['a0', 'b0', 'c0'],
579 * ['a1', 'b1', 'c1']
580 * ],
581 * sorts: [false, true]
582 * }
583 *
584 * @type {Object}
585 */
586 this.customOptions = f.custom_options;
587
588 /*** Filter operators ***/
589 /**
590 * Regular expression operator for input filter. Defaults to 'rgx:'
591 * @type {String}
592 */
593 this.rgxOperator = defaultsStr(f.regexp_operator, 'rgx:');
594
595 /**
596 * Empty cells operator for input filter. Defaults to '[empty]'
597 * @type {String}
598 */
599 this.emOperator = defaultsStr(f.empty_operator, '[empty]');
600
601 /**
602 * Non-empty cells operator for input filter. Defaults to '[nonempty]'
603 * @type {String}
604 */
605 this.nmOperator = defaultsStr(f.nonempty_operator, '[nonempty]');
606
607 /**
608 * Logical OR operator for input filter. Defaults to '||'
609 * @type {String}
610 */
611 this.orOperator = defaultsStr(f.or_operator, '||');
612
613 /**
614 * Logical AND operator for input filter. Defaults to '&&'
615 * @type {String}
616 */
617 this.anOperator = defaultsStr(f.and_operator, '&&');
618
619 /**
620 * Greater than operator for input filter. Defaults to '>'
621 * @type {String}
622 */
623 this.grOperator = defaultsStr(f.greater_operator, '>');
624
625 /**
626 * Lower than operator for input filter. Defaults to '<'
627 * @type {String}
628 */
629 this.lwOperator = defaultsStr(f.lower_operator, '<');
630
631 /**
632 * Lower than or equal operator for input filter. Defaults to '<='
633 * @type {String}
634 */
635 this.leOperator = defaultsStr(f.lower_equal_operator, '<=');
636
637 /**
638 * Greater than or equal operator for input filter. Defaults to '>='
639 * @type {String}
640 */
641 this.geOperator = defaultsStr(f.greater_equal_operator, '>=');
642
643 /**
644 * Inequality operator for input filter. Defaults to '!'
645 * @type {String}
646 */
647 this.dfOperator = defaultsStr(f.different_operator, '!');
648
649 /**
650 * Like operator for input filter. Defaults to '*'
651 * @type {String}
652 */
653 this.lkOperator = defaultsStr(f.like_operator, '*');
654
655 /**
656 * Strict equality operator for input filter. Defaults to '='
657 * @type {String}
658 */
659 this.eqOperator = defaultsStr(f.equal_operator, '=');
660
661 /**
662 * Starts with operator for input filter. Defaults to '='
663 * @type {String}
664 */
665 this.stOperator = defaultsStr(f.start_with_operator, '{');
666
667 /**
668 * Ends with operator for input filter. Defaults to '='
669 * @type {String}
670 */
671 this.enOperator = defaultsStr(f.end_with_operator, '}');
672
673 // this.curExp = f.cur_exp || '^[¥£€$]';
674
675 /**
676 * Stored values separator
677 * @type {String}
678 */
679 this.separator = defaultsStr(f.separator, ',');
680
681 /**
682 * Enable rows counter UI component
683 * @type {Boolean|Object}
684 */
685 this.rowsCounter = isObj(f.rows_counter) || Boolean(f.rows_counter);
686
687 /**
688 * Enable status bar UI component
689 * @type {Boolean|Object}
690 */
691 this.statusBar = isObj(f.status_bar) || Boolean(f.status_bar);
692
693 /**
694 * Enable activity/spinner indicator UI component
695 * @type {Boolean|Object}
696 */
697 this.loader = isObj(f.loader) || Boolean(f.loader);
698
699 /*** validation - reset buttons/links ***/
700 /**
701 * Enable filters submission button
702 * @type {Boolean}
703 */
704 this.displayBtn = Boolean(f.btn);
705
706 /**
707 * Define filters submission button text
708 * @type {String}
709 */
710 this.btnText = defaultsStr(f.btn_text, (!this.enableIcons ? 'Go' : ''));
711
712 /**
713 * Css class for filters submission button
714 * @type {String}
715 */
716 this.btnCssClass = defaultsStr(f.btn_css_class,
717 (!this.enableIcons ? 'btnflt' : 'btnflt_icon'));
718
719 /**
720 * Enable clear button
721 * @type {Object|Boolean}
722 */
723 this.btnReset = isObj(f.btn_reset) || Boolean(f.btn_reset);
724
725 /**
726 * Callback fired before filters are cleared
727 * @type {Function}
728 */
729 this.onBeforeReset = defaultsFn(f.on_before_reset, EMPTY_FN);
730
731 /**
732 * Callback fired after filters are cleared
733 * @type {Function}
734 */
735 this.onAfterReset = defaultsFn(f.on_after_reset, EMPTY_FN);
736
737 /**
738 * Enable paging component
739 * @type {Object|Boolean}
740 */
741 this.paging = isObj(f.paging) || Boolean(f.paging);
742
743 /**
744 * Number of hidden rows
745 * @type {Number}
746 * @private
747 */
748 this.nbHiddenRows = 0;
749
750 /**
751 * Enable auto-filter behaviour, table is filtered when a user
752 * stops typing
753 * @type {Object|Boolean}
754 */
755 this.autoFilter = isObj(f.auto_filter) || Boolean(f.auto_filter);
756
757 /**
758 * Auto-filter delay in milliseconds
759 * @type {Number}
760 */
761 this.autoFilterDelay = isObj(f.auto_filter) &&
762 isNumber(f.auto_filter.delay) ?
763 f.auto_filter.delay : AUTO_FILTER_DELAY;
764
765 /**
766 * Indicate whether user is typing
767 * @type {Boolean}
768 * @private
769 */
770 this.isUserTyping = null;
771
772 /**
773 * Auto-filter interval ID
774 * @type {String}
775 * @private
776 */
777 this.autoFilterTimer = null;
778
779 /**
780 * Enable keyword highlighting behaviour
781 * @type {Boolean}
782 */
783 this.highlightKeywords = Boolean(f.highlight_keywords);
784
785 /**
786 * Enable no results message UI component
787 * @type {Object|Boolean}
788 */
789 this.noResults = isObj(f.no_results_message) ||
790 Boolean(f.no_results_message);
791
792 /**
793 * Enable state persistence
794 * @type {Object|Boolean}
795 */
796 this.state = isObj(f.state) || Boolean(f.state);
797
798 /*** data types ***/
799
800 /**
801 * Enable date type module
802 * @type {Boolean}
803 * @private
804 */
805 this.dateType = true;
806
807 /**
808 * Define default locale, default to 'en' as per Sugar Date module:
809 * https://sugarjs.com/docs/#/DateLocales
810 * @type {String}
811 */
812 this.locale = defaultsStr(f.locale, 'en');
813
814 /**
815 * Define thousands separator ',' or '.', defaults to ','
816 * @type {String}
817 */
818 this.thousandsSeparator = defaultsStr(f.thousands_separator, ',');
819
820 /**
821 * Define decimal separator ',' or '.', defaults to '.'
822 * @type {String}
823 */
824 this.decimalSeparator = defaultsStr(f.decimal_separator, '.');
825
826 /**
827 * Define data types on a column basis, possible values 'string',
828 * 'number', 'formatted-number', 'date', 'ipaddress' ie:
829 * col_types : [
830 * 'string', 'date', 'number',
831 * { type: 'formatted-number', decimal: ',', thousands: '.' },
832 * { type: 'date', locale: 'en-gb' },
833 * { type: 'date', format: ['{dd}-{months}-{yyyy|yy}'] }
834 * ]
835 *
836 * Refer to https://sugarjs.com/docs/#/DateParsing for exhaustive
837 * information on date parsing formats supported by Sugar Date
838 * @type {Array}
839 */
840 this.colTypes = isArray(f.col_types) ? f.col_types : [];
841
842 /*** ids prefixes ***/
843 /**
844 * Main prefix
845 * @private
846 */
847 this.prfxTf = 'TF';
848
849 /**
850 * Filter's ID prefix (inputs - selects)
851 * @private
852 */
853 this.prfxFlt = 'flt';
854
855 /**
856 * Button's ID prefix
857 * @private
858 */
859 this.prfxValButton = 'btn';
860
861 /**
862 * Responsive Css class
863 * @private
864 */
865 this.prfxResponsive = 'resp';
866
867 /** @private */
868 this.stickyCssClass = 'sticky';
869
870 /*** extensions ***/
871 /**
872 * List of loaded extensions
873 * @type {Array}
874 */
875 this.extensions = defaultsArr(f.extensions, []);
876
877 /*** themes ***/
878 /**
879 * Enable default theme
880 * @type {Boolean}
881 */
882 this.enableDefaultTheme = Boolean(f.enable_default_theme);
883
884 /**
885 * Determine whether themes are enables
886 * @type {Boolean}
887 * @private
888 */
889 this.hasThemes = (this.enableDefaultTheme || isArray(f.themes));
890
891 /**
892 * List of themes, ie:
893 * themes: [{ name: 'skyblue' }]
894 * @type {Array}
895 */
896 this.themes = defaultsArr(f.themes, []);
897
898 /**
899 * Define path to themes assets, defaults to
900 * 'tablefilter/style/themes/'. Usage:
901 * themes: [{ name: 'skyblue' }]
902 * @type {Array}
903 */
904 this.themesPath = this.getThemesPath();
905
906 /**
907 * Enable responsive layout
908 * @type {Boolean}
909 */
910 this.responsive = Boolean(f.responsive);
911
912 /**
913 * Enable toolbar component
914 * @type {Object|Boolean}
915 */
916 this.toolbar = isObj(f.toolbar) || Boolean(f.toolbar);
917
918 /**
919 * Enable sticky headers
920 * @type {Boolean}
921 */
922 this.stickyHeaders = Boolean(f.sticky_headers);
923
924 /**
925 * Features registry
926 * @private
927 */
928 this.Mod = {};
929
930 /**
931 * Extensions registry
932 * @private
933 */
934 this.ExtRegistry = {};
935
936 // instantiate features if needed
937 this.instantiateFeatures(FEATURES);
938 }
939
940 /**
941 * Initialise features and layout
942 */
943 init() {
944 if (this.initialized) {
945 return;
946 }
947
948 // import main stylesheet
949 this.import(this.stylesheetId, this.getStylesheetPath(), null, 'link');
950
951 let Mod = this.Mod;
952 let inpclass;
953
954 //loads theme
955 this.loadThemes();
956
957 //explicitly initialise features in given order
958 this.initFeatures([
959 DateType,
960 Help,
961 State,
962 MarkActiveColumns,
963 GridLayout,
964 Loader,
965 HighlightKeyword,
966 PopupFilter
967 ]);
968
969 //filters grid is not generated
970 if (!this.fltGrid) {
971 this._initNoFilters();
972 } else {
973 let fltrow = this._insertFiltersRow();
974
975 this.nbCells = this.getCellsNb(this.refRow);
976 this.nbFilterableRows = this.getRowsNb();
977
978 let n = this.singleFlt ? 1 : this.nbCells;
979
980 //build filters
981 for (let i = 0; i < n; i++) {
982 this.emitter.emit('before-filter-init', this, i);
983
984 let fltCell = createElm(this.fltCellTag),
985 col = this.getFilterType(i);
986
987 if (this.singleFlt) {
988 fltCell.colSpan = this.nbCells;
989 }
990 if (!this.gridLayout) {
991 fltrow.appendChild(fltCell);
992 }
993 inpclass = (i === n - 1 && this.displayBtn) ?
994 this.fltSmallCssClass : this.fltCssClass;
995
996 //only 1 input for single search
997 if (this.singleFlt) {
998 col = INPUT;
999 inpclass = this.singleFltCssClass;
1000 }
1001
1002 //drop-down filters
1003 if (col === SELECT || col === MULTIPLE) {
1004 Mod.dropdown = Mod.dropdown || new Dropdown(this);
1005 Mod.dropdown.init(i, this.isExternalFlt(), fltCell);
1006 }
1007 // checklist
1008 else if (col === CHECKLIST) {
1009 Mod.checkList = Mod.checkList || new CheckList(this);
1010 Mod.checkList.init(i, this.isExternalFlt(), fltCell);
1011 } else {
1012 this._buildInputFilter(i, inpclass, fltCell);
1013 }
1014
1015 // this adds submit button
1016 if (i === n - 1 && this.displayBtn) {
1017 this._buildSubmitButton(
1018 this.isExternalFlt() ?
1019 elm(this.externalFltIds[i]) :
1020 fltCell
1021 );
1022 }
1023
1024 this.emitter.emit('after-filter-init', this, i);
1025 }
1026
1027 this.emitter.on(['filter-focus'],
1028 (tf, filter) => this.setActiveFilterId(filter.id));
1029
1030 }//if this.fltGrid
1031
1032 /* Features */
1033 if (this.hasExcludedRows) {
1034 this.emitter.on(['after-filtering'], () => this.setExcludeRows());
1035 this.setExcludeRows();
1036 }
1037
1038 this.initFeatures([
1039 RowsCounter,
1040 StatusBar,
1041 ClearButton,
1042 AlternateRows,
1043 NoResults,
1044 Paging,
1045 Toolbar
1046 ]);
1047
1048 this.setColWidths();
1049
1050 //TF css class is added to table
1051 if (!this.gridLayout) {
1052 addClass(this.dom(), this.prfxTf);
1053 if (this.responsive) {
1054 addClass(this.dom(), this.prfxResponsive);
1055 }
1056 if (this.colWidths.length > 0) {
1057 this.setFixedLayout();
1058 }
1059 if (this.stickyHeaders && this.dom().tHead) {
1060 addClass(this.dom(), this.stickyCssClass);
1061 }
1062 }
1063
1064 /* Load extensions */
1065 this.initExtensions();
1066
1067 this.initialized = true;
1068
1069 this.onFiltersLoaded(this);
1070
1071 this.emitter.emit('initialized', this);
1072 }
1073
1074 /**
1075 * Detect <enter> key
1076 * @param {Event} evt
1077 */
1078 detectKey(evt) {
1079 if (!this.enterKey) {
1080 return;
1081 }
1082
1083 if (isKeyPressed(evt, [ENTER_KEY])) {
1084 this.filter();
1085 cancelEvt(evt);
1086 stopEvt(evt);
1087 } else {
1088 this.isUserTyping = true;
1089 root.clearInterval(this.autoFilterTimer);
1090 this.autoFilterTimer = null;
1091 }
1092 }
1093
1094 /**
1095 * Filter's keyup event: if auto-filter on, detect user is typing and filter
1096 * columns
1097 * @param {Event} evt
1098 */
1099 onKeyUp(evt) {
1100 if (!this.autoFilter) {
1101 return;
1102 }
1103 this.isUserTyping = false;
1104
1105 function filter() {
1106 root.clearInterval(this.autoFilterTimer);
1107 this.autoFilterTimer = null;
1108 if (!this.isUserTyping) {
1109 this.filter();
1110 this.isUserTyping = null;
1111 }
1112 }
1113
1114 if (isKeyPressed(evt,
1115 [ENTER_KEY, TAB_KEY, ESC_KEY, UP_ARROW_KEY, DOWN_ARROW_KEY])) {
1116 root.clearInterval(this.autoFilterTimer);
1117 this.autoFilterTimer = null;
1118 } else {
1119 if (this.autoFilterTimer !== null) {
1120 return;
1121 }
1122 this.autoFilterTimer = root.setInterval(
1123 filter.bind(this),
1124 this.autoFilterDelay);
1125 }
1126 }
1127
1128 /**
1129 * Filter's keydown event: if auto-filter on, detect user is typing
1130 */
1131 onKeyDown() {
1132 if (this.autoFilter) {
1133 this.isUserTyping = true;
1134 }
1135 }
1136
1137 /**
1138 * Filter's focus event
1139 * @param {Event} evt
1140 */
1141 onInpFocus(evt) {
1142 let elm = targetEvt(evt);
1143 this.emitter.emit('filter-focus', this, elm);
1144 }
1145
1146 /**
1147 * Filter's blur event: if auto-filter on, clear interval on filter blur
1148 */
1149 onInpBlur() {
1150 if (this.autoFilter) {
1151 this.isUserTyping = false;
1152 root.clearInterval(this.autoFilterTimer);
1153 }
1154 this.emitter.emit('filter-blur', this);
1155 }
1156
1157 /**
1158 * Insert filters row at initialization
1159 */
1160 _insertFiltersRow() {
1161 // TODO: prevent filters row generation for popup filters too,
1162 // to reduce and simplify headers row index adjusting across lib modules
1163 // (GridLayout, PopupFilter etc)
1164 if (this.gridLayout) {
1165 return;
1166 }
1167 let fltrow;
1168
1169 let thead = tag(this.dom(), 'thead');
1170 if (thead.length > 0) {
1171 fltrow = thead[0].insertRow(this.filtersRowIndex);
1172 } else {
1173 fltrow = this.dom().insertRow(this.filtersRowIndex);
1174 }
1175
1176 fltrow.className = this.fltsRowCssClass;
1177
1178 if (this.isExternalFlt()) {
1179 fltrow.style.display = NONE;
1180 }
1181
1182 this.emitter.emit('filters-row-inserted', this, fltrow);
1183 return fltrow;
1184 }
1185
1186 /**
1187 * Initialize filtersless table
1188 */
1189 _initNoFilters() {
1190 if (this.fltGrid) {
1191 return;
1192 }
1193 this.refRow = this.refRow > 0 ? this.refRow - 1 : 0;
1194 this.nbFilterableRows = this.getRowsNb();
1195 }
1196
1197 /**
1198 * Build input filter type
1199 * @param {Number} colIndex Column index
1200 * @param {String} cssClass Css class applied to filter
1201 * @param {DOMElement} container Container DOM element
1202 */
1203 _buildInputFilter(colIndex, cssClass, container) {
1204 let col = this.getFilterType(colIndex);
1205 let externalFltTgtId = this.isExternalFlt() ?
1206 this.externalFltIds[colIndex] : null;
1207 let inpType = col === INPUT ? 'text' : 'hidden';
1208 let inp = createElm(INPUT,
1209 ['id', this.buildFilterId(colIndex)],
1210 ['type', inpType], ['ct', colIndex]);
1211
1212 if (inpType !== 'hidden' && this.watermark) {
1213 inp.setAttribute('placeholder',
1214 this.isWatermarkArray ? (this.watermark[colIndex] || '') :
1215 this.watermark
1216 );
1217 }
1218 inp.className = cssClass || this.fltCssClass;
1219 addEvt(inp, 'focus', (evt) => this.onInpFocus(evt));
1220
1221 //filter is appended in custom element
1222 if (externalFltTgtId) {
1223 elm(externalFltTgtId).appendChild(inp);
1224 } else {
1225 container.appendChild(inp);
1226 }
1227
1228 this.fltIds.push(inp.id);
1229
1230 addEvt(inp, 'keypress', (evt) => this.detectKey(evt));
1231 addEvt(inp, 'keydown', () => this.onKeyDown());
1232 addEvt(inp, 'keyup', (evt) => this.onKeyUp(evt));
1233 addEvt(inp, 'blur', () => this.onInpBlur());
1234 }
1235
1236 /**
1237 * Build submit button
1238 * @param {DOMElement} container Container DOM element
1239 */
1240 _buildSubmitButton(container) {
1241 let btn = createElm(INPUT,
1242 ['type', 'button'],
1243 ['value', this.btnText]
1244 );
1245 btn.className = this.btnCssClass;
1246
1247 //filter is appended in container element
1248 container.appendChild(btn);
1249
1250 addEvt(btn, 'click', () => this.filter());
1251 }
1252
1253 /**
1254 * Conditionally istantiate each feature class in passed collection if
1255 * required by configuration and add it to the features registry. A feature
1256 * class meta information contains a `name` field and optional `altName` and
1257 * `alwaysInstantiate` fields
1258 * @param {Array} [features=[]]
1259 * @private
1260 */
1261 instantiateFeatures(features = []) {
1262 features.forEach(featureCls => {
1263 let Cls = featureCls;
1264
1265 // assign meta info if not present
1266 Cls.meta = Cls.meta || {name: null, altName: null};
1267 Cls.meta.name = toCamelCase(Cls.name);
1268 let {name, altName, alwaysInstantiate} = Cls.meta;
1269 let prop = altName || name;
1270
1271 if (!this.hasConfig || this[prop] === true
1272 || Boolean(alwaysInstantiate)) {
1273 this.Mod[name] = this.Mod[name] || new Cls(this);
1274 }
1275 });
1276 }
1277
1278 /**
1279 * Initialise each feature class in passed collection.
1280 * @param {Array} [features=[]]
1281 * @private
1282 */
1283 initFeatures(features = []) {
1284 features.forEach(featureCls => {
1285 let {name, altName} = featureCls.meta;
1286 let prop = altName || name;
1287
1288 if (this[prop] === true && this.Mod[name]) {
1289 this.Mod[name].init();
1290 }
1291 });
1292 }
1293
1294 /**
1295 * Return a feature instance for a given name
1296 * @param {String} name Name of the feature
1297 * @return {Object}
1298 */
1299 feature(name) {
1300 return this.Mod[name];
1301 }
1302
1303 /**
1304 * Initialise all the extensions defined in the configuration object
1305 */
1306 initExtensions() {
1307 let exts = this.extensions;
1308 if (exts.length === 0) {
1309 return;
1310 }
1311
1312 // Set config's publicPath dynamically for Webpack...
1313 __webpack_public_path__ = this.basePath;
1314
1315 this.emitter.emit('before-loading-extensions', this);
1316
1317 exts.forEach((ext) => {
1318 this.loadExtension(ext);
1319 });
1320 this.emitter.emit('after-loading-extensions', this);
1321 }
1322
1323 /**
1324 * Load an extension module
1325 * @param {Object} ext Extension config object
1326 */
1327 loadExtension(ext) {
1328 if (!ext || !ext.name || this.hasExtension(ext.name)) {
1329 return;
1330 }
1331
1332 let {name, path} = ext;
1333 let modulePath;
1334
1335 if (name && path) {
1336 modulePath = ext.path + name;
1337 } else {
1338 name = name.replace('.js', '');
1339 modulePath = 'extensions/{}/{}'.replace(/{}/g, name);
1340 }
1341
1342 // Require pattern for Webpack
1343 require(['./' + modulePath], (mod) => {
1344 /* eslint-disable */
1345 let inst = new mod.default(this, ext);
1346 /* eslint-enable */
1347 inst.init();
1348 this.ExtRegistry[name] = inst;
1349 });
1350 }
1351
1352 /**
1353 * Get an extension instance
1354 * @param {String} name Name of the extension
1355 * @return {Object} Extension instance
1356 */
1357 extension(name) {
1358 return this.ExtRegistry[name];
1359 }
1360
1361 /**
1362 * Check passed extension name exists
1363 * @param {String} name Name of the extension
1364 * @return {Boolean}
1365 */
1366 hasExtension(name) {
1367 return !isEmpty(this.ExtRegistry[name]);
1368 }
1369
1370 /**
1371 * Register the passed extension instance with associated name
1372 * @param {Object} inst Extension instance
1373 * @param {String} name Name of the extension
1374 */
1375 registerExtension(inst, name) {
1376 this.ExtRegistry[name] = inst;
1377 }
1378
1379 /**
1380 * Destroy all the extensions store in extensions registry
1381 */
1382 destroyExtensions() {
1383 let reg = this.ExtRegistry;
1384
1385 Object.keys(reg).forEach((key) => {
1386 reg[key].destroy();
1387 reg[key] = undefined;
1388 });
1389 }
1390
1391 /**
1392 * Load themes defined in the configuration object
1393 */
1394 loadThemes() {
1395 if (!this.hasThemes) {
1396 return;
1397 }
1398
1399 let themes = this.themes;
1400 this.emitter.emit('before-loading-themes', this);
1401
1402 //Default theme config
1403 if (this.enableDefaultTheme) {
1404 let defaultTheme = { name: 'default' };
1405 this.themes.push(defaultTheme);
1406 }
1407
1408 themes.forEach((theme, i) => {
1409 let {name, path} = theme;
1410 let styleId = this.prfxTf + name;
1411 if (name && !path) {
1412 path = this.themesPath + name + '/' + name + '.css';
1413 }
1414 else if (!name && theme.path) {
1415 name = 'theme{0}'.replace('{0}', i);
1416 }
1417
1418 if (!this.isImported(path, 'link')) {
1419 this.import(styleId, path, null, 'link');
1420 }
1421 });
1422
1423 // Enable loader indicator
1424 this.loader = true;
1425
1426 this.emitter.emit('after-loading-themes', this);
1427 }
1428
1429 /**
1430 * Return stylesheet DOM element for a given theme name
1431 * @return {DOMElement} stylesheet element
1432 */
1433 getStylesheet(name = 'default') {
1434 return elm(this.prfxTf + name);
1435 }
1436
1437 /**
1438 * Destroy filter grid
1439 */
1440 destroy() {
1441 if (!this.initialized) {
1442 return;
1443 }
1444
1445 let emitter = this.emitter;
1446
1447 if (this.isExternalFlt() && !this.popupFilters) {
1448 this.removeExternalFlts();
1449 }
1450
1451 this.destroyExtensions();
1452
1453 this.validateAllRows();
1454
1455 // broadcast destroy event modules and extensions are subscribed to
1456 emitter.emit('destroy', this);
1457
1458 if (this.fltGrid && !this.gridLayout) {
1459 this.dom().deleteRow(this.filtersRowIndex);
1460 }
1461
1462 // unsubscribe to events
1463 if (this.hasExcludedRows) {
1464 emitter.off(['after-filtering'], () => this.setExcludeRows());
1465 }
1466
1467 this.emitter.off(['filter-focus'],
1468 (tf, filter) => this.setActiveFilterId(filter.id));
1469
1470 removeClass(this.dom(), this.prfxTf);
1471 removeClass(this.dom(), this.prfxResponsive);
1472 if (this.dom().tHead) {
1473 removeClass(this.dom().tHead, this.stickyCssClass);
1474 }
1475
1476 this.nbHiddenRows = 0;
1477 this.validRowsIndex = [];
1478 this.fltIds = [];
1479 this.initialized = false;
1480 }
1481
1482 /**
1483 * Remove all the external column filters
1484 */
1485 removeExternalFlts() {
1486 if (!this.isExternalFlt()) {
1487 return;
1488 }
1489 let ids = this.externalFltIds;
1490 ids.forEach((id) => {
1491 let externalFlt = elm(id);
1492 if (externalFlt) {
1493 externalFlt.innerHTML = '';
1494 }
1495 });
1496 }
1497
1498 /**
1499 * Check if given column implements a filter with custom options
1500 * @param {Number} colIndex Column's index
1501 * @return {Boolean}
1502 */
1503 isCustomOptions(colIndex) {
1504 return this.hasCustomOptions &&
1505 this.customOptions.cols.indexOf(colIndex) !== -1;
1506 }
1507
1508 /**
1509 * Returns an array [[value0, value1 ...],[text0, text1 ...]] with the
1510 * custom options values and texts
1511 * @param {Number} colIndex Column's index
1512 * @return {Array}
1513 */
1514 getCustomOptions(colIndex) {
1515 if (isEmpty(colIndex) || !this.isCustomOptions(colIndex)) {
1516 return;
1517 }
1518
1519 let customOptions = this.customOptions;
1520 let cols = customOptions.cols;
1521 let optTxt = [], optArray = [];
1522 let index = cols.indexOf(colIndex);
1523 let slcValues = customOptions.values[index];
1524 let slcTexts = customOptions.texts[index];
1525 let slcSort = customOptions.sorts[index];
1526
1527 for (let r = 0, len = slcValues.length; r < len; r++) {
1528 optArray.push(slcValues[r]);
1529 if (slcTexts[r]) {
1530 optTxt.push(slcTexts[r]);
1531 } else {
1532 optTxt.push(slcValues[r]);
1533 }
1534 }
1535 if (slcSort) {
1536 optArray.sort();
1537 optTxt.sort();
1538 }
1539 return [optArray, optTxt];
1540 }
1541
1542 /**
1543 * Filter the table by retrieving the data from each cell in every single
1544 * row and comparing it to the search term for current column. A row is
1545 * hidden when all the search terms are not found in inspected row.
1546 */
1547 filter() {
1548 if (!this.fltGrid || !this.initialized) {
1549 return;
1550 }
1551
1552 let emitter = this.emitter;
1553
1554 //fire onbefore callback
1555 this.onBeforeFilter(this);
1556 emitter.emit('before-filtering', this);
1557
1558 let hiddenRows = 0;
1559
1560 this.validRowsIndex = [];
1561 // search args
1562 let searchArgs = this.getFiltersValue();
1563
1564 let eachRow = this.eachRow();
1565 eachRow(
1566 (row, k) => {
1567 // already filtered rows display re-init
1568 row.style.display = '';
1569
1570 let cells = row.cells;
1571 let nbCells = cells.length;
1572
1573 let occurence = [],
1574 isMatch = true,
1575 //only for single filter search
1576 isSingleFltMatch = false;
1577
1578 // this loop retrieves cell data
1579 for (let j = 0; j < nbCells; j++) {
1580 //searched keyword
1581 let sA = searchArgs[this.singleFlt ? 0 : j];
1582
1583 if (sA === '') {
1584 continue;
1585 }
1586
1587 let cellValue = matchCase(this.getCellValue(cells[j]),
1588 this.caseSensitive);
1589
1590 //multiple search parameter operator ||
1591 let sAOrSplit = sA.toString().split(this.orOperator),
1592 //multiple search || parameter boolean
1593 hasMultiOrSA = sAOrSplit.length > 1,
1594 //multiple search parameter operator &&
1595 sAAndSplit = sA.toString().split(this.anOperator),
1596 //multiple search && parameter boolean
1597 hasMultiAndSA = sAAndSplit.length > 1;
1598
1599 //detect operators or array query
1600 if (isArray(sA) || hasMultiOrSA || hasMultiAndSA) {
1601 let cS, s;
1602 let found = false;
1603
1604 if (isArray(sA)) {
1605 s = sA;
1606 } else {
1607 s = hasMultiOrSA ? sAOrSplit : sAAndSplit;
1608 }
1609 // isolate search term and check occurence in cell data
1610 for (let w = 0, len = s.length; w < len; w++) {
1611 cS = trim(s[w]);
1612 found = this._match(cS, cellValue, cells[j]);
1613
1614 if (found) {
1615 emitter.emit('highlight-keyword', this,
1616 cells[j], cS);
1617 }
1618 if ((hasMultiOrSA && found) ||
1619 (hasMultiAndSA && !found)) {
1620 break;
1621 }
1622 if (isArray(sA) && found) {
1623 break;
1624 }
1625 }
1626 occurence[j] = found;
1627
1628 }
1629 //single search parameter
1630 else {
1631 occurence[j] =
1632 this._match(trim(sA), cellValue, cells[j]);
1633 if (occurence[j]) {
1634 emitter.emit('highlight-keyword', this, cells[j],
1635 sA);
1636 }
1637 }
1638
1639 if (!occurence[j]) {
1640 isMatch = false;
1641 }
1642
1643 if (this.singleFlt &&
1644 this.singleFltExcludeCols.indexOf(j) === -1 &&
1645 occurence[j]) {
1646 isSingleFltMatch = true;
1647 }
1648
1649 emitter.emit('cell-processed', this, j, cells[j]);
1650 }//for j
1651
1652 if (isSingleFltMatch) {
1653 isMatch = true;
1654 }
1655
1656 this.validateRow(k, isMatch);
1657 if (!isMatch) {
1658 hiddenRows++;
1659 }
1660
1661 emitter.emit('row-processed', this, k,
1662 this.validRowsIndex.length - 1, isMatch);
1663 },
1664 // continue condition
1665 (row) => row.cells.length !== this.nbCells
1666 );
1667
1668 this.nbHiddenRows = hiddenRows;
1669
1670 //fire onafterfilter callback
1671 this.onAfterFilter(this);
1672
1673 emitter.emit('after-filtering', this, searchArgs);
1674 }
1675
1676 /**
1677 * Match search term in cell data
1678 * @param {String} term Search term
1679 * @param {String} cellValue Cell data
1680 * @param {DOMElement} cell Current cell
1681 * @return {Boolean}
1682 * @private
1683 */
1684 _match(term, cellValue, cell) {
1685 let numData;
1686 let colIdx = cell.cellIndex;
1687 let decimal = this.getDecimal(colIdx);
1688 let reLe = new RegExp(this.leOperator),
1689 reGe = new RegExp(this.geOperator),
1690 reL = new RegExp(this.lwOperator),
1691 reG = new RegExp(this.grOperator),
1692 reD = new RegExp(this.dfOperator),
1693 reLk = new RegExp(rgxEsc(this.lkOperator)),
1694 reEq = new RegExp(this.eqOperator),
1695 reSt = new RegExp(this.stOperator),
1696 reEn = new RegExp(this.enOperator),
1697 // re_an = new RegExp(this.anOperator),
1698 // re_cr = new RegExp(this.curExp),
1699 reEm = this.emOperator,
1700 reNm = this.nmOperator,
1701 reRe = new RegExp(rgxEsc(this.rgxOperator));
1702
1703 term = matchCase(term, this.caseSensitive);
1704
1705 let occurence = false;
1706
1707 //Search arg operator tests
1708 let hasLO = reL.test(term),
1709 hasLE = reLe.test(term),
1710 hasGR = reG.test(term),
1711 hasGE = reGe.test(term),
1712 hasDF = reD.test(term),
1713 hasEQ = reEq.test(term),
1714 hasLK = reLk.test(term),
1715 // hatermN = re_an.test(term),
1716 hasST = reSt.test(term),
1717 hasEN = reEn.test(term),
1718 hasEM = (reEm === term),
1719 hasNM = (reNm === term),
1720 hasRE = reRe.test(term);
1721
1722 // Check for dates or resolve date type
1723 if (this.hasType(colIdx, [DATE])) {
1724 let dte1, dte2;
1725
1726 let dateType = this.Mod.dateType;
1727 let isValidDate = dateType.isValid.bind(dateType);
1728 let parseDate = dateType.parse.bind(dateType);
1729 let locale = dateType.getLocale(colIdx);
1730
1731 // Search arg dates tests
1732 let isLDate = hasLO &&
1733 isValidDate(term.replace(reL, ''), locale);
1734 let isLEDate = hasLE &&
1735 isValidDate(term.replace(reLe, ''), locale);
1736 let isGDate = hasGR &&
1737 isValidDate(term.replace(reG, ''), locale);
1738 let isGEDate = hasGE &&
1739 isValidDate(term.replace(reGe, ''), locale);
1740 let isDFDate = hasDF &&
1741 isValidDate(term.replace(reD, ''), locale);
1742 let isEQDate = hasEQ &&
1743 isValidDate(term.replace(reEq, ''), locale);
1744
1745 dte1 = parseDate(cellValue, locale);
1746
1747 // lower equal date
1748 if (isLEDate) {
1749 dte2 = parseDate(term.replace(reLe, ''), locale);
1750 occurence = dte1 <= dte2;
1751 }
1752 // lower date
1753 else if (isLDate) {
1754 dte2 = parseDate(term.replace(reL, ''), locale);
1755 occurence = dte1 < dte2;
1756 }
1757 // greater equal date
1758 else if (isGEDate) {
1759 dte2 = parseDate(term.replace(reGe, ''), locale);
1760 occurence = dte1 >= dte2;
1761 }
1762 // greater date
1763 else if (isGDate) {
1764 dte2 = parseDate(term.replace(reG, ''), locale);
1765 occurence = dte1 > dte2;
1766 }
1767 // different date
1768 else if (isDFDate) {
1769 dte2 = parseDate(term.replace(reD, ''), locale);
1770 occurence = dte1.toString() !== dte2.toString();
1771 }
1772 // equal date
1773 else if (isEQDate) {
1774 dte2 = parseDate(term.replace(reEq, ''), locale);
1775 occurence = dte1.toString() === dte2.toString();
1776 }
1777 // searched keyword with * operator doesn't have to be a date
1778 else if (reLk.test(term)) {// like date
1779 occurence = contains(term.replace(reLk, ''), cellValue,
1780 false, this.caseSensitive);
1781 }
1782 else if (isValidDate(term)) {
1783 dte2 = parseDate(term, locale);
1784 occurence = dte1.toString() === dte2.toString();
1785 }
1786 //empty
1787 else if (hasEM) {
1788 occurence = !cell.hasChildNodes();
1789 }
1790 //non-empty
1791 else if (hasNM) {
1792 occurence = cell.hasChildNodes();
1793 } else {
1794 occurence = contains(term, cellValue,
1795 this.isExactMatch(colIdx), this.caseSensitive);
1796 }
1797 } else {
1798 // Convert to number anyways to auto-resolve type in case not
1799 // defined by configuration. Order is important first try to
1800 // parse formatted number then fallback to Number coercion
1801 // to avoid false positives with Number
1802 numData = parseNb(cellValue, decimal) || Number(cellValue);
1803
1804 // first checks if there is any operator (<,>,<=,>=,!,*,=,{,},
1805 // rgx:)
1806
1807 //regexp
1808 if (hasRE) {
1809 //in case regexp throws
1810 try {
1811 //operator is removed
1812 let srchArg = term.replace(reRe, '');
1813 let rgx = new RegExp(srchArg);
1814 occurence = rgx.test(cellValue);
1815 } catch (ex) {
1816 occurence = false;
1817 }
1818 }
1819 // lower equal
1820 else if (hasLE) {
1821 occurence = numData <= parseNb(
1822 term.replace(reLe, ''),
1823 decimal
1824 );
1825 }
1826 //greater equal
1827 else if (hasGE) {
1828 occurence = numData >= parseNb(
1829 term.replace(reGe, ''),
1830 decimal
1831 );
1832 }
1833 //lower
1834 else if (hasLO) {
1835 occurence = numData < parseNb(
1836 term.replace(reL, ''),
1837 decimal
1838 );
1839 }
1840 //greater
1841 else if (hasGR) {
1842 occurence = numData > parseNb(
1843 term.replace(reG, ''),
1844 decimal
1845 );
1846 }
1847 //different
1848 else if (hasDF) {
1849 occurence = contains(term.replace(reD, ''), cellValue,
1850 false, this.caseSensitive) ? false : true;
1851 }
1852 //like
1853 else if (hasLK) {
1854 occurence = contains(term.replace(reLk, ''), cellValue,
1855 false, this.caseSensitive);
1856 }
1857 //equal
1858 else if (hasEQ) {
1859 occurence = contains(term.replace(reEq, ''), cellValue,
1860 true, this.caseSensitive);
1861 }
1862 //starts with
1863 else if (hasST) {
1864 occurence = cellValue.indexOf(term.replace(reSt, '')) === 0 ?
1865 true : false;
1866 }
1867 //ends with
1868 else if (hasEN) {
1869 let searchArg = term.replace(reEn, '');
1870 occurence =
1871 cellValue.lastIndexOf(searchArg, cellValue.length - 1) ===
1872 (cellValue.length - 1) - (searchArg.length - 1) &&
1873 cellValue.lastIndexOf(searchArg, cellValue.length - 1)
1874 > -1 ? true : false;
1875 }
1876 //empty
1877 else if (hasEM) {
1878 occurence = !cell.hasChildNodes();
1879 }
1880 //non-empty
1881 else if (hasNM) {
1882 occurence = cell.hasChildNodes();
1883 } else {
1884 // If numeric type data, perform a strict equality test and
1885 // fallback to unformatted number string comparison
1886 if (numData &&
1887 this.hasType(colIdx, [NUMBER, FORMATTED_NUMBER]) &&
1888 !this.singleFlt) {
1889 // parseNb can return 0 for strings which are not
1890 // formatted numbers, in that case return the original
1891 // string. TODO: handle this in parseNb
1892 term = parseNb(term, decimal) || term;
1893 occurence = numData === term ||
1894 contains(term.toString(), numData.toString(),
1895 this.isExactMatch(colIdx), this.caseSensitive);
1896 } else {
1897 // Finally test search term is contained in cell data
1898 occurence = contains(
1899 term,
1900 cellValue,
1901 this.isExactMatch(colIdx),
1902 this.caseSensitive,
1903 this.ignoresDiacritics(colIdx)
1904 );
1905 }
1906 }
1907
1908 }//else
1909
1910 return occurence;
1911 }
1912
1913 /**
1914 * Return the data of a specified column
1915 * @param {Number} colIndex Column index
1916 * @param {Boolean} [includeHeaders=false] Include headers row
1917 * @param {Array} [exclude=[]] List of row indexes to be excluded
1918 * @return Flat list of data for a column
1919 */
1920 getColumnData(colIndex, includeHeaders = false, exclude = []) {
1921 return this.getColValues(colIndex, includeHeaders, true, exclude);
1922 }
1923
1924 /**
1925 * Return the values of a specified column
1926 * @param {Number} colIndex Column index
1927 * @param {Boolean} [includeHeaders=false] Include headers row
1928 * @param {Array} [exclude=[]] List of row indexes to be excluded
1929 * @return Flat list of values for a column
1930 */
1931 getColumnValues(colIndex, includeHeaders = false, exclude = []) {
1932 return this.getColValues(colIndex, includeHeaders, false, exclude);
1933 }
1934
1935 /**
1936 * Return the data of a specified column
1937 * @param {Number} colIndex Column index
1938 * @param {Boolean} [includeHeaders=false] Include headers row
1939 * @param {Boolean} [typed=false] Return a typed value
1940 * @param {Array} [exclude=[]] List of row indexes to be excluded
1941 * @return {Array} Flat list of data for a column
1942 * @private
1943 */
1944 getColValues(
1945 colIndex,
1946 includeHeaders = false,
1947 typed = false,
1948 exclude = []
1949 ) {
1950 let colValues = [];
1951 let getContent = typed ? this.getCellData.bind(this) :
1952 this.getCellValue.bind(this);
1953
1954 if (includeHeaders) {
1955 colValues.push(this.getHeadersText()[colIndex]);
1956 }
1957
1958 let eachRow = this.eachRow();
1959 eachRow((row, i) => {
1960 // checks if current row index appears in exclude array
1961 let isExludedRow = exclude.indexOf(i) !== -1;
1962 let cells = row.cells;
1963
1964 // checks if row has exact cell # and is not excluded
1965 if (cells.length === this.nbCells && !isExludedRow) {
1966 let data = getContent(cells[colIndex]);
1967 colValues.push(data);
1968 }
1969 });
1970 return colValues;
1971 }
1972
1973 /**
1974 * Return the filter's value of a specified column
1975 * @param {Number} index Column index
1976 * @return {String} Filter value
1977 */
1978 getFilterValue(index) {
1979 if (!this.fltGrid) {
1980 return;
1981 }
1982 let fltValue = '';
1983 let flt = this.getFilterElement(index);
1984 if (!flt) {
1985 return fltValue;
1986 }
1987
1988 let fltColType = this.getFilterType(index);
1989 if (fltColType !== MULTIPLE && fltColType !== CHECKLIST) {
1990 fltValue = flt.value;
1991 }
1992 //mutiple select
1993 else if (fltColType === MULTIPLE) {
1994 fltValue = this.feature('dropdown').getValues(index);
1995 }
1996 //checklist
1997 else if (fltColType === CHECKLIST) {
1998 fltValue = this.feature('checkList').getValues(index);
1999 }
2000 //return an empty string if collection is empty or contains a single
2001 //empty string
2002 if (isArray(fltValue) && fltValue.length === 0 ||
2003 (fltValue.length === 1 && fltValue[0] === '')) {
2004 fltValue = '';
2005 }
2006
2007 return fltValue;
2008 }
2009
2010 /**
2011 * Return the filters' values
2012 * @return {Array} List of filters' values
2013 */
2014 getFiltersValue() {
2015 if (!this.fltGrid) {
2016 return;
2017 }
2018 let searchArgs = [];
2019
2020 this.fltIds.forEach((id, i) => {
2021 let fltValue = this.getFilterValue(i);
2022 if (isArray(fltValue)) {
2023 searchArgs.push(fltValue);
2024 } else {
2025 searchArgs.push(trim(fltValue));
2026 }
2027 });
2028 return searchArgs;
2029 }
2030
2031 /**
2032 * Return the ID of a specified column's filter
2033 * @param {Number} index Column's index
2034 * @return {String} ID of the filter element
2035 */
2036 getFilterId(index) {
2037 if (!this.fltGrid) {
2038 return;
2039 }
2040 return this.fltIds[index];
2041 }
2042
2043 /**
2044 * Return the list of ids of filters matching a specified type.
2045 * Note: hidden filters are also returned
2046 *
2047 * @param {String} type Filter type string ('input', 'select', 'multiple',
2048 * 'checklist')
2049 * @param {Boolean} bool If true returns columns indexes instead of IDs
2050 * @return {[type]} List of element IDs or column indexes
2051 */
2052 getFiltersByType(type, bool) {
2053 if (!this.fltGrid) {
2054 return;
2055 }
2056 let arr = [];
2057 for (let i = 0, len = this.fltIds.length; i < len; i++) {
2058 let fltType = this.getFilterType(i);
2059 if (fltType === type.toLowerCase()) {
2060 let a = bool ? i : this.fltIds[i];
2061 arr.push(a);
2062 }
2063 }
2064 return arr;
2065 }
2066
2067 /**
2068 * Return the filter's DOM element for a given column
2069 * @param {Number} index Column's index
2070 * @return {DOMElement}
2071 */
2072 getFilterElement(index) {
2073 return elm(this.fltIds[index]);
2074 }
2075
2076 /**
2077 * Return the number of cells for a given row index
2078 * @param {Number} rowIndex Index of the row
2079 * @return {Number} Number of cells
2080 */
2081 getCellsNb(rowIndex = 0) {
2082 let tr = this.dom().rows[rowIndex >= 0 ? rowIndex : 0];
2083 return tr ? tr.cells.length : 0;
2084 }
2085
2086 /**
2087 * Return the number of working rows starting from reference row if
2088 * defined
2089 * @param {Boolean} includeHeaders Include the headers row(s)
2090 * @return {Number} Number of working rows
2091 */
2092 getRowsNb(includeHeaders) {
2093 let nbRows = this.getWorkingRows().length;
2094 if (this.dom().tHead) {
2095 return includeHeaders ?
2096 nbRows + this.dom().querySelectorAll('thead > tr').length :
2097 nbRows;
2098 }
2099 return includeHeaders ? nbRows : nbRows - this.refRow;
2100 }
2101
2102 /**
2103 * Return the collection of the working rows, that is, the rows belonging
2104 * to the tbody section(s)
2105 * @returns {Array}
2106 */
2107 getWorkingRows() {
2108 return doc.querySelectorAll(`table#${this.id} > tbody > tr`);
2109 }
2110
2111 /**
2112 * Return the text content of a given cell
2113 * @param {DOMElement} Cell's DOM element
2114 * @return {String}
2115 */
2116 getCellValue(cell) {
2117 let idx = cell.cellIndex;
2118 let cellParser = this.cellParser;
2119 // Invoke cellParser for this column if any
2120 if (cellParser.cols.indexOf(idx) !== -1) {
2121 return cellParser.parse(this, cell, idx);
2122 } else {
2123 return getText(cell);
2124 }
2125 }
2126
2127 /**
2128 * Return the typed data of a given cell based on the column type definition
2129 * @param {DOMElement} cell Cell's DOM element
2130 * @return {String|Number|Date}
2131 */
2132 getCellData(cell) {
2133 let colIndex = cell.cellIndex;
2134 let value = this.getCellValue(cell);
2135
2136 if (this.hasType(colIndex, [FORMATTED_NUMBER])) {
2137 return parseNb(value, this.getDecimal(colIndex));
2138 }
2139 else if (this.hasType(colIndex, [NUMBER])) {
2140 return Number(value);
2141 }
2142 else if (this.hasType(colIndex, [DATE])){
2143 let dateType = this.Mod.dateType;
2144 return dateType.parse(value, dateType.getLocale(colIndex));
2145 }
2146
2147 return value;
2148 }
2149
2150 /**
2151 * Return the table data based on its columns data type definitions
2152 * with following structure:
2153 * [
2154 * [rowIndex, [data0, data1...]],
2155 * [rowIndex, [data0, data1...]]
2156 * ]
2157 * @param {Boolean} [includeHeaders=false] Include headers row
2158 * @param {Boolean} [excludeHiddenCols=false] Exclude hidden columns
2159 * @return {Array}
2160 */
2161 getData(includeHeaders = false, excludeHiddenCols = false) {
2162 return this.getTableData(includeHeaders, excludeHiddenCols, true);
2163 }
2164
2165 /**
2166 * Return the table values with following structure:
2167 * [
2168 * [rowIndex, [value0, value1...]],
2169 * [rowIndex, [value0, value1...]]
2170 * ]
2171 * @param {Boolean} [includeHeaders=false] Include headers row
2172 * @param {Boolean} [excludeHiddenCols=false] Exclude hidden columns
2173 * @return {Array}
2174 */
2175 getValues(includeHeaders = false, excludeHiddenCols = false) {
2176 return this.getTableData(includeHeaders, excludeHiddenCols, false);
2177 }
2178
2179 /**
2180 * Return the table data with following structure:
2181 * [
2182 * [rowIndex, [value0, value1...]],
2183 * [rowIndex, [value0, value1...]]
2184 * ]
2185 * @param {Boolean} [includeHeaders=false] Include headers row
2186 * @param {Boolean} [excludeHiddenCols=false] Exclude hidden columns
2187 * @param {Boolean} [typed=false] Return typed value
2188 * @return {Array}
2189 * @private
2190 *
2191 * TODO: provide an API returning data in JSON format
2192 */
2193 getTableData(
2194 includeHeaders = false,
2195 excludeHiddenCols = false,
2196 typed = false
2197 ) {
2198 let tblData = [];
2199 let getContent = typed ? this.getCellData.bind(this) :
2200 this.getCellValue.bind(this);
2201
2202 if (includeHeaders) {
2203 let headers = this.getHeadersText(excludeHiddenCols);
2204 tblData.push([this.getHeadersRowIndex(), headers]);
2205 }
2206
2207 let eachRow = this.eachRow();
2208 eachRow((row, k) => {
2209 let rowData = [k, []];
2210 let cells = row.cells;
2211 for (let j = 0, len = cells.length; j < len; j++) {
2212 if (excludeHiddenCols && this.hasExtension('colsVisibility')) {
2213 if (this.extension('colsVisibility').isColHidden(j)) {
2214 continue;
2215 }
2216 }
2217 let cellContent = getContent(cells[j]);
2218 rowData[1].push(cellContent);
2219 }
2220 tblData.push(rowData);
2221 });
2222 return tblData;
2223 }
2224
2225 /**
2226 * Return the filtered table data based on its columns data type definitions
2227 * with following structure:
2228 * [
2229 * [rowIndex, [data0, data1...]],
2230 * [rowIndex, [data0, data1...]]
2231 * ]
2232 * @param {Boolean} [includeHeaders=false] Include headers row
2233 * @param {Boolean} [excludeHiddenCols=false] Exclude hidden columns
2234 * @return {Array}
2235 *
2236 * TODO: provide an API returning data in JSON format
2237 */
2238 getFilteredData(includeHeaders = false, excludeHiddenCols = false) {
2239 return this.filteredData(includeHeaders, excludeHiddenCols, true);
2240 }
2241
2242 /**
2243 * Return the filtered table values with following structure:
2244 * [
2245 * [rowIndex, [value0, value1...]],
2246 * [rowIndex, [value0, value1...]]
2247 * ]
2248 * @param {Boolean} [includeHeaders=false] Include headers row
2249 * @param {Boolean} [excludeHiddenCols=false] Exclude hidden columns
2250 * @return {Array}
2251 *
2252 * TODO: provide an API returning data in JSON format
2253 */
2254 getFilteredValues(includeHeaders = false, excludeHiddenCols = false) {
2255 return this.filteredData(includeHeaders, excludeHiddenCols, false);
2256 }
2257
2258 /**
2259 * Return the filtered data with following structure:
2260 * [
2261 * [rowIndex, [value0, value1...]],
2262 * [rowIndex, [value0, value1...]]
2263 * ]
2264 * @param {Boolean} [includeHeaders=false] Include headers row
2265 * @param {Boolean} [excludeHiddenCols=false] Exclude hidden columns
2266 * @param {Boolean} [typed=false] Return typed value
2267 * @return {Array}
2268 * @private
2269 *
2270 * TODO: provide an API returning data in JSON format
2271 */
2272 filteredData(
2273 includeHeaders = false,
2274 excludeHiddenCols = false,
2275 typed = false
2276 ) {
2277 if (this.validRowsIndex.length === 0) {
2278 return [];
2279 }
2280 let rows = this.dom().rows,
2281 filteredData = [];
2282 let getContent = typed ? this.getCellData.bind(this) :
2283 this.getCellValue.bind(this);
2284
2285 if (includeHeaders) {
2286 let headers = this.getHeadersText(excludeHiddenCols);
2287 filteredData.push([this.getHeadersRowIndex(), headers]);
2288 }
2289
2290 let validRows = this.getValidRows(true);
2291 for (let i = 0; i < validRows.length; i++) {
2292 let rData = [this.validRowsIndex[i], []],
2293 cells = rows[this.validRowsIndex[i]].cells;
2294 for (let k = 0; k < cells.length; k++) {
2295 if (excludeHiddenCols && this.hasExtension('colsVisibility')) {
2296 if (this.extension('colsVisibility').isColHidden(k)) {
2297 continue;
2298 }
2299 }
2300 let cellValue = getContent(cells[k]);
2301 rData[1].push(cellValue);
2302 }
2303 filteredData.push(rData);
2304 }
2305 return filteredData;
2306 }
2307
2308 /**
2309 * Return the filtered data for a given column index
2310 * @param {any} colIndex Colmun's index
2311 * @param {boolean} [includeHeaders=false] Optional Include headers row
2312 * @param {any} [exclude=[]] Optional List of row indexes to be excluded
2313 * @return {Array} Flat list of typed values [data0, data1, data2...]
2314 *
2315 * TODO: provide an API returning data in JSON format
2316 */
2317 getFilteredColumnData(colIndex, includeHeaders = false, exclude = []) {
2318 return this.getFilteredDataCol(
2319 colIndex, includeHeaders, true, exclude, false);
2320 }
2321
2322 /**
2323 * Return the filtered and visible data for a given column index
2324 * @param {any} colIndex Colmun's index
2325 * @param {boolean} [includeHeaders=false] Optional Include headers row
2326 * @param {any} [exclude=[]] Optional List of row indexes to be excluded
2327 * @return {Array} Flat list of typed values [data0, data1, data2...]
2328 *
2329 * TODO: provide an API returning data in JSON format
2330 */
2331 getVisibleColumnData(colIndex, includeHeaders = false, exclude = []) {
2332 return this.getFilteredDataCol(
2333 colIndex, includeHeaders, true, exclude, true);
2334 }
2335
2336 /**
2337 * Return the filtered values for a given column index
2338 * @param {any} colIndex Colmun's index
2339 * @param {boolean} [includeHeaders=false] Optional Include headers row
2340 * @param {any} [exclude=[]] Optional List of row indexes to be excluded
2341 * @return {Array} Flat list of values ['value0', 'value1', 'value2'...]
2342 *
2343 * TODO: provide an API returning data in JSON format
2344 */
2345 getFilteredColumnValues(colIndex, includeHeaders = false, exclude = []) {
2346 return this.getFilteredDataCol(
2347 colIndex, includeHeaders, false, exclude, false);
2348 }
2349
2350 /**
2351 * Return the filtered and visible values for a given column index
2352 * @param {any} colIndex Colmun's index
2353 * @param {boolean} [includeHeaders=false] Optional Include headers row
2354 * @param {any} [exclude=[]] Optional List of row indexes to be excluded
2355 * @return {Array} Flat list of values ['value0', 'value1', 'value2'...]
2356 *
2357 * TODO: provide an API returning data in JSON format
2358 */
2359 getVisibleColumnValues(colIndex, includeHeaders = false, exclude = []) {
2360 return this.getFilteredDataCol(
2361 colIndex, includeHeaders, false, exclude, true);
2362 }
2363
2364 /**
2365 * Return the filtered data for a given column index
2366 * @param {Number} colIndex Colmun's index
2367 * @param {Boolean} [includeHeaders=false] Include headers row
2368 * @param {Boolean} [typed=false] Return typed value
2369 * @param {Array} [exclude=[]] List of row indexes to be excluded
2370 * @param {Boolean} [visible=true] Return only filtered and visible data
2371 * (relevant for paging)
2372 * @return {Array} Flat list of values ['val0','val1','val2'...]
2373 * @private
2374 *
2375 * TODO: provide an API returning data in JSON format
2376 */
2377 getFilteredDataCol(
2378 colIndex,
2379 includeHeaders = false,
2380 typed = false,
2381 exclude = [],
2382 visible = true
2383 ) {
2384 if (isUndef(colIndex)) {
2385 return [];
2386 }
2387
2388 let rows = this.dom().rows;
2389 let getContent = typed ? this.getCellData.bind(this) :
2390 this.getCellValue.bind(this);
2391
2392 // ensure valid rows index do not contain excluded rows and row is
2393 // displayed
2394 let validRows = this.getValidRows(true).filter((rowIdx) => {
2395 return exclude.indexOf(rowIdx) === -1 &&
2396 (visible ?
2397 this.getRowDisplay(rows[rowIdx]) !== 'none' :
2398 true);
2399 });
2400
2401 // convert column value to expected type if necessary
2402 let validColValues = validRows.map((rowIdx) => {
2403 return getContent(rows[rowIdx].cells[colIndex]);
2404 });
2405
2406 if (includeHeaders) {
2407 validColValues.unshift(this.getHeadersText()[colIndex]);
2408 }
2409
2410 return validColValues;
2411 }
2412
2413 /**
2414 * Get the display value of a row
2415 * @param {HTMLTableRowElement} row DOM element of the row
2416 * @return {String} Usually 'none' or ''
2417 */
2418 getRowDisplay(row) {
2419 return row.style.display;
2420 }
2421
2422 /**
2423 * Validate/invalidate row by setting the 'validRow' attribute on the row
2424 * @param {Number} rowIndex Index of the row
2425 * @param {Boolean} isValid
2426 */
2427 validateRow(rowIndex, isValid) {
2428 let row = this.dom().rows[rowIndex];
2429 if (!row || !isBoolean(isValid)) {
2430 return;
2431 }
2432
2433 // always visible rows are valid
2434 if (this.excludeRows.indexOf(rowIndex) !== -1) {
2435 isValid = true;
2436 }
2437
2438 let displayFlag = isValid ? '' : NONE,
2439 validFlag = isValid ? 'true' : 'false';
2440 row.style.display = displayFlag;
2441
2442 if (this.paging) {
2443 row.setAttribute('validRow', validFlag);
2444 }
2445
2446 if (isValid) {
2447 if (this.validRowsIndex.indexOf(rowIndex) === -1) {
2448 this.validRowsIndex.push(rowIndex);
2449 }
2450
2451 this.onRowValidated(this, rowIndex);
2452 this.emitter.emit('row-validated', this, rowIndex);
2453 }
2454 }
2455
2456 /**
2457 * Validate all filterable rows
2458 */
2459 validateAllRows() {
2460 if (!this.initialized) {
2461 return;
2462 }
2463 this.validRowsIndex = [];
2464 for (let k = this.refRow; k < this.nbFilterableRows; k++) {
2465 this.validateRow(k, true);
2466 }
2467 }
2468
2469 /**
2470 * Set search value to a given filter
2471 * @param {Number} index Column's index
2472 * @param {String or Array} query searcharg Search term
2473 */
2474 setFilterValue(index, query = '') {
2475 if (!this.fltGrid) {
2476 return;
2477 }
2478 let slc = this.getFilterElement(index),
2479 fltColType = this.getFilterType(index);
2480
2481 if (!slc) {
2482 return;
2483 }
2484
2485 //multiple selects
2486 if (fltColType === MULTIPLE) {
2487 let values = isArray(query) ? query :
2488 query.split(' ' + this.orOperator + ' ');
2489
2490 if (this.loadFltOnDemand && !this.initialized) {
2491 this.emitter.emit('build-select-filter', this, index,
2492 this.linkedFilters, this.isExternalFlt());
2493 }
2494
2495 this.emitter.emit('select-options', this, index, values);
2496 }
2497 //checklist
2498 else if (fltColType === CHECKLIST) {
2499 let values = [];
2500 if (this.loadFltOnDemand && !this.initialized) {
2501 this.emitter.emit('build-checklist-filter', this, index,
2502 this.linkedFilters);
2503 }
2504 if (isArray(query)) {
2505 values = query;
2506 } else {
2507 query = matchCase(query, this.caseSensitive);
2508 values = query.split(' ' + this.orOperator + ' ');
2509 }
2510
2511 this.emitter.emit('select-checklist-options', this, index, values);
2512 }
2513 else {
2514 if (this.loadFltOnDemand && !this.initialized) {
2515 this.emitter.emit('build-select-filter', this, index,
2516 this.linkedFilters, this.isExternalFlt());
2517 }
2518 slc.value = query;
2519 }
2520 }
2521
2522 /**
2523 * Make passed or default working table element width fixed
2524 * @param {TableElement} tbl optional table DOM element
2525 */
2526 setFixedLayout(tbl = this.dom()) {
2527 let colWidths = this.colWidths;
2528 let tableWidth = tbl.clientWidth;
2529
2530 if (colWidths.length > 0) {
2531 let defaultWidth = this.defaultColWidth;
2532 tableWidth = colWidths
2533 .reduce((x, y) =>
2534 parseInt((x || defaultWidth), 10) +
2535 parseInt((y || defaultWidth), 10)
2536 );
2537 }
2538
2539 tbl.style.width = `${tableWidth}px`;
2540 tbl.style.tableLayout = 'fixed';
2541 }
2542
2543 /**
2544 * Set passed or default working table columns' widths with configuration
2545 * values
2546 * @param {TableElement} tbl optional table DOM element
2547 */
2548 setColWidths(tbl = this.dom()) {
2549 let colWidths = this.colWidths;
2550 if (colWidths.length === 0) {
2551 return;
2552 }
2553
2554 let colTags = tag(tbl, 'col');
2555 let tblHasColTag = colTags.length > 0;
2556 let frag = !tblHasColTag ? doc.createDocumentFragment() : null;
2557
2558 this.eachCol((k) => {
2559 let col;
2560 if (tblHasColTag) {
2561 col = colTags[k];
2562 } else {
2563 col = createElm('col');
2564 frag.appendChild(col);
2565 }
2566 col.style.width = colWidths[k];
2567 });
2568
2569 if (!tblHasColTag) {
2570 tbl.insertBefore(frag, tbl.firstChild);
2571 }
2572 }
2573
2574 /**
2575 * Exclude rows from actions
2576 */
2577 setExcludeRows() {
2578 if (!this.hasExcludedRows) {
2579 return;
2580 }
2581 this.excludeRows.forEach((rowIdx) => this.validateRow(rowIdx, true));
2582 }
2583
2584 /**
2585 * Clear all the filters' values
2586 */
2587 clearFilters() {
2588 if (!this.fltGrid) {
2589 return;
2590 }
2591
2592 this.emitter.emit('before-clearing-filters', this);
2593 this.onBeforeReset(this, this.getFiltersValue());
2594
2595 for (let i = 0, len = this.fltIds.length; i < len; i++) {
2596 this.setFilterValue(i, '');
2597 }
2598
2599 this.filter();
2600
2601 this.onAfterReset(this);
2602 this.emitter.emit('after-clearing-filters', this);
2603 }
2604
2605 /**
2606 * Return the ID of the current active filter
2607 * @return {String}
2608 */
2609 getActiveFilterId() {
2610 return this.activeFilterId;
2611 }
2612
2613 /**
2614 * Set the ID of the current active filter
2615 * @param {String} filterId Element ID
2616 */
2617 setActiveFilterId(filterId) {
2618 this.activeFilterId = filterId;
2619 }
2620
2621 /**
2622 * Return the column index for a given filter ID
2623 * @param {string} [filterId=''] Filter ID
2624 * @return {Number} Column index
2625 */
2626 getColumnIndexFromFilterId(filterId = '') {
2627 let idx = filterId.split('_')[0];
2628 idx = idx.split(this.prfxFlt)[1];
2629 return parseInt(idx, 10);
2630 }
2631
2632 /**
2633 * Build filter element ID for a given column index
2634 * @param {any} colIndex
2635 * @return {String} Filter element ID string
2636 * @private
2637 */
2638 buildFilterId(colIndex) {
2639 return `${this.prfxFlt}${colIndex}_${this.id}`;
2640 }
2641
2642 /**
2643 * Check if has external filters
2644 * @returns {Boolean}
2645 * @private
2646 */
2647 isExternalFlt() {
2648 return this.externalFltIds.length > 0;
2649 }
2650
2651 /**
2652 * Returns styles path
2653 * @returns {String}
2654 * @private
2655 */
2656 getStylePath() {
2657 return defaultsStr(this.config.style_path, this.basePath + 'style/');
2658 }
2659
2660 /**
2661 * Returns main stylesheet path
2662 * @returns {String}
2663 * @private
2664 */
2665 getStylesheetPath() {
2666 return defaultsStr(this.config.stylesheet,
2667 this.getStylePath() + 'tablefilter.css');
2668 }
2669
2670 /**
2671 * Returns themes path
2672 * @returns {String}
2673 * @private
2674 */
2675 getThemesPath() {
2676 return defaultsStr(this.config.themes_path,
2677 this.getStylePath() + 'themes/');
2678 }
2679
2680 /**
2681 * Make specified column's filter active
2682 * @param colIndex Index of a column
2683 */
2684 activateFilter(colIndex) {
2685 if (isUndef(colIndex)) {
2686 return;
2687 }
2688 this.setActiveFilterId(this.getFilterId(colIndex));
2689 }
2690
2691 /**
2692 * Determine if passed filter column implements exact query match
2693 * @param {Number} colIndex Column index
2694 * @return {Boolean}
2695 */
2696 isExactMatch(colIndex) {
2697 let fltType = this.getFilterType(colIndex);
2698 return this.exactMatchByCol[colIndex] || this.exactMatch ||
2699 fltType !== INPUT;
2700 }
2701
2702 /**
2703 * Check if passed row is valid
2704 * @param {Number} rowIndex Row index
2705 * @return {Boolean}
2706 */
2707 isRowValid(rowIndex) {
2708 return this.getValidRows().indexOf(rowIndex) !== -1;
2709 }
2710
2711 /**
2712 * Check if passed row is visible
2713 * @param {Number} rowIndex Row index
2714 * @return {Boolean}
2715 */
2716 isRowDisplayed(rowIndex) {
2717 let row = this.dom().rows[rowIndex];
2718 return this.getRowDisplay(row) === '';
2719 }
2720
2721 /**
2722 * Check if specified column filter ignores diacritics.
2723 * Note this is only applicable to input filter types.
2724 * @param {Number} colIndex Column index
2725 * @return {Boolean}
2726 */
2727 ignoresDiacritics(colIndex) {
2728 let ignoreDiac = this.ignoreDiacritics;
2729 if (isArray(ignoreDiac)) {
2730 return ignoreDiac[colIndex];
2731 }
2732 return Boolean(ignoreDiac);
2733 }
2734
2735 /**
2736 * Return clear all text for specified filter column
2737 * @param {Number} colIndex Column index
2738 * @return {String}
2739 */
2740 getClearFilterText(colIndex) {
2741 let clearText = this.clearFilterText;
2742 if (isArray(clearText)) {
2743 return clearText[colIndex];
2744 }
2745 return clearText;
2746 }
2747
2748 /**
2749 * Column iterator invoking continue and break condition callbacks if any
2750 * then calling supplied callback for each item
2751 * @param {Function} [fn=EMPTY_FN] callback
2752 * @param {Function} [continueFn=EMPTY_FN] continue condition callback
2753 * @param {Function} [breakFn=EMPTY_FN] break condition callback
2754 */
2755 eachCol(fn = EMPTY_FN, continueFn = EMPTY_FN, breakFn = EMPTY_FN) {
2756 let len = this.getCellsNb(this.refRow);
2757 for (let i = 0; i < len; i++) {
2758 if (continueFn(i) === true) {
2759 continue;
2760 }
2761 if (breakFn(i) === true) {
2762 break;
2763 }
2764 fn(i);
2765 }
2766 }
2767
2768 /**
2769 * Rows iterator starting from supplied row index or defaulting to reference
2770 * row index. Closure function accepts a callback function and optional
2771 * continue and break callbacks.
2772 * @param {Number} startIdx Row index from which filtering starts
2773 */
2774 eachRow(startIdx = this.refRow) {
2775 return (fn = EMPTY_FN, continueFn = EMPTY_FN, breakFn = EMPTY_FN) => {
2776 let rows = this.dom().rows;
2777 let len = this.getRowsNb(true);
2778 for (let i = startIdx; i < len; i++) {
2779 if (continueFn(rows[i], i) === true) {
2780 continue;
2781 }
2782 if (breakFn(rows[i], i) === true) {
2783 break;
2784 }
2785 fn(rows[i], i);
2786 }
2787 };
2788 }
2789
2790 /**
2791 * Check if passed script or stylesheet is already imported
2792 * @param {String} filePath Ressource path
2793 * @param {String} type Possible values: 'script' or 'link'
2794 * @return {Boolean}
2795 */
2796 isImported(filePath, type = 'script') {
2797 let imported = false,
2798 attr = type === 'script' ? 'src' : 'href',
2799 files = tag(doc, type);
2800 for (let i = 0, len = files.length; i < len; i++) {
2801 if (isUndef(files[i][attr])) {
2802 continue;
2803 }
2804 if (files[i][attr].match(filePath)) {
2805 imported = true;
2806 break;
2807 }
2808 }
2809 return imported;
2810 }
2811
2812 /**
2813 * Import script or stylesheet
2814 * @param {String} fileId Ressource ID
2815 * @param {String} filePath Ressource path
2816 * @param {Function} callback Callback
2817 * @param {String} type Possible values: 'script' or 'link'
2818 */
2819 import(fileId, filePath, callback, type = 'script') {
2820 if (this.isImported(filePath, type)) {
2821 return;
2822 }
2823 let o = this,
2824 isLoaded = false,
2825 file,
2826 head = tag(doc, 'head')[0];
2827
2828 if (type.toLowerCase() === 'link') {
2829 file = createElm('link',
2830 ['id', fileId], ['type', 'text/css'],
2831 ['rel', 'stylesheet'], ['href', filePath]
2832 );
2833 } else {
2834 file = createElm('script',
2835 ['id', fileId],
2836 ['type', 'text/javascript'], ['src', filePath]
2837 );
2838 }
2839
2840 //Browser <> IE onload event works only for scripts, not for stylesheets
2841 file.onload = file.onreadystatechange = () => {
2842 if (!isLoaded &&
2843 (!this.readyState || this.readyState === 'loaded' ||
2844 this.readyState === 'complete')) {
2845 isLoaded = true;
2846 if (typeof callback === 'function') {
2847 callback.call(null, o);
2848 }
2849 }
2850 };
2851 file.onerror = () => {
2852 throw new Error(`TableFilter could not load: ${filePath}`);
2853 };
2854 head.appendChild(file);
2855 }
2856
2857 /**
2858 * Check if table has filters grid
2859 * @return {Boolean}
2860 */
2861 isInitialized() {
2862 return this.initialized;
2863 }
2864
2865 /**
2866 * Get list of filter IDs
2867 * @return {Array} List of filters ids
2868 */
2869 getFiltersId() {
2870 return this.fltIds || [];
2871 }
2872
2873 /**
2874 * Get filtered (valid) rows indexes
2875 * @param {Boolean} reCalc Force calculation of filtered rows list
2876 * @return {Array} List of row indexes
2877 */
2878 getValidRows(reCalc) {
2879 if (!reCalc) {
2880 return this.validRowsIndex;
2881 }
2882
2883 this.validRowsIndex = [];
2884
2885 let eachRow = this.eachRow();
2886 eachRow((row) => {
2887 if (!this.paging) {
2888 if (this.getRowDisplay(row) !== NONE) {
2889 this.validRowsIndex.push(row.rowIndex);
2890 }
2891 } else {
2892 if (row.getAttribute('validRow') === 'true' ||
2893 row.getAttribute('validRow') === null) {
2894 this.validRowsIndex.push(row.rowIndex);
2895 }
2896 }
2897 });
2898 return this.validRowsIndex;
2899 }
2900
2901 /**
2902 * Get the index of the row containing the filters
2903 * @return {Number}
2904 */
2905 getFiltersRowIndex() {
2906 return this.filtersRowIndex;
2907 }
2908
2909 /**
2910 * Get the index of the headers row
2911 * @return {Number}
2912 */
2913 getHeadersRowIndex() {
2914 return this.headersRow;
2915 }
2916
2917 /**
2918 * Get the row index from where the filtering process start (1st filterable
2919 * row)
2920 * @return {Number}
2921 */
2922 getStartRowIndex() {
2923 return this.refRow;
2924 }
2925
2926 /**
2927 * Get the index of the last row
2928 * @return {Number}
2929 */
2930 getLastRowIndex() {
2931 let nbRows = this.getRowsNb(true);
2932 return (nbRows - 1);
2933 }
2934
2935 /**
2936 * Determine whether the specified column has one of the passed types
2937 * @param {Number} colIndex Column index
2938 * @param {Array} [types=[]] List of column types
2939 * @return {Boolean}
2940 */
2941 hasType(colIndex, types = []) {
2942 if (this.colTypes.length === 0) {
2943 return false;
2944 }
2945 let colType = this.colTypes[colIndex];
2946 if (isObj(colType)) {
2947 colType = colType.type;
2948 }
2949 return types.indexOf(colType) !== -1;
2950 }
2951
2952 /**
2953 * Get the header DOM element for a given column index
2954 * @param {Number} colIndex Column index
2955 * @return {Element}
2956 */
2957 getHeaderElement(colIndex) {
2958 let table = this.gridLayout ? this.Mod.gridLayout.headTbl : this.dom();
2959 let tHead = tag(table, 'thead');
2960 let rowIdx = this.getHeadersRowIndex();
2961 let header;
2962 if (tHead.length === 0) {
2963 header = table.rows[rowIdx].cells[colIndex];
2964 }
2965 if (tHead.length === 1) {
2966 header = tHead[0].rows[rowIdx].cells[colIndex];
2967 }
2968 return header;
2969 }
2970
2971 /**
2972 * Return the list of headers' text
2973 * @param {Boolean} excludeHiddenCols Optional: exclude hidden columns
2974 * @return {Array} list of headers' text
2975 */
2976 getHeadersText(excludeHiddenCols = false) {
2977 let headers = [];
2978 this.eachCol(
2979 (j) => {
2980 let header = this.getHeaderElement(j);
2981 let headerText = getFirstTextNode(header);
2982 headers.push(headerText);
2983 },
2984 // continue condition function
2985 (j) => {
2986 if (excludeHiddenCols && this.hasExtension('colsVisibility')) {
2987 return this.extension('colsVisibility').isColHidden(j);
2988 }
2989 return false;
2990 }
2991 );
2992 return headers;
2993 }
2994
2995 /**
2996 * Return the filter type for a specified column
2997 * @param {Number} colIndex Column's index
2998 * @return {String}
2999 */
3000 getFilterType(colIndex) {
3001 return this.filterTypes[colIndex];
3002 }
3003
3004 /**
3005 * Get the total number of filterable rows
3006 * @return {Number}
3007 */
3008 getFilterableRowsNb() {
3009 return this.getRowsNb(false);
3010 }
3011
3012 /**
3013 * Return the total number of valid rows
3014 * @param {Boolean} [reCalc=false] Forces calculation of filtered rows
3015 * @return {Number}
3016 */
3017 getValidRowsNb(reCalc = false) {
3018 return this.getValidRows(reCalc).length;
3019 }
3020
3021 /**
3022 * Return the working DOM element
3023 * @return {HTMLTableElement}
3024 */
3025 dom() {
3026 return this.tbl;
3027 }
3028
3029 /**
3030 * Return the decimal separator for supplied column as per column type
3031 * configuration or global setting
3032 * @param {Number} colIndex Column index
3033 * @returns {String} '.' or ','
3034 */
3035 getDecimal(colIndex) {
3036 let decimal = this.decimalSeparator;
3037 if (this.hasType(colIndex, [FORMATTED_NUMBER])) {
3038 let colType = this.colTypes[colIndex];
3039 if (colType.hasOwnProperty('decimal')) {
3040 decimal = colType.decimal;
3041 }
3042 }
3043 return decimal;
3044 }
3045
3046 /**
3047 * Get the configuration object (literal object)
3048 * @return {Object}
3049 */
3050 config() {
3051 return this.cfg;
3052 }
3053}