Home Reference Source

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;
    }
}