UNPKG

16.9 kBJavaScriptView Raw
1import {BaseDropdown} from './baseDropdown';
2import {
3 addClass, createCheckItem, createText, createElm, elm, removeClass, tag
4} from '../dom';
5import {has} from '../array';
6import {matchCase, trim, rgxEsc} from '../string';
7import {addEvt, removeEvt, targetEvt} from '../event';
8import {isEmpty} from '../types';
9import {CHECKLIST, NONE} from '../const';
10import {defaultsStr, defaultsBool} from '../settings';
11
12/**
13 * Checklist filter UI component
14 * @export
15 * @class CheckList
16 * @extends {BaseDropdown}
17 */
18export class CheckList extends BaseDropdown {
19
20 /**
21 * Creates an instance of CheckList
22 * @param {TableFilter} tf TableFilter instance
23 */
24 constructor(tf) {
25 super(tf, CheckList);
26
27 let f = this.config;
28
29 /**
30 * List of container DOM elements
31 * @type {Array}
32 */
33 this.containers = [];
34
35 /**
36 * Css class for the container of the checklist filter (div)
37 * @type {String}
38 */
39 this.containerCssClass = defaultsStr(f.div_checklist_css_class,
40 'div_checklist');
41
42 /**
43 * Css class for the checklist filter element (ul)
44 * @type {String}
45 */
46 this.filterCssClass = defaultsStr(f.checklist_css_class,
47 'flt_checklist');
48
49 /**
50 * Css class for the item of a checklist (li)
51 * @type {String}
52 */
53 this.itemCssClass = defaultsStr(f.checklist_item_css_class,
54 'flt_checklist_item');
55
56 /**
57 * Css class for a selected item of a checklist (li)
58 * @type {String}
59 */
60 this.selectedItemCssClass = defaultsStr(
61 f.checklist_selected_item_css_class,
62 'flt_checklist_slc_item'
63 );
64
65 /**
66 * Text placed in the filter's container when load filter on demand
67 * feature is enabled
68 * @type {String}
69 */
70 this.activateText = defaultsStr(
71 f.activate_checklist_text,
72 'Click to load filter data'
73 );
74
75 /**
76 * Css class for a disabled item of a checklist (li)
77 * @type {String}
78 */
79 this.disabledItemCssClass = defaultsStr(
80 f.checklist_item_disabled_css_class,
81 'flt_checklist_item_disabled'
82 );
83
84 /**
85 * Enable the reset filter option as first item
86 * @type {Boolean}
87 */
88 this.enableResetOption = defaultsBool(f.enable_checklist_reset_filter,
89 true);
90
91 /**
92 * Prefix for container element ID
93 * @type {String}
94 * @private
95 */
96 this.prfx = 'chkdiv_';
97 }
98
99 /**
100 * Checklist option click event handler
101 * @param {Event} evt
102 * @private
103 */
104 optionClick(evt) {
105 let elm = targetEvt(evt);
106 let tf = this.tf;
107
108 this.emitter.emit('filter-focus', tf, elm);
109 this.setItemOption(elm);
110 tf.filter();
111 }
112
113 /**
114 * Checklist container click event handler for load-on-demand feature
115 * @param {Event} evt
116 * @private
117 */
118 onCheckListClick(evt) {
119 let elm = targetEvt(evt);
120 if (this.tf.loadFltOnDemand && elm.getAttribute('filled') === '0') {
121 let ct = elm.getAttribute('ct');
122 let div = this.containers[ct];
123 this.build(ct);
124 removeEvt(div, 'click', (evt) => this.onCheckListClick(evt));
125 }
126 }
127
128 /**
129 * Refresh all checklist filters
130 */
131 refreshAll() {
132 let colIdxs = this.tf.getFiltersByType(CHECKLIST, true);
133 this.refreshFilters(colIdxs);
134 }
135
136 /**
137 * Initialize checklist filter
138 * @param {Number} colIndex Column index
139 * @param {Boolean} isExternal External filter flag
140 * @param {DOMElement} container Dom element containing the filter
141 */
142 init(colIndex, isExternal, container) {
143 let tf = this.tf;
144 let externalFltTgtId = isExternal ?
145 tf.externalFltIds[colIndex] : null;
146
147 let divCont = createElm('div',
148 ['id', `${this.prfx}${colIndex}_${tf.id}`],
149 ['ct', colIndex], ['filled', '0']);
150 divCont.className = this.containerCssClass;
151
152 //filter is appended in desired element
153 if (externalFltTgtId) {
154 elm(externalFltTgtId).appendChild(divCont);
155 } else {
156 container.appendChild(divCont);
157 }
158
159 this.containers[colIndex] = divCont;
160 tf.fltIds.push(tf.buildFilterId(colIndex));
161
162 if (!tf.loadFltOnDemand) {
163 this.build(colIndex);
164 } else {
165 addEvt(divCont, 'click', (evt) => this.onCheckListClick(evt));
166 divCont.appendChild(createText(this.activateText));
167 }
168
169 this.emitter.on(
170 ['build-checklist-filter'],
171 (tf, colIndex, isLinked) => this.build(colIndex, isLinked)
172 );
173
174 this.emitter.on(
175 ['select-checklist-options'],
176 (tf, colIndex, values) => this.selectOptions(colIndex, values)
177 );
178
179 this.emitter.on(['rows-changed'], () => this.refreshAll());
180
181 this.emitter.on(['after-filtering'], () => this.linkFilters());
182
183 /** @inherited */
184 this.initialized = true;
185 }
186
187 /**
188 * Build checklist UI
189 * @param {Number} colIndex Column index
190 * @param {Boolean} isLinked Enable linked filters behaviour
191 */
192 build(colIndex, isLinked = false) {
193 let tf = this.tf;
194 colIndex = Number(colIndex);
195
196 this.emitter.emit('before-populating-filter', tf, colIndex);
197
198 /** @inherited */
199 this.opts = [];
200 /** @inherited */
201 this.optsTxt = [];
202
203 let flt = this.containers[colIndex];
204 let ul = createElm('ul',
205 ['id', tf.fltIds[colIndex]],
206 ['colIndex', colIndex]);
207 ul.className = this.filterCssClass;
208
209 let caseSensitive = tf.caseSensitive;
210 /** @inherited */
211 this.isCustom = tf.isCustomOptions(colIndex);
212
213 //Retrieves custom values
214 if (this.isCustom) {
215 let customValues = tf.getCustomOptions(colIndex);
216 this.opts = customValues[0];
217 this.optsTxt = customValues[1];
218 }
219
220 let activeIdx;
221 let activeFilterId = tf.getActiveFilterId();
222
223 if (isLinked && activeFilterId) {
224 activeIdx = tf.getColumnIndexFromFilterId(activeFilterId);
225 }
226
227 let filteredDataCol = [];
228 if (isLinked && tf.disableExcludedOptions) {
229 /** @inherited */
230 this.excludedOpts = [];
231 }
232
233 flt.innerHTML = '';
234
235 let eachRow = tf.eachRow();
236 eachRow(
237 (row) => {
238 let cellValue = tf.getCellValue(row.cells[colIndex]);
239 //Vary Peter's patch
240 let cellString = matchCase(cellValue, caseSensitive);
241 // checks if celldata is already in array
242 if (!has(this.opts, cellString, caseSensitive)) {
243 this.opts.push(cellValue);
244 }
245 let filteredCol = filteredDataCol[colIndex];
246 if (isLinked && tf.disableExcludedOptions) {
247 if (!filteredCol) {
248 filteredCol = tf.getVisibleColumnValues(colIndex);
249 }
250 if (!has(filteredCol, cellString, caseSensitive) &&
251 !has(this.excludedOpts, cellString, caseSensitive)) {
252 this.excludedOpts.push(cellValue);
253 }
254 }
255 },
256 // continue conditions function
257 (row, k) => {
258 // excluded rows don't need to appear on selects as always valid
259 if (tf.excludeRows.indexOf(k) !== -1) {
260 return true;
261 }
262
263 // checks if row has expected number of cells
264 if (row.cells.length !== tf.nbCells || this.isCustom) {
265 return true;
266 }
267
268 if (isLinked && !this.isValidLinkedValue(k, activeIdx)) {
269 return true;
270 }
271 }
272 );
273
274 //sort options
275 this.opts = this.sortOptions(colIndex, this.opts);
276 if (this.excludedOpts) {
277 this.excludedOpts = this.sortOptions(colIndex, this.excludedOpts);
278 }
279
280 this.addChecks(colIndex, ul);
281
282 if (tf.loadFltOnDemand) {
283 flt.innerHTML = '';
284 }
285 flt.appendChild(ul);
286 flt.setAttribute('filled', '1');
287
288 this.emitter.emit('after-populating-filter', tf, colIndex, flt);
289 }
290
291 /**
292 * Add checklist options
293 * @param {Number} colIndex Column index
294 * @param {Object} ul Ul element
295 * @private
296 */
297 addChecks(colIndex, ul) {
298 let tf = this.tf;
299 let chkCt = this.addTChecks(colIndex, ul);
300
301 for (let y = 0; y < this.opts.length; y++) {
302 let val = this.opts[y]; //item value
303 let lbl = this.isCustom ? this.optsTxt[y] : val; //item text
304 let fltId = tf.fltIds[colIndex];
305 let lblIdx = y + chkCt;
306 let li = createCheckItem(`${fltId}_${lblIdx}`, val, lbl,
307 ['data-idx', lblIdx]);
308 li.className = this.itemCssClass;
309
310 if (tf.linkedFilters && tf.disableExcludedOptions &&
311 has(this.excludedOpts, matchCase(val, tf.caseSensitive),
312 tf.caseSensitive)) {
313 addClass(li, this.disabledItemCssClass);
314 li.check.disabled = true;
315 li.disabled = true;
316 } else {
317 addEvt(li.check, 'click', evt => this.optionClick(evt));
318 }
319 ul.appendChild(li);
320
321 if (val === '') {
322 //item is hidden
323 li.style.display = NONE;
324 }
325 }
326 }
327
328 /**
329 * Add checklist header option
330 * @param {Number} colIndex Column index
331 * @param {Object} ul Ul element
332 * @private
333 */
334 addTChecks(colIndex, ul) {
335 let tf = this.tf;
336 let chkCt = 1;
337 let fltId = tf.fltIds[colIndex];
338 let li0 = createCheckItem(`${fltId}_0`, '',
339 tf.getClearFilterText(colIndex), ['data-idx', 0]);
340 li0.className = this.itemCssClass;
341 ul.appendChild(li0);
342
343 addEvt(li0.check, 'click', evt => this.optionClick(evt));
344
345 if (!this.enableResetOption) {
346 li0.style.display = NONE;
347 }
348
349 if (tf.enableEmptyOption) {
350 let li1 = createCheckItem(`${fltId}_1`, tf.emOperator,
351 tf.emptyText, ['data-idx', 1]);
352 li1.className = this.itemCssClass;
353 ul.appendChild(li1);
354 addEvt(li1.check, 'click', evt => this.optionClick(evt));
355 chkCt++;
356 }
357
358 if (tf.enableNonEmptyOption) {
359 let li2 = createCheckItem(`${fltId}_2`, tf.nmOperator,
360 tf.nonEmptyText, ['data-idx', 2]);
361 li2.className = this.itemCssClass;
362 ul.appendChild(li2);
363 addEvt(li2.check, 'click', evt => this.optionClick(evt));
364 chkCt++;
365 }
366 return chkCt;
367 }
368
369 /**
370 * Set/unset value of passed item option in filter's DOM element attribute
371 * @param {Object} o checklist option DOM element
372 * @private
373 */
374 setItemOption(o) {
375 if (!o) {
376 return;
377 }
378
379 let tf = this.tf;
380 let chkValue = o.value; //checked item value
381 let chkIndex = o.dataset.idx;
382 let colIdx = tf.getColumnIndexFromFilterId(o.id);
383 let n = tf.getFilterElement(parseInt(colIdx, 10));
384 let items = n.childNodes;
385 let li = items[chkIndex];
386 //selected values (ul tag)
387 let slcValues = n.getAttribute('value') || '';
388 //selected items indexes (ul tag)
389 let slcIndexes = n.getAttribute('indexes') || '';
390
391 if (o.checked) {
392 //show all item
393 if (chkValue === '') {
394 //items indexes
395 let indexes = slcIndexes.split(tf.separator);
396 indexes.forEach(idx => {
397 idx = Number(idx);
398 let li = items[idx];
399 let chx = tag(li, 'input')[0];
400 if (chx && idx > 0) {
401 chx.checked = false;
402 removeClass(li, this.selectedItemCssClass);
403 }
404 });
405
406 n.setAttribute('value', '');
407 n.setAttribute('indexes', '');
408
409 } else {
410 let indexes = slcIndexes + chkIndex + tf.separator;
411 let values =
412 trim(slcValues + ' ' + chkValue + ' ' + tf.orOperator);
413
414 n.setAttribute('value', values);
415 n.setAttribute('indexes', indexes);
416
417 //uncheck first option
418 let chx0 = tag(items[0], 'input')[0];
419 if (chx0) {
420 chx0.checked = false;
421 }
422 }
423
424 removeClass(items[0], this.selectedItemCssClass);
425 addClass(li, this.selectedItemCssClass);
426 } else { //removes values and indexes
427 let replaceValue =
428 new RegExp(rgxEsc(chkValue + ' ' + tf.orOperator));
429 let values = slcValues.replace(replaceValue, '');
430 let replaceIndex = new RegExp(rgxEsc(chkIndex + tf.separator));
431 let indexes = slcIndexes.replace(replaceIndex, '');
432
433 n.setAttribute('value', trim(values));
434 n.setAttribute('indexes', indexes);
435
436 removeClass(li, this.selectedItemCssClass);
437 }
438 }
439
440 /**
441 * Select filter options programmatically
442 * @param {Number} colIndex Column index
443 * @param {Array} values Array of option values to select
444 */
445 selectOptions(colIndex, values = []) {
446 let tf = this.tf;
447 let flt = tf.getFilterElement(colIndex);
448 if (!flt || values.length === 0) {
449 return;
450 }
451
452 let lis = tag(flt, 'li');
453
454 flt.setAttribute('value', '');
455 flt.setAttribute('indexes', '');
456
457 [].forEach.call(lis, (li) => {
458 let chk = tag(li, 'input')[0];
459 let chkVal = matchCase(chk.value, tf.caseSensitive);
460
461 if (chkVal !== '' && has(values, chkVal, tf.caseSensitive)) {
462 chk.checked = true;
463 } else {
464 // Check non-empty-text or empty-text option
465 if (values.indexOf(tf.nmOperator) !== -1 &&
466 chkVal === matchCase(tf.nonEmptyText, tf.caseSensitive)) {
467 chk.checked = true;
468 }
469 else if (values.indexOf(tf.emOperator) !== -1 &&
470 chkVal === matchCase(tf.emptyText, tf.caseSensitive)) {
471 chk.checked = true;
472 } else {
473 chk.checked = false;
474 }
475 }
476 this.setItemOption(chk);
477 });
478 }
479
480 /**
481 * Get filter values for a given column index
482 * @param {Number} colIndex Column index
483 * @returns {Array} values Collection of selected values
484 */
485 getValues(colIndex) {
486 let tf = this.tf;
487 let flt = tf.getFilterElement(colIndex);
488 if (!flt) {
489 return [];
490 }
491
492 let fltAttr = flt.getAttribute('value');
493 let values = isEmpty(fltAttr) ? '' : fltAttr;
494 //removes last operator ||
495 values = values.substr(0, values.length - 3);
496 //turn || separated values into array
497 values = values.split(' ' + tf.orOperator + ' ');
498
499 return values;
500 }
501
502 /**
503 * Destroy CheckList instance
504 */
505 destroy() {
506 this.emitter.off(
507 ['build-checklist-filter'],
508 (tf, colIndex, isLinked) => this.build(colIndex, isLinked)
509 );
510 this.emitter.off(
511 ['select-checklist-options'],
512 (tf, colIndex, values) => this.selectOptions(colIndex, values)
513 );
514 this.emitter.off(['rows-changed'], () => this.refreshAll());
515 this.emitter.off(['after-filtering'], () => this.linkFilters());
516
517 this.initialized = false;
518 }
519}