Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
// This file is part of Moodle - http://moodle.org/
2
//
3
// Moodle is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// Moodle is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
15
 
16
/**
17
 * Course section format component.
18
 *
19
 * @module     core_courseformat/local/content/section
20
 * @class      core_courseformat/local/content/section
21
 * @copyright  2021 Ferran Recio <ferran@moodle.com>
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
import Header from 'core_courseformat/local/content/section/header';
26
import DndSection from 'core_courseformat/local/courseeditor/dndsection';
27
import Templates from 'core/templates';
11 efrain 28
import Pending from "core/pending";
1 efrain 29
 
30
export default class extends DndSection {
31
 
32
    /**
33
     * Constructor hook.
34
     */
35
    create() {
36
        // Optional component name for debugging.
37
        this.name = 'content_section';
38
        // Default query selectors.
39
        this.selectors = {
1441 ariadna 40
            ACTIONMENU: '.section-actions',
1 efrain 41
            SECTION_ITEM: `[data-for='section_title']`,
42
            CM: `[data-for="cmitem"]`,
43
            SECTIONINFO: `[data-for="sectioninfo"]`,
44
            SECTIONBADGES: `[data-region="sectionbadges"]`,
45
            SHOWSECTION: `[data-action="sectionShow"]`,
46
            HIDESECTION: `[data-action="sectionHide"]`,
47
            ACTIONTEXT: `.menu-action-text`,
48
            ICON: `.icon`,
49
        };
50
        // Most classes will be loaded later by DndCmItem.
51
        this.classes = {
52
            LOCKED: 'editinprogress',
53
            HASDESCRIPTION: 'description',
54
            HIDE: 'd-none',
55
            HIDDEN: 'hidden',
56
            CURRENT: 'current',
57
        };
58
 
59
        // We need our id to watch specific events.
60
        this.id = this.element.dataset.id;
61
    }
62
 
63
    /**
64
     * Initial state ready method.
65
     *
66
     * @param {Object} state the initial state
67
     */
68
    stateReady(state) {
69
        this.configState(state);
70
        // Drag and drop is only available for components compatible course formats.
71
        if (this.reactive.isEditing && this.reactive.supportComponents) {
72
            // Section zero and other formats sections may not have a title to drag.
73
            const sectionItem = this.getElement(this.selectors.SECTION_ITEM);
74
            if (sectionItem) {
75
                // Init the inner dragable element.
76
                const headerComponent = new Header({
77
                    ...this,
78
                    element: sectionItem,
79
                    fullregion: this.element,
80
                });
81
                this.configDragDrop(headerComponent);
82
            }
83
        }
11 efrain 84
        this._openSectionIfNecessary();
1 efrain 85
    }
86
 
87
    /**
11 efrain 88
     * Open the section if the anchored activity is inside.
89
     */
90
    async _openSectionIfNecessary() {
91
        const pageCmInfo = this.reactive.getPageAnchorCmInfo();
92
        if (!pageCmInfo || pageCmInfo.sectionid !== this.id) {
93
            return;
94
        }
95
        await this.reactive.dispatch('sectionContentCollapsed', [this.id], false);
96
        const pendingOpen = new Pending(`courseformat/section:openSectionIfNecessary`);
97
        setTimeout(() => {
1441 ariadna 98
            document.querySelector("#" + pageCmInfo.anchor).scrollIntoView();
11 efrain 99
            this.reactive.dispatch('setPageItem', 'cm', pageCmInfo.id);
100
            pendingOpen.resolve();
101
        }, 250);
102
    }
103
 
104
    /**
1 efrain 105
     * Component watchers.
106
     *
107
     * @returns {Array} of watchers
108
     */
109
    getWatchers() {
110
        return [
111
            {watch: `section[${this.id}]:updated`, handler: this._refreshSection},
112
        ];
113
    }
114
 
115
    /**
116
     * Validate if the drop data can be dropped over the component.
117
     *
118
     * @param {Object} dropdata the exported drop data.
119
     * @returns {boolean}
120
     */
121
    validateDropData(dropdata) {
122
        // If the format uses one section per page sections dropping in the content is ignored.
1441 ariadna 123
        if (dropdata?.type === 'section' && (this.reactive?.sectionReturn ?? this.reactive?.pageSectionId) !== null) {
1 efrain 124
            return false;
125
        }
126
        return super.validateDropData(dropdata);
127
    }
128
 
129
    /**
130
     * Get the last CM element of that section.
131
     *
132
     * @returns {element|null}
133
     */
134
    getLastCm() {
135
        const cms = this.getElements(this.selectors.CM);
136
        // DndUpload may add extra elements so :last-child selector cannot be used.
137
        if (!cms || cms.length === 0) {
138
            return null;
139
        }
1441 ariadna 140
        const lastCm = cms[cms.length - 1];
141
        // If it is a delegated section return the last item overall.
142
        if (this.section.component !== null) {
143
            return lastCm;
144
        }
145
        // If it is a regular section and the last item overall has a parent cm, return the parent instead.
146
        const parentSection = lastCm.parentNode.closest(this.selectors.CM);
147
        return parentSection ?? lastCm;
1 efrain 148
    }
149
 
150
    /**
1441 ariadna 151
     * Get a fallback element when there is no CM in the section.
152
     *
153
     * @returns {element|null} the las course module element of the section.
154
     */
155
    getLastCmFallback() {
156
        // The sectioninfo is always present, even when the section is empty.
157
        return this.getElement(this.selectors.SECTIONINFO);
158
    }
159
 
160
    /**
1 efrain 161
     * Update a content section using the state information.
162
     *
163
     * @param {object} param
164
     * @param {Object} param.element details the update details.
165
     */
166
    _refreshSection({element}) {
167
        // Update classes.
168
        this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);
169
        this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);
170
        this.element.classList.toggle(this.classes.HIDDEN, !element.visible ?? false);
171
        this.element.classList.toggle(this.classes.CURRENT, element.current ?? false);
172
        this.locked = element.locked;
173
        // The description box classes depends on the section state.
174
        const sectioninfo = this.getElement(this.selectors.SECTIONINFO);
175
        if (sectioninfo) {
176
            sectioninfo.classList.toggle(this.classes.HASDESCRIPTION, element.hasrestrictions);
177
        }
178
        // Update section badges and menus.
179
        this._updateBadges(element);
180
        this._updateActionsMenu(element);
181
    }
182
 
183
    /**
184
     * Update a section badges using the state information.
185
     *
186
     * @param {object} section the section state.
187
     */
188
    _updateBadges(section) {
189
        const current = this.getElement(`${this.selectors.SECTIONBADGES} [data-type='iscurrent']`);
190
        current?.classList.toggle(this.classes.HIDE, !section.current);
191
 
192
        const hiddenFromStudents = this.getElement(`${this.selectors.SECTIONBADGES} [data-type='hiddenfromstudents']`);
193
        hiddenFromStudents?.classList.toggle(this.classes.HIDE, section.visible);
194
    }
195
 
196
    /**
197
     * Update a section action menus.
198
     *
199
     * @param {object} section the section state.
200
     */
201
    async _updateActionsMenu(section) {
202
        let selector;
203
        let newAction;
204
        if (section.visible) {
205
            selector = this.selectors.SHOWSECTION;
206
            newAction = 'sectionHide';
207
        } else {
208
            selector = this.selectors.HIDESECTION;
209
            newAction = 'sectionShow';
210
        }
211
        // Find the affected action.
1441 ariadna 212
        const affectedAction = this._getActionMenu(selector);
1 efrain 213
        if (!affectedAction) {
214
            return;
215
        }
216
        // Change action.
217
        affectedAction.dataset.action = newAction;
218
        // Change text.
219
        const actionText = affectedAction.querySelector(this.selectors.ACTIONTEXT);
220
        if (affectedAction.dataset?.swapname && actionText) {
221
            const oldText = actionText?.innerText;
222
            actionText.innerText = affectedAction.dataset.swapname;
223
            affectedAction.dataset.swapname = oldText;
224
        }
225
        // Change icon.
226
        const icon = affectedAction.querySelector(this.selectors.ICON);
227
        if (affectedAction.dataset?.swapicon && icon) {
228
            const newIcon = affectedAction.dataset.swapicon;
11 efrain 229
            affectedAction.dataset.swapicon = affectedAction.dataset.icon;
230
            affectedAction.dataset.icon = newIcon;
1 efrain 231
            if (newIcon) {
232
                const pixHtml = await Templates.renderPix(newIcon, 'core');
233
                Templates.replaceNode(icon, pixHtml, '');
234
            }
235
        }
236
    }
1441 ariadna 237
 
238
    /**
239
     * Get the action menu element from the selector.
240
     *
241
     * @param {string} selector The selector to find the action menu.
242
     * @returns The action menu element.
243
     */
244
    _getActionMenu(selector) {
245
        return document.querySelector(`${this.selectors.ACTIONMENU}[data-sectionid='${this.id}'] ${selector}`);
246
    }
1 efrain 247
}