Proyectos de Subversion Moodle

Rev

Rev 11 | Mostrar el archivo completo | | | Autoría | Ultima modificación | Ver Log |

Rev 11 Rev 1441
Línea 21... Línea 21...
21
 * @copyright  2020 Ferran Recio <ferran@moodle.com>
21
 * @copyright  2020 Ferran Recio <ferran@moodle.com>
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
23
 */
Línea 24... Línea 24...
24
 
24
 
-
 
25
import {BaseComponent} from 'core/reactive';
25
import {BaseComponent} from 'core/reactive';
26
import Collapse from 'theme_boost/bootstrap/collapse';
26
import {debounce} from 'core/utils';
27
import {throttle, debounce} from 'core/utils';
27
import {getCurrentCourseEditor} from 'core_courseformat/courseeditor';
28
import {getCurrentCourseEditor} from 'core_courseformat/courseeditor';
28
import Config from 'core/config';
29
import Config from 'core/config';
29
import inplaceeditable from 'core/inplace_editable';
30
import inplaceeditable from 'core/inplace_editable';
30
import Section from 'core_courseformat/local/content/section';
31
import Section from 'core_courseformat/local/content/section';
31
import CmItem from 'core_courseformat/local/content/section/cmitem';
32
import CmItem from 'core_courseformat/local/content/section/cmitem';
32
import Fragment from 'core/fragment';
33
import Fragment from 'core/fragment';
33
import Templates from 'core/templates';
34
import Templates from 'core/templates';
34
import DispatchActions from 'core_courseformat/local/content/actions';
35
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';
36
import * as CourseEvents from 'core_course/events';
-
 
37
import Pending from 'core/pending';
Línea 38... Línea 38...
38
import Pending from 'core/pending';
38
import log from "core/log";
Línea 39... Línea 39...
39
 
39
 
40
export default class Component extends BaseComponent {
40
export default class Component extends BaseComponent {
Línea 53... Línea 53...
53
            SECTION_ITEM: `[data-for='section_title']`,
53
            SECTION_ITEM: `[data-for='section_title']`,
54
            SECTION_CMLIST: `[data-for='cmlist']`,
54
            SECTION_CMLIST: `[data-for='cmlist']`,
55
            COURSE_SECTIONLIST: `[data-for='course_sectionlist']`,
55
            COURSE_SECTIONLIST: `[data-for='course_sectionlist']`,
56
            CM: `[data-for='cmitem']`,
56
            CM: `[data-for='cmitem']`,
57
            TOGGLER: `[data-action="togglecoursecontentsection"]`,
57
            TOGGLER: `[data-action="togglecoursecontentsection"]`,
58
            COLLAPSE: `[data-toggle="collapse"]`,
58
            COLLAPSE: `[data-bs-toggle="collapse"]`,
59
            TOGGLEALL: `[data-toggle="toggleall"]`,
59
            TOGGLEALL: `[data-toggle="toggleall"]`,
60
            // Formats can override the activity tag but a default one is needed to create new elements.
60
            // Formats can override the activity tag but a default one is needed to create new elements.
61
            ACTIVITYTAG: 'li',
61
            ACTIVITYTAG: 'li',
62
            SECTIONTAG: 'li',
62
            SECTIONTAG: 'li',
63
        };
63
        };
Línea 77... Línea 77...
77
        this.dettachedCms = {};
77
        this.dettachedCms = {};
78
        this.dettachedSections = {};
78
        this.dettachedSections = {};
79
        // Index of sections and cms components.
79
        // Index of sections and cms components.
80
        this.sections = {};
80
        this.sections = {};
81
        this.cms = {};
81
        this.cms = {};
82
        // The page section return.
82
        // The section number and ID of the displayed page.
83
        this.sectionReturn = descriptor.sectionReturn ?? null;
83
        this.sectionReturn = descriptor?.sectionReturn ?? null;
-
 
84
        this.pageSectionId = descriptor?.pageSectionId ?? null;
84
        this.debouncedReloads = new Map();
85
        this.debouncedReloads = new Map();
85
    }
86
    }
Línea 86... Línea 87...
86
 
87
 
87
    /**
88
    /**
88
     * Static method to create a component instance form the mustahce template.
89
     * Static method to create a component instance form the mustahce template.
89
     *
90
     *
90
     * @param {string} target the DOM main element or its ID
91
     * @param {string} target the DOM main element or its ID
91
     * @param {object} selectors optional css selector overrides
92
     * @param {object} selectors optional css selector overrides
-
 
93
     * @param {number} sectionReturn the section number of the displayed page
92
     * @param {number} sectionReturn the content section return
94
     * @param {number} pageSectionId the section ID of the displayed page
93
     * @return {Component}
95
     * @return {Component}
94
     */
96
     */
-
 
97
    static init(target, selectors, sectionReturn, pageSectionId) {
-
 
98
        let element = document.querySelector(target);
-
 
99
        // TODO Remove this if condition as part of MDL-83851.
-
 
100
        if (!element) {
-
 
101
            log.debug('Init component with id is deprecated, use a query selector instead.');
-
 
102
            element = document.getElementById(target);
95
    static init(target, selectors, sectionReturn) {
103
        }
96
        return new Component({
104
        return new Component({
97
            element: document.getElementById(target),
105
            element,
98
            reactive: getCurrentCourseEditor(),
106
            reactive: getCurrentCourseEditor(),
99
            selectors,
107
            selectors,
-
 
108
            sectionReturn,
100
            sectionReturn,
109
            pageSectionId,
101
        });
110
        });
Línea 102... Línea 111...
102
    }
111
    }
103
 
112
 
Línea 149... Línea 158...
149
 
158
 
150
        // Capture page scroll to update page item.
159
        // Capture page scroll to update page item.
151
        this.addEventListener(
160
        this.addEventListener(
152
            document,
161
            document,
153
            "scroll",
162
            "scroll",
154
            this._scrollHandler
163
            throttle(this._scrollHandler.bind(this), 50)
155
        );
164
        );
Línea 156... Línea 165...
156
    }
165
    }
157
 
166
 
Línea 172... Línea 181...
172
 
181
 
Línea 173... Línea 182...
173
        if (sectionlink || isChevron) {
182
        if (sectionlink || isChevron) {
174
 
183
 
175
            const section = event.target.closest(this.selectors.SECTION);
184
            const section = event.target.closest(this.selectors.SECTION);
-
 
185
            const toggler = section.querySelector(this.selectors.COLLAPSE);
-
 
186
            let isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;
-
 
187
            // If the click was on the chevron, Bootstrap already toggled the section before this event.
-
 
188
            if (isChevron) {
Línea 176... Línea 189...
176
            const toggler = section.querySelector(this.selectors.COLLAPSE);
189
                isCollapsed = !isCollapsed;
177
            const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;
190
            }
178
 
191
 
179
            const sectionId = section.getAttribute('data-id');
192
            const sectionId = section.getAttribute('data-id');
Línea 213... Línea 226...
213
     * @returns {Array} of watchers
226
     * @returns {Array} of watchers
214
     */
227
     */
215
    getWatchers() {
228
    getWatchers() {
216
        // Section return is a global page variable but most formats define it just before start printing
229
        // 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.
230
        // the course content. This is the reason why we define this page setting here.
218
        this.reactive.sectionReturn = this.sectionReturn;
231
        this.reactive.sectionReturn = this?.sectionReturn ?? null;
-
 
232
        this.reactive.pageSectionId = this?.pageSectionId ?? null;
Línea 219... Línea 233...
219
 
233
 
220
        // Check if the course format is compatible with reactive components.
234
        // Check if the course format is compatible with reactive components.
221
        if (!this.reactive.supportComponents) {
235
        if (!this.reactive.supportComponents) {
222
            return [];
236
            return [];
Línea 261... Línea 275...
261
            cmNameFor.textContent = element.name;
275
            cmNameFor.textContent = element.name;
262
        });
276
        });
263
    }
277
    }
Línea 264... Línea 278...
264
 
278
 
265
    /**
279
    /**
266
     * Update section collapsed state via bootstrap 4 if necessary.
280
     * Update section collapsed state via bootstrap if necessary.
267
     *
281
     *
268
     * Formats that do not use bootstrap 4 must override this method in order to keep the section
282
     * Formats that do not use bootstrap must override this method in order to keep the section
269
     * toggling working.
283
     * toggling working.
270
     *
284
     *
271
     * @param {object} args
285
     * @param {object} args
272
     * @param {Object} args.state The state data
286
     * @param {Object} args.state The state data
Línea 289... Línea 303...
289
            collapsibleId = collapsibleId.replace('#', '');
303
            collapsibleId = collapsibleId.replace('#', '');
290
            const collapsible = document.getElementById(collapsibleId);
304
            const collapsible = document.getElementById(collapsibleId);
291
            if (!collapsible) {
305
            if (!collapsible) {
292
                return;
306
                return;
293
            }
307
            }
294
 
-
 
295
            // Course index is based on Bootstrap 4 collapsibles. To collapse them we need jQuery to
308
            if (element.contentcollapsed) {
296
            // interact with collapsibles methods. Hopefully, this will change in Bootstrap 5 because
309
                Collapse.getOrCreateInstance(collapsible, {toggle: false}).hide();
297
            // it does not require jQuery anymore (when MDL-71979 is integrated).
310
            } else {
298
            jQuery(collapsible).collapse(element.contentcollapsed ? 'hide' : 'show');
311
                Collapse.getOrCreateInstance(collapsible, {toggle: false}).show();
-
 
312
            }
299
        }
313
        }
Línea 300... Línea 314...
300
 
314
 
301
        this._refreshAllSectionsToggler(state);
315
        this._refreshAllSectionsToggler(state);
Línea 309... Línea 323...
309
    _refreshAllSectionsToggler(state) {
323
    _refreshAllSectionsToggler(state) {
310
        const target = this.getElement(this.selectors.TOGGLEALL);
324
        const target = this.getElement(this.selectors.TOGGLEALL);
311
        if (!target) {
325
        if (!target) {
312
            return;
326
            return;
313
        }
327
        }
-
 
328
 
-
 
329
        const sectionIsCollapsible = this._getCollapsibleSections();
-
 
330
 
314
        // Check if we have all sections collapsed/expanded.
331
        // Check if we have all sections collapsed/expanded.
315
        let allcollapsed = true;
332
        let allcollapsed = true;
316
        let allexpanded = true;
333
        let allexpanded = true;
317
        state.section.forEach(
334
        state.section.forEach(
318
            section => {
335
            section => {
-
 
336
                if (sectionIsCollapsible[section.id]) {
319
                allcollapsed = allcollapsed && section.contentcollapsed;
337
                    allcollapsed = allcollapsed && section.contentcollapsed;
320
                allexpanded = allexpanded && !section.contentcollapsed;
338
                    allexpanded = allexpanded && !section.contentcollapsed;
-
 
339
                }
321
            }
340
            }
322
        );
341
        );
323
        if (allcollapsed) {
342
        if (allcollapsed) {
324
            target.classList.add(this.classes.COLLAPSED);
343
            target.classList.add(this.classes.COLLAPSED);
325
            target.setAttribute('aria-expanded', false);
344
            target.setAttribute('aria-expanded', false);
Línea 329... Línea 348...
329
            target.setAttribute('aria-expanded', true);
348
            target.setAttribute('aria-expanded', true);
330
        }
349
        }
331
    }
350
    }
Línea 332... Línea 351...
332
 
351
 
-
 
352
    /**
-
 
353
     * Find collapsible sections.
-
 
354
     */
-
 
355
    _getCollapsibleSections() {
-
 
356
        let sectionIsCollapsible = {};
-
 
357
        const togglerDoms = this.element.querySelectorAll(this.selectors.COLLAPSE);
-
 
358
        for (let togglerDom of togglerDoms) {
-
 
359
            const headerDom = togglerDom.closest(this.selectors.SECTION_ITEM);
-
 
360
            if (headerDom) {
-
 
361
                sectionIsCollapsible[headerDom.dataset.id] = true;
-
 
362
            }
-
 
363
        }
-
 
364
        return sectionIsCollapsible;
-
 
365
    }
-
 
366
 
333
    /**
367
    /**
334
     * Setup the component to start a transaction.
368
     * Setup the component to start a transaction.
335
     *
369
     *
336
     * Some of the course actions replaces the current DOM element with a new one before updating the
370
     * 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.
371
     * course state. This means the component cannot preload any index properly until the transaction starts.
Línea 443... Línea 477...
443
 
477
 
444
    /**
478
    /**
445
     * Refresh a section cm list.
479
     * Refresh a section cm list.
446
     *
480
     *
-
 
481
     * @param {Object} param
447
     * @param {Object} param
482
     * @param {Object} param.state the full state object.
448
     * @param {Object} param.element details the update details.
483
     * @param {Object} param.element details the update details.
449
     */
484
     */
450
    _refreshSectionCmlist({element}) {
485
    _refreshSectionCmlist({state, element}) {
451
        const cmlist = element.cmlist ?? [];
486
        const cmlist = element.cmlist ?? [];
452
        const section = this.getElement(this.selectors.SECTION, element.id);
487
        const section = this.getElement(this.selectors.SECTION, element.id);
453
        const listparent = section?.querySelector(this.selectors.SECTION_CMLIST);
488
        const listparent = section?.querySelector(this.selectors.SECTION_CMLIST);
454
        // A method to create a fake element to be replaced when the item is ready.
489
        // A method to create a fake element to be replaced when the item is ready.
455
        const createCm = this._createCmItem.bind(this);
490
        const createCm = this._createCmItem.bind(this);
456
        if (listparent) {
491
        if (listparent) {
457
            this._fixOrder(listparent, cmlist, this.selectors.CM, this.dettachedCms, createCm);
492
            this._fixOrder(listparent, cmlist, this.selectors.CM, this.dettachedCms, createCm);
-
 
493
        }
458
        }
494
        this._refreshAllSectionsToggler(state);
Línea 459... Línea 495...
459
    }
495
    }
460
 
496
 
461
    /**
497
    /**
462
     * Refresh the section list.
498
     * Refresh the section list.
463
     *
499
     *
464
     * @param {Object} param
500
     * @param {Object} param
465
     * @param {Object} param.state the full state object.
501
     * @param {Object} param.state the full state object.
466
     */
502
     */
467
    _refreshCourseSectionlist({state}) {
503
    _refreshCourseSectionlist({state}) {
468
        // If we have a section return means we only show a single section so no need to fix order.
504
        // 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) {
505
        if ((this.reactive?.sectionReturn ?? this.reactive?.pageSectionId) !== null) {
470
            return;
506
            return;
471
        }
507
        }
472
        const sectionlist = this.reactive.getExporter().listedSectionIds(state);
508
        const sectionlist = this.reactive.getExporter().listedSectionIds(state);
473
        const listparent = this.getElement(this.selectors.COURSE_SECTIONLIST);
509
        const listparent = this.getElement(this.selectors.COURSE_SECTIONLIST);
474
        // For now section cannot be created at a frontend level.
510
        // For now section cannot be created at a frontend level.
475
        const createSection = this._createSectionItem.bind(this);
511
        const createSection = this._createSectionItem.bind(this);
476
        if (listparent) {
512
        if (listparent) {
-
 
513
            this._fixOrder(listparent, sectionlist, this.selectors.SECTION, this.dettachedSections, createSection);
477
            this._fixOrder(listparent, sectionlist, this.selectors.SECTION, this.dettachedSections, createSection);
514
        }
Línea 478... Línea 515...
478
        }
515
        this._refreshAllSectionsToggler(state);
479
    }
516
    }
480
 
517
 
Línea 572... Línea 609...
572
                'cmitem',
609
                'cmitem',
573
                Config.courseContextId,
610
                Config.courseContextId,
574
                {
611
                {
575
                    id: cmId,
612
                    id: cmId,
576
                    courseid: Config.courseId,
613
                    courseid: Config.courseId,
577
                    sr: this.reactive.sectionReturn ?? null,
614
                    sr: this.reactive?.sectionReturn ?? null,
-
 
615
                    pagesectionid: this.reactive?.pageSectionId ?? null,
578
                }
616
                }
579
            );
617
            );
580
            promise.then((html, js) => {
618
            promise.then((html, js) => {
581
                // Other state change can reload the CM or the section before this one.
619
                // Other state change can reload the CM or the section before this one.
582
                if (!document.contains(cmitem)) {
620
                if (!document.contains(cmitem)) {
Línea 639... Línea 677...
639
                'section',
677
                'section',
640
                Config.courseContextId,
678
                Config.courseContextId,
641
                {
679
                {
642
                    id: element.id,
680
                    id: element.id,
643
                    courseid: Config.courseId,
681
                    courseid: Config.courseId,
644
                    sr: this.reactive.sectionReturn ?? null,
682
                    sr: this.reactive?.sectionReturn ?? null,
-
 
683
                    pagesectionid: this.reactive?.pageSectionId ?? null,
645
                }
684
                }
646
            );
685
            );
647
            promise.then((html, js) => {
686
            promise.then((html, js) => {
648
                Templates.replaceNode(sectionitem, html, js);
687
                Templates.replaceNode(sectionitem, html, js);
649
                this._indexContents();
688
                this._indexContents();
Línea 744... Línea 783...
744
            if (currentitem !== item) {
783
            if (currentitem !== item) {
745
                container.insertBefore(item, currentitem);
784
                container.insertBefore(item, currentitem);
746
            }
785
            }
747
        });
786
        });
Línea 748... Línea -...
748
 
-
 
749
        // Dndupload add a fake element we need to keep.
-
 
750
        let dndFakeActivity;
-
 
751
 
787
 
-
 
788
        // Remove the remaining elements.
752
        // Remove the remaining elements.
789
        const orphanElements = [];
753
        while (container.children.length > neworder.length) {
790
        while (container.children.length > neworder.length) {
-
 
791
            const lastchild = container.lastChild;
-
 
792
            // Any orphan element is always displayed after the listed elements.
754
            const lastchild = container.lastChild;
793
            // Also, some third-party plugins can use a fake dndupload-preview indicator.
755
            if (lastchild?.classList?.contains('dndupload-preview')) {
794
            if (lastchild?.classList?.contains('dndupload-preview') || lastchild.dataset?.orphan) {
756
                dndFakeActivity = lastchild;
795
                orphanElements.push(lastchild);
757
            } else {
796
            } else {
758
                dettachedelements[lastchild?.dataset?.id ?? 0] = lastchild;
797
                dettachedelements[lastchild?.dataset?.id ?? 0] = lastchild;
759
            }
798
            }
760
            container.removeChild(lastchild);
799
            container.removeChild(lastchild);
761
        }
800
        }
762
        // Restore dndupload fake element.
801
        // Restore orphan elements.
763
        if (dndFakeActivity) {
802
        orphanElements.forEach((element) => {
764
            container.append(dndFakeActivity);
803
            container.append(element);
765
        }
804
        });
766
    }
805
    }