UNPKG

13.8 kBJavaScriptView Raw
1import {Feature} from '../feature';
2import {isUndef, EMPTY_FN} from '../types';
3import {createElm, removeElm} from '../dom';
4import {addEvt, cancelEvt, stopEvt, targetEvt, removeEvt} from '../event';
5import {INPUT, NONE, CHECKLIST, MULTIPLE} from '../const';
6import {root} from '../root';
7import {defaultsStr, defaultsBool, defaultsArr, defaultsFn} from '../settings';
8
9/**
10 * Pop-up filter component
11 * @export
12 * @class PopupFilter
13 * @extends {Feature}
14 */
15export class PopupFilter extends Feature {
16
17 /**
18 * Creates an instance of PopupFilter
19 * @param {TableFilter} tf TableFilter instance
20 */
21 constructor(tf) {
22 super(tf, PopupFilter);
23
24 // Configuration object
25 let f = this.config.popup_filters || {};
26
27 /**
28 * Close active popup filter upon filtering, enabled by default
29 * @type {Boolean}
30 */
31 this.closeOnFiltering = defaultsBool(f.close_on_filtering, true);
32
33 /**
34 * Filter icon path
35 * @type {String}
36 */
37 this.iconPath = defaultsStr(f.image, tf.themesPath + 'icn_filter.gif');
38
39 /**
40 * Active filter icon path
41 * @type {string}
42 */
43 this.activeIconPath = defaultsStr(f.image_active,
44 tf.themesPath + 'icn_filterActive.gif');
45
46 /**
47 * HTML for the filter icon
48 * @type {string}
49 */
50 this.iconHtml = defaultsStr(f.image_html,
51 '<img src="' + this.iconPath + '" alt="Column filter" />');
52
53 /**
54 * Css class assigned to the popup container element
55 * @type {String}
56 */
57 this.placeholderCssClass = defaultsStr(f.placeholder_css_class,
58 'popUpPlaceholder');
59
60 /**
61 * Css class assigned to filter container element
62 * @type {String}
63 */
64 this.containerCssClass = defaultsStr(f.div_css_class, 'popUpFilter');
65
66 /**
67 * Ensure filter's container element width matches column width, enabled
68 * by default
69 * @type {Boolean}
70 */
71 this.adjustToContainer = defaultsBool(f.adjust_to_container, true);
72
73 /**
74 * Callback fired before a popup filter is opened
75 * @type {Function}
76 */
77 this.onBeforeOpen = defaultsFn(f.on_before_popup_filter_open, EMPTY_FN);
78
79 /**
80 * Callback fired after a popup filter is opened
81 * @type {Function}
82 */
83 this.onAfterOpen = defaultsFn(f.on_after_popup_filter_open, EMPTY_FN);
84
85 /**
86 * Callback fired before a popup filter is closed
87 * @type {Function}
88 */
89 this.onBeforeClose = defaultsFn(f.on_before_popup_filter_close,
90 EMPTY_FN);
91
92 /**
93 * Callback fired after a popup filter is closed
94 * @type {Function}
95 */
96 this.onAfterClose = defaultsFn(f.on_after_popup_filter_close, EMPTY_FN);
97
98 /**
99 * Collection of filters spans
100 * @type {Array}
101 * @private
102 */
103 this.fltSpans = [];
104
105 /**
106 * Collection of filters icons
107 * @type {Array}
108 * @private
109 */
110 this.fltIcons = [];
111
112 /**
113 * Collection of filters icons cached after pop-up filters are removed
114 * @type {Array}
115 * @private
116 */
117 this.filtersCache = null;
118
119 /**
120 * Collection of filters containers
121 * @type {Array}
122 * @private
123 */
124 this.fltElms = defaultsArr(this.filtersCache, []);
125
126 /**
127 * Prefix for pop-up filter container ID
128 * @type {String}
129 * @private
130 */
131 this.prfxDiv = 'popup_';
132
133 /**
134 * Column index of popup filter currently active
135 * @type {Number}
136 * @private
137 */
138 this.activeFilterIdx = -1;
139 }
140
141 /**
142 * Click event handler for pop-up filter icon
143 * @private
144 */
145 onClick(evt) {
146 let elm = targetEvt(evt).parentNode;
147 let colIndex = parseInt(elm.getAttribute('ci'), 10);
148
149 this.closeAll(colIndex);
150 this.toggle(colIndex);
151
152 if (this.adjustToContainer) {
153 let cont = this.fltElms[colIndex],
154 header = this.tf.getHeaderElement(colIndex),
155 headerWidth = header.clientWidth * 0.95;
156 cont.style.width = parseInt(headerWidth, 10) + 'px';
157 }
158 cancelEvt(evt);
159 stopEvt(evt);
160 }
161
162 /**
163 * Mouse-up event handler handling popup filter auto-close behaviour
164 * @private
165 */
166 onMouseup(evt) {
167 if (this.activeFilterIdx === -1) {
168 return;
169 }
170 let targetElm = targetEvt(evt);
171 let activeFlt = this.fltElms[this.activeFilterIdx];
172 let icon = this.fltIcons[this.activeFilterIdx];
173
174 if (icon === targetElm) {
175 return;
176 }
177
178 while (targetElm && targetElm !== activeFlt) {
179 targetElm = targetElm.parentNode;
180 }
181
182 if (targetElm !== activeFlt) {
183 this.close(this.activeFilterIdx);
184 }
185
186 return;
187 }
188
189 /**
190 * Initialize DOM elements
191 */
192 init() {
193 if (this.initialized) {
194 return;
195 }
196
197 let tf = this.tf;
198
199 // Enable external filters
200 tf.externalFltIds = [''];
201
202 // Override filters row index supplied by configuration
203 tf.filtersRowIndex = 0;
204
205 // Override headers row index if no grouped headers
206 // TODO: Because of the filters row generation, headers row index needs
207 // adjusting: prevent useless row generation
208 if (tf.headersRow <= 1 && isNaN(tf.config().headers_row_index)) {
209 tf.headersRow = 0;
210 }
211
212 // Adjust headers row index for grid-layout mode
213 // TODO: Because of the filters row generation, headers row index needs
214 // adjusting: prevent useless row generation
215 if (tf.gridLayout) {
216 tf.headersRow--;
217 this.buildIcons();
218 }
219
220 // subscribe to events
221 this.emitter.on(['before-filtering'], () => this.setIconsState());
222 this.emitter.on(['after-filtering'], () => this.closeAll());
223 this.emitter.on(['cell-processed'],
224 (tf, cellIndex) => this.changeState(cellIndex, true));
225 this.emitter.on(['filters-row-inserted'], () => this.buildIcons());
226 this.emitter.on(['before-filter-init'],
227 (tf, colIndex) => this.build(colIndex));
228
229 /** @inherited */
230 this.initialized = true;
231 }
232
233 /**
234 * Reset previously destroyed feature
235 */
236 reset() {
237 this.enable();
238 this.init();
239 this.buildIcons();
240 this.buildAll();
241 }
242
243 /**
244 * Build all filters icons
245 */
246 buildIcons() {
247 let tf = this.tf;
248
249 // TODO: Because of the filters row generation, headers row index needs
250 // adjusting: prevent useless row generation
251 tf.headersRow++;
252
253 tf.eachCol(
254 (i) => {
255 let icon = createElm('span', ['ci', i]);
256 icon.innerHTML = this.iconHtml;
257 let header = tf.getHeaderElement(i);
258 header.appendChild(icon);
259 addEvt(icon, 'click', (evt) => this.onClick(evt));
260 this.fltSpans[i] = icon;
261 this.fltIcons[i] = icon.firstChild;
262 },
263 // continue condition function
264 (i) => tf.getFilterType(i) === NONE
265 );
266 }
267
268 /**
269 * Build all pop-up filters elements
270 */
271 buildAll() {
272 for (let i = 0; i < this.filtersCache.length; i++) {
273 this.build(i, this.filtersCache[i]);
274 }
275 }
276
277 /**
278 * Build a specified pop-up filter elements
279 * @param {Number} colIndex Column index
280 * @param {Object} div Optional container DOM element
281 */
282 build(colIndex, div) {
283 let tf = this.tf;
284 let contId = `${this.prfxDiv}${tf.id}_${colIndex}`;
285 let placeholder = createElm('div', ['class', this.placeholderCssClass]);
286 let cont = div ||
287 createElm('div', ['id', contId], ['class', this.containerCssClass]);
288 tf.externalFltIds[colIndex] = cont.id;
289 placeholder.appendChild(cont);
290
291 let header = tf.getHeaderElement(colIndex);
292 header.insertBefore(placeholder, header.firstChild);
293 addEvt(cont, 'click', (evt) => stopEvt(evt));
294 this.fltElms[colIndex] = cont;
295 }
296
297 /**
298 * Toggle visibility of specified filter
299 * @param {Number} colIndex Column index
300 */
301 toggle(colIndex) {
302 if (!this.isOpen(colIndex)) {
303 this.open(colIndex);
304 } else {
305 this.close(colIndex);
306 }
307 }
308
309 /**
310 * Open popup filter of specified column
311 * @param {Number} colIndex Column index
312 */
313 open(colIndex) {
314 let tf = this.tf,
315 container = this.fltElms[colIndex];
316
317 this.onBeforeOpen(this, container, colIndex);
318
319 container.style.display = 'block';
320 this.activeFilterIdx = colIndex;
321 addEvt(root, 'mouseup', (evt) => this.onMouseup(evt));
322
323 if (tf.getFilterType(colIndex) === INPUT) {
324 let flt = tf.getFilterElement(colIndex);
325 if (flt) {
326 flt.focus();
327 }
328 }
329
330 this.onAfterOpen(this, container, colIndex);
331 }
332
333 /**
334 * Close popup filter of specified column
335 * @param {Number} colIndex Column index
336 */
337 close(colIndex) {
338 let container = this.fltElms[colIndex];
339
340 this.onBeforeClose(this, container, colIndex);
341
342 container.style.display = NONE;
343 if (this.activeFilterIdx === colIndex) {
344 this.activeFilterIdx = -1;
345 }
346 removeEvt(root, 'mouseup', (evt) => this.onMouseup(evt));
347
348 this.onAfterClose(this, container, colIndex);
349 }
350
351 /**
352 * Check if popup filter for specified column is open
353 * @param {Number} colIndex Column index
354 * @returns {Boolean}
355 */
356 isOpen(colIndex) {
357 return this.fltElms[colIndex].style.display === 'block';
358 }
359
360 /**
361 * Close all filters excepted for the specified one if any
362 * @param {Number} exceptIdx Column index of the filter to not close
363 */
364 closeAll(exceptIdx) {
365 // Do not close filters only if argument is undefined and close on
366 // filtering option is disabled
367 if (isUndef(exceptIdx) && !this.closeOnFiltering) {
368 return;
369 }
370 for (let i = 0; i < this.fltElms.length; i++) {
371 if (i === exceptIdx) {
372 continue;
373 }
374 let fltType = this.tf.getFilterType(i);
375 let isMultipleFilter =
376 (fltType === CHECKLIST || fltType === MULTIPLE);
377
378 // Always hide all single selection filter types but hide multiple
379 // selection filter types only if index set
380 if (!isMultipleFilter || !isUndef(exceptIdx)) {
381 this.close(i);
382 }
383 }
384 }
385
386 /**
387 * Build all the icons representing the pop-up filters
388 */
389 setIconsState() {
390 for (let i = 0; i < this.fltIcons.length; i++) {
391 this.changeState(i, false);
392 }
393 }
394
395 /**
396 * Apply specified icon state
397 * @param {Number} colIndex Column index
398 * @param {Boolean} active Apply active state
399 */
400 changeState(colIndex, active) {
401 let icon = this.fltIcons[colIndex];
402 if (icon) {
403 icon.src = active ? this.activeIconPath : this.iconPath;
404 }
405 }
406
407 /**
408 * Remove pop-up filters
409 */
410 destroy() {
411 if (!this.initialized) {
412 return;
413 }
414
415 this.filtersCache = [];
416 for (let i = 0; i < this.fltElms.length; i++) {
417 let container = this.fltElms[i],
418 placeholder = container.parentNode,
419 icon = this.fltSpans[i],
420 iconImg = this.fltIcons[i];
421 if (container) {
422 removeElm(container);
423 this.filtersCache[i] = container;
424 }
425 container = null;
426 if (placeholder) {
427 removeElm(placeholder);
428 }
429 placeholder = null;
430 if (icon) {
431 removeElm(icon);
432 }
433 icon = null;
434 if (iconImg) {
435 removeElm(iconImg);
436 }
437 iconImg = null;
438 }
439 this.fltElms = [];
440 this.fltSpans = [];
441 this.fltIcons = [];
442
443 // TODO: expose an API to handle external filter IDs
444 this.tf.externalFltIds = [];
445
446 // unsubscribe to events
447 this.emitter.off(['before-filtering'], () => this.setIconsState());
448 this.emitter.off(['after-filtering'], () => this.closeAll());
449 this.emitter.off(['cell-processed'],
450 (tf, cellIndex) => this.changeState(cellIndex, true));
451 this.emitter.off(['filters-row-inserted'], () => this.buildIcons());
452 this.emitter.off(['before-filter-init'],
453 (tf, colIndex) => this.build(colIndex));
454
455 this.initialized = false;
456 }
457
458}
459
460// TODO: remove as soon as feature name is fixed
461PopupFilter.meta = {altName: 'popupFilters'};