Proyectos de Subversion Moodle

Rev

Rev 1 | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |

{"version":3,"file":"courseeditor.min.js","sources":["../../../src/local/courseeditor/courseeditor.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\nimport {getString} from 'core/str';\nimport {Reactive} from 'core/reactive';\nimport notification from 'core/notification';\nimport Exporter from 'core_courseformat/local/courseeditor/exporter';\nimport log from 'core/log';\nimport ajax from 'core/ajax';\nimport * as Storage from 'core/sessionstorage';\nimport {uploadFilesToCourse} from 'core_courseformat/local/courseeditor/fileuploader';\n\n/**\n * Main course editor module.\n *\n * All formats can register new components on this object to create new reactive\n * UI components that watch the current course state.\n *\n * @module     core_courseformat/local/courseeditor/courseeditor\n * @class     core_courseformat/local/courseeditor/courseeditor\n * @copyright  2021 Ferran Recio <ferran@moodle.com>\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class extends Reactive {\n\n    /**\n     * The current state cache key\n     *\n     * The state cache is considered dirty if the state changes from the last page or\n     * if the page has editing mode on.\n     *\n     * @attribute stateKey\n     * @type number|null\n     * @default 1\n     * @package\n     */\n    stateKey = 1;\n\n    /**\n     * The current page section return\n     * @attribute sectionReturn\n     * @type number\n     * @default null\n     */\n    sectionReturn = null;\n\n    /**\n     * Set up the course editor when the page is ready.\n     *\n     * The course can only be loaded once per instance. Otherwise an error is thrown.\n     *\n     * The backend can inform the module of the current state key. This key changes every time some\n     * update in the course affect the current user state. Some examples are:\n     *  - The course content has been edited\n     *  - The user marks some activity as completed\n     *  - The user collapses or uncollapses a section (it is stored as a user preference)\n     *\n     * @param {number} courseId course id\n     * @param {string} serverStateKey the current backend course cache reference\n     */\n    async loadCourse(courseId, serverStateKey) {\n\n        if (this.courseId) {\n            throw new Error(`Cannot load ${courseId}, course already loaded with id ${this.courseId}`);\n        }\n\n        if (!serverStateKey) {\n            // The server state key is not provided, we use a invalid statekey to force reloading.\n            serverStateKey = `invalidStateKey_${Date.now()}`;\n        }\n\n        // Default view format setup.\n        this._editing = false;\n        this._supportscomponents = false;\n        this._fileHandlers = null;\n\n        this.courseId = courseId;\n\n        let stateData;\n\n        const storeStateKey = Storage.get(`course/${courseId}/stateKey`);\n        try {\n            // Check if the backend state key is the same we have in our session storage.\n            if (!this.isEditing && serverStateKey == storeStateKey) {\n                stateData = JSON.parse(Storage.get(`course/${courseId}/staticState`));\n            }\n            if (!stateData) {\n                stateData = await this.getServerCourseState();\n            }\n\n        } catch (error) {\n            log.error(\"EXCEPTION RAISED WHILE INIT COURSE EDITOR\");\n            log.error(error);\n            return;\n        }\n\n        // The bulk editing only applies to the frontend and the state data is not created in the backend.\n        stateData.bulk = {\n            enabled: false,\n            selectedType: '',\n            selection: [],\n        };\n\n        this.setInitialState(stateData);\n\n        // In editing mode, the session cache is considered dirty always.\n        if (this.isEditing) {\n            this.stateKey = null;\n        } else {\n            // Check if the last state is the same as the cached one.\n            const newState = JSON.stringify(stateData);\n            const previousState = Storage.get(`course/${courseId}/staticState`);\n            if (previousState !== newState || storeStateKey !== serverStateKey) {\n                Storage.set(`course/${courseId}/staticState`, newState);\n                Storage.set(`course/${courseId}/stateKey`, stateData?.course?.statekey ?? serverStateKey);\n            }\n            this.stateKey = Storage.get(`course/${courseId}/stateKey`);\n        }\n\n        this._loadFileHandlers();\n\n        this._pageAnchorCmInfo = this._scanPageAnchorCmInfo();\n    }\n\n    /**\n     * Load the file hanlders promise.\n     */\n    _loadFileHandlers() {\n        // Load the course file extensions.\n        this._fileHandlersPromise = new Promise((resolve) => {\n            if (!this.isEditing) {\n                resolve([]);\n                return;\n            }\n            // Check the cache.\n            const handlersCacheKey = `course/${this.courseId}/fileHandlers`;\n\n            const cacheValue = Storage.get(handlersCacheKey);\n            if (cacheValue) {\n                try {\n                    const cachedHandlers = JSON.parse(cacheValue);\n                    resolve(cachedHandlers);\n                    return;\n                } catch (error) {\n                    log.error(\"ERROR PARSING CACHED FILE HANDLERS\");\n                }\n            }\n            // Call file handlers webservice.\n            ajax.call([{\n                methodname: 'core_courseformat_file_handlers',\n                args: {\n                    courseid: this.courseId,\n                }\n            }])[0].then((handlers) => {\n                Storage.set(handlersCacheKey, JSON.stringify(handlers));\n                resolve(handlers);\n                return;\n            }).catch(error => {\n                log.error(error);\n                resolve([]);\n                return;\n            });\n        });\n    }\n\n    /**\n     * Setup the current view settings\n     *\n     * @param {Object} setup format, page and course settings\n     * @param {boolean} setup.editing if the page is in edit mode\n     * @param {boolean} setup.supportscomponents if the format supports components for content\n     * @param {string} setup.cacherev the backend cached state revision\n     * @param {Array} setup.overriddenStrings optional overridden strings\n     */\n    setViewFormat(setup) {\n        this._editing = setup.editing ?? false;\n        this._supportscomponents = setup.supportscomponents ?? false;\n        const overriddenStrings = setup.overriddenStrings ?? [];\n        this._overriddenStrings = overriddenStrings.reduce(\n            (indexed, currentValue) => indexed.set(currentValue.key, currentValue),\n            new Map()\n        );\n    }\n\n    /**\n     * Execute a get string for a possible format overriden editor string.\n     *\n     * Return the proper getString promise for an editor string using the core_courseformat\n     * of the format_PLUGINNAME compoment depending on the current view format setup.\n     * @param {String} key the string key\n     * @param {string|undefined} param The param for variable expansion in the string.\n     * @returns {Promise<String>} a getString promise\n     */\n    getFormatString(key, param) {\n        if (this._overriddenStrings.has(key)) {\n            const override = this._overriddenStrings.get(key);\n            return getString(key, override.component ?? 'core_courseformat', param);\n        }\n        // All format overridable strings are from core_courseformat lang file.\n        return getString(key, 'core_courseformat', param);\n    }\n\n    /**\n     * Load the current course state from the server.\n     *\n     * @returns {Object} the current course state\n     */\n    async getServerCourseState() {\n        const courseState = await ajax.call([{\n            methodname: 'core_courseformat_get_state',\n            args: {\n                courseid: this.courseId,\n            }\n        }])[0];\n\n        const stateData = JSON.parse(courseState);\n\n        return {\n            course: {},\n            section: [],\n            cm: [],\n            ...stateData,\n        };\n    }\n\n    /**\n     * Return the current edit mode.\n     *\n     * Components should use this method to check if edit mode is active.\n     *\n     * @return {boolean} if edit is enabled\n     */\n    get isEditing() {\n        return this._editing ?? false;\n    }\n\n    /**\n     * Return a data exporter to transform state part into mustache contexts.\n     *\n     * @return {Exporter} the exporter class\n     */\n    getExporter() {\n        return new Exporter(this);\n    }\n\n    /**\n     * Return if the current course support components to refresh the content.\n     *\n     * @returns {boolean} if the current content support components\n     */\n    get supportComponents() {\n        return this._supportscomponents ?? false;\n    }\n\n    /**\n     * Return the course file handlers promise.\n     * @returns {Promise} the promise for file handlers.\n     */\n    async getFileHandlersPromise() {\n        return this._fileHandlersPromise ?? [];\n    }\n\n    /**\n     * Upload a file list to the course.\n     *\n     * This method is a wrapper to the course file uploader.\n     *\n     * @param {number} sectionId the section id\n     * @param {number} sectionNum the section number\n     * @param {Array} files and array of files\n     * @return {Promise} the file queue promise\n     */\n    uploadFiles(sectionId, sectionNum, files) {\n        return uploadFilesToCourse(this.courseId, sectionId, sectionNum, files);\n    }\n\n    /**\n     * Get a value from the course editor static storage if any.\n     *\n     * The course editor static storage uses the sessionStorage to store values from the\n     * components. This is used to prevent unnecesary template loadings on every page. However,\n     * the storage does not work if no sessionStorage can be used (in debug mode for example),\n     * if the page is in editing mode or if the initial state change from the last page.\n     *\n     * @param {string} key the key to get\n     * @return {boolean|string} the storage value or false if cannot be loaded\n     */\n    getStorageValue(key) {\n        if (this.isEditing || !this.stateKey) {\n            return false;\n        }\n        const dataJson = Storage.get(`course/${this.courseId}/${key}`);\n        if (!dataJson) {\n            return false;\n        }\n        // Check the stateKey.\n        try {\n            const data = JSON.parse(dataJson);\n            if (data?.stateKey !== this.stateKey) {\n                return false;\n            }\n            return data.value;\n        } catch (error) {\n            return false;\n        }\n    }\n\n    /**\n     * Stores a value into the course editor static storage if available\n     *\n     * @param {String} key the key to store\n     * @param {*} value the value to store (must be compatible with JSON,stringify)\n     * @returns {boolean} true if the value is stored\n     */\n    setStorageValue(key, value) {\n        // Values cannot be stored on edit mode.\n        if (this.isEditing) {\n            return false;\n        }\n        const data = {\n            stateKey: this.stateKey,\n            value,\n        };\n        return Storage.set(`course/${this.courseId}/${key}`, JSON.stringify(data));\n    }\n\n    /**\n     * Convert a file dragging event into a proper dragging file list.\n     * @param {DataTransfer} dataTransfer the event to convert\n     * @return {Array} of file list info.\n     */\n    getFilesDraggableData(dataTransfer) {\n        const exporter = this.getExporter();\n        return exporter.fileDraggableData(this.state, dataTransfer);\n    }\n\n    /**\n     * Dispatch a change in the state.\n     *\n     * Usually reactive modules throw an error directly to the components when something\n     * goes wrong. However, course editor can directly display a notification.\n     *\n     * @method dispatch\n     * @param {mixed} args any number of params the mutation needs.\n     */\n    async dispatch(...args) {\n        try {\n            await super.dispatch(...args);\n        } catch (error) {\n            // Display error modal.\n            notification.exception(error);\n            // Force unlock all elements.\n            super.dispatch('unlockAll');\n        }\n    }\n\n    /**\n     * Calculate the cm info from the current page anchor.\n     *\n     * @returns {Object|null} the cm info or null if not found.\n     */\n    _scanPageAnchorCmInfo() {\n        const anchor = new URL(window.location.href).hash;\n        if (!anchor.startsWith('#module-')) {\n            return null;\n        }\n        // The anchor is always #module-CMID.\n        const cmid = anchor.split('-')[1];\n        return this.stateManager.get('cm', parseInt(cmid));\n    }\n\n    /**\n     * Return the current page anchor cm info.\n     */\n    getPageAnchorCmInfo() {\n        return this._pageAnchorCmInfo;\n    }\n}\n"],"names":["Reactive","courseId","serverStateKey","this","Error","stateData","Date","now","_editing","_supportscomponents","_fileHandlers","storeStateKey","Storage","get","isEditing","JSON","parse","getServerCourseState","error","bulk","enabled","selectedType","selection","setInitialState","stateKey","newState","stringify","set","_stateData","course","_stateData$course","statekey","_loadFileHandlers","_pageAnchorCmInfo","_scanPageAnchorCmInfo","_fileHandlersPromise","Promise","resolve","handlersCacheKey","cacheValue","cachedHandlers","call","methodname","args","courseid","then","handlers","catch","setViewFormat","setup","editing","supportscomponents","overriddenStrings","_overriddenStrings","reduce","indexed","currentValue","key","Map","getFormatString","param","has","override","component","courseState","ajax","section","cm","getExporter","Exporter","supportComponents","uploadFiles","sectionId","sectionNum","files","getStorageValue","dataJson","data","value","setStorageValue","getFilesDraggableData","dataTransfer","fileDraggableData","state","super","dispatch","exception","anchor","URL","window","location","href","hash","startsWith","cmid","split","stateManager","parseInt","getPageAnchorCmInfo"],"mappings":"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"}