Source: ssc.cookie.js

/**
 * @file A Singleton wrapper for the document cookie.
 * @author Marc-Oliver Stühmer <stuehmer@codefoo.org>
 * @license MIT
 * @copyright 2021 Marc-Oliver Stühmer
 */

window.SSC = window.SSC || {};

/**
 * <b>NOTE:</b> The cookie cannot be instantiated directly! Use [SSC.Cookie.getInstance()]{@link SSC.Cookie.getInstance} instead.
 *
 * @class
 * @classdesc A Singleton wrapper for the document cookie.
 * @see SSC.Cookie.getInstance
 *
 * @example
 * const cookie = SSC.Cookie.getInstance('testcookie');
 * cookie
 *     .set('foo', 'bar')
 *     .set('baz', 42)
 *     .setSecure()
 *     .write();
 * console.log(cookie.getAll());
 */
SSC.Cookie = (() => {
    'use strict';

    /**
     * Container object with Singleton instances of Cookie class
     *
     * @private
     *
     * @type {Object}
     */
    const instances = {};

    /**
     * The actual cookie class.
     *
     * @private
     *
     * @param {string} [cookieName]  The name of the cookie
     */
    const Cookie = function (cookieName) {
        /**
         * The current cookie name
         *
         * @private
         *
         * @type {string}
         */
        let cname = cookieName;

        /**
         * The cookie's data
         *
         * @private
         *
         * @type {Object}
         */
        let data = {};

        /**
         * The cookie's expiry time
         *
         * @private
         *
         * @type {string}
         */
        let expires;

        /**
         * The path for which the cookie is active
         *
         * @private
         *
         * @type {string}
         */
        let path = '/';

        /**
         * The cookie domain
         *
         * @private
         *
         * @type {string}
         */
        let domain;

        /**
         * Use SSL when sending the cookie to the server?
         *
         * @private
         *
         * @type {boolean}
         */
        let sec = false;

        /**
         * Write the internal data object into the document cookie.
         *
         * @private
         *
         * @throws {RangeError}  If the cookie size exceeds 4093 bytes
         */
        const write = () => {
            let str = cname + '=' + encodeURIComponent(JSON.stringify(data));

            if (str.length > 4093) {
                throw new RangeError('Cookie size exceeds 4093 bytes');
            }

            if (expires) {
                str += '; expires=' + expires;
            }
            str += '; path=' + path;
            if (domain) {
                str += '; domain=' + domain;
            }
            if (sec) {
                str += '; secure';
            }
            document.cookie = str;
        };

        /**
         * Read the document cookie into the internal data object.
         *
         * @private
         *
         * @return {boolean}  Was the operation successful?
         */
        const read = () => {
            let start;
            let str = document.cookie;
            if (str.length > 0 && (start = str.indexOf(cname + '=')) != -1) {
                const end = str.indexOf(';', start);
                if (end == -1) {
                    str = str.substring(start + cname.length + 1);
                } else {
                    str = str.substring(start + cname.length + 1, end);
                }

                data = JSON.parse(decodeURIComponent(str));

                return true;
            }
            data = {};

            return false;
        };

        /**
         * Persist the set data and all settings into the document cookie.
         *
         * @method write
         * @instance
         * @memberOf SSC.Cookie
         */
        this.write = () => {
            write();
        };

        /**
         * Retrieve the value for the given key.
         *
         * @method get
         * @instance
         * @memberOf SSC.Cookie
         *
         * @param {string} key  The key to get the value for
         *
         * @return {mixed|undefined}  The value for the given key
         */
        this.get = (key) => {
            if (typeof key == 'string') {
                return data[key];
            }
            return undefined;
        };

        /**
         * Set the value of the given key.<br />
         * Call the [write()]{@link SSC.Cookie#write} function to persist the data into the document cookie.
         *
         * @method set
         * @instance
         * @memberOf SSC.Cookie
         *
         * @param {string} key    The key to set the value of
         * @param {mixed}  value  The value to set
         *
         * @return {SSC.Cookie}  The Cookie object (for method chaining)
         *
         * @throws {TypeError}  If "key" is not a string or "value" is undefined
         */
        this.set = (key, value) => {
            if (typeof key != 'string') {
                throw new TypeError('Key must be a string');
            }
            if (value === undefined) {
                throw new TypeError(`Value must not be undefined for key "${key}"`);
            }

            data[key] = value;

            return this;
        };

        /**
         * Remove the element with the given key.<br />
         * Call the [write()]{@link SSC.Cookie#write} function to persist the data into the document cookie.
         *
         * @method remove
         * @instance
         * @memberOf SSC.Cookie
         *
         * @param {string} key  The key of the element to remove
         *
         * @return {mixed|undefined}  The value of the removed element
         */
        this.remove = (key) => {
            if (typeof key == 'string') {
                const value = data[key];
                delete data[key];

                return value;
            }
            return undefined;
        };

        /**
         * Return all cookie data as an object.
         *
         * @method getAll
         * @instance
         * @memberOf SSC.Cookie
         *
         * @return {Object}  The cookie's data
         */
        this.getAll = () => {
            return data;
        };

        /**
         * Set the cookie's expiry time in RFC-2822 format (DAY, DD MMM YYYY hh:mm:ss GMT) or
         * from an instantiated Date object (defaults to end of session).<br />
         * Call the [write()]{@link SSC.Cookie#write} function to persist the setting into the document cookie.
         *
         * @method setExpiry
         * @instance
         * @memberOf SSC.Cookie
         *
         * @param {string|Date} expiry  The expiry time to set
         *
         * @return {SSC.Cookie}  The Cookie object (for method chaining)
         */
        this.setExpiry = (expiry) => {
            if (Object.prototype.toString.call(expiry) == '[object Date]') {
                expiry = expiry.toGMTString();
            }
            expires = expiry;

            return this;
        };

        /**
         * Set the expiry time as Unix timestamp.<br />
         * Call the [write()]{@link SSC.Cookie#write} function to persist the setting into the document cookie.
         *
         * @method setExpiryTimestamp
         * @instance
         * @memberOf SSC.Cookie
         *
         * @param {number} expiry  The expiry time to set
         *
         * @return {SSC.Cookie}  The Cookie object (for method chaining)
         */
        this.setExpiryTimestamp = (expiry) => {
            this.setExpiry(new Date(expiry * 1000));

            return this;
        };

        /**
         * Set the path for which the cookie is active (defaults to "/").<br />
         * Call the [write()]{@link SSC.Cookie#write} function to persist the setting into the document cookie.
         *
         * @method setPath
         * @instance
         * @memberOf SSC.Cookie
         *
         * @param {string} cookiePath  The path to set
         *
         * @return {SSC.Cookie}  The Cookie object (for method chaining)
         */
        this.setPath = (cookiePath) => {
            path = cookiePath;

            return this;
        };

        /**
         * Set the cookie domain (defaults to current domain).<br />
         * Call the [write()]{@link SSC.Cookie#write} function to persist the setting into the document cookie.
         *
         * @method setDomain
         * @instance
         * @memberOf SSC.Cookie
         *
         * @param {string} cookieDomain  The domain to set
         *
         * @return {SSC.Cookie}  The Cookie object (for method chaining)
         */
        this.setDomain = (cookieDomain) => {
            domain = cookieDomain;

            return this;
        };

        /**
         * Set secure mode (SSL) when sending the cookie to the server (defaults to false).<br />
         * Call the [write()]{@link SSC.Cookie#write} function to persist the setting into the document cookie.
         *
         * @method setSecure
         * @instance
         * @memberOf SSC.Cookie
         *
         * @param {boolean} [secure=true]  Use secure mode?
         *
         * @return {SSC.Cookie}  The Cookie object (for method chaining)
         */
        this.setSecure = (secure = true) => {
            sec = secure;

            return this;
        };

        /**
         * Delete the cookie.
         *
         * @method destroy
         * @instance
         * @memberOf SSC.Cookie
         */
        this.destroy = () => {
            this.setExpiry('Tue, 01 Jan 2008 00:00:00 GMT');
            write();
            data = {};
        };

        // Constructor
        read();
    };

    return /** @lends SSC.Cookie */ {
        /**
         * Get Singleton instance of Cookie, depending on the given name.
         *
         * @param {string} [name=sscdata]  The name of the cookie
         *
         * @return {SSC.Cookie}  Singleton instance of Cookie class
         */
        getInstance: (name = 'sscdata') => {
            if (!instances[name]) {
                instances[name] = new Cookie(name);
            }
            return instances[name];
        },

        /**
         * Test whether the user's browser accepts cookies.
         *
         * @return {boolean}  Are cookies allowed?
         */
        isWritable: () => {
            if (typeof document.cookie != 'string') {
                return false;
            }
            document.cookie = '_testcookie=test;';
            const writable = document.cookie.indexOf('_testcookie=test') != -1 ? true : false;
            document.cookie = '_testcookie=; expires=Tue, 01 Jan 2008 00:00:00 GMT;';

            return writable;
        },
    };
})();