UNPKG

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