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
 * Various actions on modules and sections in the editing mode - hiding, duplicating, deleting, etc.
18
 *
19
 * @module     core_course/actions
20
 * @copyright  2016 Marina Glancy
21
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 * @since      3.3
23
 */
24
define(
25
    [
26
        'jquery',
27
        'core/ajax',
28
        'core/templates',
29
        'core/notification',
30
        'core/str',
31
        'core/url',
32
        'core/yui',
33
        'core/modal_copy_to_clipboard',
34
        'core/modal_save_cancel',
35
        'core/modal_events',
36
        'core/key_codes',
37
        'core/log',
38
        'core_courseformat/courseeditor',
39
        'core/event_dispatcher',
40
        'core_course/events'
41
    ],
42
    function(
43
        $,
44
        ajax,
45
        templates,
46
        notification,
47
        str,
48
        url,
49
        Y,
50
        ModalCopyToClipboard,
51
        ModalSaveCancel,
52
        ModalEvents,
53
        KeyCodes,
54
        log,
55
        editor,
56
        EventDispatcher,
57
        CourseEvents
58
    ) {
59
 
60
        // Eventually, core_courseformat/local/content/actions will handle all actions for
61
        // component compatible formats and the default actions.js won't be necessary anymore.
62
        // Meanwhile, we filter the migrated actions.
63
        const componentActions = [
64
            'moveSection', 'moveCm', 'addSection', 'deleteSection', 'cmDelete', 'cmDuplicate', 'sectionHide', 'sectionShow',
65
            'cmHide', 'cmShow', 'cmStealth', 'sectionHighlight', 'sectionUnhighlight', 'cmMoveRight', 'cmMoveLeft',
66
            'cmNoGroups', 'cmVisibleGroups', 'cmSeparateGroups',
67
        ];
68
 
69
        // The course reactive instance.
70
        const courseeditor = editor.getCurrentCourseEditor();
71
 
72
        // The current course format name (loaded on init).
73
        let formatname;
74
 
75
        var CSS = {
76
            EDITINPROGRESS: 'editinprogress',
77
            SECTIONDRAGGABLE: 'sectiondraggable',
78
            EDITINGMOVE: 'editing_move'
79
        };
80
        var SELECTOR = {
81
            ACTIVITYLI: 'li.activity',
82
            ACTIONAREA: '.actions',
83
            ACTIVITYACTION: 'a.cm-edit-action',
84
            MENU: '.moodle-actionmenu[data-enhance=moodle-core-actionmenu]',
85
            TOGGLE: '.toggle-display,.dropdown-toggle',
86
            SECTIONLI: 'li.section',
87
            SECTIONACTIONMENU: '.section_action_menu',
11 efrain 88
            SECTIONACTIONMENUTRIGGER: '.section-actions',
1 efrain 89
            SECTIONITEM: '[data-for="section_title"]',
90
            ADDSECTIONS: '.changenumsections [data-add-sections]',
91
            SECTIONBADGES: '[data-region="sectionbadges"]',
92
        };
93
 
94
        Y.use('moodle-course-coursebase', function() {
95
            var courseformatselector = M.course.format.get_section_selector();
96
            if (courseformatselector) {
97
                SELECTOR.SECTIONLI = courseformatselector;
98
            }
99
        });
100
 
101
        /**
102
         * Dispatch event wrapper.
103
         *
104
         * Old jQuery events will be replaced by native events gradually.
105
         *
106
         * @method dispatchEvent
107
         * @param {String} eventName The name of the event
108
         * @param {Object} detail Any additional details to pass into the eveent
109
         * @param {Node|HTMLElement} container The point at which to dispatch the event
110
         * @param {Object} options
111
         * @param {Boolean} options.bubbles Whether to bubble up the DOM
112
         * @param {Boolean} options.cancelable Whether preventDefault() can be called
113
         * @param {Boolean} options.composed Whether the event can bubble across the ShadowDOM boundary
114
         * @returns {CustomEvent}
115
         */
116
        const dispatchEvent = function(eventName, detail, container, options) {
117
            // Most actions still uses jQuery node instead of regular HTMLElement.
118
            if (!(container instanceof Element) && container.get !== undefined) {
119
                container = container.get(0);
120
            }
121
            return EventDispatcher.dispatchEvent(eventName, detail, container, options);
122
        };
123
 
124
        /**
125
         * Wrapper for Y.Moodle.core_course.util.cm.getId
126
         *
127
         * @param {JQuery} element
128
         * @returns {Integer}
129
         */
130
        var getModuleId = function(element) {
131
            // Check if we have a data-id first.
132
            const item = element.get(0);
133
            if (item.dataset.id) {
134
                return item.dataset.id;
135
            }
136
            // Use YUI way if data-id is not present.
137
            let id;
138
            Y.use('moodle-course-util', function(Y) {
139
                id = Y.Moodle.core_course.util.cm.getId(Y.Node(item));
140
            });
141
            return id;
142
        };
143
 
144
        /**
145
         * Wrapper for Y.Moodle.core_course.util.cm.getName
146
         *
147
         * @param {JQuery} element
148
         * @returns {String}
149
         */
150
        var getModuleName = function(element) {
151
            var name;
152
            Y.use('moodle-course-util', function(Y) {
153
                name = Y.Moodle.core_course.util.cm.getName(Y.Node(element.get(0)));
154
            });
155
            // Check if we have the name in the course state.
156
            const state = courseeditor.state;
157
            const cmid = getModuleId(element);
158
            if (!name && state && cmid) {
159
                name = state.cm.get(cmid)?.name;
160
            }
161
            return name;
162
        };
163
 
164
        /**
165
         * Wrapper for M.util.add_spinner for an activity
166
         *
167
         * @param {JQuery} activity
168
         * @returns {Node}
169
         */
170
        var addActivitySpinner = function(activity) {
171
            activity.addClass(CSS.EDITINPROGRESS);
172
            var actionarea = activity.find(SELECTOR.ACTIONAREA).get(0);
173
            if (actionarea) {
174
                var spinner = M.util.add_spinner(Y, Y.Node(actionarea));
175
                spinner.show();
176
                // Lock the activity state element.
177
                if (activity.data('id') !== undefined) {
178
                    courseeditor.dispatch('cmLock', [activity.data('id')], true);
179
                }
180
                return spinner;
181
            }
182
            return null;
183
        };
184
 
185
        /**
186
         * Wrapper for M.util.add_spinner for a section
187
         *
188
         * @param {JQuery} sectionelement
189
         * @returns {Node}
190
         */
191
        var addSectionSpinner = function(sectionelement) {
192
            sectionelement.addClass(CSS.EDITINPROGRESS);
193
            var actionarea = sectionelement.find(SELECTOR.SECTIONACTIONMENU).get(0);
194
            if (actionarea) {
195
                var spinner = M.util.add_spinner(Y, Y.Node(actionarea));
196
                spinner.show();
197
                // Lock the section state element.
198
                if (sectionelement.data('id') !== undefined) {
199
                    courseeditor.dispatch('sectionLock', [sectionelement.data('id')], true);
200
                }
201
                return spinner;
202
            }
203
            return null;
204
        };
205
 
206
        /**
207
         * Wrapper for M.util.add_lightbox
208
         *
209
         * @param {JQuery} sectionelement
210
         * @returns {Node}
211
         */
212
        var addSectionLightbox = function(sectionelement) {
213
            const item = sectionelement.get(0);
214
            var lightbox = M.util.add_lightbox(Y, Y.Node(item));
215
            if (item.dataset.for == 'section' && item.dataset.id) {
216
                courseeditor.dispatch('sectionLock', [item.dataset.id], true);
217
                lightbox.setAttribute('data-state', 'section');
218
                lightbox.setAttribute('data-state-id', item.dataset.id);
219
            }
220
            lightbox.show();
221
            return lightbox;
222
        };
223
 
224
        /**
225
         * Removes the spinner element
226
         *
227
         * @param {JQuery} element
228
         * @param {Node} spinner
229
         * @param {Number} delay
230
         */
231
        var removeSpinner = function(element, spinner, delay) {
232
            window.setTimeout(function() {
233
                element.removeClass(CSS.EDITINPROGRESS);
234
                if (spinner) {
235
                    spinner.hide();
236
                }
237
                // Unlock the state element.
238
                if (element.data('id') !== undefined) {
239
                    const mutation = (element.data('for') === 'section') ? 'sectionLock' : 'cmLock';
240
                    courseeditor.dispatch(mutation, [element.data('id')], false);
241
                }
242
            }, delay);
243
        };
244
 
245
        /**
246
         * Removes the lightbox element
247
         *
248
         * @param {Node} lightbox lighbox YUI element returned by addSectionLightbox
249
         * @param {Number} delay
250
         */
251
        var removeLightbox = function(lightbox, delay) {
252
            if (lightbox) {
253
                window.setTimeout(function() {
254
                    lightbox.hide();
255
                    // Unlock state if necessary.
256
                    if (lightbox.getAttribute('data-state')) {
257
                        courseeditor.dispatch(
258
                            `${lightbox.getAttribute('data-state')}Lock`,
259
                            [lightbox.getAttribute('data-state-id')],
260
                            false
261
                        );
262
                    }
263
                }, delay);
264
            }
265
        };
266
 
267
        /**
268
         * Initialise action menu for the element (section or module)
269
         *
270
         * @param {String} elementid CSS id attribute of the element
271
         */
272
        var initActionMenu = function(elementid) {
273
            // Initialise action menu in the new activity.
274
            Y.use('moodle-course-coursebase', function() {
275
                M.course.coursebase.invoke_function('setup_for_resource', '#' + elementid);
276
            });
277
            if (M.core.actionmenu && M.core.actionmenu.newDOMNode) {
278
                M.core.actionmenu.newDOMNode(Y.one('#' + elementid));
279
            }
280
        };
281
 
282
        /**
283
         * Returns focus to the element that was clicked or "Edit" link if element is no longer visible.
284
         *
285
         * @param {String} elementId CSS id attribute of the element
286
         * @param {String} action data-action property of the element that was clicked
287
         */
288
        var focusActionItem = function(elementId, action) {
289
            var mainelement = $('#' + elementId);
290
            var selector = '[data-action=' + action + ']';
291
            if (action === 'groupsseparate' || action === 'groupsvisible' || action === 'groupsnone') {
292
                // New element will have different data-action.
293
                selector = '[data-action=groupsseparate],[data-action=groupsvisible],[data-action=groupsnone]';
294
            }
295
            if (mainelement.find(selector).is(':visible')) {
296
                mainelement.find(selector).focus();
297
            } else {
298
                // Element not visible, focus the "Edit" link.
299
                mainelement.find(SELECTOR.MENU).find(SELECTOR.TOGGLE).focus();
300
            }
301
        };
302
 
303
        /**
304
         * Find next <a> after the element
305
         *
306
         * @param {JQuery} mainElement element that is about to be deleted
307
         * @returns {JQuery}
308
         */
309
        var findNextFocusable = function(mainElement) {
310
            var tabables = $("a:visible");
311
            var isInside = false;
312
            var foundElement = null;
313
            tabables.each(function() {
314
                if ($.contains(mainElement[0], this)) {
315
                    isInside = true;
316
                } else if (isInside) {
317
                    foundElement = this;
318
                    return false; // Returning false in .each() is equivalent to "break;" inside the loop in php.
319
                }
320
                return true;
321
            });
322
            return foundElement;
323
        };
324
 
325
        /**
326
         * Performs an action on a module (moving, deleting, duplicating, hiding, etc.)
327
         *
328
         * @param {JQuery} moduleElement activity element we perform action on
329
         * @param {Number} cmid
330
         * @param {JQuery} target the element (menu item) that was clicked
331
         */
332
        var editModule = function(moduleElement, cmid, target) {
333
            var action = target.attr('data-action');
334
            var spinner = addActivitySpinner(moduleElement);
335
            var promises = ajax.call([{
336
                methodname: 'core_course_edit_module',
337
                args: {id: cmid,
338
                    action: action,
339
                    sectionreturn: target.attr('data-sectionreturn') ? target.attr('data-sectionreturn') : null
340
                }
341
            }], true);
342
 
343
            var lightbox;
344
            if (action === 'duplicate') {
345
                lightbox = addSectionLightbox(target.closest(SELECTOR.SECTIONLI));
346
            }
347
            $.when.apply($, promises)
348
                .done(function(data) {
349
                    var elementToFocus = findNextFocusable(moduleElement);
350
                    moduleElement.replaceWith(data);
351
                    let affectedids = [];
352
                    // Initialise action menu for activity(ies) added as a result of this.
353
                    $('<div>' + data + '</div>').find(SELECTOR.ACTIVITYLI).each(function(index) {
354
                        initActionMenu($(this).attr('id'));
355
                        if (index === 0) {
356
                            focusActionItem($(this).attr('id'), action);
357
                            elementToFocus = null;
358
                        }
359
                        // Save any activity id in cmids.
360
                        affectedids.push(getModuleId($(this)));
361
                    });
362
                    // In case of activity deletion focus the next focusable element.
363
                    if (elementToFocus) {
364
                        elementToFocus.focus();
365
                    }
366
                    // Remove spinner and lightbox with a delay.
367
                    removeSpinner(moduleElement, spinner, 400);
368
                    removeLightbox(lightbox, 400);
369
                    // Trigger event that can be observed by course formats.
370
                    moduleElement.trigger($.Event('coursemoduleedited', {ajaxreturn: data, action: action}));
371
 
372
                    // Modify cm state.
373
                    courseeditor.dispatch('legacyActivityAction', action, cmid, affectedids);
374
 
375
                }).fail(function(ex) {
376
                    // Remove spinner and lightbox.
377
                    removeSpinner(moduleElement, spinner);
378
                    removeLightbox(lightbox);
379
                    // Trigger event that can be observed by course formats.
380
                    var e = $.Event('coursemoduleeditfailed', {exception: ex, action: action});
381
                    moduleElement.trigger(e);
382
                    if (!e.isDefaultPrevented()) {
383
                        notification.exception(ex);
384
                    }
385
                });
386
        };
387
 
388
        /**
389
         * Requests html for the module via WS core_course_get_module and updates the module on the course page
390
         *
391
         * Used after d&d of the module to another section
392
         *
393
         * @param {JQuery|Element} element
394
         * @param {Number} cmid
395
         * @param {Number} sectionreturn
396
         * @return {Promise} the refresh promise
397
         */
398
        var refreshModule = function(element, cmid, sectionreturn) {
399
 
400
            if (sectionreturn === undefined) {
401
                sectionreturn = courseeditor.sectionReturn;
402
            }
403
 
404
            const activityElement = $(element);
405
            var spinner = addActivitySpinner(activityElement);
406
            var promises = ajax.call([{
407
                methodname: 'core_course_get_module',
408
                args: {id: cmid, sectionreturn: sectionreturn}
409
            }], true);
410
 
411
            return new Promise((resolve, reject) => {
412
                $.when.apply($, promises)
413
                    .done(function(data) {
414
                        removeSpinner(activityElement, spinner, 400);
415
                        replaceActivityHtmlWith(data);
416
                        resolve(data);
417
                    }).fail(function() {
418
                        removeSpinner(activityElement, spinner);
419
                        reject();
420
                    });
421
            });
422
        };
423
 
424
        /**
425
         * Requests html for the section via WS core_course_edit_section and updates the section on the course page
426
         *
427
         * @param {JQuery|Element} element
428
         * @param {Number} sectionid
429
         * @param {Number} sectionreturn
430
         * @return {Promise} the refresh promise
431
         */
432
        var refreshSection = function(element, sectionid, sectionreturn) {
433
 
434
            if (sectionreturn === undefined) {
435
                sectionreturn = courseeditor.sectionReturn;
436
            }
437
 
438
            const sectionElement = $(element);
439
            const action = 'refresh';
440
            const promises = ajax.call([{
441
                methodname: 'core_course_edit_section',
442
                args: {id: sectionid, action, sectionreturn},
443
            }], true);
444
 
445
            var spinner = addSectionSpinner(sectionElement);
446
            return new Promise((resolve, reject) => {
447
                $.when.apply($, promises)
448
                    .done(dataencoded => {
449
 
450
                        removeSpinner(sectionElement, spinner);
451
                        const data = $.parseJSON(dataencoded);
452
 
453
                        const newSectionElement = $(data.content);
454
                        sectionElement.replaceWith(newSectionElement);
455
 
456
                        // Init modules menus.
457
                        $(`${SELECTOR.SECTIONLI}#${sectionid} ${SELECTOR.ACTIVITYLI}`).each(
458
                            (index, activity) => {
459
                                initActionMenu(activity.data('id'));
460
                            }
461
                        );
462
 
463
                        // Trigger event that can be observed by course formats.
464
                        const event = dispatchEvent(
465
                            CourseEvents.sectionRefreshed,
466
                            {
467
                                ajaxreturn: data,
468
                                action: action,
469
                                newSectionElement: newSectionElement.get(0),
470
                            },
471
                            newSectionElement
472
                        );
473
 
474
                        if (!event.defaultPrevented) {
475
                            defaultEditSectionHandler(
476
                                newSectionElement, $(SELECTOR.SECTIONLI + '#' + sectionid),
477
                                data,
478
                                formatname,
479
                                sectionid
480
                            );
481
                        }
482
                        resolve(data);
483
                    }).fail(ex => {
484
                        // Trigger event that can be observed by course formats.
485
                        const event = dispatchEvent(
486
                            'coursesectionrefreshfailed',
487
                            {exception: ex, action: action},
488
                            sectionElement
489
                        );
490
                        if (!event.defaultPrevented) {
491
                            notification.exception(ex);
492
                        }
493
                        reject();
494
                    });
495
            });
496
        };
497
 
498
        /**
499
         * Displays the delete confirmation to delete a module
500
         *
501
         * @param {JQuery} mainelement activity element we perform action on
502
         * @param {function} onconfirm function to execute on confirm
503
         */
504
        var confirmDeleteModule = function(mainelement, onconfirm) {
505
            var modtypename = mainelement.attr('class').match(/modtype_([^\s]*)/)[1];
506
            var modulename = getModuleName(mainelement);
507
 
508
            str.get_string('pluginname', modtypename).done(function(pluginname) {
509
                var plugindata = {
510
                    type: pluginname,
511
                    name: modulename
512
                };
513
                str.get_strings([
514
                    {key: 'confirm', component: 'core'},
515
                    {key: modulename === null ? 'deletechecktype' : 'deletechecktypename', param: plugindata},
516
                    {key: 'yes'},
517
                    {key: 'no'}
518
                ]).done(function(s) {
519
                        notification.confirm(s[0], s[1], s[2], s[3], onconfirm);
520
                    }
521
                );
522
            });
523
        };
524
 
525
        /**
526
         * Displays the delete confirmation to delete a section
527
         *
528
         * @param {String} message confirmation message
529
         * @param {function} onconfirm function to execute on confirm
530
         */
531
        var confirmEditSection = function(message, onconfirm) {
532
            str.get_strings([
533
                {key: 'confirm'}, // TODO link text
534
                {key: 'yes'},
535
                {key: 'no'}
536
            ]).done(function(s) {
537
                    notification.confirm(s[0], message, s[1], s[2], onconfirm);
538
                }
539
            );
540
        };
541
 
542
        /**
543
         * Replaces an action menu item with another one (for example Show->Hide, Set marker->Remove marker)
544
         *
545
         * @param {JQuery} actionitem
546
         * @param {String} image new image name ("i/show", "i/hide", etc.)
547
         * @param {String} stringname new string for the action menu item
548
         * @param {String} stringcomponent
549
         * @param {String} newaction new value for data-action attribute of the link
550
         * @return {Promise} promise which is resolved when the replacement has completed
551
         */
552
        var replaceActionItem = function(actionitem, image, stringname,
553
                                           stringcomponent, newaction) {
554
 
555
            var stringRequests = [{key: stringname, component: stringcomponent}];
556
            // Do not provide an icon with duplicate, different text to the menu item.
557
 
558
            return str.get_strings(stringRequests).then(function(strings) {
559
                actionitem.find('span.menu-action-text').html(strings[0]);
560
 
561
                return templates.renderPix(image, 'core');
562
            }).then(function(pixhtml) {
563
                actionitem.find('.icon').replaceWith(pixhtml);
564
                actionitem.attr('data-action', newaction);
565
                return;
566
            }).catch(notification.exception);
567
        };
568
 
569
        /**
570
         * Default post-processing for section AJAX edit actions.
571
         *
572
         * This can be overridden in course formats by listening to event coursesectionedited:
573
         *
574
         * $('body').on('coursesectionedited', 'li.section', function(e) {
575
         *     var action = e.action,
576
         *         sectionElement = $(e.target),
577
         *         data = e.ajaxreturn;
578
         *     // ... Do some processing here.
579
         *     e.preventDefault(); // Prevent default handler.
580
         * });
581
         *
582
         * @param {JQuery} sectionElement
583
         * @param {JQuery} actionItem
584
         * @param {Object} data
585
         * @param {String} courseformat
586
         * @param {Number} sectionid
587
         */
588
        var defaultEditSectionHandler = function(sectionElement, actionItem, data, courseformat, sectionid) {
589
            var action = actionItem.attr('data-action');
590
            if (action === 'hide' || action === 'show') {
591
                if (action === 'hide') {
592
                    sectionElement.addClass('hidden');
593
                    setSectionBadge(sectionElement[0], 'hiddenfromstudents', true, false);
594
                    replaceActionItem(actionItem, 'i/show',
595
                        'showfromothers', 'format_' + courseformat, 'show');
596
                } else {
597
                    setSectionBadge(sectionElement[0], 'hiddenfromstudents', false, false);
598
                    sectionElement.removeClass('hidden');
599
                    replaceActionItem(actionItem, 'i/hide',
600
                        'hidefromothers', 'format_' + courseformat, 'hide');
601
                }
602
                // Replace the modules with new html (that indicates that they are now hidden or not hidden).
603
                if (data.modules !== undefined) {
604
                    for (var i in data.modules) {
605
                        replaceActivityHtmlWith(data.modules[i]);
606
                    }
607
                }
608
                // Replace the section availability information.
609
                if (data.section_availability !== undefined) {
610
                    sectionElement.find('.section_availability').first().replaceWith(data.section_availability);
611
                }
612
                // Modify course state.
613
                const section = courseeditor.state.section.get(sectionid);
614
                if (section !== undefined) {
615
                    courseeditor.dispatch('sectionState', [sectionid]);
616
                }
617
            } else if (action === 'setmarker') {
618
                var oldmarker = $(SELECTOR.SECTIONLI + '.current'),
619
                    oldActionItem = oldmarker.find(SELECTOR.SECTIONACTIONMENU + ' ' + 'a[data-action=removemarker]');
620
                oldmarker.removeClass('current');
621
                replaceActionItem(oldActionItem, 'i/marker',
622
                    'highlight', 'core', 'setmarker');
623
                sectionElement.addClass('current');
624
                replaceActionItem(actionItem, 'i/marked',
625
                    'highlightoff', 'core', 'removemarker');
626
                courseeditor.dispatch('legacySectionAction', action, sectionid);
627
                setSectionBadge(sectionElement[0], 'iscurrent', true, true);
628
            } else if (action === 'removemarker') {
629
                sectionElement.removeClass('current');
630
                replaceActionItem(actionItem, 'i/marker',
631
                    'highlight', 'core', 'setmarker');
632
                courseeditor.dispatch('legacySectionAction', action, sectionid);
633
                setSectionBadge(sectionElement[0], 'iscurrent', false, true);
634
            }
635
        };
636
 
637
        /**
638
         * Get the focused element path in an activity if any.
639
         *
640
         * This method is used to restore focus when the activity HTML is refreshed.
641
         * Only the main course editor elements can be refocused as they are always present
642
         * even if the activity content changes.
643
         *
644
         * @param {String} id the element id the activity element
645
         * @return {String|undefined} the inner path of the focused element or undefined
646
         */
647
        const getActivityFocusedElement = function(id) {
648
            const element = document.getElementById(id);
649
            if (!element || !element.contains(document.activeElement)) {
650
                return undefined;
651
            }
652
            // Check if the actions menu toggler is focused.
653
            if (element.querySelector(SELECTOR.ACTIONAREA).contains(document.activeElement)) {
654
                return `${SELECTOR.ACTIONAREA} [tabindex="0"]`;
655
            }
656
            // Return the current element id if any.
657
            if (document.activeElement.id) {
658
                return `#${document.activeElement.id}`;
659
            }
660
            return undefined;
661
        };
662
 
663
        /**
664
         * Replaces the course module with the new html (used to update module after it was edited or its visibility was changed).
665
         *
666
         * @param {String} activityHTML
667
         */
668
        var replaceActivityHtmlWith = function(activityHTML) {
669
            $('<div>' + activityHTML + '</div>').find(SELECTOR.ACTIVITYLI).each(function() {
670
                // Extract id from the new activity html.
671
                var id = $(this).attr('id');
672
                // Check if the current focused element is inside the activity.
673
                let focusedPath = getActivityFocusedElement(id);
674
                // Find the existing element with the same id and replace its contents with new html.
675
                $(SELECTOR.ACTIVITYLI + '#' + id).replaceWith(activityHTML);
676
                // Initialise action menu.
677
                initActionMenu(id);
678
                // Re-focus the previous elements.
679
                if (focusedPath) {
680
                    const newItem = document.getElementById(id);
681
                    newItem.querySelector(focusedPath)?.focus();
682
                }
683
 
684
            });
685
        };
686
 
687
        /**
688
         * Performs an action on a module (moving, deleting, duplicating, hiding, etc.)
689
         *
690
         * @param {JQuery} sectionElement section element we perform action on
691
         * @param {Nunmber} sectionid
692
         * @param {JQuery} target the element (menu item) that was clicked
693
         * @param {String} courseformat
694
         * @return {boolean} true the action call is sent to the server or false if it is ignored.
695
         */
696
        var editSection = function(sectionElement, sectionid, target, courseformat) {
697
            var action = target.attr('data-action'),
698
                sectionreturn = target.attr('data-sectionreturn') ? target.attr('data-sectionreturn') : null;
699
 
700
            // Filter direct component handled actions.
701
            if (courseeditor.supportComponents && componentActions.includes(action)) {
702
                return false;
703
            }
704
 
705
            var spinner = addSectionSpinner(sectionElement);
706
            var promises = ajax.call([{
707
                methodname: 'core_course_edit_section',
708
                args: {id: sectionid, action: action, sectionreturn: sectionreturn}
709
            }], true);
710
 
711
            var lightbox = addSectionLightbox(sectionElement);
712
            $.when.apply($, promises)
713
                .done(function(dataencoded) {
714
                    var data = $.parseJSON(dataencoded);
715
                    removeSpinner(sectionElement, spinner);
716
                    removeLightbox(lightbox);
717
                    sectionElement.find(SELECTOR.SECTIONACTIONMENU).find(SELECTOR.TOGGLE).focus();
718
                    // Trigger event that can be observed by course formats.
719
                    var e = $.Event('coursesectionedited', {ajaxreturn: data, action: action});
720
                    sectionElement.trigger(e);
721
                    if (!e.isDefaultPrevented()) {
722
                        defaultEditSectionHandler(sectionElement, target, data, courseformat, sectionid);
723
                    }
724
                }).fail(function(ex) {
725
                    // Remove spinner and lightbox.
726
                    removeSpinner(sectionElement, spinner);
727
                    removeLightbox(lightbox);
728
                    // Trigger event that can be observed by course formats.
729
                    var e = $.Event('coursesectioneditfailed', {exception: ex, action: action});
730
                    sectionElement.trigger(e);
731
                    if (!e.isDefaultPrevented()) {
732
                        notification.exception(ex);
733
                    }
734
                });
735
            return true;
736
        };
737
 
738
        /**
739
         * Sets the section badge in the section header.
740
         *
741
         * @param {JQuery} sectionElement section element we perform action on
742
         * @param {String} badgetype the type of badge this is for
743
         * @param {bool} add true to add, false to remove
744
         * @param {boolean} removeOther in case of adding a badge, whether to remove all other.
745
         */
746
        var setSectionBadge = function(sectionElement, badgetype, add, removeOther) {
747
            const sectionbadges = sectionElement.querySelector(SELECTOR.SECTIONBADGES);
748
            if (!sectionbadges) {
749
                return;
750
            }
751
            const badge = sectionbadges.querySelector('[data-type="' + badgetype + '"]');
752
            if (!badge) {
753
                return;
754
            }
755
            if (add) {
756
                if (removeOther) {
757
                    document.querySelectorAll('[data-type="' + badgetype + '"]').forEach((b) => {
758
                        b.classList.add('d-none');
759
                    });
760
                }
761
                badge.classList.remove('d-none');
762
            } else {
763
                badge.classList.add('d-none');
764
            }
765
        };
766
 
767
        // Register a function to be executed after D&D of an activity.
768
        Y.use('moodle-course-coursebase', function() {
769
            M.course.coursebase.register_module({
770
                // Ignore camelcase eslint rule for the next line because it is an expected name of the callback.
771
                // eslint-disable-next-line camelcase
772
                set_visibility_resource_ui: function(args) {
773
                    var mainelement = $(args.element.getDOMNode());
774
                    var cmid = getModuleId(mainelement);
775
                    if (cmid) {
776
                        var sectionreturn = mainelement.find('.' + CSS.EDITINGMOVE).attr('data-sectionreturn');
777
                        refreshModule(mainelement, cmid, sectionreturn);
778
                    }
779
                },
780
                /**
781
                 * Update the course state when some cm is moved via YUI.
782
                 * @param {*} params
783
                 */
784
                updateMovedCmState: (params) => {
785
                    const state = courseeditor.state;
786
 
787
                    // Update old section.
788
                    const cm = state.cm.get(params.cmid);
789
                    if (cm !== undefined) {
790
                        courseeditor.dispatch('sectionState', [cm.sectionid]);
791
                    }
792
                    // Update cm state.
793
                    courseeditor.dispatch('cmState', [params.cmid]);
794
                },
795
                /**
796
                 * Update the course state when some section is moved via YUI.
797
                 */
798
                updateMovedSectionState: () => {
799
                    courseeditor.dispatch('courseState');
800
                },
801
            });
802
        });
803
 
804
        // From Moodle 4.0 all edit actions are being re-implemented as state mutation.
805
        // This means all method from this "actions" module will be deprecated when all the course
806
        // interface is migrated to reactive components.
807
        // Most legacy actions did not provide enough information to regenarate the course so they
808
        // use the mutations courseState, sectionState and cmState to get the updated state from
809
        // the server. However, some activity actions where we can prevent an extra webservice
810
        // call by implementing an adhoc mutation.
811
        courseeditor.addMutations({
812
            /**
813
             * Compatibility function to update Moodle 4.0 course state using legacy actions.
814
             *
815
             * This method only updates some actions which does not require to use cmState mutation
816
             * to get updated data form the server.
817
             *
818
             * @param {Object} statemanager the current state in read write mode
819
             * @param {String} action the performed action
820
             * @param {Number} cmid the affected course module id
821
             * @param {Array} affectedids all affected cm ids (for duplicate action)
822
             */
823
            legacyActivityAction: function(statemanager, action, cmid, affectedids) {
824
 
825
                const state = statemanager.state;
826
                const cm = state.cm.get(cmid);
827
                if (cm === undefined) {
828
                    return;
829
                }
830
                const section = state.section.get(cm.sectionid);
831
                if (section === undefined) {
832
                    return;
833
                }
834
 
835
                // Send the element is locked.
836
                courseeditor.dispatch('cmLock', [cm.id], true);
837
 
838
                // Now we do the real mutation.
839
                statemanager.setReadOnly(false);
840
 
841
                // This unlocked will take effect when the read only is restored.
842
                cm.locked = false;
843
 
844
                switch (action) {
845
                    case 'delete':
846
                        // Remove from section.
847
                        section.cmlist = section.cmlist.reduce(
848
                            (cmlist, current) => {
849
                                if (current != cmid) {
850
                                    cmlist.push(current);
851
                                }
852
                                return cmlist;
853
                            },
854
                            []
855
                        );
856
                        // Delete form list.
857
                        state.cm.delete(cmid);
858
                        break;
859
 
860
                    case 'hide':
861
                    case 'show':
862
                    case 'duplicate':
863
                        courseeditor.dispatch('cmState', affectedids);
864
                        break;
865
                }
866
                statemanager.setReadOnly(true);
867
            },
868
            legacySectionAction: function(statemanager, action, sectionid) {
869
 
870
                const state = statemanager.state;
871
                const section = state.section.get(sectionid);
872
                if (section === undefined) {
873
                    return;
874
                }
875
 
876
                // Send the element is locked. Reactive events are only triggered when the state
877
                // read only mode is restored. We want to notify the interface the element is
878
                // locked so we need to do a quick lock operation before performing the rest
879
                // of the mutation.
880
                statemanager.setReadOnly(false);
881
                section.locked = true;
882
                statemanager.setReadOnly(true);
883
 
884
                // Now we do the real mutation.
885
                statemanager.setReadOnly(false);
886
 
887
                // This locked will take effect when the read only is restored.
888
                section.locked = false;
889
 
890
                switch (action) {
891
                    case 'setmarker':
892
                        // Remove previous marker.
893
                        state.section.forEach((current) => {
894
                            if (current.id != sectionid) {
895
                                current.current = false;
896
                            }
897
                        });
898
                        section.current = true;
899
                        break;
900
 
901
                    case 'removemarker':
902
                        section.current = false;
903
                        break;
904
                }
905
                statemanager.setReadOnly(true);
906
            },
907
        });
908
 
909
        return /** @alias module:core_course/actions */ {
910
 
911
            /**
912
             * Initialises course page
913
             *
914
             * @method init
915
             * @param {String} courseformat name of the current course format (for fetching strings)
916
             */
917
            initCoursePage: function(courseformat) {
918
 
919
                formatname = courseformat;
920
 
921
                // Add a handler for course module actions.
922
                $('body').on('click keypress', SELECTOR.ACTIVITYLI + ' ' +
923
                        SELECTOR.ACTIVITYACTION + '[data-action]', function(e) {
924
                    if (e.type === 'keypress' && e.keyCode !== 13) {
925
                        return;
926
                    }
927
                    var actionItem = $(this),
928
                        moduleElement = actionItem.closest(SELECTOR.ACTIVITYLI),
929
                        action = actionItem.attr('data-action'),
930
                        moduleId = getModuleId(moduleElement);
931
                    switch (action) {
932
                        case 'moveleft':
933
                        case 'moveright':
934
                        case 'delete':
935
                        case 'duplicate':
936
                        case 'hide':
937
                        case 'stealth':
938
                        case 'show':
939
                        case 'groupsseparate':
940
                        case 'groupsvisible':
941
                        case 'groupsnone':
942
                            break;
943
                        default:
944
                            // Nothing to do here!
945
                            return;
946
                    }
947
                    if (!moduleId) {
948
                        return;
949
                    }
950
                    e.preventDefault();
951
                    if (action === 'delete') {
952
                        // Deleting requires confirmation.
953
                        confirmDeleteModule(moduleElement, function() {
954
                            editModule(moduleElement, moduleId, actionItem);
955
                        });
956
                    } else {
957
                        editModule(moduleElement, moduleId, actionItem);
958
                    }
959
                });
960
 
11 efrain 961
                // Add a handler for section action menu.
962
                $('body').on('click keypress',
963
                            SELECTOR.SECTIONACTIONMENUTRIGGER + '[data-sectionid] ' +
1 efrain 964
                            'a[data-action]', function(e) {
965
                    if (e.type === 'keypress' && e.keyCode !== 13) {
966
                        return;
967
                    }
968
                    var actionItem = $(this),
969
                        sectionElement = actionItem.closest(SELECTOR.SECTIONLI),
11 efrain 970
                        sectionId = actionItem.closest(SELECTOR.SECTIONACTIONMENUTRIGGER).attr('data-sectionid');
1 efrain 971
 
972
                    if (actionItem.attr('data-action') === 'permalink') {
973
                        e.preventDefault();
974
                        ModalCopyToClipboard.create({
975
                            text: actionItem.attr('href'),
976
                        }, str.get_string('sectionlink', 'course')
977
                        );
978
                        return;
979
                    }
980
 
981
                    let isExecuted = true;
982
                    if (actionItem.attr('data-confirm')) {
983
                        // Action requires confirmation.
984
                        confirmEditSection(actionItem.attr('data-confirm'), function() {
985
                            isExecuted = editSection(sectionElement, sectionId, actionItem, courseformat);
986
                        });
987
                    } else {
988
                        isExecuted = editSection(sectionElement, sectionId, actionItem, courseformat);
989
                    }
990
                    // Prevent any other module from capturing the action if it is already in execution.
991
                    if (isExecuted) {
992
                        e.preventDefault();
993
                    }
994
                });
995
 
996
                // The section and activity names are edited using inplace editable.
997
                // The "update" jQuery event must be captured in order to update the course state.
998
                $('body').on('updated', `${SELECTOR.SECTIONITEM} [data-inplaceeditable]`, function(e) {
999
                    if (e.ajaxreturn && e.ajaxreturn.itemid) {
1000
                        const state = courseeditor.state;
1001
                        const section = state.section.get(e.ajaxreturn.itemid);
1002
                        if (section !== undefined) {
1003
                            courseeditor.dispatch('sectionState', [e.ajaxreturn.itemid]);
1004
                        }
1005
                    }
1006
                });
1007
                $('body').on('updated', `${SELECTOR.ACTIVITYLI} [data-inplaceeditable]`, function(e) {
1008
                    if (e.ajaxreturn && e.ajaxreturn.itemid) {
1009
                        courseeditor.dispatch('cmState', [e.ajaxreturn.itemid]);
1010
                    }
1011
                });
1012
 
1013
                // Component-based formats don't use modals to create sections.
1014
                if (courseeditor.supportComponents && componentActions.includes('addSection')) {
1015
                    return;
1016
                }
1017
 
1018
                // Add a handler for "Add sections" link to ask for a number of sections to add.
1019
                const trigger = $(SELECTOR.ADDSECTIONS);
1020
                const modalTitle = trigger.attr('data-add-sections');
1021
                const newSections = trigger.attr('data-new-sections');
1022
                str.get_string('numberweeks')
1023
                .then(function(strNumberSections) {
1024
                    var modalBody = $('<div><label for="add_section_numsections"></label> ' +
1025
                        '<input id="add_section_numsections" type="number" min="1" max="' + newSections + '" value="1"></div>');
1026
                    modalBody.find('label').html(strNumberSections);
1027
 
1028
                    return modalBody.html();
1029
                })
1030
                .then((body) => ModalSaveCancel.create({
1031
                    body,
1032
                    title: modalTitle,
1033
                }))
1034
                .then(function(modal) {
1035
                    var numSections = $(modal.getBody()).find('#add_section_numsections'),
1036
                    addSections = function() {
1037
                        // Check if value of the "Number of sections" is a valid positive integer and redirect
1038
                        // to adding a section script.
1039
                        if ('' + parseInt(numSections.val()) === numSections.val() && parseInt(numSections.val()) >= 1) {
1040
                            document.location = trigger.attr('href') + '&numsections=' + parseInt(numSections.val());
1041
                        }
1042
                    };
1043
                    modal.setSaveButtonText(modalTitle);
1044
                    modal.getRoot().on(ModalEvents.shown, function() {
1045
                        // When modal is shown focus and select the input and add a listener to keypress of "Enter".
1046
                        numSections.focus().select().on('keydown', function(e) {
1047
                            if (e.keyCode === KeyCodes.enter) {
1048
                                addSections();
1049
                            }
1050
                        });
1051
                    });
1052
                    modal.getRoot().on(ModalEvents.save, function(e) {
1053
                        // When modal "Add" button is pressed.
1054
                        e.preventDefault();
1055
                        addSections();
1056
                    });
1057
 
1058
                    trigger.on('click', (e) => {
1059
                        e.preventDefault();
1060
                        modal.show();
1061
                    });
1062
 
1063
                    return modal;
1064
                })
1065
                .catch(notification.exception);
1066
            },
1067
 
1068
            /**
1069
             * Replaces a section action menu item with another one (for example Show->Hide, Set marker->Remove marker)
1070
             *
1071
             * This method can be used by course formats in their listener to the coursesectionedited event
1072
             *
1073
             * @deprecated since Moodle 3.9
1074
             * @param {JQuery} sectionelement
1075
             * @param {String} selector CSS selector inside the section element, for example "a[data-action=show]"
1076
             * @param {String} image new image name ("i/show", "i/hide", etc.)
1077
             * @param {String} stringname new string for the action menu item
1078
             * @param {String} stringcomponent
1079
             * @param {String} newaction new value for data-action attribute of the link
1080
             */
1081
            replaceSectionActionItem: function(sectionelement, selector, image, stringname,
1082
                                                    stringcomponent, newaction) {
1083
                log.debug('replaceSectionActionItem() is deprecated and will be removed.');
1084
                var actionitem = sectionelement.find(SELECTOR.SECTIONACTIONMENU + ' ' + selector);
1085
                replaceActionItem(actionitem, image, stringname, stringcomponent, newaction);
1086
            },
1087
            // Method to refresh a module.
1088
            refreshModule,
1089
            refreshSection,
1090
        };
1091
    });