Home Reference Source

js/models/HashManager.js

/**
 * Manages location hashes.
 */
export default class HashManager {

    /**
     * Global object
     * @type {HashManager}
     */
    static shared = new HashManager();

    /**
     * Takes no args
     */
    constructor() {
        // Attempt to get current hash
        const currentHash = window.location.hash.substring(1);
        const parts = currentHash.substring(1).split("?");

        // Attempt to decode URI
        let navigationSection,
            contextSection;

        if (currentHash[0] !== '!') {
            navigationSection = "";
            contextSection = "";
        } else {
            [navigationSection = "", contextSection = ""] = parts;
        }

        let existingData = new Map(),
            existingNavigation = [];

        if (navigationSection) {
            existingNavigation = navigationSection.substring(1).split('/');
        }

        if (contextSection) {
            existingData = new Map(contextSection.split('&')
                .map(entry => entry.split('='))
                .filter(entry => entry.length !== 2)
                .map((parts) => parts.map(decodeURIComponent)));
        }

        this.state = existingData;
        this.navigation = existingNavigation;
    }

    /**
     * Updates the state to store a new key
     * @param {string} key
     * @param {string} value
     */
    setContextValue(key, value) {
        this.state.set(key, value);
        this.resyncHash();
    }

    /**
     * Sets the navigation key. If depth is greater than supported than empty.
     * @param {number} depth - navigation item depth.
     * @param {string} position - navigation string value
     */
    setNavigationValue(depth, position) {
        this.navigation[depth] = position;
        this.resyncHash();
    }

    /**
     * Gets navigation value for depth
     * @param {number} depth - navigation item depth
     * @return {string} position string
     */
    getNavigationValue(depth) {
        return this.navigation[depth];
    }

    /**
     * Obtains the value of a key
     * @param {string} key
     * @return {?string}
     */
    getContextValue(key) {
        return this.state.get(key);
    }

    /**
     * Returns string representing navigation hash
     * @type {string}
     */
    get locationHash() {
        if (this.navigation.length === 0) return '';

        return `/${this.navigation.join("/")}`;
    }

    /**
     * Gets hash repesenting value
     */
    get valueHash() {
        const entries = [...this.state.entries()];

        if (entries.length === 0) return '';

        return '?' + entries
            .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
            .join('&');
    }

    /**
     * Returns string representing
     * @return {string}
     */
    get hash() {
        const hash = this.locationHash + this.valueHash;
        if (hash === '') return '';
        return '!' + hash;
    }

    /**
     * You shouldn't need to call this but it updates the hash in the URL
     */
    resyncHash() {
        window.location.hash = this.hash;
    }

}