UNPKG

95.7 kBJavaScriptView Raw
1import {addEvt, cancelEvt, stopEvt, targetEvt, isKeyPressed} from './event';
2import {
3 addClass, createElm, elm, getText, getFirstTextNode, removeClass, tag
4} from './dom';
5import {contains, matchCase, rgxEsc, trim, toCamelCase, uuid} from './string';
6import {
7 isArray, isEmpty, isFn, isNumber, isObj, isString, isUndef, EMPTY_FN,
8 isBoolean
9} from './types';
10import {parse as parseNb} from './number';
11import {
12 defaultsBool, defaultsStr, defaultsFn,
13 defaultsNb, defaultsArr
14} from './settings';
15
16import {root} from './root';
17import {Emitter} from './emitter';
18import {Dropdown} from './modules/dropdown';
19import {CheckList} from './modules/checkList';
20import {DateType} from './modules/dateType';
21import {Help} from './modules/help';
22import {State} from './modules/state';
23import {GridLayout} from './modules/gridLayout';
24import {Loader} from './modules/loader';
25import {HighlightKeyword} from './modules/highlightKeywords';
26import {PopupFilter} from './modules/popupFilter';
27import {MarkActiveColumns} from './modules/markActiveColumns';
28import {RowsCounter} from './modules/rowsCounter';
29import {StatusBar} from './modules/statusBar';
30import {ClearButton} from './modules/clearButton';
31import {AlternateRows} from './modules/alternateRows';
32import {NoResults} from './modules/noResults';
33import {Paging} from './modules/paging';
34import {Toolbar} from './modules/toolbar';
35
36import {
37 INPUT, SELECT, MULTIPLE, CHECKLIST, NONE,
38 ENTER_KEY, TAB_KEY, ESC_KEY, UP_ARROW_KEY, DOWN_ARROW_KEY,
39 CELL_TAG, AUTO_FILTER_DELAY, NUMBER, DATE, FORMATTED_NUMBER
40} from './const';
41
42let doc = root.document;
43
44const FEATURES = [
45 DateType, Help, State, MarkActiveColumns, GridLayout, Loader,
46 HighlightKeyword, PopupFilter, RowsCounter, StatusBar, ClearButton,
47 AlternateRows, NoResults, Paging, Toolbar
48];
49
50/**
51 * Makes HTML tables filterable and a bit more :)
52 *
53 * @export
54 * @class TableFilter
55 */
56export class TableFilter {
57
58 /**
59 * Creates an instance of TableFilter
60 * requires `table` or `id` arguments, `row` and `configuration` optional
61 * @param {HTMLTableElement} table Table DOM element
62 * @param {String} id Table id
63 * @param {Number} row index indicating the 1st row
64 * @param {Object} configuration object
65 */
66 constructor(...args) {
67 /**
68 * ID of current instance
69 * @type {String}
70 * @private
71 */
72 this.id = null;
73
74 /**
75 * Current version
76 * @type {String}
77 */
78 this.version = '{VERSION}';
79
80 /**
81 * Current year
82 * @type {Number}
83 * @private
84 */
85 this.year = new Date().getFullYear();
86
87 /**
88 * HTML Table DOM element
89 * @type {DOMElement}
90 * @private
91 */
92 this.tbl = null;
93
94 /**
95 * Calculated row's index from which starts filtering once filters
96 * are generated
97 * @type {Number}
98 */
99 this.refRow = null;
100
101 /**
102 * Index of the headers row
103 * @type {Number}
104 * @private
105 */
106 this.headersRow = null;
107
108 /**
109 * Configuration object
110 * @type {Object}
111 * @private
112 */
113 this.cfg = {};
114
115 /**
116 * Number of rows that can be filtered
117 * @type {Number}
118 * @private
119 */
120 this.nbFilterableRows = 0;
121
122 /**
123 * Number of cells in the reference row
124 * @type {Number}
125 * @private
126 */
127 this.nbCells = null;
128
129 /**
130 * Has a configuration object
131 * @type {Object}
132 * @private
133 */
134 this.hasConfig = false;
135
136 /** @private */
137 this.initialized = false;
138
139 let startRow;
140
141 // TODO: use for-of
142 args.forEach((arg) => {
143 if (typeof arg === 'object' && arg.nodeName === 'TABLE') {
144 this.tbl = arg;
145 this.id = arg.id || `tf_${uuid()}`;
146 this.tbl.id = this.id;
147 } else if (isString(arg)) {
148 this.id = arg;
149 this.tbl = elm(arg);
150 } else if (isNumber(arg)) {
151 startRow = arg;
152 } else if (isObj(arg)) {
153 this.cfg = arg;
154 this.hasConfig = true;
155 }
156 });
157
158 if (!this.tbl || this.tbl.nodeName !== 'TABLE') {
159 throw new Error(`Could not instantiate TableFilter: HTML table
160 DOM element not found.`);
161 }
162
163 if (this.getRowsNb(true) === 0) {
164 throw new Error(`Could not instantiate TableFilter: HTML table
165 requires at least 1 row.`);
166 }
167
168 // configuration object
169 let f = this.cfg;
170
171 /**
172 * Event emitter instance
173 * @type {Emitter}
174 */
175 this.emitter = new Emitter();
176
177 // start row
178 this.refRow = isUndef(startRow) ? 2 : (startRow + 1);
179
180 /**
181 * Collection of filter type by column
182 * @type {Array}
183 * @private
184 */
185 this.filterTypes = [].map.call(
186 (this.dom().rows[this.refRow] || this.dom().rows[0]).cells,
187 (cell, idx) => {
188 let colType = this.cfg[`col_${idx}`];
189 return !colType ? INPUT : colType.toLowerCase();
190 });
191
192 /**
193 * Base path for static assets
194 * @type {String}
195 */
196 this.basePath = defaultsStr(f.base_path, 'tablefilter/');
197
198 /*** filters' grid properties ***/
199
200 /**
201 * Enable/disable filters
202 * @type {Boolean}
203 */
204 this.fltGrid = defaultsBool(f.grid, true);
205
206 /**
207 * Enable/disable grid layout (fixed headers)
208 * @type {Object|Boolean}
209 */
210 this.gridLayout = isObj(f.grid_layout) || Boolean(f.grid_layout);
211
212 /**
213 * Filters row index
214 * @type {Number}
215 */
216 this.filtersRowIndex = defaultsNb(f.filters_row_index, 0);
217
218 /**
219 * Headers row index
220 * @type {Number}
221 */
222 this.headersRow = defaultsNb(f.headers_row_index,
223 (this.filtersRowIndex === 0 ? 1 : 0));
224
225 /**
226 * Define the type of cell containing a filter (td/th)
227 * @type {String}
228 */
229 this.fltCellTag = defaultsStr(f.filters_cell_tag, CELL_TAG);
230
231 /**
232 * List of filters IDs
233 * @type {Array}
234 * @private
235 */
236 this.fltIds = [];
237
238 /**
239 * List of valid rows indexes (rows visible upon filtering)
240 * @type {Array}
241 * @private
242 */
243 this.validRowsIndex = [];
244
245 /*** filters' grid appearance ***/
246 /**
247 * Path for stylesheets
248 * @type {String}
249 */
250 this.stylePath = this.getStylePath();
251
252 /**
253 * Main stylesheet path
254 * @type {String}
255 */
256 this.stylesheet = this.getStylesheetPath();
257
258 /**
259 * Main stylesheet ID
260 * @type {String}
261 * @private
262 */
263 this.stylesheetId = this.id + '_style';
264
265 /**
266 * Css class for the filters row
267 * @type {String}
268 */
269 this.fltsRowCssClass = defaultsStr(f.flts_row_css_class, 'fltrow');
270
271 /**
272 * Enable/disable icons (paging, reset button)
273 * @type {Boolean}
274 */
275 this.enableIcons = defaultsBool(f.enable_icons, true);
276
277 /**
278 * Enable/disable alternating rows
279 * @type {Boolean}
280 */
281 this.alternateRows = Boolean(f.alternate_rows);
282
283 /**
284 * Columns widths array
285 * @type {Array}
286 */
287 this.colWidths = defaultsArr(f.col_widths, []);
288
289 /**
290 * Default column width when column widths are defined
291 */
292 this.defaultColWidth = defaultsNb(f.default_col_width, 100);
293
294 /**
295 * Css class for a filter element
296 * @type {String}
297 */
298 this.fltCssClass = defaultsStr(f.flt_css_class, 'flt');
299
300 /**
301 * Css class for multiple select filters
302 * @type {String}
303 */
304 this.fltMultiCssClass = defaultsStr(f.flt_multi_css_class, 'flt_multi');
305
306 /**
307 * Css class for small filter (when submit button is active)
308 * @type {String}
309 */
310 this.fltSmallCssClass = defaultsStr(f.flt_small_css_class, 'flt_s');
311
312 /**
313 * Css class for single filter type
314 * @type {String}
315 */
316 this.singleFltCssClass = defaultsStr((f.single_filter || {}).css_class,
317 'single_flt');
318
319 /*** filters' grid behaviours ***/
320
321 /**
322 * Enable/disable enter key for input type filters
323 * @type {Boolean}
324 */
325 this.enterKey = defaultsBool(f.enter_key, true);
326
327 /**
328 * Callback fired before filtering process starts
329 * @type {Function}
330 */
331 this.onBeforeFilter = defaultsFn(f.on_before_filter, EMPTY_FN);
332
333 /**
334 * Callback fired after filtering process is completed
335 * @type {Function}
336 */
337 this.onAfterFilter = defaultsFn(f.on_after_filter, EMPTY_FN);
338
339 /**
340 * Enable/disable case sensitivity filtering
341 * @type {Boolean}
342 */
343 this.caseSensitive = Boolean(f.case_sensitive);
344
345 /**
346 * Indicate whether exact match filtering is enabled on a per column
347 * basis
348 * @type {Boolean}
349 * @private
350 */
351 this.hasExactMatchByCol = isArray(f.columns_exact_match);
352
353 /**
354 * Exact match filtering per column array
355 * @type {Array}
356 */
357 this.exactMatchByCol = this.hasExactMatchByCol ?
358 f.columns_exact_match : [];
359
360 /**
361 * Globally enable/disable exact match filtering
362 * @type {Boolean}
363 */
364 this.exactMatch = Boolean(f.exact_match);
365
366 /**
367 * Ignore diacritics globally or on a column basis
368 * @type {Boolean|Array}
369 */
370 this.ignoreDiacritics = f.ignore_diacritics;
371
372 /**
373 * Enable/disable linked filters filtering mode
374 * @type {Boolean}
375 */
376 this.linkedFilters = Boolean(f.linked_filters);
377
378 /**
379 * Enable/disable readonly state for excluded options when
380 * linked filters filtering mode is on
381 * @type {Boolean}
382 */
383 this.disableExcludedOptions = Boolean(f.disable_excluded_options);
384
385 /**
386 * Active filter ID
387 * @type {String}
388 * @private
389 */
390 this.activeFilterId = null;
391
392 /**
393 * Determine if there are excluded rows from filtering
394 * @type {Boolean}
395 * @private
396 */
397 this.hasExcludedRows = Boolean(isArray(f.exclude_rows) &&
398 f.exclude_rows.length > 0);
399
400 /**
401 * List of row indexes to be excluded from filtering
402 * @type {Array}
403 */
404 this.excludeRows = defaultsArr(f.exclude_rows, []);
405
406 /**
407 * List of containers IDs where external filters will be generated
408 * @type {Array}
409 */
410 this.externalFltIds = defaultsArr(f.external_flt_ids, []);
411
412 /**
413 * Callback fired after filters are generated
414 * @type {Function}
415 */
416 this.onFiltersLoaded = defaultsFn(f.on_filters_loaded, EMPTY_FN);
417
418 /**
419 * Enable/disable single filter mode
420 * @type {Boolean|Object}
421 */
422 this.singleFlt = isObj(f.single_filter) || Boolean(f.single_filter);
423
424 /**
425 * Specify columns to be excluded from single filter search, by default
426 * searching in all columns:
427 * single_filter: {
428 * exclude_cols: [2, 7]
429 * }
430 */
431 this.singleFltExcludeCols = isObj(f.single_filter) &&
432 isArray(f.single_filter.exclude_cols) ?
433 f.single_filter.exclude_cols : [];
434
435 /**
436 * Callback fired after a row is validated during filtering
437 * @type {Function}
438 */
439 this.onRowValidated = defaultsFn(f.on_row_validated, EMPTY_FN);
440
441 /**
442 * Specify which column implements a custom cell parser to retrieve the
443 * cell value:
444 * cell_parser: {
445 * cols: [0, 2],
446 * parse: function(tf, cell, colIndex) {
447 * // custom cell parser logic here
448 * return cellValue;
449 * }
450 * }
451 * @type {Object}
452 */
453 this.cellParser = isObj(f.cell_parser) && isFn(f.cell_parser.parse) &&
454 isArray(f.cell_parser.cols) ?
455 f.cell_parser : { cols: [], parse: EMPTY_FN };
456
457 /**
458 * Global watermark text for input filter type or watermark for each
459 * filter if an array is supplied
460 * @type {String|Array}
461 */
462 this.watermark = f.watermark || '';
463
464 /**
465 * Indicate whether watermark is on a per column basis
466 * @type {Boolean}
467 * @private
468 */
469 this.isWatermarkArray = isArray(this.watermark);
470
471 /**
472 * Indicate whether help UI component is disabled
473 * @type {Boolean}
474 */
475 this.help = isUndef(f.help_instructions) ? undefined :
476 (isObj(f.help_instructions) || Boolean(f.help_instructions));
477
478 /**
479 * Indicate whether pop-up filters UI is enabled
480 * @type {Boolean|Object}
481 */
482 this.popupFilters = isObj(f.popup_filters) || Boolean(f.popup_filters);
483
484 /**
485 * Indicate whether filtered (active) columns indicator is enabled
486 * @type {Boolean}
487 */
488 this.markActiveColumns = isObj(f.mark_active_columns) ||
489 Boolean(f.mark_active_columns);
490
491 /*** select filter's customisation and behaviours ***/
492 /**
493 * Text for clear option in drop-down filter types (1st option)
494 * @type {String|Array}
495 */
496 this.clearFilterText = defaultsStr(f.clear_filter_text, 'Clear');
497
498 /**
499 * Indicate whether empty option is enabled in drop-down filter types
500 * @type {Boolean}
501 */
502 this.enableEmptyOption = Boolean(f.enable_empty_option);
503
504 /**
505 * Text for empty option in drop-down filter types
506 * @type {String}
507 */
508 this.emptyText = defaultsStr(f.empty_text, '(Empty)');
509
510 /**
511 * Indicate whether non-empty option is enabled in drop-down filter
512 * types
513 * @type {Boolean}
514 */
515 this.enableNonEmptyOption = Boolean(f.enable_non_empty_option);
516
517 /**
518 * Text for non-empty option in drop-down filter types
519 * @type {String}
520 */
521 this.nonEmptyText = defaultsStr(f.non_empty_text, '(Non empty)');
522
523 /**
524 * Indicate whether drop-down filter types filter the table by default
525 * on change event
526 * @type {Boolean}
527 */
528 this.onSlcChange = defaultsBool(f.on_change, true);
529
530 /**
531 * Make drop-down filter types options sorted in alpha-numeric manner
532 * by default globally or on a column basis
533 * @type {Boolean|Array}
534 */
535 this.sortSlc = isUndef(f.sort_select) ? true :
536 isArray(f.sort_select) ? f.sort_select : Boolean(f.sort_select);
537
538 /**
539 * Indicate whether options in drop-down filter types are sorted in a
540 * ascending numeric manner
541 * @type {Boolean}
542 * @private
543 */
544 this.isSortNumAsc = Boolean(f.sort_num_asc);
545
546 /**
547 * List of columns implementing options sorting in a ascending numeric
548 * manner
549 * @type {Array}
550 */
551 this.sortNumAsc = this.isSortNumAsc ? f.sort_num_asc : [];
552
553 /**
554 * Indicate whether options in drop-down filter types are sorted in a
555 * descending numeric manner
556 * @type {Boolean}
557 * @private
558 */
559 this.isSortNumDesc = Boolean(f.sort_num_desc);
560
561 /**
562 * List of columns implementing options sorting in a descending numeric
563 * manner
564 * @type {Array}
565 */
566 this.sortNumDesc = this.isSortNumDesc ? f.sort_num_desc : [];
567
568 /**
569 * Indicate whether drop-down filter types are populated on demand at
570 * first usage
571 * @type {Boolean}
572 */
573 this.loadFltOnDemand = Boolean(f.load_filters_on_demand);
574
575 /**
576 * Indicate whether custom drop-down filter options are implemented
577 * @type {Boolean}
578 */
579 this.hasCustomOptions = isObj(f.custom_options);
580
581 /**
582 * Custom options definition of a per column basis, ie:
583 * custom_options: {
584 * cols:[0, 1],
585 * texts: [
586 * ['a0', 'b0', 'c0'],
587 * ['a1', 'b1', 'c1']
588 * ],
589 * values: [
590 * ['a0', 'b0', 'c0'],
591 * ['a1', 'b1', 'c1']
592 * ],
593 * sorts: [false, true]
594 * }
595 *
596 * @type {Object}
597 */
598 this.customOptions = f.custom_options;
599
600 /*** Filter operators ***/
601 /**
602 * Regular expression operator for input filter. Defaults to 'rgx:'
603 * @type {String}
604 */
605 this.rgxOperator = defaultsStr(f.regexp_operator, 'rgx:');
606
607 /**
608 * Empty cells operator for input filter. Defaults to '[empty]'
609 * @type {String}
610 */
611 this.emOperator = defaultsStr(f.empty_operator, '[empty]');
612
613 /**
614 * Non-empty cells operator for input filter. Defaults to '[nonempty]'
615 * @type {String}
616 */
617 this.nmOperator = defaultsStr(f.nonempty_operator, '[nonempty]');
618
619 /**
620 * Logical OR operator for input filter. Defaults to '||'
621 * @type {String}
622 */
623 this.orOperator = defaultsStr(f.or_operator, '||');
624
625 /**
626 * Logical AND operator for input filter. Defaults to '&&'
627 * @type {String}
628 */
629 this.anOperator = defaultsStr(f.and_operator, '&&');
630
631 /**
632 * Greater than operator for input filter. Defaults to '>'
633 * @type {String}
634 */
635 this.grOperator = defaultsStr(f.greater_operator, '>');
636
637 /**
638 * Lower than operator for input filter. Defaults to '<'
639 * @type {String}
640 */
641 this.lwOperator = defaultsStr(f.lower_operator, '<');
642
643 /**
644 * Lower than or equal operator for input filter. Defaults to '<='
645 * @type {String}
646 */
647 this.leOperator = defaultsStr(f.lower_equal_operator, '<=');
648
649 /**
650 * Greater than or equal operator for input filter. Defaults to '>='
651 * @type {String}
652 */
653 this.geOperator = defaultsStr(f.greater_equal_operator, '>=');
654
655 /**
656 * Inequality operator for input filter. Defaults to '!'
657 * @type {String}
658 */
659 this.dfOperator = defaultsStr(f.different_operator, '!');
660
661 /**
662 * Like operator for input filter. Defaults to '*'
663 * @type {String}
664 */
665 this.lkOperator = defaultsStr(f.like_operator, '*');
666
667 /**
668 * Strict equality operator for input filter. Defaults to '='
669 * @type {String}
670 */
671 this.eqOperator = defaultsStr(f.equal_operator, '=');
672
673 /**
674 * Starts with operator for input filter. Defaults to '='
675 * @type {String}
676 */
677 this.stOperator = defaultsStr(f.start_with_operator, '{');
678
679 /**
680 * Ends with operator for input filter. Defaults to '='
681 * @type {String}
682 */
683 this.enOperator = defaultsStr(f.end_with_operator, '}');
684
685 // this.curExp = f.cur_exp || '^[¥£€$]';
686
687 /**
688 * Stored values separator
689 * @type {String}
690 */
691 this.separator = defaultsStr(f.separator, ',');
692
693 /**
694 * Enable rows counter UI component
695 * @type {Boolean|Object}
696 */
697 this.rowsCounter = isObj(f.rows_counter) || Boolean(f.rows_counter);
698
699 /**
700 * Enable status bar UI component
701 * @type {Boolean|Object}
702 */
703 this.statusBar = isObj(f.status_bar) || Boolean(f.status_bar);
704
705 /**
706 * Enable activity/spinner indicator UI component
707 * @type {Boolean|Object}
708 */
709 this.loader = isObj(f.loader) || Boolean(f.loader);
710
711 /*** validation - reset buttons/links ***/
712 /**
713 * Enable filters submission button
714 * @type {Boolean}
715 */
716 this.displayBtn = Boolean(f.btn);
717
718 /**
719 * Define filters submission button text
720 * @type {String}
721 */
722 this.btnText = defaultsStr(f.btn_text, (!this.enableIcons ? 'Go' : ''));
723
724 /**
725 * Css class for filters submission button
726 * @type {String}
727 */
728 this.btnCssClass = defaultsStr(f.btn_css_class,
729 (!this.enableIcons ? 'btnflt' : 'btnflt_icon'));
730
731 /**
732 * Enable clear button
733 * @type {Object|Boolean}
734 */
735 this.btnReset = isObj(f.btn_reset) || Boolean(f.btn_reset);
736
737 /**
738 * Callback fired before filters are cleared
739 * @type {Function}
740 */
741 this.onBeforeReset = defaultsFn(f.on_before_reset, EMPTY_FN);
742
743 /**
744 * Callback fired after filters are cleared
745 * @type {Function}
746 */
747 this.onAfterReset = defaultsFn(f.on_after_reset, EMPTY_FN);
748
749 /**
750 * Enable paging component
751 * @type {Object|Boolean}
752 */
753 this.paging = isObj(f.paging) || Boolean(f.paging);
754
755 /**
756 * Number of hidden rows
757 * @type {Number}
758 * @private
759 */
760 this.nbHiddenRows = 0;
761
762 /**
763 * Enable auto-filter behaviour, table is filtered when a user
764 * stops typing
765 * @type {Object|Boolean}
766 */
767 this.autoFilter = isObj(f.auto_filter) || Boolean(f.auto_filter);
768
769 /**
770 * Auto-filter delay in milliseconds
771 * @type {Number}
772 */
773 this.autoFilterDelay = isObj(f.auto_filter) &&
774 isNumber(f.auto_filter.delay) ?
775 f.auto_filter.delay : AUTO_FILTER_DELAY;
776
777 /**
778 * Indicate whether user is typing
779 * @type {Boolean}
780 * @private
781 */
782 this.isUserTyping = null;
783
784 /**
785 * Auto-filter interval ID
786 * @type {String}
787 * @private
788 */
789 this.autoFilterTimer = null;
790
791 /**
792 * Enable keyword highlighting behaviour
793 * @type {Boolean}
794 */
795 this.highlightKeywords = Boolean(f.highlight_keywords);
796
797 /**
798 * Enable no results message UI component
799 * @type {Object|Boolean}
800 */
801 this.noResults = isObj(f.no_results_message) ||
802 Boolean(f.no_results_message);
803
804 /**
805 * Enable state persistence
806 * @type {Object|Boolean}
807 */
808 this.state = isObj(f.state) || Boolean(f.state);
809
810 /*** data types ***/
811
812 /**
813 * Enable date type module
814 * @type {Boolean}
815 * @private
816 */
817 this.dateType = true;
818
819 /**
820 * Define default locale, default to 'en' as per Sugar Date module:
821 * https://sugarjs.com/docs/#/DateLocales
822 * @type {String}
823 */
824 this.locale = defaultsStr(f.locale, 'en');
825
826 /**
827 * Define thousands separator ',' or '.', defaults to ','
828 * @type {String}
829 */
830 this.thousandsSeparator = defaultsStr(f.thousands_separator, ',');
831
832 /**
833 * Define decimal separator ',' or '.', defaults to '.'
834 * @type {String}
835 */
836 this.decimalSeparator = defaultsStr(f.decimal_separator, '.');
837
838 /**
839 * Define data types on a column basis, possible values 'string',
840 * 'number', 'formatted-number', 'date', 'ipaddress' ie:
841 * col_types : [
842 * 'string', 'date', 'number',
843 * { type: 'formatted-number', decimal: ',', thousands: '.' },
844 * { type: 'date', locale: 'en-gb' },
845 * { type: 'date', format: ['{dd}-{months}-{yyyy|yy}'] }
846 * ]
847 *
848 * Refer to https://sugarjs.com/docs/#/DateParsing for exhaustive
849 * information on date parsing formats supported by Sugar Date
850 * @type {Array}
851 */
852 this.colTypes = isArray(f.col_types) ? f.col_types : [];
853
854 /*** ids prefixes ***/
855 /**
856 * Main prefix
857 * @private
858 */
859 this.prfxTf = 'TF';
860
861 /**
862 * Filter's ID prefix (inputs - selects)
863 * @private
864 */
865 this.prfxFlt = 'flt';
866
867 /**
868 * Button's ID prefix
869 * @private
870 */
871 this.prfxValButton = 'btn';
872
873 /**
874 * Responsive Css class
875 * @private
876 */
877 this.prfxResponsive = 'resp';
878
879 /** @private */
880 this.stickyCssClass = 'sticky';
881
882 /*** extensions ***/
883 /**
884 * List of loaded extensions
885 * @type {Array}
886 */
887 this.extensions = defaultsArr(f.extensions, []);
888
889 /*** themes ***/
890 /**
891 * Enable default theme
892 * @type {Boolean}
893 */
894 this.enableDefaultTheme = Boolean(f.enable_default_theme);
895
896 /**
897 * Determine whether themes are enables
898 * @type {Boolean}
899 * @private
900 */
901 this.hasThemes = (this.enableDefaultTheme || isArray(f.themes));
902
903 /**
904 * List of themes, ie:
905 * themes: [{ name: 'skyblue' }]
906 * @type {Array}
907 */
908 this.themes = defaultsArr(f.themes, []);
909
910 /**
911 * Define path to themes assets, defaults to
912 * 'tablefilter/style/themes/'. Usage:
913 * themes: [{ name: 'skyblue' }]
914 * @type {Array}
915 */
916 this.themesPath = this.getThemesPath();
917
918 /**
919 * Enable responsive layout
920 * @type {Boolean}
921 */
922 this.responsive = Boolean(f.responsive);
923
924 /**
925 * Enable toolbar component
926 * @type {Object|Boolean}
927 */
928 this.toolbar = isObj(f.toolbar) || Boolean(f.toolbar);
929
930 /**
931 * Enable sticky headers
932 * @type {Boolean}
933 */
934 this.stickyHeaders = Boolean(f.sticky_headers);
935
936 /**
937 * Features registry
938 * @private
939 */
940 this.Mod = {};
941
942 /**
943 * Extensions registry
944 * @private
945 */
946 this.ExtRegistry = {};
947
948 // instantiate features if needed
949 this.instantiateFeatures(FEATURES);
950 }
951
952 /**
953 * Initialise features and layout
954 */
955 init() {
956 if (this.initialized) {
957 return;
958 }
959
960 // import main stylesheet
961 this.import(this.stylesheetId, this.getStylesheetPath(), null, 'link');
962
963 let Mod = this.Mod;
964 let inpclass;
965
966 //loads theme
967 this.loadThemes();
968
969 //explicitly initialise features in given order
970 this.initFeatures([
971 DateType,
972 Help,
973 State,
974 MarkActiveColumns,
975 GridLayout,
976 Loader,
977 HighlightKeyword,
978 PopupFilter
979 ]);
980
981 //filters grid is not generated
982 if (!this.fltGrid) {
983 this._initNoFilters();
984 } else {
985 let fltrow = this._insertFiltersRow();
986
987 this.nbCells = this.getCellsNb(this.refRow);
988 this.nbFilterableRows = this.getRowsNb();
989
990 let n = this.singleFlt ? 1 : this.nbCells;
991
992 //build filters
993 for (let i = 0; i < n; i++) {
994 this.emitter.emit('before-filter-init', this, i);
995
996 let fltCell = createElm(this.fltCellTag),
997 col = this.getFilterType(i);
998
999 if (this.singleFlt) {
1000 fltCell.colSpan = this.nbCells;
1001 }
1002 if (!this.gridLayout) {
1003 fltrow.appendChild(fltCell);
1004 }
1005 inpclass = (i === n - 1 && this.displayBtn) ?
1006 this.fltSmallCssClass : this.fltCssClass;
1007
1008 //only 1 input for single search
1009 if (this.singleFlt) {
1010 col = INPUT;
1011 inpclass = this.singleFltCssClass;
1012 }
1013
1014 //drop-down filters
1015 if (col === SELECT || col === MULTIPLE) {
1016 Mod.dropdown = Mod.dropdown || new Dropdown(this);
1017 Mod.dropdown.init(i, this.isExternalFlt(), fltCell);
1018 }
1019 // checklist
1020 else if (col === CHECKLIST) {
1021 Mod.checkList = Mod.checkList || new CheckList(this);
1022 Mod.checkList.init(i, this.isExternalFlt(), fltCell);
1023 } else {
1024 this._buildInputFilter(i, inpclass, fltCell);
1025 }
1026
1027 // this adds submit button
1028 if (i === n - 1 && this.displayBtn) {
1029 this._buildSubmitButton(
1030 this.isExternalFlt() ?
1031 elm(this.externalFltIds[i]) :
1032 fltCell
1033 );
1034 }
1035
1036 this.emitter.emit('after-filter-init', this, i);
1037 }
1038
1039 this.emitter.on(['filter-focus'],
1040 (tf, filter) => this.setActiveFilterId(filter.id));
1041
1042 }//if this.fltGrid
1043
1044 /* Features */
1045 if (this.hasExcludedRows) {
1046 this.emitter.on(['after-filtering'], () => this.setExcludeRows());
1047 this.setExcludeRows();
1048 }
1049
1050 this.initFeatures([
1051 RowsCounter,
1052 StatusBar,
1053 ClearButton,
1054 AlternateRows,
1055 NoResults,
1056 Paging,
1057 Toolbar
1058 ]);
1059
1060 this.setColWidths();
1061
1062 //TF css class is added to table
1063 if (!this.gridLayout) {
1064 addClass(this.dom(), this.prfxTf);
1065 if (this.responsive) {
1066 addClass(this.dom(), this.prfxResponsive);
1067 }
1068 if (this.colWidths.length > 0) {
1069 this.setFixedLayout();
1070 }
1071 if (this.stickyHeaders && this.dom().tHead) {
1072 addClass(this.dom(), this.stickyCssClass);
1073 }
1074 }
1075
1076 /* Load extensions */
1077 this.initExtensions();
1078
1079 this.initialized = true;
1080
1081 this.onFiltersLoaded(this);
1082
1083 this.emitter.emit('initialized', this);
1084 }
1085
1086 /**
1087 * Detect <enter> key
1088 * @param {Event} evt
1089 */
1090 detectKey(evt) {
1091 if (!this.enterKey) {
1092 return;
1093 }
1094
1095 if (isKeyPressed(evt, [ENTER_KEY])) {
1096 this.filter();
1097 cancelEvt(evt);
1098 stopEvt(evt);
1099 } else {
1100 this.isUserTyping = true;
1101 root.clearInterval(this.autoFilterTimer);
1102 this.autoFilterTimer = null;
1103 }
1104 }
1105
1106 /**
1107 * Filter's keyup event: if auto-filter on, detect user is typing and filter
1108 * columns
1109 * @param {Event} evt
1110 */
1111 onKeyUp(evt) {
1112 if (!this.autoFilter) {
1113 return;
1114 }
1115 this.isUserTyping = false;
1116
1117 function filter() {
1118 root.clearInterval(this.autoFilterTimer);
1119 this.autoFilterTimer = null;
1120 if (!this.isUserTyping) {
1121 this.filter();
1122 this.isUserTyping = null;
1123 }
1124 }
1125
1126 if (isKeyPressed(evt,
1127 [ENTER_KEY, TAB_KEY, ESC_KEY, UP_ARROW_KEY, DOWN_ARROW_KEY])) {
1128 root.clearInterval(this.autoFilterTimer);
1129 this.autoFilterTimer = null;
1130 } else {
1131 if (this.autoFilterTimer !== null) {
1132 return;
1133 }
1134 this.autoFilterTimer = root.setInterval(
1135 filter.bind(this),
1136 this.autoFilterDelay);
1137 }
1138 }
1139
1140 /**
1141 * Filter's keydown event: if auto-filter on, detect user is typing
1142 */
1143 onKeyDown() {
1144 if (this.autoFilter) {
1145 this.isUserTyping = true;
1146 }
1147 }
1148
1149 /**
1150 * Filter's focus event
1151 * @param {Event} evt
1152 */
1153 onInpFocus(evt) {
1154 let elm = targetEvt(evt);
1155 this.emitter.emit('filter-focus', this, elm);
1156 }
1157
1158 /**
1159 * Filter's blur event: if auto-filter on, clear interval on filter blur
1160 */
1161 onInpBlur() {
1162 if (this.autoFilter) {
1163 this.isUserTyping = false;
1164 root.clearInterval(this.autoFilterTimer);
1165 }
1166 this.emitter.emit('filter-blur', this);
1167 }
1168
1169 /**
1170 * Insert filters row at initialization
1171 */
1172 _insertFiltersRow() {
1173 // TODO: prevent filters row generation for popup filters too,
1174 // to reduce and simplify headers row index adjusting across lib modules
1175 // (GridLayout, PopupFilter etc)
1176 if (this.gridLayout) {
1177 return;
1178 }
1179 let fltrow;
1180
1181 let thead = tag(this.dom(), 'thead');
1182 if (thead.length > 0) {
1183 fltrow = thead[0].insertRow(this.filtersRowIndex);
1184 } else {
1185 fltrow = this.dom().insertRow(this.filtersRowIndex);
1186 }
1187
1188 fltrow.className = this.fltsRowCssClass;
1189
1190 if (this.isExternalFlt()) {
1191 fltrow.style.display = NONE;
1192 }
1193
1194 this.emitter.emit('filters-row-inserted', this, fltrow);
1195 return fltrow;
1196 }
1197
1198 /**
1199 * Initialize filtersless table
1200 */
1201 _initNoFilters() {
1202 if (this.fltGrid) {
1203 return;
1204 }
1205 this.refRow = this.refRow > 0 ? this.refRow - 1 : 0;
1206 this.nbFilterableRows = this.getRowsNb();
1207 }
1208
1209 /**
1210 * Build input filter type
1211 * @param {Number} colIndex Column index
1212 * @param {String} cssClass Css class applied to filter
1213 * @param {DOMElement} container Container DOM element
1214 */
1215 _buildInputFilter(colIndex, cssClass, container) {
1216 let col = this.getFilterType(colIndex);
1217 let externalFltTgtId = this.isExternalFlt() ?
1218 this.externalFltIds[colIndex] : null;
1219 let inpType = col === INPUT ? 'text' : 'hidden';
1220 let inp = createElm(INPUT,
1221 ['id', this.buildFilterId(colIndex)],
1222 ['type', inpType], ['ct', colIndex]);
1223
1224 if (inpType !== 'hidden' && this.watermark) {
1225 inp.setAttribute('placeholder',
1226 this.isWatermarkArray ? (this.watermark[colIndex] || '') :
1227 this.watermark
1228 );
1229 }
1230 inp.className = cssClass || this.fltCssClass;
1231 addEvt(inp, 'focus', (evt) => this.onInpFocus(evt));
1232
1233 //filter is appended in custom element
1234 if (externalFltTgtId) {
1235 elm(externalFltTgtId).appendChild(inp);
1236 } else {
1237 container.appendChild(inp);
1238 }
1239
1240 this.fltIds.push(inp.id);
1241
1242 addEvt(inp, 'keypress', (evt) => this.detectKey(evt));
1243 addEvt(inp, 'keydown', () => this.onKeyDown());
1244 addEvt(inp, 'keyup', (evt) => this.onKeyUp(evt));
1245 addEvt(inp, 'blur', () => this.onInpBlur());
1246 }
1247
1248 /**
1249 * Build submit button
1250 * @param {DOMElement} container Container DOM element
1251 */
1252 _buildSubmitButton(container) {
1253 let btn = createElm(INPUT,
1254 ['type', 'button'],
1255 ['value', this.btnText]
1256 );
1257 btn.className = this.btnCssClass;
1258
1259 //filter is appended in container element
1260 container.appendChild(btn);
1261
1262 addEvt(btn, 'click', () => this.filter());
1263 }
1264
1265 /**
1266 * Conditionally istantiate each feature class in passed collection if
1267 * required by configuration and add it to the features registry. A feature
1268 * class meta information contains a `name` field and optional `altName` and
1269 * `alwaysInstantiate` fields
1270 * @param {Array} [features=[]]
1271 * @private
1272 */
1273 instantiateFeatures(features = []) {
1274 features.forEach(featureCls => {
1275 let Cls = featureCls;
1276
1277 // assign meta info if not present
1278 Cls.meta = Cls.meta || {name: null, altName: null};
1279 Cls.meta.name = toCamelCase(Cls.name);
1280 let {name, altName, alwaysInstantiate} = Cls.meta;
1281 let prop = altName || name;
1282
1283 if (!this.hasConfig || this[prop] === true
1284 || Boolean(alwaysInstantiate)) {
1285 this.Mod[name] = this.Mod[name] || new Cls(this);
1286 }
1287 });
1288 }
1289
1290 /**
1291 * Initialise each feature class in passed collection.
1292 * @param {Array} [features=[]]
1293 * @private
1294 */
1295 initFeatures(features = []) {
1296 features.forEach(featureCls => {
1297 let {name, altName} = featureCls.meta;
1298 let prop = altName || name;
1299
1300 if (this[prop] === 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
1479 this.emitter.off(['filter-focus'],
1480 (tf, filter) => this.setActiveFilterId(filter.id));
1481
1482 removeClass(this.dom(), this.prfxTf);
1483 removeClass(this.dom(), this.prfxResponsive);
1484 if (this.dom().tHead) {
1485 removeClass(this.dom().tHead, this.stickyCssClass);
1486 }
1487
1488 this.nbHiddenRows = 0;
1489 this.validRowsIndex = [];
1490 this.fltIds = [];
1491 this.initialized = false;
1492 }
1493
1494 /**
1495 * Remove all the external column filters
1496 */
1497 removeExternalFlts() {
1498 if (!this.isExternalFlt()) {
1499 return;
1500 }
1501 let ids = this.externalFltIds;
1502 ids.forEach((id) => {
1503 let externalFlt = elm(id);
1504 if (externalFlt) {
1505 externalFlt.innerHTML = '';
1506 }
1507 });
1508 }
1509
1510 /**
1511 * Check if given column implements a filter with custom options
1512 * @param {Number} colIndex Column's index
1513 * @return {Boolean}
1514 */
1515 isCustomOptions(colIndex) {
1516 return this.hasCustomOptions &&
1517 this.customOptions.cols.indexOf(colIndex) !== -1;
1518 }
1519
1520 /**
1521 * Returns an array [[value0, value1 ...],[text0, text1 ...]] with the
1522 * custom options values and texts
1523 * @param {Number} colIndex Column's index
1524 * @return {Array}
1525 */
1526 getCustomOptions(colIndex) {
1527 if (isEmpty(colIndex) || !this.isCustomOptions(colIndex)) {
1528 return;
1529 }
1530
1531 let customOptions = this.customOptions;
1532 let cols = customOptions.cols;
1533 let optTxt = [], optArray = [];
1534 let index = cols.indexOf(colIndex);
1535 let slcValues = customOptions.values[index];
1536 let slcTexts = customOptions.texts[index];
1537 let slcSort = customOptions.sorts[index];
1538
1539 for (let r = 0, len = slcValues.length; r < len; r++) {
1540 optArray.push(slcValues[r]);
1541 if (slcTexts[r]) {
1542 optTxt.push(slcTexts[r]);
1543 } else {
1544 optTxt.push(slcValues[r]);
1545 }
1546 }
1547 if (slcSort) {
1548 optArray.sort();
1549 optTxt.sort();
1550 }
1551 return [optArray, optTxt];
1552 }
1553
1554 /**
1555 * Filter the table by retrieving the data from each cell in every single
1556 * row and comparing it to the search term for current column. A row is
1557 * hidden when all the search terms are not found in inspected row.
1558 */
1559 filter() {
1560 if (!this.fltGrid || !this.initialized) {
1561 return;
1562 }
1563
1564 let emitter = this.emitter;
1565
1566 //fire onbefore callback
1567 this.onBeforeFilter(this);
1568 emitter.emit('before-filtering', this);
1569
1570 let hiddenRows = 0;
1571
1572 this.validRowsIndex = [];
1573 // search args
1574 let searchArgs = this.getFiltersValue();
1575
1576 let eachRow = this.eachRow();
1577 eachRow(
1578 (row, k) => {
1579 // already filtered rows display re-init
1580 row.style.display = '';
1581
1582 let cells = row.cells;
1583 let nbCells = cells.length;
1584
1585 let occurence = [],
1586 isMatch = true,
1587 //only for single filter search
1588 isSingleFltMatch = false;
1589
1590 // this loop retrieves cell data
1591 for (let j = 0; j < nbCells; j++) {
1592 //searched keyword
1593 let sA = searchArgs[this.singleFlt ? 0 : j];
1594
1595 if (sA === '') {
1596 continue;
1597 }
1598
1599 let cellValue = matchCase(this.getCellValue(cells[j]),
1600 this.caseSensitive);
1601
1602 //multiple search parameter operator ||
1603 let sAOrSplit = sA.toString().split(this.orOperator),
1604 //multiple search || parameter boolean
1605 hasMultiOrSA = sAOrSplit.length > 1,
1606 //multiple search parameter operator &&
1607 sAAndSplit = sA.toString().split(this.anOperator),
1608 //multiple search && parameter boolean
1609 hasMultiAndSA = sAAndSplit.length > 1;
1610
1611 //detect operators or array query
1612 if (isArray(sA) || hasMultiOrSA || hasMultiAndSA) {
1613 let cS, s;
1614 let found = false;
1615
1616 if (isArray(sA)) {
1617 s = sA;
1618 } else {
1619 s = hasMultiOrSA ? sAOrSplit : sAAndSplit;
1620 }
1621 // isolate search term and check occurence in cell data
1622 for (let w = 0, len = s.length; w < len; w++) {
1623 cS = trim(s[w]);
1624 found = this._match(cS, cellValue, cells[j]);
1625
1626 if (found) {
1627 emitter.emit('highlight-keyword', this,
1628 cells[j], cS);
1629 }
1630 if ((hasMultiOrSA && found) ||
1631 (hasMultiAndSA && !found)) {
1632 break;
1633 }
1634 if (isArray(sA) && found) {
1635 break;
1636 }
1637 }
1638 occurence[j] = found;
1639
1640 }
1641 //single search parameter
1642 else {
1643 occurence[j] =
1644 this._match(trim(sA), cellValue, cells[j]);
1645 if (occurence[j]) {
1646 emitter.emit('highlight-keyword', this, cells[j],
1647 sA);
1648 }
1649 }
1650
1651 if (!occurence[j]) {
1652 isMatch = false;
1653 }
1654
1655 if (this.singleFlt &&
1656 this.singleFltExcludeCols.indexOf(j) === -1 &&
1657 occurence[j]) {
1658 isSingleFltMatch = true;
1659 }
1660
1661 emitter.emit('cell-processed', this, j, cells[j]);
1662 }//for j
1663
1664 if (isSingleFltMatch) {
1665 isMatch = true;
1666 }
1667
1668 this.validateRow(k, isMatch);
1669 if (!isMatch) {
1670 hiddenRows++;
1671 }
1672
1673 emitter.emit('row-processed', this, k,
1674 this.validRowsIndex.length - 1, isMatch);
1675 },
1676 // continue condition
1677 (row) => row.cells.length !== this.nbCells
1678 );
1679
1680 this.nbHiddenRows = hiddenRows;
1681
1682 //fire onafterfilter callback
1683 this.onAfterFilter(this);
1684
1685 emitter.emit('after-filtering', this, searchArgs);
1686 }
1687
1688 /**
1689 * Match search term in cell data
1690 * @param {String} term Search term
1691 * @param {String} cellValue Cell data
1692 * @param {DOMElement} cell Current cell
1693 * @return {Boolean}
1694 * @private
1695 */
1696 _match(term, cellValue, cell) {
1697 let numData;
1698 let colIdx = cell.cellIndex;
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 = !cell.hasChildNodes();
1801 }
1802 //non-empty
1803 else if (hasNM) {
1804 occurence = cell.hasChildNodes();
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 = !cell.hasChildNodes();
1891 }
1892 //non-empty
1893 else if (hasNM) {
1894 occurence = cell.hasChildNodes();
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 //multiple selects
2498 if (fltColType === MULTIPLE) {
2499 let values = isArray(query) ? query :
2500 query.split(' ' + this.orOperator + ' ');
2501
2502 if (this.loadFltOnDemand && !this.initialized) {
2503 this.emitter.emit('build-select-filter', this, index,
2504 this.linkedFilters, this.isExternalFlt());
2505 }
2506
2507 this.emitter.emit('select-options', this, index, values);
2508 }
2509 //checklist
2510 else if (fltColType === CHECKLIST) {
2511 let values = [];
2512 if (this.loadFltOnDemand && !this.initialized) {
2513 this.emitter.emit('build-checklist-filter', this, index,
2514 this.linkedFilters);
2515 }
2516 if (isArray(query)) {
2517 values = query;
2518 } else {
2519 query = matchCase(query, this.caseSensitive);
2520 values = query.split(' ' + this.orOperator + ' ');
2521 }
2522
2523 this.emitter.emit('select-checklist-options', this, index, values);
2524 }
2525 else {
2526 if (this.loadFltOnDemand && !this.initialized) {
2527 this.emitter.emit('build-select-filter', this, index,
2528 this.linkedFilters, this.isExternalFlt());
2529 }
2530 slc.value = query;
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 * Determine if passed filter column implements exact query match
2705 * @param {Number} colIndex Column index
2706 * @return {Boolean}
2707 */
2708 isExactMatch(colIndex) {
2709 let fltType = this.getFilterType(colIndex);
2710 return this.exactMatchByCol[colIndex] || this.exactMatch ||
2711 fltType !== INPUT;
2712 }
2713
2714 /**
2715 * Check if passed row is valid
2716 * @param {Number} rowIndex Row index
2717 * @return {Boolean}
2718 */
2719 isRowValid(rowIndex) {
2720 return this.getValidRows().indexOf(rowIndex) !== -1;
2721 }
2722
2723 /**
2724 * Check if passed row is visible
2725 * @param {Number} rowIndex Row index
2726 * @return {Boolean}
2727 */
2728 isRowDisplayed(rowIndex) {
2729 let row = this.dom().rows[rowIndex];
2730 return this.getRowDisplay(row) === '';
2731 }
2732
2733 /**
2734 * Check if specified column filter ignores diacritics.
2735 * Note this is only applicable to input filter types.
2736 * @param {Number} colIndex Column index
2737 * @return {Boolean}
2738 */
2739 ignoresDiacritics(colIndex) {
2740 let ignoreDiac = this.ignoreDiacritics;
2741 if (isArray(ignoreDiac)) {
2742 return ignoreDiac[colIndex];
2743 }
2744 return Boolean(ignoreDiac);
2745 }
2746
2747 /**
2748 * Return clear all text for specified filter column
2749 * @param {Number} colIndex Column index
2750 * @return {String}
2751 */
2752 getClearFilterText(colIndex) {
2753 let clearText = this.clearFilterText;
2754 if (isArray(clearText)) {
2755 return clearText[colIndex];
2756 }
2757 return clearText;
2758 }
2759
2760 /**
2761 * Column iterator invoking continue and break condition callbacks if any
2762 * then calling supplied callback for each item
2763 * @param {Function} [fn=EMPTY_FN] callback
2764 * @param {Function} [continueFn=EMPTY_FN] continue condition callback
2765 * @param {Function} [breakFn=EMPTY_FN] break condition callback
2766 */
2767 eachCol(fn = EMPTY_FN, continueFn = EMPTY_FN, breakFn = EMPTY_FN) {
2768 let len = this.getCellsNb(this.refRow);
2769 for (let i = 0; i < len; i++) {
2770 if (continueFn(i) === true) {
2771 continue;
2772 }
2773 if (breakFn(i) === true) {
2774 break;
2775 }
2776 fn(i);
2777 }
2778 }
2779
2780 /**
2781 * Rows iterator starting from supplied row index or defaulting to reference
2782 * row index. Closure function accepts a callback function and optional
2783 * continue and break callbacks.
2784 * @param {Number} startIdx Row index from which filtering starts
2785 */
2786 eachRow(startIdx = this.refRow) {
2787 return (fn = EMPTY_FN, continueFn = EMPTY_FN, breakFn = EMPTY_FN) => {
2788 let rows = this.dom().rows;
2789 let len = this.getRowsNb(true);
2790 for (let i = startIdx; i < len; i++) {
2791 if (continueFn(rows[i], i) === true) {
2792 continue;
2793 }
2794 if (breakFn(rows[i], i) === true) {
2795 break;
2796 }
2797 fn(rows[i], i);
2798 }
2799 };
2800 }
2801
2802 /**
2803 * Check if passed script or stylesheet is already imported
2804 * @param {String} filePath Ressource path
2805 * @param {String} type Possible values: 'script' or 'link'
2806 * @return {Boolean}
2807 */
2808 isImported(filePath, type = 'script') {
2809 let imported = false,
2810 attr = type === 'script' ? 'src' : 'href',
2811 files = tag(doc, type);
2812 for (let i = 0, len = files.length; i < len; i++) {
2813 if (isUndef(files[i][attr])) {
2814 continue;
2815 }
2816 if (files[i][attr].match(filePath)) {
2817 imported = true;
2818 break;
2819 }
2820 }
2821 return imported;
2822 }
2823
2824 /**
2825 * Import script or stylesheet
2826 * @param {String} fileId Ressource ID
2827 * @param {String} filePath Ressource path
2828 * @param {Function} callback Callback
2829 * @param {String} type Possible values: 'script' or 'link'
2830 */
2831 import(fileId, filePath, callback, type = 'script') {
2832 if (this.isImported(filePath, type)) {
2833 return;
2834 }
2835 let o = this,
2836 isLoaded = false,
2837 file,
2838 head = tag(doc, 'head')[0];
2839
2840 if (type.toLowerCase() === 'link') {
2841 file = createElm('link',
2842 ['id', fileId], ['type', 'text/css'],
2843 ['rel', 'stylesheet'], ['href', filePath]
2844 );
2845 } else {
2846 file = createElm('script',
2847 ['id', fileId],
2848 ['type', 'text/javascript'], ['src', filePath]
2849 );
2850 }
2851
2852 //Browser <> IE onload event works only for scripts, not for stylesheets
2853 file.onload = file.onreadystatechange = () => {
2854 if (!isLoaded &&
2855 (!this.readyState || this.readyState === 'loaded' ||
2856 this.readyState === 'complete')) {
2857 isLoaded = true;
2858 if (typeof callback === 'function') {
2859 callback.call(null, o);
2860 }
2861 }
2862 };
2863 file.onerror = () => {
2864 throw new Error(`TableFilter could not load: ${filePath}`);
2865 };
2866 head.appendChild(file);
2867 }
2868
2869 /**
2870 * Check if table has filters grid
2871 * @return {Boolean}
2872 */
2873 isInitialized() {
2874 return this.initialized;
2875 }
2876
2877 /**
2878 * Get list of filter IDs
2879 * @return {Array} List of filters ids
2880 */
2881 getFiltersId() {
2882 return this.fltIds || [];
2883 }
2884
2885 /**
2886 * Get filtered (valid) rows indexes
2887 * @param {Boolean} reCalc Force calculation of filtered rows list
2888 * @return {Array} List of row indexes
2889 */
2890 getValidRows(reCalc) {
2891 if (!reCalc) {
2892 return this.validRowsIndex;
2893 }
2894
2895 this.validRowsIndex = [];
2896
2897 let eachRow = this.eachRow();
2898 eachRow((row) => {
2899 if (!this.paging) {
2900 if (this.getRowDisplay(row) !== NONE) {
2901 this.validRowsIndex.push(row.rowIndex);
2902 }
2903 } else {
2904 if (row.getAttribute('validRow') === 'true' ||
2905 row.getAttribute('validRow') === null) {
2906 this.validRowsIndex.push(row.rowIndex);
2907 }
2908 }
2909 });
2910 return this.validRowsIndex;
2911 }
2912
2913 /**
2914 * Get the index of the row containing the filters
2915 * @return {Number}
2916 */
2917 getFiltersRowIndex() {
2918 return this.filtersRowIndex;
2919 }
2920
2921 /**
2922 * Get the index of the headers row
2923 * @return {Number}
2924 */
2925 getHeadersRowIndex() {
2926 return this.headersRow;
2927 }
2928
2929 /**
2930 * Get the row index from where the filtering process start (1st filterable
2931 * row)
2932 * @return {Number}
2933 */
2934 getStartRowIndex() {
2935 return this.refRow;
2936 }
2937
2938 /**
2939 * Get the index of the last row
2940 * @return {Number}
2941 */
2942 getLastRowIndex() {
2943 let nbRows = this.getRowsNb(true);
2944 return (nbRows - 1);
2945 }
2946
2947 /**
2948 * Determine whether the specified column has one of the passed types
2949 * @param {Number} colIndex Column index
2950 * @param {Array} [types=[]] List of column types
2951 * @return {Boolean}
2952 */
2953 hasType(colIndex, types = []) {
2954 if (this.colTypes.length === 0) {
2955 return false;
2956 }
2957 let colType = this.colTypes[colIndex];
2958 if (isObj(colType)) {
2959 colType = colType.type;
2960 }
2961 return types.indexOf(colType) !== -1;
2962 }
2963
2964 /**
2965 * Get the header DOM element for a given column index
2966 * @param {Number} colIndex Column index
2967 * @return {Element}
2968 */
2969 getHeaderElement(colIndex) {
2970 let table = this.gridLayout ? this.Mod.gridLayout.headTbl : this.dom();
2971 let tHead = tag(table, 'thead');
2972 let rowIdx = this.getHeadersRowIndex();
2973 let header;
2974 if (tHead.length === 0) {
2975 header = table.rows[rowIdx].cells[colIndex];
2976 }
2977 if (tHead.length === 1) {
2978 header = tHead[0].rows[rowIdx].cells[colIndex];
2979 }
2980 return header;
2981 }
2982
2983 /**
2984 * Return the list of headers' text
2985 * @param {Boolean} excludeHiddenCols Optional: exclude hidden columns
2986 * @return {Array} list of headers' text
2987 */
2988 getHeadersText(excludeHiddenCols = false) {
2989 let headers = [];
2990 this.eachCol(
2991 (j) => {
2992 let header = this.getHeaderElement(j);
2993 let headerText = getFirstTextNode(header);
2994 headers.push(headerText);
2995 },
2996 // continue condition function
2997 (j) => {
2998 if (excludeHiddenCols && this.hasExtension('colsVisibility')) {
2999 return this.extension('colsVisibility').isColHidden(j);
3000 }
3001 return false;
3002 }
3003 );
3004 return headers;
3005 }
3006
3007 /**
3008 * Return the filter type for a specified column
3009 * @param {Number} colIndex Column's index
3010 * @return {String}
3011 */
3012 getFilterType(colIndex) {
3013 return this.filterTypes[colIndex];
3014 }
3015
3016 /**
3017 * Get the total number of filterable rows
3018 * @return {Number}
3019 */
3020 getFilterableRowsNb() {
3021 return this.getRowsNb(false);
3022 }
3023
3024 /**
3025 * Return the total number of valid rows
3026 * @param {Boolean} [reCalc=false] Forces calculation of filtered rows
3027 * @return {Number}
3028 */
3029 getValidRowsNb(reCalc = false) {
3030 return this.getValidRows(reCalc).length;
3031 }
3032
3033 /**
3034 * Return the working DOM element
3035 * @return {HTMLTableElement}
3036 */
3037 dom() {
3038 return this.tbl;
3039 }
3040
3041 /**
3042 * Return the decimal separator for supplied column as per column type
3043 * configuration or global setting
3044 * @param {Number} colIndex Column index
3045 * @returns {String} '.' or ','
3046 */
3047 getDecimal(colIndex) {
3048 let decimal = this.decimalSeparator;
3049 if (this.hasType(colIndex, [FORMATTED_NUMBER])) {
3050 let colType = this.colTypes[colIndex];
3051 if (colType.hasOwnProperty('decimal')) {
3052 decimal = colType.decimal;
3053 }
3054 }
3055 return decimal;
3056 }
3057
3058 /**
3059 * Get the configuration object (literal object)
3060 * @return {Object}
3061 */
3062 config() {
3063 return this.cfg;
3064 }
3065}