UNPKG

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