UNPKG

15.6 kBJavaScriptView Raw
1import {Feature} from '../feature';
2import {Hash} from './hash';
3import {Storage} from './storage';
4import {isEmpty} from '../string';
5import {isArray, isNull, isString, isUndef} from '../types';
6import {defaultsBool, defaultsNb} from '../settings';
7
8/**
9 * Features state object persistable with localStorage, cookie or URL hash
10 *
11 * @export
12 * @class State
13 * @extends {Feature}
14 */
15export class State extends Feature {
16
17 /**
18 * Creates an instance of State
19 * @param {TableFilter} tf TableFilter instance
20 */
21 constructor(tf) {
22 super(tf, State);
23
24 let cfg = this.config.state || {};
25
26 /**
27 * Determines whether state is persisted with URL hash
28 * @type {Boolean}
29 */
30 this.enableHash = cfg === true ||
31 (isArray(cfg.types) && cfg.types.indexOf('hash') !== -1);
32
33 /**
34 * Determines whether state is persisted with localStorage
35 * @type {Boolean}
36 */
37 this.enableLocalStorage = isArray(cfg.types) &&
38 cfg.types.indexOf('local_storage') !== -1;
39
40 /**
41 * Determines whether state is persisted with localStorage
42 * @type {Boolean}
43 */
44 this.enableCookie = isArray(cfg.types) &&
45 cfg.types.indexOf('cookie') !== -1;
46
47 /**
48 * Persist filters values, enabled by default
49 * @type {Boolean}
50 */
51 this.persistFilters = defaultsBool(cfg.filters, true);
52
53 /**
54 * Persist current page number when paging is enabled
55 * @type {Boolean}
56 */
57 this.persistPageNumber = Boolean(cfg.page_number);
58
59 /**
60 * Persist page length when paging is enabled
61 * @type {Boolean}
62 */
63 this.persistPageLength = Boolean(cfg.page_length);
64
65 /**
66 * Persist column sorting
67 * @type {Boolean}
68 */
69 this.persistSort = Boolean(cfg.sort);
70
71 /**
72 * Persist columns visibility
73 * @type {Boolean}
74 */
75 this.persistColsVisibility = Boolean(cfg.columns_visibility);
76
77 /**
78 * Persist filters row visibility
79 * @type {Boolean}
80 */
81 this.persistFiltersVisibility = Boolean(cfg.filters_visibility);
82
83 /**
84 * Cookie duration in hours
85 * @type {Boolean}
86 */
87 this.cookieDuration = defaultsNb(parseInt(cfg.cookie_duration, 10),
88 87600);
89
90 /**
91 * Enable Storage if localStorage or cookie is required
92 * @type {Boolean}
93 * @private
94 */
95 this.enableStorage = this.enableLocalStorage || this.enableCookie;
96
97 /**
98 * Storage instance if storage is required
99 * @type {Storage}
100 * @private
101 */
102 this.storage = null;
103
104 /**
105 * Hash instance if URL hash is required
106 * @type {Boolean}
107 * @private
108 */
109 this.hash = null;
110
111 /**
112 * Current page number
113 * @type {Number}
114 * @private
115 */
116 this.pageNb = null;
117
118 /**
119 * Current page length
120 * @type {Number}
121 * @private
122 */
123 this.pageLength = null;
124
125 /**
126 * Current column sorting
127 * @type {Object}
128 * @private
129 */
130 this.sort = null;
131
132 /**
133 * Current hidden columns
134 * @type {Object}
135 * @private
136 */
137 this.hiddenCols = null;
138
139 /**
140 * Filters row visibility
141 * @type {Boolean}
142 * @private
143 */
144 this.filtersVisibility = null;
145
146 /**
147 * State object
148 * @type {Object}
149 * @private
150 */
151 this.state = {};
152
153 /**
154 * Prefix for column ID
155 * @type {String}
156 * @private
157 */
158 this.prfxCol = 'col_';
159
160 /**
161 * Prefix for page number ID
162 * @type {String}
163 * @private
164 */
165 this.pageNbKey = 'page';
166
167 /**
168 * Prefix for page length ID
169 * @type {String}
170 * @private
171 */
172 this.pageLengthKey = 'page_length';
173
174 /**
175 * Prefix for filters visibility ID
176 * @type {String}
177 * @private
178 */
179 this.filtersVisKey = 'filters_visibility';
180 }
181
182 /**
183 * Initializes State instance
184 */
185 init() {
186 if (this.initialized) {
187 return;
188 }
189
190 this.emitter.on(['after-filtering'], () => this.update());
191 this.emitter.on(['after-page-change', 'after-clearing-filters'],
192 (tf, pageNb) => this.updatePage(pageNb));
193 this.emitter.on(['after-page-length-change'],
194 (tf, pageLength) => this.updatePageLength(pageLength));
195 this.emitter.on(['column-sorted'],
196 (tf, index, descending) => this.updateSort(index, descending));
197 this.emitter.on(['sort-initialized'], () => this._syncSort());
198 this.emitter.on(['columns-visibility-initialized'],
199 () => this._syncColsVisibility());
200 this.emitter.on(['column-shown', 'column-hidden'], (tf, feature,
201 colIndex, hiddenCols) => this.updateColsVisibility(hiddenCols));
202 this.emitter.on(['filters-visibility-initialized'],
203 () => this._syncFiltersVisibility());
204 this.emitter.on(['filters-toggled'],
205 (tf, extension, visible) => this.updateFiltersVisibility(visible));
206
207 if (this.enableHash) {
208 this.hash = new Hash(this);
209 this.hash.init();
210 }
211 if (this.enableStorage) {
212 this.storage = new Storage(this);
213 this.storage.init();
214 }
215
216 /** @inherited */
217 this.initialized = true;
218 }
219
220
221 /**
222 * Update state object based on current features state
223 */
224 update() {
225 if (!this.isEnabled()) {
226 return;
227 }
228 let state = this.state;
229 let tf = this.tf;
230
231 if (this.persistFilters) {
232 let filterValues = tf.getFiltersValue();
233
234 filterValues.forEach((val, idx) => {
235 let key = `${this.prfxCol}${idx}`;
236
237 if (isString(val) && isEmpty(val)) {
238 if (state.hasOwnProperty(key)) {
239 state[key].flt = undefined;
240 }
241 } else {
242 state[key] = state[key] || {};
243 state[key].flt = val;
244 }
245 });
246 }
247
248 if (this.persistPageNumber) {
249 if (isNull(this.pageNb)) {
250 state[this.pageNbKey] = undefined;
251 } else {
252 state[this.pageNbKey] = this.pageNb;
253 }
254 }
255
256 if (this.persistPageLength) {
257 if (isNull(this.pageLength)) {
258 state[this.pageLengthKey] = undefined;
259 } else {
260 state[this.pageLengthKey] = this.pageLength;
261 }
262 }
263
264 if (this.persistSort) {
265 if (!isNull(this.sort)) {
266 // Remove previuosly sorted column
267 Object.keys(state).forEach((key) => {
268 if (key.indexOf(this.prfxCol) !== -1 && state[key]) {
269 state[key].sort = undefined;
270 }
271 });
272
273 let key = `${this.prfxCol}${this.sort.column}`;
274 state[key] = state[key] || {};
275 state[key].sort = { descending: this.sort.descending };
276 }
277 }
278
279 if (this.persistColsVisibility) {
280 if (!isNull(this.hiddenCols)) {
281 // Clear previuosly hidden columns
282 Object.keys(state).forEach((key) => {
283 if (key.indexOf(this.prfxCol) !== -1 && state[key]) {
284 state[key].hidden = undefined;
285 }
286 });
287
288 this.hiddenCols.forEach((colIdx) => {
289 let key = `${this.prfxCol}${colIdx}`;
290 state[key] = state[key] || {};
291 state[key].hidden = true;
292 });
293 }
294 }
295
296 if (this.persistFiltersVisibility) {
297 if (isNull(this.filtersVisibility)) {
298 state[this.filtersVisKey] = undefined;
299 } else {
300 state[this.filtersVisKey] = this.filtersVisibility;
301 }
302 }
303
304 this.emitter.emit('state-changed', tf, state);
305 }
306
307 /**
308 * Refresh page number field on page number changes
309 *
310 * @param {Number} pageNb Current page number
311 */
312 updatePage(pageNb) {
313 this.pageNb = pageNb;
314 this.update();
315 }
316
317 /**
318 * Refresh page length field on page length changes
319 *
320 * @param {Number} pageLength Current page length value
321 */
322 updatePageLength(pageLength) {
323 this.pageLength = pageLength;
324 this.update();
325 }
326
327 /**
328 * Refresh column sorting information on sort changes
329 *
330 * @param index {Number} Column index
331 * @param {Boolean} descending Descending manner
332 */
333 updateSort(index, descending) {
334 this.sort = {
335 column: index,
336 descending: descending
337 };
338 this.update();
339 }
340
341 /**
342 * Refresh hidden columns information on columns visibility changes
343 *
344 * @param {Array} hiddenCols Columns indexes
345 */
346 updateColsVisibility(hiddenCols) {
347 this.hiddenCols = hiddenCols;
348 this.update();
349 }
350
351 /**
352 * Refresh filters visibility on filters visibility change
353 *
354 * @param {Boolean} visible Visibility flad
355 */
356 updateFiltersVisibility(visible) {
357 this.filtersVisibility = visible;
358 this.update();
359 }
360
361 /**
362 * Override state field
363 *
364 * @param state State object
365 */
366 override(state) {
367 this.state = state;
368 this.emitter.emit('state-changed', this.tf, state);
369 }
370
371 /**
372 * Sync stored features state
373 */
374 sync() {
375 let state = this.state;
376 let tf = this.tf;
377
378 this._syncFilters();
379
380 if (this.persistPageNumber) {
381 let pageNumber = state[this.pageNbKey];
382 this.emitter.emit('change-page', tf, pageNumber);
383 }
384
385 if (this.persistPageLength) {
386 let pageLength = state[this.pageLengthKey];
387 this.emitter.emit('change-page-results', tf, pageLength);
388 }
389
390 this._syncSort();
391 this._syncColsVisibility();
392 this._syncFiltersVisibility();
393 }
394
395 /**
396 * Override current state with passed one and sync features
397 *
398 * @param {Object} state State object
399 */
400 overrideAndSync(state) {
401 // To prevent state to react to features changes, state is temporarily
402 // disabled
403 this.disable();
404 // State is overriden with passed state object
405 this.override(state);
406 // New hash state is applied to features
407 this.sync();
408 // State is re-enabled
409 this.enable();
410 }
411
412 /**
413 * Sync filters with stored values and filter table
414 *
415 * @private
416 */
417 _syncFilters() {
418 if (!this.persistFilters) {
419 return;
420 }
421 let state = this.state;
422 let tf = this.tf;
423
424 // clear all filters
425 // TODO: use tf.clearFilters() once it allows to not filter the table
426 tf.eachCol((colIdx) => tf.setFilterValue(colIdx, ''));
427
428 Object.keys(state).forEach((key) => {
429 if (key.indexOf(this.prfxCol) !== -1) {
430 let colIdx = parseInt(key.replace(this.prfxCol, ''), 10);
431 let val = state[key].flt;
432 tf.setFilterValue(colIdx, val);
433 }
434 });
435
436 tf.filter();
437 }
438
439 /**
440 * Sync sorted column with stored sorting information and sort table
441 *
442 * @private
443 */
444 _syncSort() {
445 if (!this.persistSort) {
446 return;
447 }
448 let state = this.state;
449 let tf = this.tf;
450
451 Object.keys(state).forEach((key) => {
452 if (key.indexOf(this.prfxCol) !== -1) {
453 let colIdx = parseInt(key.replace(this.prfxCol, ''), 10);
454 if (!isUndef(state[key].sort)) {
455 let sort = state[key].sort;
456 this.emitter.emit('sort', tf, colIdx, sort.descending);
457 }
458 }
459 });
460 }
461
462 /**
463 * Sync hidden columns with stored information
464 *
465 * @private
466 */
467 _syncColsVisibility() {
468 if (!this.persistColsVisibility) {
469 return;
470 }
471 let state = this.state;
472 let tf = this.tf;
473 let hiddenCols = [];
474
475 Object.keys(state).forEach((key) => {
476 if (key.indexOf(this.prfxCol) !== -1) {
477 let colIdx = parseInt(key.replace(this.prfxCol, ''), 10);
478 if (!isUndef(state[key].hidden)) {
479 hiddenCols.push(colIdx);
480 }
481 }
482 });
483
484 hiddenCols.forEach((colIdx) => {
485 this.emitter.emit('hide-column', tf, colIdx);
486 });
487 }
488
489 /**
490 * Sync filters visibility with stored information
491 *
492 * @private
493 */
494 _syncFiltersVisibility() {
495 if (!this.persistFiltersVisibility) {
496 return;
497 }
498 let state = this.state;
499 let tf = this.tf;
500 let filtersVisibility = state[this.filtersVisKey];
501
502 this.filtersVisibility = filtersVisibility;
503 this.emitter.emit('show-filters', tf, filtersVisibility);
504 }
505
506 /**
507 * Destroy State instance
508 */
509 destroy() {
510 if (!this.initialized) {
511 return;
512 }
513
514 this.state = {};
515
516 this.emitter.off(['after-filtering'], () => this.update());
517 this.emitter.off(['after-page-change', 'after-clearing-filters'],
518 (tf, pageNb) => this.updatePage(pageNb));
519 this.emitter.off(['after-page-length-change'],
520 (tf, index) => this.updatePageLength(index));
521 this.emitter.off(['column-sorted'],
522 (tf, index, descending) => this.updateSort(index, descending));
523 this.emitter.off(['sort-initialized'], () => this._syncSort());
524 this.emitter.off(['columns-visibility-initialized'],
525 () => this._syncColsVisibility());
526 this.emitter.off(['column-shown', 'column-hidden'], (tf, feature,
527 colIndex, hiddenCols) => this.updateColsVisibility(hiddenCols));
528 this.emitter.off(['filters-visibility-initialized'],
529 () => this._syncFiltersVisibility());
530 this.emitter.off(['filters-toggled'],
531 (tf, extension, visible) => this.updateFiltersVisibility(visible));
532
533 if (this.enableHash) {
534 this.hash.destroy();
535 this.hash = null;
536 }
537
538 if (this.enableStorage) {
539 this.storage.destroy();
540 this.storage = null;
541 }
542
543 this.initialized = false;
544 }
545}