Proyectos de Subversion Moodle

Rev

Rev 1 | | 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 index main component.
18
 *
19
 * @module     core_courseformat/local/content
20
 * @class      core_courseformat/local/content
21
 * @copyright  2020 Ferran Recio <ferran@moodle.com>
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
import {BaseComponent} from 'core/reactive';
26
import {debounce} from 'core/utils';
27
import {getCurrentCourseEditor} from 'core_courseformat/courseeditor';
28
import Config from 'core/config';
29
import inplaceeditable from 'core/inplace_editable';
30
import Section from 'core_courseformat/local/content/section';
31
import CmItem from 'core_courseformat/local/content/section/cmitem';
32
import Fragment from 'core/fragment';
33
import Templates from 'core/templates';
34
import DispatchActions from 'core_courseformat/local/content/actions';
35
import * as CourseEvents from 'core_course/events';
36
// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.
37
import jQuery from 'jquery';
38
import Pending from 'core/pending';
39
 
40
export default class Component extends BaseComponent {
41
 
42
    /**
43
     * Constructor hook.
44
     *
45
     * @param {Object} descriptor the component descriptor
46
     */
47
    create(descriptor) {
48
        // Optional component name for debugging.
49
        this.name = 'course_format';
50
        // Default query selectors.
51
        this.selectors = {
52
            SECTION: `[data-for='section']`,
53
            SECTION_ITEM: `[data-for='section_title']`,
54
            SECTION_CMLIST: `[data-for='cmlist']`,
55
            COURSE_SECTIONLIST: `[data-for='course_sectionlist']`,
56
            CM: `[data-for='cmitem']`,
57
            TOGGLER: `[data-action="togglecoursecontentsection"]`,
58
            COLLAPSE: `[data-toggle="collapse"]`,
59
            TOGGLEALL: `[data-toggle="toggleall"]`,
60
            // Formats can override the activity tag but a default one is needed to create new elements.
61
            ACTIVITYTAG: 'li',
62
            SECTIONTAG: 'li',
63
        };
64
        this.selectorGenerators = {
65
            cmNameFor: (id) => `[data-cm-name-for='${id}']`,
66
            sectionNameFor: (id) => `[data-section-name-for='${id}']`,
67
        };
68
        // Default classes to toggle on refresh.
69
        this.classes = {
70
            COLLAPSED: `collapsed`,
71
            // Course content classes.
72
            ACTIVITY: `activity`,
73
            STATEDREADY: `stateready`,
74
            SECTION: `section`,
75
        };
76
        // Array to save dettached elements during element resorting.
77
        this.dettachedCms = {};
78
        this.dettachedSections = {};
79
        // Index of sections and cms components.
80
        this.sections = {};
81
        this.cms = {};
82
        // The page section return.
83
        this.sectionReturn = descriptor.sectionReturn ?? null;
84
        this.debouncedReloads = new Map();
85
    }
86
 
87
    /**
88
     * Static method to create a component instance form the mustahce template.
89
     *
90
     * @param {string} target the DOM main element or its ID
91
     * @param {object} selectors optional css selector overrides
92
     * @param {number} sectionReturn the content section return
93
     * @return {Component}
94
     */
95
    static init(target, selectors, sectionReturn) {
96
        return new Component({
97
            element: document.getElementById(target),
98
            reactive: getCurrentCourseEditor(),
99
            selectors,
100
            sectionReturn,
101
        });
102
    }
103
 
104
    /**
105
     * Initial state ready method.
106
     *
107
     * @param {Object} state the state data
108
     */
109
    stateReady(state) {
110
        this._indexContents();
111
        // Activate section togglers.
112
        this.addEventListener(this.element, 'click', this._sectionTogglers);
113
 
114
        // Collapse/Expand all sections button.
115
        const toogleAll = this.getElement(this.selectors.TOGGLEALL);
116
        if (toogleAll) {
117
 
118
            // Ensure collapse menu button adds aria-controls attribute referring to each collapsible element.
119
            const collapseElements = this.getElements(this.selectors.COLLAPSE);
120
            const collapseElementIds = [...collapseElements].map(element => element.id);
121
            toogleAll.setAttribute('aria-controls', collapseElementIds.join(' '));
122
 
123
            this.addEventListener(toogleAll, 'click', this._allSectionToggler);
124
            this.addEventListener(toogleAll, 'keydown', e => {
125
                // Collapse/expand all sections when Space key is pressed on the toggle button.
126
                if (e.key === ' ') {
127
                    this._allSectionToggler(e);
128
                }
129
            });
130
            this._refreshAllSectionsToggler(state);
131
        }
132
 
133
        if (this.reactive.supportComponents) {
134
            // Actions are only available in edit mode.
135
            if (this.reactive.isEditing) {
136
                new DispatchActions(this);
137
            }
138
 
139
            // Mark content as state ready.
140
            this.element.classList.add(this.classes.STATEDREADY);
141
        }
142
 
143
        // Capture completion events.
144
        this.addEventListener(
145
            this.element,
146
            CourseEvents.manualCompletionToggled,
147
            this._completionHandler
148
        );
149
 
150
        // Capture page scroll to update page item.
151
        this.addEventListener(
152
            document,
153
            "scroll",
154
            this._scrollHandler
155
        );
156
    }
157
 
158
    /**
159
     * Setup sections toggler.
160
     *
161
     * Toggler click is delegated to the main course content element because new sections can
162
     * appear at any moment and this way we prevent accidental double bindings.
163
     *
164
     * @param {Event} event the triggered event
165
     */
166
    _sectionTogglers(event) {
167
        const sectionlink = event.target.closest(this.selectors.TOGGLER);
168
        const closestCollapse = event.target.closest(this.selectors.COLLAPSE);
169
        // Assume that chevron is the only collapse toggler in a section heading;
170
        // I think this is the most efficient way to verify at the moment.
171
        const isChevron = closestCollapse?.closest(this.selectors.SECTION_ITEM);
172
 
173
        if (sectionlink || isChevron) {
174
 
175
            const section = event.target.closest(this.selectors.SECTION);
176
            const toggler = section.querySelector(this.selectors.COLLAPSE);
177
            const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;
178
 
179
            const sectionId = section.getAttribute('data-id');
180
            this.reactive.dispatch(
181
                'sectionContentCollapsed',
182
                [sectionId],
183
                !isCollapsed,
184
            );
185
        }
186
    }
187
 
188
    /**
189
     * Handle the collapse/expand all sections button.
190
     *
191
     * Toggler click is delegated to the main course content element because new sections can
192
     * appear at any moment and this way we prevent accidental double bindings.
193
     *
194
     * @param {Event} event the triggered event
195
     */
196
    _allSectionToggler(event) {
197
        event.preventDefault();
198
 
199
        const target = event.target.closest(this.selectors.TOGGLEALL);
200
        const isAllCollapsed = target.classList.contains(this.classes.COLLAPSED);
201
 
202
        const course = this.reactive.get('course');
203
        this.reactive.dispatch(
204
            'sectionContentCollapsed',
205
            course.sectionlist ?? [],
206
            !isAllCollapsed
207
        );
208
    }
209
 
210
    /**
211
     * Return the component watchers.
212
     *
213
     * @returns {Array} of watchers
214
     */
215
    getWatchers() {
216
        // Section return is a global page variable but most formats define it just before start printing
217
        // the course content. This is the reason why we define this page setting here.
218
        this.reactive.sectionReturn = this.sectionReturn;
219
 
220
        // Check if the course format is compatible with reactive components.
221
        if (!this.reactive.supportComponents) {
222
            return [];
223
        }
224
        return [
225
            // State changes that require to reload some course modules.
226
            {watch: `cm.visible:updated`, handler: this._reloadCm},
227
            {watch: `cm.stealth:updated`, handler: this._reloadCm},
228
            {watch: `cm.sectionid:updated`, handler: this._reloadCm},
229
            {watch: `cm.indent:updated`, handler: this._reloadCm},
230
            {watch: `cm.groupmode:updated`, handler: this._reloadCm},
231
            {watch: `cm.name:updated`, handler: this._refreshCmName},
232
            // Update section number and title.
233
            {watch: `section.number:updated`, handler: this._refreshSectionNumber},
234
            {watch: `section.title:updated`, handler: this._refreshSectionTitle},
235
            // Collapse and expand sections.
236
            {watch: `section.contentcollapsed:updated`, handler: this._refreshSectionCollapsed},
237
            // Sections and cm sorting.
238
            {watch: `transaction:start`, handler: this._startProcessing},
239
            {watch: `course.sectionlist:updated`, handler: this._refreshCourseSectionlist},
240
            {watch: `section.cmlist:updated`, handler: this._refreshSectionCmlist},
241
            // Section visibility.
242
            {watch: `section.visible:updated`, handler: this._reloadSection},
243
            // Reindex sections and cms.
244
            {watch: `state:updated`, handler: this._indexContents},
245
        ];
246
    }
247
 
248
    /**
249
     * Update a course module name on the whole page.
250
     *
251
     * @param {object} param
252
     * @param {Object} param.element details the update details.
253
     */
254
    _refreshCmName({element}) {
255
        // Update classes.
256
        // Replace the text content of the cm name.
257
        const allCmNamesFor = this.getElements(
258
            this.selectorGenerators.cmNameFor(element.id)
259
        );
260
        allCmNamesFor.forEach((cmNameFor) => {
261
            cmNameFor.textContent = element.name;
262
        });
263
    }
264
 
265
    /**
266
     * Update section collapsed state via bootstrap 4 if necessary.
267
     *
268
     * Formats that do not use bootstrap 4 must override this method in order to keep the section
269
     * toggling working.
270
     *
271
     * @param {object} args
272
     * @param {Object} args.state The state data
273
     * @param {Object} args.element The element to update
274
     */
275
    _refreshSectionCollapsed({state, element}) {
276
        const target = this.getElement(this.selectors.SECTION, element.id);
277
        if (!target) {
278
            throw new Error(`Unknown section with ID ${element.id}`);
279
        }
280
        // Check if it is already done.
281
        const toggler = target.querySelector(this.selectors.COLLAPSE);
282
        const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;
283
 
284
        if (element.contentcollapsed !== isCollapsed) {
285
            let collapsibleId = toggler.dataset.target ?? toggler.getAttribute("href");
286
            if (!collapsibleId) {
287
                return;
288
            }
289
            collapsibleId = collapsibleId.replace('#', '');
290
            const collapsible = document.getElementById(collapsibleId);
291
            if (!collapsible) {
292
                return;
293
            }
294
 
295
            // Course index is based on Bootstrap 4 collapsibles. To collapse them we need jQuery to
296
            // interact with collapsibles methods. Hopefully, this will change in Bootstrap 5 because
297
            // it does not require jQuery anymore (when MDL-71979 is integrated).
298
            jQuery(collapsible).collapse(element.contentcollapsed ? 'hide' : 'show');
299
        }
300
 
301
        this._refreshAllSectionsToggler(state);
302
    }
303
 
304
    /**
305
     * Refresh the collapse/expand all sections element.
306
     *
307
     * @param {Object} state The state data
308
     */
309
    _refreshAllSectionsToggler(state) {
310
        const target = this.getElement(this.selectors.TOGGLEALL);
311
        if (!target) {
312
            return;
313
        }
314
        // Check if we have all sections collapsed/expanded.
315
        let allcollapsed = true;
316
        let allexpanded = true;
317
        state.section.forEach(
318
            section => {
319
                allcollapsed = allcollapsed && section.contentcollapsed;
320
                allexpanded = allexpanded && !section.contentcollapsed;
321
            }
322
        );
323
        if (allcollapsed) {
324
            target.classList.add(this.classes.COLLAPSED);
325
            target.setAttribute('aria-expanded', false);
326
        }
327
        if (allexpanded) {
328
            target.classList.remove(this.classes.COLLAPSED);
329
            target.setAttribute('aria-expanded', true);
330
        }
331
    }
332
 
333
    /**
334
     * Setup the component to start a transaction.
335
     *
336
     * Some of the course actions replaces the current DOM element with a new one before updating the
337
     * course state. This means the component cannot preload any index properly until the transaction starts.
338
     *
339
     */
340
    _startProcessing() {
341
        // During a section or cm sorting, some elements could be dettached from the DOM and we
342
        // need to store somewhare in case they are needed later.
343
        this.dettachedCms = {};
344
        this.dettachedSections = {};
345
    }
346
 
347
    /**
348
     * Activity manual completion listener.
349
     *
350
     * @param {Event} event the custom ecent
351
     */
352
    _completionHandler({detail}) {
353
        if (detail === undefined) {
354
            return;
355
        }
356
        this.reactive.dispatch('cmCompletion', [detail.cmid], detail.completed);
357
    }
358
 
359
    /**
360
     * Check the current page scroll and update the active element if necessary.
361
     */
362
    _scrollHandler() {
363
        const pageOffset = window.scrollY;
364
        const items = this.reactive.getExporter().allItemsArray(this.reactive.state);
365
        // Check what is the active element now.
366
        let pageItem = null;
367
        items.every(item => {
368
            const index = (item.type === 'section') ? this.sections : this.cms;
369
            if (index[item.id] === undefined) {
370
                return true;
371
            }
372
 
373
            const element = index[item.id].element;
374
            pageItem = item;
375
            return pageOffset >= element.offsetTop;
376
        });
377
        if (pageItem) {
378
            this.reactive.dispatch('setPageItem', pageItem.type, pageItem.id);
379
        }
380
    }
381
 
382
    /**
383
     * Update a course section when the section number changes.
384
     *
385
     * The courseActions module used for most course section tools still depends on css classes and
386
     * section numbers (not id). To prevent inconsistencies when a section is moved, we need to refresh
387
     * the
388
     *
389
     * Course formats can override the section title rendering so the frontend depends heavily on backend
390
     * rendering. Luckily in edit mode we can trigger a title update using the inplace_editable module.
391
     *
392
     * @param {Object} param
393
     * @param {Object} param.element details the update details.
394
     */
395
    _refreshSectionNumber({element}) {
396
        // Find the element.
397
        const target = this.getElement(this.selectors.SECTION, element.id);
398
        if (!target) {
399
            // Job done. Nothing to refresh.
400
            return;
401
        }
402
        // Update section numbers in all data, css and YUI attributes.
403
        target.id = `section-${element.number}`;
404
        // YUI uses section number as section id in data-sectionid, in principle if a format use components
405
        // don't need this sectionid attribute anymore, but we keep the compatibility in case some plugin
406
        // use it for legacy purposes.
407
        target.dataset.sectionid = element.number;
408
        // The data-number is the attribute used by components to store the section number.
409
        target.dataset.number = element.number;
410
 
411
        // Update title and title inplace editable, if any.
412
        const inplace = inplaceeditable.getInplaceEditable(target.querySelector(this.selectors.SECTION_ITEM));
413
        if (inplace) {
414
            // The course content HTML can be modified at any moment, so the function need to do some checkings
415
            // to make sure the inplace editable still represents the same itemid.
416
            const currentvalue = inplace.getValue();
417
            const currentitemid = inplace.getItemId();
418
            // Unnamed sections must be recalculated.
419
            if (inplace.getValue() === '') {
420
                // The value to send can be an empty value if it is a default name.
421
                if (currentitemid == element.id && (currentvalue != element.rawtitle || element.rawtitle == '')) {
422
                    inplace.setValue(element.rawtitle);
423
                }
424
            }
425
        }
426
    }
427
 
428
    /**
429
     * Update a course section name on the whole page.
430
     *
431
     * @param {object} param
432
     * @param {Object} param.element details the update details.
433
     */
434
    _refreshSectionTitle({element}) {
435
        // Replace the text content of the section name in the whole page.
436
        const allSectionNamesFor = document.querySelectorAll(
437
            this.selectorGenerators.sectionNameFor(element.id)
438
        );
439
        allSectionNamesFor.forEach((sectionNameFor) => {
440
            sectionNameFor.textContent = element.title;
441
        });
442
    }
443
 
444
    /**
445
     * Refresh a section cm list.
446
     *
447
     * @param {Object} param
448
     * @param {Object} param.element details the update details.
449
     */
450
    _refreshSectionCmlist({element}) {
451
        const cmlist = element.cmlist ?? [];
452
        const section = this.getElement(this.selectors.SECTION, element.id);
453
        const listparent = section?.querySelector(this.selectors.SECTION_CMLIST);
454
        // A method to create a fake element to be replaced when the item is ready.
455
        const createCm = this._createCmItem.bind(this);
456
        if (listparent) {
457
            this._fixOrder(listparent, cmlist, this.selectors.CM, this.dettachedCms, createCm);
458
        }
459
    }
460
 
461
    /**
462
     * Refresh the section list.
463
     *
464
     * @param {Object} param
465
     * @param {Object} param.state the full state object.
466
     */
467
    _refreshCourseSectionlist({state}) {
468
        // If we have a section return means we only show a single section so no need to fix order.
469
        if (this.reactive.sectionReturn !== null) {
470
            return;
471
        }
472
        const sectionlist = this.reactive.getExporter().listedSectionIds(state);
473
        const listparent = this.getElement(this.selectors.COURSE_SECTIONLIST);
474
        // For now section cannot be created at a frontend level.
475
        const createSection = this._createSectionItem.bind(this);
476
        if (listparent) {
477
            this._fixOrder(listparent, sectionlist, this.selectors.SECTION, this.dettachedSections, createSection);
478
        }
479
    }
480
 
481
    /**
482
     * Regenerate content indexes.
483
     *
484
     * This method is used when a legacy action refresh some content element.
485
     */
486
    _indexContents() {
487
        // Find unindexed sections.
488
        this._scanIndex(
489
            this.selectors.SECTION,
490
            this.sections,
491
            (item) => {
492
                return new Section(item);
493
            }
494
        );
495
 
496
        // Find unindexed cms.
497
        this._scanIndex(
498
            this.selectors.CM,
499
            this.cms,
500
            (item) => {
501
                return new CmItem(item);
502
            }
503
        );
504
    }
505
 
506
    /**
507
     * Reindex a content (section or cm) of the course content.
508
     *
509
     * This method is used internally by _indexContents.
510
     *
511
     * @param {string} selector the DOM selector to scan
512
     * @param {*} index the index attribute to update
513
     * @param {*} creationhandler method to create a new indexed element
514
     */
515
    _scanIndex(selector, index, creationhandler) {
516
        const items = this.getElements(`${selector}:not([data-indexed])`);
517
        items.forEach((item) => {
518
            if (!item?.dataset?.id) {
519
                return;
520
            }
521
            // Delete previous item component.
522
            if (index[item.dataset.id] !== undefined) {
523
                index[item.dataset.id].unregister();
524
            }
525
            // Create the new component.
526
            index[item.dataset.id] = creationhandler({
527
                ...this,
528
                element: item,
529
            });
530
            // Mark as indexed.
531
            item.dataset.indexed = true;
532
        });
533
    }
534
 
535
    /**
536
     * Reload a course module contents.
537
     *
538
     * Most course module HTML is still strongly backend dependant.
539
     * Some changes require to get a new version of the module.
540
     *
541
     * @param {object} param0 the watcher details
542
     * @param {object} param0.element the state object
543
     */
544
    _reloadCm({element}) {
545
        if (!this.getElement(this.selectors.CM, element.id)) {
546
            return;
547
        }
548
        const debouncedReload = this._getDebouncedReloadCm(element.id);
549
        debouncedReload();
550
    }
551
 
552
    /**
553
     * Generate or get a reload CM debounced function.
554
     * @param {Number} cmId
555
     * @returns {Function} the debounced reload function
556
     */
557
    _getDebouncedReloadCm(cmId) {
558
        const pendingKey = `courseformat/content:reloadCm_${cmId}`;
559
        let debouncedReload = this.debouncedReloads.get(pendingKey);
560
        if (debouncedReload) {
561
            return debouncedReload;
562
        }
563
        const reload = () => {
564
            const pendingReload = new Pending(pendingKey);
565
            this.debouncedReloads.delete(pendingKey);
566
            const cmitem = this.getElement(this.selectors.CM, cmId);
567
            if (!cmitem) {
568
                return pendingReload.resolve();
569
            }
570
            const promise = Fragment.loadFragment(
571
                'core_courseformat',
572
                'cmitem',
573
                Config.courseContextId,
574
                {
575
                    id: cmId,
576
                    courseid: Config.courseId,
577
                    sr: this.reactive.sectionReturn ?? null,
578
                }
579
            );
580
            promise.then((html, js) => {
581
                // Other state change can reload the CM or the section before this one.
582
                if (!document.contains(cmitem)) {
583
                    pendingReload.resolve();
584
                    return false;
585
                }
586
                Templates.replaceNode(cmitem, html, js);
587
                this._indexContents();
588
                pendingReload.resolve();
589
                return true;
590
            }).catch(() => {
591
                pendingReload.resolve();
592
            });
593
            return pendingReload;
594
        };
595
        debouncedReload = debounce(
596
            reload,
597
            200,
598
            {
599
                cancel: true, pending: true
600
            }
601
        );
602
        this.debouncedReloads.set(pendingKey, debouncedReload);
603
        return debouncedReload;
604
    }
605
 
606
    /**
607
     * Cancel the active reload CM debounced function, if any.
608
     * @param {Number} cmId
609
     */
610
    _cancelDebouncedReloadCm(cmId) {
611
        const pendingKey = `courseformat/content:reloadCm_${cmId}`;
612
        const debouncedReload = this.debouncedReloads.get(pendingKey);
613
        if (!debouncedReload) {
614
            return;
615
        }
616
        debouncedReload.cancel();
617
        this.debouncedReloads.delete(pendingKey);
618
    }
619
 
620
    /**
621
     * Reload a course section contents.
622
     *
623
     * Section HTML is still strongly backend dependant.
624
     * Some changes require to get a new version of the section.
625
     *
626
     * @param {details} param0 the watcher details
627
     * @param {object} param0.element the state object
628
     */
629
    _reloadSection({element}) {
630
        const pendingReload = new Pending(`courseformat/content:reloadSection_${element.id}`);
631
        const sectionitem = this.getElement(this.selectors.SECTION, element.id);
632
        if (sectionitem) {
633
            // Cancel any pending reload because the section will reload cms too.
634
            for (const cmId of element.cmlist) {
635
                this._cancelDebouncedReloadCm(cmId);
636
            }
637
            const promise = Fragment.loadFragment(
638
                'core_courseformat',
639
                'section',
640
                Config.courseContextId,
641
                {
642
                    id: element.id,
643
                    courseid: Config.courseId,
644
                    sr: this.reactive.sectionReturn ?? null,
645
                }
646
            );
647
            promise.then((html, js) => {
648
                Templates.replaceNode(sectionitem, html, js);
649
                this._indexContents();
650
                pendingReload.resolve();
651
            }).catch(() => {
652
                pendingReload.resolve();
653
            });
654
        }
655
    }
656
 
657
    /**
658
     * Create a new course module item in a section.
659
     *
660
     * Thos method will append a fake item in the container and trigger an ajax request to
661
     * replace the fake element by the real content.
662
     *
663
     * @param {Element} container the container element (section)
664
     * @param {Number} cmid the course-module ID
665
     * @returns {Element} the created element
666
     */
667
    _createCmItem(container, cmid) {
668
        const newItem = document.createElement(this.selectors.ACTIVITYTAG);
669
        newItem.dataset.for = 'cmitem';
670
        newItem.dataset.id = cmid;
671
        // The legacy actions.js requires a specific ID and class to refresh the CM.
672
        newItem.id = `module-${cmid}`;
673
        newItem.classList.add(this.classes.ACTIVITY);
674
        container.append(newItem);
675
        this._reloadCm({
676
            element: this.reactive.get('cm', cmid),
677
        });
678
        return newItem;
679
    }
680
 
681
    /**
682
     * Create a new section item.
683
     *
684
     * This method will append a fake item in the container and trigger an ajax request to
685
     * replace the fake element by the real content.
686
     *
687
     * @param {Element} container the container element (section)
688
     * @param {Number} sectionid the course-module ID
689
     * @returns {Element} the created element
690
     */
691
    _createSectionItem(container, sectionid) {
692
        const section = this.reactive.get('section', sectionid);
693
        const newItem = document.createElement(this.selectors.SECTIONTAG);
694
        newItem.dataset.for = 'section';
695
        newItem.dataset.id = sectionid;
696
        newItem.dataset.number = section.number;
697
        // The legacy actions.js requires a specific ID and class to refresh the section.
698
        newItem.id = `section-${sectionid}`;
699
        newItem.classList.add(this.classes.SECTION);
700
        container.append(newItem);
701
        this._reloadSection({
702
            element: section,
703
        });
704
        return newItem;
705
    }
706
 
707
    /**
708
     * Fix/reorder the section or cms order.
709
     *
710
     * @param {Element} container the HTML element to reorder.
711
     * @param {Array} neworder an array with the ids order
712
     * @param {string} selector the element selector
713
     * @param {Object} dettachedelements a list of dettached elements
714
     * @param {function} createMethod method to create missing elements
715
     */
716
    async _fixOrder(container, neworder, selector, dettachedelements, createMethod) {
717
        if (container === undefined) {
718
            return;
719
        }
720
 
721
        // Empty lists should not be visible.
722
        if (!neworder.length) {
723
            container.classList.add('hidden');
724
            container.innerHTML = '';
725
            return;
726
        }
727
 
728
        // Grant the list is visible (in case it was empty).
729
        container.classList.remove('hidden');
730
 
731
        // Move the elements in order at the beginning of the list.
732
        neworder.forEach((itemid, index) => {
733
            let item = this.getElement(selector, itemid) ?? dettachedelements[itemid] ?? createMethod(container, itemid);
734
            if (item === undefined) {
735
                // Missing elements cannot be sorted.
736
                return;
737
            }
738
            // Get the current elemnt at that position.
739
            const currentitem = container.children[index];
740
            if (currentitem === undefined) {
741
                container.append(item);
742
                return;
743
            }
744
            if (currentitem !== item) {
745
                container.insertBefore(item, currentitem);
746
            }
747
        });
748
 
749
        // Dndupload add a fake element we need to keep.
750
        let dndFakeActivity;
751
 
752
        // Remove the remaining elements.
753
        while (container.children.length > neworder.length) {
754
            const lastchild = container.lastChild;
755
            if (lastchild?.classList?.contains('dndupload-preview')) {
756
                dndFakeActivity = lastchild;
757
            } else {
758
                dettachedelements[lastchild?.dataset?.id ?? 0] = lastchild;
759
            }
760
            container.removeChild(lastchild);
761
        }
762
        // Restore dndupload fake element.
763
        if (dndFakeActivity) {
764
            container.append(dndFakeActivity);
765
        }
766
    }
767
}