src/modules/state.js
import {Feature} from '../feature';
import {Hash} from './hash';
import {Storage} from './storage';
import {isEmpty} from '../string';
import {isArray, isNull, isString, isUndef} from '../types';
import {defaultsBool, defaultsNb} from '../settings';
/**
* Features state object persistable with localStorage, cookie or URL hash
*
* @export
* @class State
* @extends {Feature}
*/
export class State extends Feature {
/**
* Creates an instance of State
* @param {TableFilter} tf TableFilter instance
*/
constructor(tf) {
super(tf, 'state');
let cfg = this.config.state || {};
/**
* Determines whether state is persisted with URL hash
* @type {Boolean}
*/
this.enableHash = cfg === true ||
(isArray(cfg.types) && cfg.types.indexOf('hash') !== -1);
/**
* Determines whether state is persisted with localStorage
* @type {Boolean}
*/
this.enableLocalStorage = isArray(cfg.types) &&
cfg.types.indexOf('local_storage') !== -1;
/**
* Determines whether state is persisted with localStorage
* @type {Boolean}
*/
this.enableCookie = isArray(cfg.types) &&
cfg.types.indexOf('cookie') !== -1;
/**
* Persist filters values, enabled by default
* @type {Boolean}
*/
this.persistFilters = defaultsBool(cfg.filters, true);
/**
* Persist current page number when paging is enabled
* @type {Boolean}
*/
this.persistPageNumber = Boolean(cfg.page_number);
/**
* Persist page length when paging is enabled
* @type {Boolean}
*/
this.persistPageLength = Boolean(cfg.page_length);
/**
* Persist column sorting
* @type {Boolean}
*/
this.persistSort = Boolean(cfg.sort);
/**
* Persist columns visibility
* @type {Boolean}
*/
this.persistColsVisibility = Boolean(cfg.columns_visibility);
/**
* Persist filters row visibility
* @type {Boolean}
*/
this.persistFiltersVisibility = Boolean(cfg.filters_visibility);
/**
* Cookie duration in hours
* @type {Boolean}
*/
this.cookieDuration = defaultsNb(parseInt(cfg.cookie_duration, 10),
87600);
/**
* Enable Storage if localStorage or cookie is required
* @type {Boolean}
* @private
*/
this.enableStorage = this.enableLocalStorage || this.enableCookie;
/**
* Storage instance if storage is required
* @type {Storage}
* @private
*/
this.storage = null;
/**
* Hash instance if URL hash is required
* @type {Boolean}
* @private
*/
this.hash = null;
/**
* Current page number
* @type {Number}
* @private
*/
this.pageNb = null;
/**
* Current page length
* @type {Number}
* @private
*/
this.pageLength = null;
/**
* Current column sorting
* @type {Object}
* @private
*/
this.sort = null;
/**
* Current hidden columns
* @type {Object}
* @private
*/
this.hiddenCols = null;
/**
* Filters row visibility
* @type {Boolean}
* @private
*/
this.filtersVisibility = null;
/**
* State object
* @type {Object}
* @private
*/
this.state = {};
/**
* Prefix for column ID
* @type {String}
* @private
*/
this.prfxCol = 'col_';
/**
* Prefix for page number ID
* @type {String}
* @private
*/
this.pageNbKey = 'page';
/**
* Prefix for page length ID
* @type {String}
* @private
*/
this.pageLengthKey = 'page_length';
/**
* Prefix for filters visibility ID
* @type {String}
* @private
*/
this.filtersVisKey = 'filters_visibility';
}
/**
* Initializes State instance
*/
init() {
if (this.initialized) {
return;
}
this.emitter.on(['after-filtering'], () => this.update());
this.emitter.on(['after-page-change', 'after-clearing-filters'],
(tf, pageNb) => this.updatePage(pageNb));
this.emitter.on(['after-page-length-change'],
(tf, pageLength) => this.updatePageLength(pageLength));
this.emitter.on(['column-sorted'],
(tf, index, descending) => this.updateSort(index, descending));
this.emitter.on(['sort-initialized'], () => this._syncSort());
this.emitter.on(['columns-visibility-initialized'],
() => this._syncColsVisibility());
this.emitter.on(['column-shown', 'column-hidden'], (tf, feature,
colIndex, hiddenCols) => this.updateColsVisibility(hiddenCols));
this.emitter.on(['filters-visibility-initialized'],
() => this._syncFiltersVisibility());
this.emitter.on(['filters-toggled'],
(tf, extension, visible) => this.updateFiltersVisibility(visible));
if (this.enableHash) {
this.hash = new Hash(this);
this.hash.init();
}
if (this.enableStorage) {
this.storage = new Storage(this);
this.storage.init();
}
/** @inherited */
this.initialized = true;
}
/**
* Update state object based on current features state
*/
update() {
if (!this.isEnabled()) {
return;
}
let state = this.state;
let tf = this.tf;
if (this.persistFilters) {
let filterValues = tf.getFiltersValue();
filterValues.forEach((val, idx) => {
let key = `${this.prfxCol}${idx}`;
if (isString(val) && isEmpty(val)) {
if (state.hasOwnProperty(key)) {
state[key].flt = undefined;
}
} else {
state[key] = state[key] || {};
state[key].flt = val;
}
});
}
if (this.persistPageNumber) {
if (isNull(this.pageNb)) {
state[this.pageNbKey] = undefined;
} else {
state[this.pageNbKey] = this.pageNb;
}
}
if (this.persistPageLength) {
if (isNull(this.pageLength)) {
state[this.pageLengthKey] = undefined;
} else {
state[this.pageLengthKey] = this.pageLength;
}
}
if (this.persistSort) {
if (!isNull(this.sort)) {
// Remove previuosly sorted column
Object.keys(state).forEach((key) => {
if (key.indexOf(this.prfxCol) !== -1 && state[key]) {
state[key].sort = undefined;
}
});
let key = `${this.prfxCol}${this.sort.column}`;
state[key] = state[key] || {};
state[key].sort = { descending: this.sort.descending };
}
}
if (this.persistColsVisibility) {
if (!isNull(this.hiddenCols)) {
// Clear previuosly hidden columns
Object.keys(state).forEach((key) => {
if (key.indexOf(this.prfxCol) !== -1 && state[key]) {
state[key].hidden = undefined;
}
});
this.hiddenCols.forEach((colIdx) => {
let key = `${this.prfxCol}${colIdx}`;
state[key] = state[key] || {};
state[key].hidden = true;
});
}
}
if (this.persistFiltersVisibility) {
if (isNull(this.filtersVisibility)) {
state[this.filtersVisKey] = undefined;
} else {
state[this.filtersVisKey] = this.filtersVisibility;
}
}
this.emitter.emit('state-changed', tf, state);
}
/**
* Refresh page number field on page number changes
*
* @param {Number} pageNb Current page number
*/
updatePage(pageNb) {
this.pageNb = pageNb;
this.update();
}
/**
* Refresh page length field on page length changes
*
* @param {Number} pageLength Current page length value
*/
updatePageLength(pageLength) {
this.pageLength = pageLength;
this.update();
}
/**
* Refresh column sorting information on sort changes
*
* @param index {Number} Column index
* @param {Boolean} descending Descending manner
*/
updateSort(index, descending) {
this.sort = {
column: index,
descending: descending
};
this.update();
}
/**
* Refresh hidden columns information on columns visibility changes
*
* @param {Array} hiddenCols Columns indexes
*/
updateColsVisibility(hiddenCols) {
this.hiddenCols = hiddenCols;
this.update();
}
/**
* Refresh filters visibility on filters visibility change
*
* @param {Boolean} visible Visibility flad
*/
updateFiltersVisibility(visible) {
this.filtersVisibility = visible;
this.update();
}
/**
* Override state field
*
* @param state State object
*/
override(state) {
this.state = state;
this.emitter.emit('state-changed', this.tf, state);
}
/**
* Sync stored features state
*/
sync() {
let state = this.state;
let tf = this.tf;
this._syncFilters();
if (this.persistPageNumber) {
let pageNumber = state[this.pageNbKey];
this.emitter.emit('change-page', tf, pageNumber);
}
if (this.persistPageLength) {
let pageLength = state[this.pageLengthKey];
this.emitter.emit('change-page-results', tf, pageLength);
}
this._syncSort();
this._syncColsVisibility();
this._syncFiltersVisibility();
}
/**
* Override current state with passed one and sync features
*
* @param {Object} state State object
*/
overrideAndSync(state) {
// To prevent state to react to features changes, state is temporarily
// disabled
this.disable();
// State is overriden with passed state object
this.override(state);
// New hash state is applied to features
this.sync();
// State is re-enabled
this.enable();
}
/**
* Sync filters with stored values and filter table
*
* @private
*/
_syncFilters() {
if (!this.persistFilters) {
return;
}
let state = this.state;
let tf = this.tf;
// clear all filters
// TODO: use tf.clearFilters() once it allows to not filter the table
tf.eachCol((colIdx) => tf.setFilterValue(colIdx, ''));
Object.keys(state).forEach((key) => {
if (key.indexOf(this.prfxCol) !== -1) {
let colIdx = parseInt(key.replace(this.prfxCol, ''), 10);
let val = state[key].flt;
tf.setFilterValue(colIdx, val);
}
});
tf.filter();
}
/**
* Sync sorted column with stored sorting information and sort table
*
* @private
*/
_syncSort() {
if (!this.persistSort) {
return;
}
let state = this.state;
let tf = this.tf;
Object.keys(state).forEach((key) => {
if (key.indexOf(this.prfxCol) !== -1) {
let colIdx = parseInt(key.replace(this.prfxCol, ''), 10);
if (!isUndef(state[key].sort)) {
let sort = state[key].sort;
this.emitter.emit('sort', tf, colIdx, sort.descending);
}
}
});
}
/**
* Sync hidden columns with stored information
*
* @private
*/
_syncColsVisibility() {
if (!this.persistColsVisibility) {
return;
}
let state = this.state;
let tf = this.tf;
let hiddenCols = [];
Object.keys(state).forEach((key) => {
if (key.indexOf(this.prfxCol) !== -1) {
let colIdx = parseInt(key.replace(this.prfxCol, ''), 10);
if (!isUndef(state[key].hidden)) {
hiddenCols.push(colIdx);
}
}
});
hiddenCols.forEach((colIdx) => {
this.emitter.emit('hide-column', tf, colIdx);
});
}
/**
* Sync filters visibility with stored information
*
* @private
*/
_syncFiltersVisibility() {
if (!this.persistFiltersVisibility) {
return;
}
let state = this.state;
let tf = this.tf;
let filtersVisibility = state[this.filtersVisKey];
this.filtersVisibility = filtersVisibility;
this.emitter.emit('show-filters', tf, filtersVisibility);
}
/**
* Destroy State instance
*/
destroy() {
if (!this.initialized) {
return;
}
this.state = {};
this.emitter.off(['after-filtering'], () => this.update());
this.emitter.off(['after-page-change', 'after-clearing-filters'],
(tf, pageNb) => this.updatePage(pageNb));
this.emitter.off(['after-page-length-change'],
(tf, index) => this.updatePageLength(index));
this.emitter.off(['column-sorted'],
(tf, index, descending) => this.updateSort(index, descending));
this.emitter.off(['sort-initialized'], () => this._syncSort());
this.emitter.off(['columns-visibility-initialized'],
() => this._syncColsVisibility());
this.emitter.off(['column-shown', 'column-hidden'], (tf, feature,
colIndex, hiddenCols) => this.updateColsVisibility(hiddenCols));
this.emitter.off(['filters-visibility-initialized'],
() => this._syncFiltersVisibility());
this.emitter.off(['filters-toggled'],
(tf, extension, visible) => this.updateFiltersVisibility(visible));
if (this.enableHash) {
this.hash.destroy();
this.hash = null;
}
if (this.enableStorage) {
this.storage.destroy();
this.storage = null;
}
this.initialized = false;
}
}