Proyectos de Subversion Moodle

Rev

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

{"version":3,"file":"actions.min.js","sources":["../../../src/local/content/actions.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 * Course state actions dispatcher.\n *\n * This module captures all data-dispatch links in the course content and dispatch the proper\n * state mutation, including any confirmation and modal required.\n *\n * @module     core_courseformat/local/content/actions\n * @class      core_courseformat/local/content/actions\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 {BaseComponent} from 'core/reactive';\nimport Modal from 'core/modal';\nimport ModalSaveCancel from 'core/modal_save_cancel';\nimport ModalDeleteCancel from 'core/modal_delete_cancel';\nimport ModalEvents from 'core/modal_events';\nimport Templates from 'core/templates';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} from 'core/str';\nimport {getFirst} from 'core/normalise';\nimport {toggleBulkSelectionAction} from 'core_courseformat/local/content/actions/bulkselection';\nimport * as CourseEvents from 'core_course/events';\nimport Pending from 'core/pending';\nimport ContentTree from 'core_courseformat/local/courseeditor/contenttree';\n// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.\nimport jQuery from 'jquery';\n\n// Load global strings.\nprefetchStrings('core', ['movecoursesection', 'movecoursemodule', 'confirm', 'delete']);\n\n// Mutations are dispatched by the course content actions.\n// Formats can use this module addActions static method to add custom actions.\n// Direct mutations can be simple strings (mutation) name or functions.\nconst directMutations = {\n    sectionHide: 'sectionHide',\n    sectionShow: 'sectionShow',\n    cmHide: 'cmHide',\n    cmShow: 'cmShow',\n    cmStealth: 'cmStealth',\n    cmMoveRight: 'cmMoveRight',\n    cmMoveLeft: 'cmMoveLeft',\n    cmNoGroups: 'cmNoGroups',\n    cmSeparateGroups: 'cmSeparateGroups',\n    cmVisibleGroups: 'cmVisibleGroups',\n};\n\nexport default class extends BaseComponent {\n\n    /**\n     * Constructor hook.\n     */\n    create() {\n        // Optional component name for debugging.\n        this.name = 'content_actions';\n        // Default query selectors.\n        this.selectors = {\n            ACTIONLINK: `[data-action]`,\n            // Move modal selectors.\n            SECTIONLINK: `[data-for='section']`,\n            CMLINK: `[data-for='cm']`,\n            SECTIONNODE: `[data-for='sectionnode']`,\n            MODALTOGGLER: `[data-toggle='collapse']`,\n            ADDSECTION: `[data-action='addSection']`,\n            CONTENTTREE: `#destination-selector`,\n            ACTIONMENU: `.action-menu`,\n            ACTIONMENUTOGGLER: `[data-toggle=\"dropdown\"]`,\n            // Availability modal selectors.\n            OPTIONSRADIO: `[type='radio']`,\n        };\n        // Component css classes.\n        this.classes = {\n            DISABLED: `text-body`,\n            ITALIC: `font-italic`,\n        };\n    }\n\n    /**\n     * Add extra actions to the module.\n     *\n     * @param {array} actions array of methods to execute\n     */\n    static addActions(actions) {\n        for (const [action, mutationReference] of Object.entries(actions)) {\n            if (typeof mutationReference !== 'function' && typeof mutationReference !== 'string') {\n                throw new Error(`${action} action must be a mutation name or a function`);\n            }\n            directMutations[action] = mutationReference;\n        }\n    }\n\n    /**\n     * Initial state ready method.\n     *\n     * @param {Object} state the state data.\n     *\n     */\n    stateReady(state) {\n        // Delegate dispatch clicks.\n        this.addEventListener(\n            this.element,\n            'click',\n            this._dispatchClick\n        );\n        // Check section limit.\n        this._checkSectionlist({state});\n        // Add an Event listener to recalculate limits it if a section HTML is altered.\n        this.addEventListener(\n            this.element,\n            CourseEvents.sectionRefreshed,\n            () => this._checkSectionlist({state})\n        );\n    }\n\n    /**\n     * Return the component watchers.\n     *\n     * @returns {Array} of watchers\n     */\n    getWatchers() {\n        return [\n            // Check section limit.\n            {watch: `course.sectionlist:updated`, handler: this._checkSectionlist},\n        ];\n    }\n\n    _dispatchClick(event) {\n        const target = event.target.closest(this.selectors.ACTIONLINK);\n        if (!target) {\n            return;\n        }\n        if (target.classList.contains(this.classes.DISABLED)) {\n            event.preventDefault();\n            return;\n        }\n\n        // Invoke proper method.\n        const actionName = target.dataset.action;\n        const methodName = this._actionMethodName(actionName);\n\n        if (this[methodName] !== undefined) {\n            this[methodName](target, event);\n            return;\n        }\n\n        // Check direct mutations or mutations handlers.\n        if (directMutations[actionName] !== undefined) {\n            if (typeof directMutations[actionName] === 'function') {\n                directMutations[actionName](target, event);\n                return;\n            }\n            this._requestMutationAction(target, event, directMutations[actionName]);\n            return;\n        }\n    }\n\n    _actionMethodName(name) {\n        const requestName = name.charAt(0).toUpperCase() + name.slice(1);\n        return `_request${requestName}`;\n    }\n\n    /**\n     * Check the section list and disable some options if needed.\n     *\n     * @param {Object} detail the update details.\n     * @param {Object} detail.state the state object.\n     */\n    _checkSectionlist({state}) {\n        // Disable \"add section\" actions if the course max sections has been exceeded.\n        this._setAddSectionLocked(state.course.sectionlist.length > state.course.maxsections);\n    }\n\n    /**\n     * Return the ids represented by this element.\n     *\n     * Depending on the dataset attributes the action could represent a single id\n     * or a bulk actions with all the current selected ids.\n     *\n     * @param {HTMLElement} target\n     * @returns {Number[]} array of Ids\n     */\n    _getTargetIds(target) {\n        let ids = [];\n        if (target?.dataset?.id) {\n            ids.push(target.dataset.id);\n        }\n        const bulkType = target?.dataset?.bulk;\n        if (!bulkType) {\n            return ids;\n        }\n        const bulk = this.reactive.get('bulk');\n        if (bulk.enabled && bulk.selectedType === bulkType) {\n            ids = [...ids, ...bulk.selection];\n        }\n        return ids;\n    }\n\n    /**\n     * Handle a move section request.\n     *\n     * @param {Element} target the dispatch action element\n     * @param {Event} event the triggered event\n     */\n    async _requestMoveSection(target, event) {\n        // Check we have an id.\n        const sectionIds = this._getTargetIds(target);\n        if (sectionIds.length == 0) {\n            return;\n        }\n\n        event.preventDefault();\n\n        const pendingModalReady = new Pending(`courseformat/actions:prepareMoveSectionModal`);\n\n        // The section edit menu to refocus on end.\n        const editTools = this._getClosestActionMenuToogler(target);\n\n        // Collect section information from the state.\n        const exporter = this.reactive.getExporter();\n        const data = exporter.course(this.reactive.state);\n        let titleText = null;\n\n        // Add the target section id and title.\n        let sectionInfo = null;\n        if (sectionIds.length == 1) {\n            sectionInfo = this.reactive.get('section', sectionIds[0]);\n            data.sectionid = sectionInfo.id;\n            data.sectiontitle = sectionInfo.title;\n            data.information = await this.reactive.getFormatString('sectionmove_info', data.sectiontitle);\n            titleText = this.reactive.getFormatString('sectionmove_title');\n        } else {\n            data.information = await this.reactive.getFormatString('sectionsmove_info', sectionIds.length);\n            titleText = this.reactive.getFormatString('sectionsmove_title');\n        }\n\n\n        // Create the modal.\n        // Build the modal parameters from the event data.\n        const modal = await this._modalBodyRenderedPromise(Modal, {\n            title: titleText,\n            body: Templates.render('core_courseformat/local/content/movesection', data),\n        });\n\n        const modalBody = getFirst(modal.getBody());\n\n        // Disable current selected section ids.\n        sectionIds.forEach(sectionId => {\n            const currentElement = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-id='${sectionId}']`);\n            this._disableLink(currentElement);\n        });\n\n        // Setup keyboard navigation.\n        new ContentTree(\n            modalBody.querySelector(this.selectors.CONTENTTREE),\n            {\n                SECTION: this.selectors.SECTIONNODE,\n                TOGGLER: this.selectors.MODALTOGGLER,\n                COLLAPSE: this.selectors.MODALTOGGLER,\n            },\n            true\n        );\n\n        // Capture click.\n        modalBody.addEventListener('click', (event) => {\n            const target = event.target;\n            if (!target.matches('a') || target.dataset.for != 'section' || target.dataset.id === undefined) {\n                return;\n            }\n            if (target.getAttribute('aria-disabled')) {\n                return;\n            }\n            event.preventDefault();\n            this.reactive.dispatch('sectionMoveAfter', sectionIds, target.dataset.id);\n            this._destroyModal(modal, editTools);\n        });\n\n        pendingModalReady.resolve();\n    }\n\n    /**\n     * Handle a move cm request.\n     *\n     * @param {Element} target the dispatch action element\n     * @param {Event} event the triggered event\n     */\n    async _requestMoveCm(target, event) {\n        // Check we have an id.\n        const cmIds = this._getTargetIds(target);\n        if (cmIds.length == 0) {\n            return;\n        }\n\n        event.preventDefault();\n\n        const pendingModalReady = new Pending(`courseformat/actions:prepareMoveCmModal`);\n\n        // The section edit menu to refocus on end.\n        const editTools = this._getClosestActionMenuToogler(target);\n\n        // Collect information from the state.\n        const exporter = this.reactive.getExporter();\n        const data = exporter.course(this.reactive.state);\n\n        let titleText = null;\n        if (cmIds.length == 1) {\n            const cmInfo = this.reactive.get('cm', cmIds[0]);\n            data.cmid = cmInfo.id;\n            data.cmname = cmInfo.name;\n            data.information = await this.reactive.getFormatString('cmmove_info', data.cmname);\n            titleText = this.reactive.getFormatString('cmmove_title');\n        } else {\n            data.information = await this.reactive.getFormatString('cmsmove_info', cmIds.length);\n            titleText = this.reactive.getFormatString('cmsmove_title');\n        }\n\n        // Create the modal.\n        // Build the modal parameters from the event data.\n        const modal = await this._modalBodyRenderedPromise(Modal, {\n            title: titleText,\n            body: Templates.render('core_courseformat/local/content/movecm', data),\n        });\n\n        const modalBody = getFirst(modal.getBody());\n\n        // Disable current selected section ids.\n        cmIds.forEach(cmId => {\n            const currentElement = modalBody.querySelector(`${this.selectors.CMLINK}[data-id='${cmId}']`);\n            this._disableLink(currentElement);\n        });\n\n        // Setup keyboard navigation.\n        new ContentTree(\n            modalBody.querySelector(this.selectors.CONTENTTREE),\n            {\n                SECTION: this.selectors.SECTIONNODE,\n                TOGGLER: this.selectors.MODALTOGGLER,\n                COLLAPSE: this.selectors.MODALTOGGLER,\n                ENTER: this.selectors.SECTIONLINK,\n            }\n        );\n\n        // Open the cm section node if possible (Bootstrap 4 uses jQuery to interact with collapsibles).\n        // All jQuery in this code can be replaced when MDL-71979 is integrated.\n        cmIds.forEach(cmId => {\n            const currentElement = modalBody.querySelector(`${this.selectors.CMLINK}[data-id='${cmId}']`);\n            const sectionnode = currentElement.closest(this.selectors.SECTIONNODE);\n            const toggler = jQuery(sectionnode).find(this.selectors.MODALTOGGLER);\n            let collapsibleId = toggler.data('target') ?? toggler.attr('href');\n            if (collapsibleId) {\n                // We cannot be sure we have # in the id element name.\n                collapsibleId = collapsibleId.replace('#', '');\n                const expandNode = modalBody.querySelector(`#${collapsibleId}`);\n                jQuery(expandNode).collapse('show');\n            }\n        });\n\n        modalBody.addEventListener('click', (event) => {\n            const target = event.target;\n            if (!target.matches('a') || target.dataset.for === undefined || target.dataset.id === undefined) {\n                return;\n            }\n            if (target.getAttribute('aria-disabled')) {\n                return;\n            }\n            event.preventDefault();\n\n            let targetSectionId;\n            let targetCmId;\n            if (target.dataset.for == 'cm') {\n                const dropData = exporter.cmDraggableData(this.reactive.state, target.dataset.id);\n                targetSectionId = dropData.sectionid;\n                targetCmId = dropData.nextcmid;\n            } else {\n                const section = this.reactive.get('section', target.dataset.id);\n                targetSectionId = target.dataset.id;\n                targetCmId = section?.cmlist[0];\n            }\n            this.reactive.dispatch('cmMove', cmIds, targetSectionId, targetCmId);\n            this._destroyModal(modal, editTools);\n        });\n\n        pendingModalReady.resolve();\n    }\n\n    /**\n     * Handle a create section request.\n     *\n     * @param {Element} target the dispatch action element\n     * @param {Event} event the triggered event\n     */\n    async _requestAddSection(target, event) {\n        event.preventDefault();\n        this.reactive.dispatch('addSection', target.dataset.id ?? 0);\n    }\n\n    /**\n     * Handle a delete section request.\n     *\n     * @param {Element} target the dispatch action element\n     * @param {Event} event the triggered event\n     */\n    async _requestDeleteSection(target, event) {\n        const sectionIds = this._getTargetIds(target);\n        if (sectionIds.length == 0) {\n            return;\n        }\n\n        event.preventDefault();\n\n        // We don't need confirmation to delete empty sections.\n        let needsConfirmation = sectionIds.some(sectionId => {\n            const sectionInfo = this.reactive.get('section', sectionId);\n            const cmList = sectionInfo.cmlist ?? [];\n            return (cmList.length || sectionInfo.hassummary || sectionInfo.rawtitle);\n        });\n        if (!needsConfirmation) {\n            this.reactive.dispatch('sectionDelete', sectionIds);\n            return;\n        }\n\n        let bodyText = null;\n        let titleText = null;\n        if (sectionIds.length == 1) {\n            titleText = this.reactive.getFormatString('sectiondelete_title');\n            const sectionInfo = this.reactive.get('section', sectionIds[0]);\n            bodyText = this.reactive.getFormatString('sectiondelete_info', {name: sectionInfo.title});\n        } else {\n            titleText = this.reactive.getFormatString('sectionsdelete_title');\n            bodyText = this.reactive.getFormatString('sectionsdelete_info', {count: sectionIds.length});\n        }\n\n        const modal = await this._modalBodyRenderedPromise(ModalDeleteCancel, {\n            title: titleText,\n            body: bodyText,\n        });\n\n        modal.getRoot().on(\n            ModalEvents.delete,\n            e => {\n                // Stop the default save button behaviour which is to close the modal.\n                e.preventDefault();\n                modal.destroy();\n                this.reactive.dispatch('sectionDelete', sectionIds);\n            }\n        );\n    }\n\n    /**\n     * Handle a toggle cm selection.\n     *\n     * @param {Element} target the dispatch action element\n     * @param {Event} event the triggered event\n     */\n    async _requestToggleSelectionCm(target, event) {\n        toggleBulkSelectionAction(this.reactive, target, event, 'cm');\n    }\n\n    /**\n     * Handle a toggle section selection.\n     *\n     * @param {Element} target the dispatch action element\n     * @param {Event} event the triggered event\n     */\n    async _requestToggleSelectionSection(target, event) {\n        toggleBulkSelectionAction(this.reactive, target, event, 'section');\n    }\n\n    /**\n     * Basic mutation action helper.\n     *\n     * @param {Element} target the dispatch action element\n     * @param {Event} event the triggered event\n     * @param {string} mutationName the mutation name\n     */\n    async _requestMutationAction(target, event, mutationName) {\n        if (!target.dataset.id && target.dataset.for !== 'bulkaction') {\n            return;\n        }\n        event.preventDefault();\n        if (target.dataset.for === 'bulkaction') {\n            // If the mutation is a bulk action we use the current selection.\n            this.reactive.dispatch(mutationName, this.reactive.get('bulk').selection);\n        } else {\n            this.reactive.dispatch(mutationName, [target.dataset.id]);\n        }\n    }\n\n    /**\n     * Handle a course module duplicate request.\n     *\n     * @param {Element} target the dispatch action element\n     * @param {Event} event the triggered event\n     */\n    async _requestCmDuplicate(target, event) {\n        const cmIds = this._getTargetIds(target);\n        if (cmIds.length == 0) {\n            return;\n        }\n        const sectionId = target.dataset.sectionid ?? null;\n        event.preventDefault();\n        this.reactive.dispatch('cmDuplicate', cmIds, sectionId);\n    }\n\n    /**\n     * Handle a delete cm request.\n     *\n     * @param {Element} target the dispatch action element\n     * @param {Event} event the triggered event\n     */\n    async _requestCmDelete(target, event) {\n        const cmIds = this._getTargetIds(target);\n        if (cmIds.length == 0) {\n            return;\n        }\n\n        event.preventDefault();\n\n        let bodyText = null;\n        let titleText = null;\n        if (cmIds.length == 1) {\n            const cmInfo = this.reactive.get('cm', cmIds[0]);\n            titleText = getString('cmdelete_title', 'core_courseformat');\n            bodyText = getString(\n                'cmdelete_info',\n                'core_courseformat',\n                {\n                    type: cmInfo.modname,\n                    name: cmInfo.name,\n                }\n            );\n        } else {\n            titleText = getString('cmsdelete_title', 'core_courseformat');\n            bodyText = getString(\n                'cmsdelete_info',\n                'core_courseformat',\n                {count: cmIds.length}\n            );\n        }\n\n        const modal = await this._modalBodyRenderedPromise(ModalDeleteCancel, {\n            title: titleText,\n            body: bodyText,\n        });\n\n        modal.getRoot().on(\n            ModalEvents.delete,\n            e => {\n                // Stop the default save button behaviour which is to close the modal.\n                e.preventDefault();\n                modal.destroy();\n                this.reactive.dispatch('cmDelete', cmIds);\n            }\n        );\n    }\n\n    /**\n     * Handle a cm availability change request.\n     *\n     * @param {Element} target the dispatch action element\n     */\n    async _requestCmAvailability(target) {\n        const cmIds = this._getTargetIds(target);\n        if (cmIds.length == 0) {\n            return;\n        }\n        // Show the availability modal to decide which action to trigger.\n        const exporter = this.reactive.getExporter();\n        const data = {\n            allowstealth: exporter.canUseStealth(this.reactive.state, cmIds),\n        };\n        const modal = await this._modalBodyRenderedPromise(ModalSaveCancel, {\n            title: getString('availability', 'core'),\n            body: Templates.render('core_courseformat/local/content/cm/availabilitymodal', data),\n            saveButtonText: getString('apply', 'core'),\n        });\n\n        this._setupMutationRadioButtonModal(modal, cmIds);\n    }\n\n    /**\n     * Handle a section availability change request.\n     *\n     * @param {Element} target the dispatch action element\n     */\n    async _requestSectionAvailability(target) {\n        const sectionIds = this._getTargetIds(target);\n        if (sectionIds.length == 0) {\n            return;\n        }\n        const title = (sectionIds.length == 1) ? 'sectionavailability_title' : 'sectionsavailability_title';\n        // Show the availability modal to decide which action to trigger.\n        const modal = await this._modalBodyRenderedPromise(ModalSaveCancel, {\n            title: this.reactive.getFormatString(title),\n            body: Templates.render('core_courseformat/local/content/section/availabilitymodal', []),\n            saveButtonText: getString('apply', 'core'),\n        });\n\n        this._setupMutationRadioButtonModal(modal, sectionIds);\n    }\n\n    /**\n     * Add events to a mutation selector radio buttons modal.\n     * @param {Modal} modal\n     * @param {Number[]} ids the section or cm ids to apply the mutation\n     */\n    _setupMutationRadioButtonModal(modal, ids) {\n        // The save button is not enabled until the user selects an option.\n        modal.setButtonDisabled('save', true);\n\n        const submitFunction = (radio) => {\n            const mutation = radio?.value;\n            if (!mutation) {\n                return false;\n            }\n            this.reactive.dispatch(mutation, ids);\n            return true;\n        };\n\n        const modalBody = getFirst(modal.getBody());\n        const radioOptions = modalBody.querySelectorAll(this.selectors.OPTIONSRADIO);\n        radioOptions.forEach(radio => {\n            radio.addEventListener('change', () => {\n                modal.setButtonDisabled('save', false);\n            });\n            radio.parentNode.addEventListener('click', () => {\n                radio.checked = true;\n                modal.setButtonDisabled('save', false);\n            });\n            radio.parentNode.addEventListener('dblclick', dbClickEvent => {\n                if (submitFunction(radio)) {\n                    dbClickEvent.preventDefault();\n                    modal.destroy();\n                }\n            });\n        });\n\n        modal.getRoot().on(\n            ModalEvents.save,\n            () => {\n                const radio = modalBody.querySelector(`${this.selectors.OPTIONSRADIO}:checked`);\n                submitFunction(radio);\n            }\n        );\n    }\n\n    /**\n     * Disable all add sections actions.\n     *\n     * @param {boolean} locked the new locked value.\n     */\n    _setAddSectionLocked(locked) {\n        const targets = this.getElements(this.selectors.ADDSECTION);\n        targets.forEach(element => {\n            element.classList.toggle(this.classes.DISABLED, locked);\n            element.classList.toggle(this.classes.ITALIC, locked);\n            this.setElementLocked(element, locked);\n        });\n    }\n\n    /**\n     * Replace an element with a copy with a different tag name.\n     *\n     * @param {Element} element the original element\n     */\n    _disableLink(element) {\n        if (element) {\n            element.style.pointerEvents = 'none';\n            element.style.userSelect = 'none';\n            element.classList.add(this.classes.DISABLED);\n            element.classList.add(this.classes.ITALIC);\n            element.setAttribute('aria-disabled', true);\n            element.addEventListener('click', event => event.preventDefault());\n        }\n    }\n\n    /**\n     * Render a modal and return a body ready promise.\n     *\n     * @param {Modal} ModalClass the modal class\n     * @param {object} modalParams the modal params\n     * @return {Promise} the modal body ready promise\n     */\n    _modalBodyRenderedPromise(ModalClass, modalParams) {\n        return new Promise((resolve, reject) => {\n            ModalClass.create(modalParams).then((modal) => {\n                modal.setRemoveOnClose(true);\n                // Handle body loading event.\n                modal.getRoot().on(ModalEvents.bodyRendered, () => {\n                    resolve(modal);\n                });\n                // Configure some extra modal params.\n                if (modalParams.saveButtonText !== undefined) {\n                    modal.setSaveButtonText(modalParams.saveButtonText);\n                }\n                if (modalParams.deleteButtonText !== undefined) {\n                    modal.setDeleteButtonText(modalParams.saveButtonText);\n                }\n                modal.show();\n                return;\n            }).catch(() => {\n                reject(`Cannot load modal content`);\n            });\n        });\n    }\n\n    /**\n     * Hide and later destroy a modal.\n     *\n     * Behat will fail if we remove the modal while some boostrap collapse is executing.\n     *\n     * @param {Modal} modal\n     * @param {HTMLElement} element the dom element to focus on.\n     */\n    _destroyModal(modal, element) {\n        modal.hide();\n        const pendingDestroy = new Pending(`courseformat/actions:destroyModal`);\n        if (element) {\n            element.focus();\n        }\n        setTimeout(() =>{\n            modal.destroy();\n            pendingDestroy.resolve();\n        }, 500);\n    }\n\n    /**\n     * Get the closest actions menu toggler to an action element.\n     *\n     * @param {HTMLElement} element the action link element\n     * @returns {HTMLElement|undefined}\n     */\n    _getClosestActionMenuToogler(element) {\n        const actionMenu = element.closest(this.selectors.ACTIONMENU);\n        if (!actionMenu) {\n            return undefined;\n        }\n        return actionMenu.querySelector(this.selectors.ACTIONMENUTOGGLER);\n    }\n}\n"],"names":["directMutations","sectionHide","sectionShow","cmHide","cmShow","cmStealth","cmMoveRight","cmMoveLeft","cmNoGroups","cmSeparateGroups","cmVisibleGroups","BaseComponent","create","name","selectors","ACTIONLINK","SECTIONLINK","CMLINK","SECTIONNODE","MODALTOGGLER","ADDSECTION","CONTENTTREE","ACTIONMENU","ACTIONMENUTOGGLER","OPTIONSRADIO","classes","DISABLED","ITALIC","actions","action","mutationReference","Object","entries","Error","stateReady","state","addEventListener","this","element","_dispatchClick","_checkSectionlist","CourseEvents","sectionRefreshed","getWatchers","watch","handler","event","target","closest","classList","contains","preventDefault","actionName","dataset","methodName","_actionMethodName","undefined","_requestMutationAction","requestName","charAt","toUpperCase","slice","_setAddSectionLocked","course","sectionlist","length","maxsections","_getTargetIds","ids","_target$dataset","id","push","bulkType","_target$dataset2","bulk","reactive","get","enabled","selectedType","selection","sectionIds","pendingModalReady","Pending","editTools","_getClosestActionMenuToogler","data","getExporter","titleText","sectionInfo","sectionid","sectiontitle","title","information","getFormatString","modal","_modalBodyRenderedPromise","Modal","body","Templates","render","modalBody","getBody","forEach","sectionId","currentElement","querySelector","_disableLink","ContentTree","SECTION","TOGGLER","COLLAPSE","matches","for","getAttribute","dispatch","_destroyModal","resolve","cmIds","exporter","cmInfo","cmid","cmname","cmId","ENTER","sectionnode","toggler","find","collapsibleId","attr","replace","expandNode","collapse","targetSectionId","targetCmId","dropData","cmDraggableData","nextcmid","section","cmlist","some","hassummary","rawtitle","bodyText","count","ModalDeleteCancel","getRoot","on","ModalEvents","delete","e","destroy","mutationName","type","modname","allowstealth","canUseStealth","ModalSaveCancel","saveButtonText","_setupMutationRadioButtonModal","setButtonDisabled","submitFunction","radio","mutation","value","querySelectorAll","parentNode","checked","dbClickEvent","save","locked","getElements","toggle","setElementLocked","style","pointerEvents","userSelect","add","setAttribute","ModalClass","modalParams","Promise","reject","then","setRemoveOnClose","bodyRendered","setSaveButtonText","deleteButtonText","setDeleteButtonText","show","catch","hide","pendingDestroy","focus","setTimeout","actionMenu"],"mappings":"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"}