UNPKG

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