Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

{"version":3,"file":"statemanager.min.js","sources":["../../../src/local/reactive/statemanager.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Reactive simple state manager.\n *\n * The state manager contains the state data, trigger update events and\n * can lock and unlock the state data.\n *\n * This file contains the three main elements of the state manager:\n * - State manager: the public class to alter the state, dispatch events and process update messages.\n * - Proxy handler: a private class to keep track of the state object changes.\n * - StateMap class: a private class extending Map class that triggers event when a state list is modifed.\n *\n * @module     core/local/reactive/statemanager\n * @class      StateManager\n * @copyright  2021 Ferran Recio <ferran@moodle.com>\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Logger from 'core/local/reactive/logger';\n\n/**\n * State manager class.\n *\n * This class handle the reactive state and ensure only valid mutations can modify the state.\n * It also provide methods to apply batch state update messages (see processUpdates function doc\n * for more details on update messages).\n *\n * Implementing a deep state manager is complex and will require many frontend resources. To keep\n * the state fast and simple, the state can ONLY store two kind of data:\n *  - Object with attributes\n *  - Sets of objects with id attributes.\n *\n * This is an example of a valid state:\n *\n * {\n *  course: {\n *      name: 'course name',\n *      shortname: 'courseshort',\n *      sectionlist: [21, 34]\n *  },\n *  sections: [\n *      {id: 21, name: 'Topic 1', visible: true},\n *      {id: 34, name: 'Topic 2', visible: false,\n *  ],\n * }\n *\n * The following cases are NOT allowed at a state ROOT level (throws an exception if they are assigned):\n *  - Simple values (strings, boolean...).\n *  - Arrays of simple values.\n *  - Array of objects without ID attribute (all arrays will be converted to maps and requires an ID).\n *\n * Thanks to those limitations it can simplify the state update messages and the event names. If You\n * need to store simple data, just group them in an object.\n *\n * To grant any state change triggers the proper events, the class uses two private structures:\n * - proxy handler: any object stored in the state is proxied using this class.\n * - StateMap class: any object set in the state will be converted to StateMap using the\n *   objects id attribute.\n */\nexport default class StateManager {\n\n    /**\n     * Create a basic reactive state store.\n     *\n     * The state manager is meant to work with native JS events. To ensure each reactive module can use\n     * it in its own way, the parent element must provide a valid event dispatcher function and an optional\n     * DOM element to anchor the event.\n     *\n     * @param {function} dispatchEvent the function to dispatch the custom event when the state changes.\n     * @param {element} target the state changed custom event target (document if none provided)\n     */\n    constructor(dispatchEvent, target) {\n\n        // The dispatch event function.\n        /** @package */\n        this.dispatchEvent = dispatchEvent;\n\n        // The DOM container to trigger events.\n        /** @package */\n        this.target = target ?? document;\n\n        // State can be altered freely until initial state is set.\n        /** @package */\n        this.readonly = false;\n\n        // List of state changes pending to be published as events.\n        /** @package */\n        this.eventsToPublish = [];\n\n        // The update state types functions.\n        /** @package */\n        this.updateTypes = {\n            \"create\": this.defaultCreate.bind(this),\n            \"update\": this.defaultUpdate.bind(this),\n            \"delete\": this.defaultDelete.bind(this),\n            \"put\": this.defaultPut.bind(this),\n            \"override\": this.defaultOverride.bind(this),\n            \"remove\": this.defaultRemove.bind(this),\n            \"prepareFields\": this.defaultPrepareFields.bind(this),\n        };\n\n        // The state_loaded event is special because it only happens one but all components\n        // may react to that state, even if they are registered after the setIinitialState.\n        // For these reason we use a promise for that event.\n        this.initialPromise = new Promise((resolve) => {\n            const initialStateDone = (event) => {\n                resolve(event.detail.state);\n            };\n            this.target.addEventListener('state:loaded', initialStateDone);\n        });\n\n        this.logger = new Logger();\n    }\n\n    /**\n     * Loads the initial state.\n     *\n     * Note this method will trigger a state changed event with \"state:loaded\" actionname.\n     *\n     * The state mode will be set to read only when the initial state is loaded.\n     *\n     * @param {object} initialState\n     */\n    setInitialState(initialState) {\n\n        if (this.state !== undefined) {\n            throw Error('Initial state can only be initialized ones');\n        }\n\n        // Create the state object.\n        const state = new Proxy({}, new Handler('state', this, true));\n        for (const [prop, propValue] of Object.entries(initialState)) {\n            state[prop] = propValue;\n        }\n        this.state = state;\n\n        // When the state is loaded we can lock it to prevent illegal changes.\n        this.readonly = true;\n\n        this.dispatchEvent({\n            action: 'state:loaded',\n            state: this.state,\n        }, this.target);\n    }\n\n    /**\n     * Generate a promise that will be resolved when the initial state is loaded.\n     *\n     * In most cases the final state will be loaded using an ajax call. This is the reason\n     * why states manager are created unlocked and won't be reactive until the initial state is set.\n     *\n     * @return {Promise} the resulting promise\n     */\n    getInitialPromise() {\n        return this.initialPromise;\n    }\n\n    /**\n     * Locks or unlocks the state to prevent illegal updates.\n     *\n     * Mutations use this method to modify the state. Once the state is updated, they must\n     * block again the state.\n     *\n     * All changes done while the state is writable will be registered using registerStateAction.\n     * When the state is set again to read only the method will trigger _publishEvents to communicate\n     * changes to all watchers.\n     *\n     * @param {bool} readonly if the state is in read only mode enabled\n     */\n    setReadOnly(readonly) {\n\n        this.readonly = readonly;\n\n        let mode = 'off';\n\n        // When the state is in readonly again is time to publish all pending events.\n        if (this.readonly) {\n            mode = 'on';\n            this._publishEvents();\n        }\n\n        // Dispatch a read only event.\n        this.dispatchEvent({\n            action: `readmode:${mode}`,\n            state: this.state,\n            element: null,\n        }, this.target);\n    }\n\n    /**\n     * Add methods to process update state messages.\n     *\n     * The state manager provide a default update, create and delete methods. However,\n     * some applications may require to override the default methods or even add new ones\n     * like \"refresh\" or \"error\".\n     *\n     * @param {Object} newFunctions the new update types functions.\n     */\n    addUpdateTypes(newFunctions) {\n        for (const [updateType, updateFunction] of Object.entries(newFunctions)) {\n            if (typeof updateFunction === 'function') {\n                this.updateTypes[updateType] = updateFunction.bind(newFunctions);\n            }\n        }\n    }\n\n    /**\n     * Process a state updates array and do all the necessary changes.\n     *\n     * Note this method unlocks the state while it is executing and relocks it\n     * when finishes.\n     *\n     * @param {array} updates\n     * @param {Object} updateTypes optional functions to override the default update types.\n     */\n    processUpdates(updates, updateTypes) {\n        if (!Array.isArray(updates)) {\n            throw Error('State updates must be an array');\n        }\n        this.setReadOnly(false);\n        updates.forEach((update) => {\n            if (update.name === undefined) {\n                throw Error('Missing state update name');\n            }\n            this.processUpdate(\n                update.name,\n                update.action,\n                update.fields,\n                updateTypes\n            );\n        });\n        this.setReadOnly(true);\n    }\n\n    /**\n     * Process a single state update.\n     *\n     * Note this method will not lock or unlock the state by itself.\n     *\n     * @param {string} updateName the state element to update\n     * @param {string} action to action to perform\n     * @param {object} fields the new data\n     * @param {Object} updateTypes optional functions to override the default update types.\n     */\n    processUpdate(updateName, action, fields, updateTypes) {\n\n        if (!fields) {\n            throw Error('Missing state update fields');\n        }\n\n        if (updateTypes === undefined) {\n            updateTypes = {};\n        }\n\n        action = action ?? 'update';\n\n        const method = updateTypes[action] ?? this.updateTypes[action];\n\n        if (method === undefined) {\n            throw Error(`Unkown update action ${action}`);\n        }\n\n        // Some state data may require some cooking before sending to the\n        // state. Reactive instances can overrdide the default fieldDefaults\n        // method to add extra logic to all updates.\n        const prepareFields = updateTypes.prepareFields ?? this.updateTypes.prepareFields;\n\n        method(this, updateName, prepareFields(this, updateName, fields));\n    }\n\n    /**\n     * Prepare fields for processing.\n     *\n     * This method is used to add default values or calculations from the frontend side.\n     *\n     * @param {Object} stateManager the state manager\n     * @param {String} updateName the state element to update\n     * @param {Object} fields the new data\n     * @returns {Object} final fields data\n     */\n    defaultPrepareFields(stateManager, updateName, fields) {\n        return fields;\n    }\n\n\n    /**\n     * Process a create state message.\n     *\n     * @param {Object} stateManager the state manager\n     * @param {String} updateName the state element to update\n     * @param {Object} fields the new data\n     */\n    defaultCreate(stateManager, updateName, fields) {\n\n        let state = stateManager.state;\n\n        // Create can be applied only to lists, not to objects.\n        if (state[updateName] instanceof StateMap) {\n            state[updateName].add(fields);\n            return;\n        }\n        state[updateName] = fields;\n    }\n\n    /**\n     * Process a delete state message.\n     *\n     * @param {Object} stateManager the state manager\n     * @param {String} updateName the state element to update\n     * @param {Object} fields the new data\n     */\n    defaultDelete(stateManager, updateName, fields) {\n\n        // Get the current value.\n        let current = stateManager.get(updateName, fields.id);\n        if (!current) {\n            throw Error(`Inexistent ${updateName} ${fields.id}`);\n        }\n\n        // Process deletion.\n        let state = stateManager.state;\n\n        if (state[updateName] instanceof StateMap) {\n            state[updateName].delete(fields.id);\n            return;\n        }\n        delete state[updateName];\n    }\n\n    /**\n     * Process a remove state message.\n     *\n     * @param {Object} stateManager the state manager\n     * @param {String} updateName the state element to update\n     * @param {Object} fields the new data\n     */\n    defaultRemove(stateManager, updateName, fields) {\n\n        // Get the current value.\n        let current = stateManager.get(updateName, fields.id);\n        if (!current) {\n            return;\n        }\n\n        // Process deletion.\n        let state = stateManager.state;\n\n        if (state[updateName] instanceof StateMap) {\n            state[updateName].delete(fields.id);\n            return;\n        }\n        delete state[updateName];\n    }\n\n    /**\n     * Process a update state message.\n     *\n     * @param {Object} stateManager the state manager\n     * @param {String} updateName the state element to update\n     * @param {Object} fields the new data\n     */\n    defaultUpdate(stateManager, updateName, fields) {\n\n        // Get the current value.\n        let current = stateManager.get(updateName, fields.id);\n        if (!current) {\n            throw Error(`Inexistent ${updateName} ${fields.id}`);\n        }\n\n        // Execute updates.\n        for (const [fieldName, fieldValue] of Object.entries(fields)) {\n            current[fieldName] = fieldValue;\n        }\n    }\n\n    /**\n     * Process a put state message.\n     *\n     * @param {Object} stateManager the state manager\n     * @param {String} updateName the state element to update\n     * @param {Object} fields the new data\n     */\n    defaultPut(stateManager, updateName, fields) {\n\n        // Get the current value.\n        let current = stateManager.get(updateName, fields.id);\n        if (current) {\n            // Update attributes.\n            for (const [fieldName, fieldValue] of Object.entries(fields)) {\n                current[fieldName] = fieldValue;\n            }\n        } else {\n            // Create new object.\n            let state = stateManager.state;\n            if (state[updateName] instanceof StateMap) {\n                state[updateName].add(fields);\n                return;\n            }\n            state[updateName] = fields;\n        }\n    }\n\n    /**\n     * Process an override state message.\n     *\n     * @param {Object} stateManager the state manager\n     * @param {String} updateName the state element to update\n     * @param {Object} fields the new data\n     */\n    defaultOverride(stateManager, updateName, fields) {\n\n        // Get the current value.\n        let current = stateManager.get(updateName, fields.id);\n        if (current) {\n            // Remove any unnecessary fields.\n            for (const [fieldName] of Object.entries(current)) {\n                if (fields[fieldName] === undefined) {\n                    delete current[fieldName];\n                }\n            }\n            // Update field.\n            for (const [fieldName, fieldValue] of Object.entries(fields)) {\n                current[fieldName] = fieldValue;\n            }\n        } else {\n            // Create the element if not exists.\n            let state = stateManager.state;\n            if (state[updateName] instanceof StateMap) {\n                state[updateName].add(fields);\n                return;\n            }\n            state[updateName] = fields;\n        }\n    }\n\n    /**\n     * Set the logger class instance.\n     *\n     * Reactive instances can provide alternative loggers to provide advanced logging.\n     * @param {Logger} logger\n     */\n    setLogger(logger) {\n        this.logger = logger;\n    }\n\n    /**\n     * Add a new log entry into the reactive logger.\n     * @param {LoggerEntry} entry\n     */\n    addLoggerEntry(entry) {\n        this.logger.add(entry);\n    }\n\n    /**\n     * Get an element from the state or form an alternative state object.\n     *\n     * The altstate param is used by external update functions that gets the current\n     * state as param.\n     *\n     * @param {String} name the state object name\n     * @param {*} id and object id for state maps.\n     * @return {Object|undefined} the state object found\n     */\n    get(name, id) {\n        const state = this.state;\n\n        let current = state[name];\n        if (current instanceof StateMap) {\n            if (id === undefined) {\n                throw Error(`Missing id for ${name} state update`);\n            }\n            current = state[name].get(id);\n        }\n\n        return current;\n    }\n\n    /**\n     * Get all element ids from the given state.\n     *\n     * @param {String} name the state object name\n     * @return {Array} the element ids.\n     */\n    getIds(name) {\n        const state = this.state;\n        const current = state[name];\n        if (!(current instanceof StateMap)) {\n            throw Error(`${name} is not an instance of StateMap`);\n        }\n        return [...state[name].keys()];\n    }\n\n    /**\n     * Register a state modification and generate the necessary events.\n     *\n     * This method is used mainly by proxy helpers to dispatch state change event.\n     * However, mutations can use it to inform components about non reactive changes\n     * in the state (only the two first levels of the state are reactive).\n     *\n     * Each action can produce several events:\n     * - The specific attribute updated, created or deleter (example: \"cm.visible:updated\")\n     * - The general state object updated, created or deleted (example: \"cm:updated\")\n     * - If the element has an ID attribute, the specific event with id (example: \"cm[42].visible:updated\")\n     * - If the element has an ID attribute, the general event with id (example: \"cm[42]:updated\")\n     * - A generic state update event \"state:update\"\n     *\n     * @param {string} field the affected state field name\n     * @param {string|null} prop the affecter field property (null if affect the full object)\n     * @param {string} action the action done (created/updated/deleted)\n     * @param {*} data the affected data\n     */\n    registerStateAction(field, prop, action, data) {\n\n        let parentAction = 'updated';\n\n        if (prop !== null) {\n            this.eventsToPublish.push({\n                eventName: `${field}.${prop}:${action}`,\n                eventData: data,\n                action,\n            });\n        } else {\n            parentAction = action;\n        }\n\n        // Trigger extra events if the element has an ID attribute.\n        if (data.id !== undefined) {\n            if (prop !== null) {\n                this.eventsToPublish.push({\n                    eventName: `${field}[${data.id}].${prop}:${action}`,\n                    eventData: data,\n                    action,\n                });\n            }\n            this.eventsToPublish.push({\n                eventName: `${field}[${data.id}]:${parentAction}`,\n                eventData: data,\n                action: parentAction,\n            });\n        }\n\n        // Register the general change.\n        this.eventsToPublish.push({\n            eventName: `${field}:${parentAction}`,\n            eventData: data,\n            action: parentAction,\n        });\n\n        // Register state updated event.\n        this.eventsToPublish.push({\n            eventName: `state:updated`,\n            eventData: data,\n            action: 'updated',\n        });\n    }\n\n    /**\n     * Internal method to publish events.\n     *\n     * This is a private method, it will be invoked when the state is set back to read only mode.\n     */\n    _publishEvents() {\n        const fieldChanges = this.eventsToPublish;\n        this.eventsToPublish = [];\n\n        // Dispatch a transaction start event.\n        this.dispatchEvent({\n            action: 'transaction:start',\n            state: this.state,\n            element: null,\n            changes: fieldChanges,\n        }, this.target);\n\n        // State changes can be registered in any order. However it will avoid many\n        // components errors if they are sorted to have creations-updates-deletes in case\n        // some component needs to create or destroy DOM elements before updating them.\n        fieldChanges.sort((a, b) => {\n            const weights = {\n                created: 0,\n                updated: 1,\n                deleted: 2,\n            };\n            const aweight = weights[a.action] ?? 0;\n            const bweight = weights[b.action] ?? 0;\n            // In case both have the same weight, the eventName length decide.\n            if (aweight === bweight) {\n                return a.eventName.length - b.eventName.length;\n            }\n            return aweight - bweight;\n        });\n\n        // List of the published events to prevent redundancies.\n        let publishedEvents = new Set();\n\n        fieldChanges.forEach((event) => {\n\n            const eventkey = `${event.eventName}.${event.eventData.id ?? 0}`;\n\n            if (!publishedEvents.has(eventkey)) {\n                this.dispatchEvent({\n                    action: event.eventName,\n                    state: this.state,\n                    element: event.eventData\n                }, this.target);\n\n                publishedEvents.add(eventkey);\n            }\n        });\n\n        // Dispatch a transaction end event.\n        this.dispatchEvent({\n            action: 'transaction:end',\n            state: this.state,\n            element: null,\n        }, this.target);\n    }\n}\n\n// Proxy helpers.\n\n/**\n * The proxy handler.\n *\n * This class will inform any value change directly to the state manager.\n *\n * The proxied variable will throw an error if it is altered when the state manager is\n * in read only mode.\n */\nclass Handler {\n\n    /**\n     * Class constructor.\n     *\n     * @param {string} name the variable name used for identify triggered actions\n     * @param {StateManager} stateManager the state manager object\n     * @param {boolean} proxyValues if new values must be proxied (used only at state root level)\n     */\n    constructor(name, stateManager, proxyValues) {\n        this.name = name;\n        this.stateManager = stateManager;\n        this.proxyValues = proxyValues ?? false;\n    }\n\n    /**\n     * Set trap to trigger events when the state changes.\n     *\n     * @param {object} obj the source object (not proxied)\n     * @param {string} prop the attribute to set\n     * @param {*} value the value to save\n     * @param {*} receiver the proxied element to be attached to events\n     * @returns {boolean} if the value is set\n     */\n    set(obj, prop, value, receiver) {\n\n        // Only mutations should be able to set state values.\n        if (this.stateManager.readonly) {\n            throw new Error(`State locked. Use mutations to change ${prop} value in ${this.name}.`);\n        }\n\n        // Check any data change.\n        if (JSON.stringify(obj[prop]) === JSON.stringify(value)) {\n            return true;\n        }\n\n        const action = (obj[prop] !== undefined) ? 'updated' : 'created';\n\n        // Proxy value if necessary (used at state root level).\n        if (this.proxyValues) {\n            if (Array.isArray(value)) {\n                obj[prop] = new StateMap(prop, this.stateManager).loadValues(value);\n            } else {\n                obj[prop] = new Proxy(value, new Handler(prop, this.stateManager));\n            }\n        } else {\n            obj[prop] = value;\n        }\n\n        // If the state is not ready yet means the initial state is not yet loaded.\n        if (this.stateManager.state === undefined) {\n            return true;\n        }\n\n        this.stateManager.registerStateAction(this.name, prop, action, receiver);\n\n        return true;\n    }\n\n    /**\n     * Delete property trap to trigger state change events.\n     *\n     * @param {*} obj the affected object (not proxied)\n     * @param {*} prop the prop to delete\n     * @returns {boolean} if prop is deleted\n     */\n    deleteProperty(obj, prop) {\n        // Only mutations should be able to set state values.\n        if (this.stateManager.readonly) {\n            throw new Error(`State locked. Use mutations to delete ${prop} in ${this.name}.`);\n        }\n        if (prop in obj) {\n\n            delete obj[prop];\n\n            this.stateManager.registerStateAction(this.name, prop, 'deleted', obj);\n        }\n        return true;\n    }\n}\n\n/**\n * Class to add events dispatching to the JS Map class.\n *\n * When the state has a list of objects (with IDs) it will be converted into a StateMap.\n * StateMap is used almost in the same way as a regular JS map. Because all elements have an\n * id attribute, it has some specific methods:\n *  - add: a convenient method to add an element without specifying the key (\"id\" attribute will be used as a key).\n *  - loadValues: to add many elements at once wihout specifying keys (\"id\" attribute will be used).\n *\n * Apart, the main difference between regular Map and MapState is that this one will inform any change to the\n * state manager.\n */\nclass StateMap extends Map {\n\n    /**\n     * Create a reactive Map.\n     *\n     * @param {string} name the property name\n     * @param {StateManager} stateManager the state manager\n     * @param {iterable} iterable an iterable object to create the Map\n     */\n    constructor(name, stateManager, iterable) {\n        // We don't have any \"this\" until be call super.\n        super(iterable);\n        this.name = name;\n        this.stateManager = stateManager;\n    }\n\n    /**\n     * Set an element into the map.\n     *\n     * Each value needs it's own id attribute. Objects without id will be rejected.\n     * The function will throw an error if the value id and the key are not the same.\n     *\n     * @param {*} key the key to store\n     * @param {*} value the value to store\n     * @returns {Map} the resulting Map object\n     */\n    set(key, value) {\n\n        // Only mutations should be able to set state values.\n        if (this.stateManager.readonly) {\n            throw new Error(`State locked. Use mutations to change ${key} value in ${this.name}.`);\n        }\n\n        // Normalize keys as string to prevent json decoding errors.\n        key = this.normalizeKey(key);\n\n        this.checkValue(value);\n\n        if (key === undefined || key === null) {\n            throw Error('State lists keys cannot be null or undefined');\n        }\n\n        // ID is mandatory and should be the same as the key.\n        if (this.normalizeKey(value.id) !== key) {\n            throw new Error(`State error: ${this.name} list element ID (${value.id}) and key (${key}) mismatch`);\n        }\n\n        const action = (super.has(key)) ? 'updated' : 'created';\n\n        // Save proxied data into the list.\n        const result = super.set(key, new Proxy(value, new Handler(this.name, this.stateManager)));\n\n        // If the state is not ready yet means the initial state is not yet loaded.\n        if (this.stateManager.state === undefined) {\n            return result;\n        }\n\n        this.stateManager.registerStateAction(this.name, null, action, super.get(key));\n\n        return result;\n    }\n\n    /**\n     * Check if a value is valid to be stored in a a State List.\n     *\n     * Only objects with id attribute can be stored in State lists.\n     *\n     * This method throws an error if the value is not valid.\n     *\n     * @param {object} value (with ID)\n     */\n    checkValue(value) {\n        if (!typeof value === 'object' && value !== null) {\n            throw Error('State lists can contain objects only');\n        }\n\n        if (value.id === undefined) {\n            throw Error('State lists elements must contain at least an id attribute');\n        }\n    }\n\n    /**\n     * Return a normalized key value for state map.\n     *\n     * Regular maps uses strict key comparissons but state maps are indexed by ID.JSON conversions\n     * and webservices sometimes do unexpected types conversions so we convert any integer key to string.\n     *\n     * @param {*} key the provided key\n     * @returns {string}\n     */\n    normalizeKey(key) {\n        return String(key).valueOf();\n    }\n\n    /**\n     * Insert a new element int a list.\n     *\n     * Each value needs it's own id attribute. Objects withouts id will be rejected.\n     *\n     * @param {object} value the value to add (needs an id attribute)\n     * @returns {Map} the resulting Map object\n     */\n    add(value) {\n        this.checkValue(value);\n        return this.set(value.id, value);\n    }\n\n    /**\n     * Return a state map element.\n     *\n     * @param {*} key the element id\n     * @return {Object}\n     */\n    get(key) {\n        return super.get(this.normalizeKey(key));\n    }\n\n    /**\n     * Check whether an element with the specified key exists or not.\n     *\n     * @param {*} key the key to find\n     * @return {boolean}\n     */\n    has(key) {\n        return super.has(this.normalizeKey(key));\n    }\n\n    /**\n     * Delete an element from the map.\n     *\n     * @param {*} key\n     * @returns {boolean}\n     */\n    delete(key) {\n        // State maps uses only string keys to avoid strict comparisons.\n        key = this.normalizeKey(key);\n\n        // Only mutations should be able to set state values.\n        if (this.stateManager.readonly) {\n            throw new Error(`State locked. Use mutations to change ${key} value in ${this.name}.`);\n        }\n\n        const previous = super.get(key);\n\n        const result = super.delete(key);\n        if (!result) {\n            return result;\n        }\n\n        this.stateManager.registerStateAction(this.name, null, 'deleted', previous);\n\n        return result;\n    }\n\n    /**\n     * Return a suitable structure for JSON conversion.\n     *\n     * This function is needed because new values are compared in JSON. StateMap has Private\n     * attributes which cannot be stringified (like this.stateManager which will produce an\n     * infinite recursivity).\n     *\n     * @returns {array}\n     */\n    toJSON() {\n        let result = [];\n        this.forEach((value) => {\n            result.push(value);\n        });\n        return result;\n    }\n\n    /**\n     * Insert a full list of values using the id attributes as keys.\n     *\n     * This method is used mainly to initialize the list. Note each element is indexed by its \"id\" attribute.\n     * This is a basic restriction of StateMap. All elements need an id attribute, otherwise it won't be saved.\n     *\n     * @param {iterable} values the values to load\n     * @returns {StateMap} return the this value\n     */\n    loadValues(values) {\n        values.forEach((data) => {\n            this.checkValue(data);\n            let key = data.id;\n            let newvalue = new Proxy(data, new Handler(this.name, this.stateManager));\n            this.set(key, newvalue);\n        });\n        return this;\n    }\n}\n"],"names":["constructor","dispatchEvent","target","document","readonly","eventsToPublish","updateTypes","this","defaultCreate","bind","defaultUpdate","defaultDelete","defaultPut","defaultOverride","defaultRemove","defaultPrepareFields","initialPromise","Promise","resolve","addEventListener","event","detail","state","logger","Logger","setInitialState","initialState","undefined","Error","Proxy","Handler","prop","propValue","Object","entries","action","getInitialPromise","setReadOnly","mode","_publishEvents","element","addUpdateTypes","newFunctions","updateType","updateFunction","processUpdates","updates","Array","isArray","forEach","update","name","processUpdate","fields","updateName","method","prepareFields","stateManager","StateMap","add","get","id","delete","current","fieldName","fieldValue","setLogger","addLoggerEntry","entry","getIds","keys","registerStateAction","field","data","parentAction","push","eventName","eventData","fieldChanges","changes","sort","a","b","weights","created","updated","deleted","aweight","bweight","length","publishedEvents","Set","eventkey","has","proxyValues","set","obj","value","receiver","JSON","stringify","loadValues","deleteProperty","Map","iterable","key","normalizeKey","checkValue","super","result","String","valueOf","previous","toJSON","values","newvalue"],"mappings":"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"}