UNPKG

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