MDL-71228 course: sorting course on section state change
[moodle.git] / course / amd / src / actions.js
blobf7c62fa359d2707729725fab78c0f341d4db08b5
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.
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/>.
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(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/url', 'core/yui',
25         'core/modal_factory', 'core/modal_events', 'core/key_codes', 'core/log', 'core_courseformat/courseeditor'],
26     function($, ajax, templates, notification, str, url, Y, ModalFactory, ModalEvents, KeyCodes, log, editor) {
28         const courseeditor = editor.getCurrentCourseEditor();
30         var CSS = {
31             EDITINPROGRESS: 'editinprogress',
32             SECTIONDRAGGABLE: 'sectiondraggable',
33             EDITINGMOVE: 'editing_move'
34         };
35         var SELECTOR = {
36             ACTIVITYLI: 'li.activity',
37             ACTIONAREA: '.actions',
38             ACTIVITYACTION: 'a.cm-edit-action',
39             MENU: '.moodle-actionmenu[data-enhance=moodle-core-actionmenu]',
40             TOGGLE: '.toggle-display,.dropdown-toggle',
41             SECTIONLI: 'li.section',
42             SECTIONACTIONMENU: '.section_action_menu',
43             ADDSECTIONS: '#changenumsections [data-add-sections]'
44         };
46         Y.use('moodle-course-coursebase', function() {
47             var courseformatselector = M.course.format.get_section_selector();
48             if (courseformatselector) {
49                 SELECTOR.SECTIONLI = courseformatselector;
50             }
51         });
53         /**
54          * Wrapper for Y.Moodle.core_course.util.cm.getId
55          *
56          * @param {JQuery} element
57          * @returns {Integer}
58          */
59         var getModuleId = function(element) {
60             var id;
61             Y.use('moodle-course-util', function(Y) {
62                 id = Y.Moodle.core_course.util.cm.getId(Y.Node(element.get(0)));
63             });
64             return id;
65         };
67         /**
68          * Wrapper for Y.Moodle.core_course.util.cm.getName
69          *
70          * @param {JQuery} element
71          * @returns {String}
72          */
73         var getModuleName = function(element) {
74             var name;
75             Y.use('moodle-course-util', function(Y) {
76                 name = Y.Moodle.core_course.util.cm.getName(Y.Node(element.get(0)));
77             });
78             return name;
79         };
81         /**
82          * Wrapper for M.util.add_spinner for an activity
83          *
84          * @param {JQuery} activity
85          * @returns {Node}
86          */
87         var addActivitySpinner = function(activity) {
88             activity.addClass(CSS.EDITINPROGRESS);
89             var actionarea = activity.find(SELECTOR.ACTIONAREA).get(0);
90             if (actionarea) {
91                 var spinner = M.util.add_spinner(Y, Y.Node(actionarea));
92                 spinner.show();
93                 return spinner;
94             }
95             return null;
96         };
98         /**
99          * Wrapper for M.util.add_spinner for a section
100          *
101          * @param {JQuery} sectionelement
102          * @returns {Node}
103          */
104         var addSectionSpinner = function(sectionelement) {
105             sectionelement.addClass(CSS.EDITINPROGRESS);
106             var actionarea = sectionelement.find(SELECTOR.SECTIONACTIONMENU).get(0);
107             if (actionarea) {
108                 var spinner = M.util.add_spinner(Y, Y.Node(actionarea));
109                 spinner.show();
110                 return spinner;
111             }
112             return null;
113         };
115         /**
116          * Wrapper for M.util.add_lightbox
117          *
118          * @param {JQuery} sectionelement
119          * @returns {Node}
120          */
121         var addSectionLightbox = function(sectionelement) {
122             var lightbox = M.util.add_lightbox(Y, Y.Node(sectionelement.get(0)));
123             lightbox.show();
124             return lightbox;
125         };
127         /**
128          * Removes the spinner element
129          *
130          * @param {JQuery} element
131          * @param {Node} spinner
132          * @param {Number} delay
133          */
134         var removeSpinner = function(element, spinner, delay) {
135             window.setTimeout(function() {
136                 element.removeClass(CSS.EDITINPROGRESS);
137                 if (spinner) {
138                     spinner.hide();
139                 }
140             }, delay);
141         };
143         /**
144          * Removes the lightbox element
145          *
146          * @param {Node} lightbox lighbox YUI element returned by addSectionLightbox
147          * @param {Number} delay
148          */
149         var removeLightbox = function(lightbox, delay) {
150             if (lightbox) {
151                 window.setTimeout(function() {
152                     lightbox.hide();
153                 }, delay);
154             }
155         };
157         /**
158          * Initialise action menu for the element (section or module)
159          *
160          * @param {String} elementid CSS id attribute of the element
161          */
162         var initActionMenu = function(elementid) {
163             // Initialise action menu in the new activity.
164             Y.use('moodle-course-coursebase', function() {
165                 M.course.coursebase.invoke_function('setup_for_resource', '#' + elementid);
166             });
167             if (M.core.actionmenu && M.core.actionmenu.newDOMNode) {
168                 M.core.actionmenu.newDOMNode(Y.one('#' + elementid));
169             }
170         };
172         /**
173          * Returns focus to the element that was clicked or "Edit" link if element is no longer visible.
174          *
175          * @param {String} elementId CSS id attribute of the element
176          * @param {String} action data-action property of the element that was clicked
177          */
178         var focusActionItem = function(elementId, action) {
179             var mainelement = $('#' + elementId);
180             var selector = '[data-action=' + action + ']';
181             if (action === 'groupsseparate' || action === 'groupsvisible' || action === 'groupsnone') {
182                 // New element will have different data-action.
183                 selector = '[data-action=groupsseparate],[data-action=groupsvisible],[data-action=groupsnone]';
184             }
185             if (mainelement.find(selector).is(':visible')) {
186                 mainelement.find(selector).focus();
187             } else {
188                 // Element not visible, focus the "Edit" link.
189                 mainelement.find(SELECTOR.MENU).find(SELECTOR.TOGGLE).focus();
190             }
191         };
193         /**
194          * Find next <a> after the element
195          *
196          * @param {JQuery} mainElement element that is about to be deleted
197          * @returns {JQuery}
198          */
199         var findNextFocusable = function(mainElement) {
200             var tabables = $("a:visible");
201             var isInside = false;
202             var foundElement = null;
203             tabables.each(function() {
204                 if ($.contains(mainElement[0], this)) {
205                     isInside = true;
206                 } else if (isInside) {
207                     foundElement = this;
208                     return false; // Returning false in .each() is equivalent to "break;" inside the loop in php.
209                 }
210             });
211             return foundElement;
212         };
214         /**
215          * Performs an action on a module (moving, deleting, duplicating, hiding, etc.)
216          *
217          * @param {JQuery} moduleElement activity element we perform action on
218          * @param {Number} cmid
219          * @param {JQuery} target the element (menu item) that was clicked
220          */
221         var editModule = function(moduleElement, cmid, target) {
222             var action = target.attr('data-action');
223             var spinner = addActivitySpinner(moduleElement);
224             var promises = ajax.call([{
225                 methodname: 'core_course_edit_module',
226                 args: {id: cmid,
227                     action: action,
228                     sectionreturn: target.attr('data-sectionreturn') ? target.attr('data-sectionreturn') : 0
229                 }
230             }], true);
232             var lightbox;
233             if (action === 'duplicate') {
234                 lightbox = addSectionLightbox(target.closest(SELECTOR.SECTIONLI));
235             }
236             $.when.apply($, promises)
237                 .done(function(data) {
238                     var elementToFocus = findNextFocusable(moduleElement);
239                     moduleElement.replaceWith(data);
240                     let affectedids = [];
241                     // Initialise action menu for activity(ies) added as a result of this.
242                     $('<div>' + data + '</div>').find(SELECTOR.ACTIVITYLI).each(function(index) {
243                         initActionMenu($(this).attr('id'));
244                         if (index === 0) {
245                             focusActionItem($(this).attr('id'), action);
246                             elementToFocus = null;
247                         }
248                         // Save any activity id in cmids.
249                         affectedids.push(getModuleId($(this)));
250                     });
251                     // In case of activity deletion focus the next focusable element.
252                     if (elementToFocus) {
253                         elementToFocus.focus();
254                     }
255                     // Remove spinner and lightbox with a delay.
256                     removeSpinner(moduleElement, spinner, 400);
257                     removeLightbox(lightbox, 400);
258                     // Trigger event that can be observed by course formats.
259                     moduleElement.trigger($.Event('coursemoduleedited', {ajaxreturn: data, action: action}));
261                     // Modify cm state.
262                     courseeditor.dispatch('legacyActivityAction', action, cmid, affectedids);
264                 }).fail(function(ex) {
265                     // Remove spinner and lightbox.
266                     removeSpinner(moduleElement, spinner);
267                     removeLightbox(lightbox);
268                     // Trigger event that can be observed by course formats.
269                     var e = $.Event('coursemoduleeditfailed', {exception: ex, action: action});
270                     moduleElement.trigger(e);
271                     if (!e.isDefaultPrevented()) {
272                         notification.exception(ex);
273                     }
274                 });
275         };
277         /**
278          * Requests html for the module via WS core_course_get_module and updates the module on the course page
279          *
280          * Used after d&d of the module to another section
281          *
282          * @param {JQuery|Element} element
283          * @param {Number} cmid
284          * @param {Number} sectionreturn
285          */
286         var refreshModule = function(element, cmid, sectionreturn) {
287             const activityElement = $(element);
288             var spinner = addActivitySpinner(activityElement);
289             var promises = ajax.call([{
290                 methodname: 'core_course_get_module',
291                 args: {id: cmid, sectionreturn: sectionreturn}
292             }], true);
294             $.when.apply($, promises)
295                 .done(function(data) {
296                     removeSpinner(activityElement, spinner, 400);
297                     replaceActivityHtmlWith(data);
298                 }).fail(function() {
299                     removeSpinner(activityElement, spinner);
300                 });
301         };
303         /**
304          * Displays the delete confirmation to delete a module
305          *
306          * @param {JQuery} mainelement activity element we perform action on
307          * @param {function} onconfirm function to execute on confirm
308          */
309         var confirmDeleteModule = function(mainelement, onconfirm) {
310             var modtypename = mainelement.attr('class').match(/modtype_([^\s]*)/)[1];
311             var modulename = getModuleName(mainelement);
313             str.get_string('pluginname', modtypename).done(function(pluginname) {
314                 var plugindata = {
315                     type: pluginname,
316                     name: modulename
317                 };
318                 str.get_strings([
319                     {key: 'confirm'},
320                     {key: modulename === null ? 'deletechecktype' : 'deletechecktypename', param: plugindata},
321                     {key: 'yes'},
322                     {key: 'no'}
323                 ]).done(function(s) {
324                         notification.confirm(s[0], s[1], s[2], s[3], onconfirm);
325                     }
326                 );
327             });
328         };
330         /**
331          * Displays the delete confirmation to delete a section
332          *
333          * @param {String} message confirmation message
334          * @param {function} onconfirm function to execute on confirm
335          */
336         var confirmEditSection = function(message, onconfirm) {
337             str.get_strings([
338                 {key: 'confirm'}, // TODO link text
339                 {key: 'yes'},
340                 {key: 'no'}
341             ]).done(function(s) {
342                     notification.confirm(s[0], message, s[1], s[2], onconfirm);
343                 }
344             );
345         };
347         /**
348          * Replaces an action menu item with another one (for example Show->Hide, Set marker->Remove marker)
349          *
350          * @param {JQuery} actionitem
351          * @param {String} image new image name ("i/show", "i/hide", etc.)
352          * @param {String} stringname new string for the action menu item
353          * @param {String} stringcomponent
354          * @param {String} newaction new value for data-action attribute of the link
355          * @return {Promise} promise which is resolved when the replacement has completed
356          */
357         var replaceActionItem = function(actionitem, image, stringname,
358                                            stringcomponent, newaction) {
360             var stringRequests = [{key: stringname, component: stringcomponent}];
361             // Do not provide an icon with duplicate, different text to the menu item.
363             return str.get_strings(stringRequests).then(function(strings) {
364                 actionitem.find('span.menu-action-text').html(strings[0]);
366                 return templates.renderPix(image, 'core');
367             }).then(function(pixhtml) {
368                 actionitem.find('.icon').replaceWith(pixhtml);
369                 actionitem.attr('data-action', newaction);
370                 return;
371             }).catch(notification.exception);
372         };
374         /**
375          * Default post-processing for section AJAX edit actions.
376          *
377          * This can be overridden in course formats by listening to event coursesectionedited:
378          *
379          * $('body').on('coursesectionedited', 'li.section', function(e) {
380          *     var action = e.action,
381          *         sectionElement = $(e.target),
382          *         data = e.ajaxreturn;
383          *     // ... Do some processing here.
384          *     e.preventDefault(); // Prevent default handler.
385          * });
386          *
387          * @param {JQuery} sectionElement
388          * @param {JQuery} actionItem
389          * @param {Object} data
390          * @param {String} courseformat
391          * @param {Number} sectionid
392          */
393         var defaultEditSectionHandler = function(sectionElement, actionItem, data, courseformat, sectionid) {
394             var action = actionItem.attr('data-action');
395             if (action === 'hide' || action === 'show') {
396                 if (action === 'hide') {
397                     sectionElement.addClass('hidden');
398                     replaceActionItem(actionItem, 'i/show',
399                         'showfromothers', 'format_' + courseformat, 'show');
400                 } else {
401                     sectionElement.removeClass('hidden');
402                     replaceActionItem(actionItem, 'i/hide',
403                         'hidefromothers', 'format_' + courseformat, 'hide');
404                 }
405                 // Replace the modules with new html (that indicates that they are now hidden or not hidden).
406                 if (data.modules !== undefined) {
407                     for (var i in data.modules) {
408                         replaceActivityHtmlWith(data.modules[i]);
409                     }
410                 }
411                 // Replace the section availability information.
412                 if (data.section_availability !== undefined) {
413                     sectionElement.find('.section_availability').first().replaceWith(data.section_availability);
414                 }
415                 // Modify course state.
416                 const section = courseeditor.state.section.get(sectionid);
417                 if (section !== undefined) {
418                     courseeditor.dispatch('sectionState', [sectionid]);
419                 }
420             } else if (action === 'setmarker') {
421                 var oldmarker = $(SELECTOR.SECTIONLI + '.current'),
422                     oldActionItem = oldmarker.find(SELECTOR.SECTIONACTIONMENU + ' ' + 'a[data-action=removemarker]');
423                 oldmarker.removeClass('current');
424                 replaceActionItem(oldActionItem, 'i/marker',
425                     'highlight', 'core', 'setmarker');
426                 sectionElement.addClass('current');
427                 replaceActionItem(actionItem, 'i/marked',
428                     'highlightoff', 'core', 'removemarker');
429                 courseeditor.dispatch('legacySectionAction', action, sectionid);
430             } else if (action === 'removemarker') {
431                 sectionElement.removeClass('current');
432                 replaceActionItem(actionItem, 'i/marker',
433                     'highlight', 'core', 'setmarker');
434                 courseeditor.dispatch('legacySectionAction', action, sectionid);
435             }
436         };
438         /**
439          * Replaces the course module with the new html (used to update module after it was edited or its visibility was changed).
440          *
441          * @param {String} activityHTML
442          */
443         var replaceActivityHtmlWith = function(activityHTML) {
444             $('<div>' + activityHTML + '</div>').find(SELECTOR.ACTIVITYLI).each(function() {
445                 // Extract id from the new activity html.
446                 var id = $(this).attr('id');
447                 // Find the existing element with the same id and replace its contents with new html.
448                 $(SELECTOR.ACTIVITYLI + '#' + id).replaceWith(activityHTML);
449                 // Initialise action menu.
450                 initActionMenu(id);
451             });
452         };
454         /**
455          * Performs an action on a module (moving, deleting, duplicating, hiding, etc.)
456          *
457          * @param {JQuery} sectionElement section element we perform action on
458          * @param {Nunmber} sectionid
459          * @param {JQuery} target the element (menu item) that was clicked
460          * @param {String} courseformat
461          */
462         var editSection = function(sectionElement, sectionid, target, courseformat) {
463             var action = target.attr('data-action'),
464                 sectionreturn = target.attr('data-sectionreturn') ? target.attr('data-sectionreturn') : 0;
465             var spinner = addSectionSpinner(sectionElement);
466             var promises = ajax.call([{
467                 methodname: 'core_course_edit_section',
468                 args: {id: sectionid, action: action, sectionreturn: sectionreturn}
469             }], true);
471             var lightbox = addSectionLightbox(sectionElement);
472             $.when.apply($, promises)
473                 .done(function(dataencoded) {
474                     var data = $.parseJSON(dataencoded);
475                     removeSpinner(sectionElement, spinner);
476                     removeLightbox(lightbox);
477                     sectionElement.find(SELECTOR.SECTIONACTIONMENU).find(SELECTOR.TOGGLE).focus();
478                     // Trigger event that can be observed by course formats.
479                     var e = $.Event('coursesectionedited', {ajaxreturn: data, action: action});
480                     sectionElement.trigger(e);
481                     if (!e.isDefaultPrevented()) {
482                         defaultEditSectionHandler(sectionElement, target, data, courseformat, sectionid);
483                     }
484                 }).fail(function(ex) {
485                     // Remove spinner and lightbox.
486                     removeSpinner(sectionElement, spinner);
487                     removeLightbox(lightbox);
488                     // Trigger event that can be observed by course formats.
489                     var e = $.Event('coursesectioneditfailed', {exception: ex, action: action});
490                     sectionElement.trigger(e);
491                     if (!e.isDefaultPrevented()) {
492                         notification.exception(ex);
493                     }
494                 });
495         };
497         // Register a function to be executed after D&D of an activity.
498         Y.use('moodle-course-coursebase', function() {
499             M.course.coursebase.register_module({
500                 // Ignore camelcase eslint rule for the next line because it is an expected name of the callback.
501                 // eslint-disable-next-line camelcase
502                 set_visibility_resource_ui: function(args) {
503                     var mainelement = $(args.element.getDOMNode());
504                     var cmid = getModuleId(mainelement);
505                     if (cmid) {
506                         var sectionreturn = mainelement.find('.' + CSS.EDITINGMOVE).attr('data-sectionreturn');
507                         refreshModule(mainelement, cmid, sectionreturn);
508                     }
509                 },
510                 /**
511                  * Update the course state when some cm is moved via YUI.
512                  * @param {*} params
513                  */
514                 updateMovedCmState: (params) => {
515                     const state = courseeditor.state;
517                     // Update old section.
518                     const cm = state.cm.get(params.cmid);
519                     if (cm !== undefined) {
520                         courseeditor.dispatch('sectionState', [cm.sectionid]);
521                     }
522                     // Update cm state.
523                     courseeditor.dispatch('cmState', [params.cmid]);
524                 },
525                 /**
526                  * Update the course state when some section is moved via YUI.
527                  */
528                 updateMovedSectionState: () => {
529                     courseeditor.dispatch('courseState');
530                 },
531             });
532         });
534         // From Moodle 4.0 all edit actions are being re-implemented as state mutation.
535         // This means all method from this "actions" module will be deprecated when all the course
536         // interface is migrated to reactive components.
537         // Most legacy actions did not provide enough information to regenarate the course so they
538         // use the mutations courseState, sectionState and cmState to get the updated state from
539         // the server. However, some activity actions where we can prevent an extra webservice
540         // call by implementing an adhoc mutation.
541         courseeditor.addMutations({
542             /**
543              * Compatibility function to update Moodle 4.0 course state using legacy actions.
544              *
545              * This method only updates some actions which does not require to use cmState mutation
546              * to get updated data form the server.
547              *
548              * @param {Object} statemanager the current state in read write mode
549              * @param {String} action the performed action
550              * @param {Number} cmid the affected course module id
551              * @param {Array} affectedids all affected cm ids (for duplicate action)
552              */
553             legacyActivityAction: function(statemanager, action, cmid, affectedids) {
555                 const state = statemanager.state;
556                 const cm = state.cm.get(cmid);
557                 if (cm === undefined) {
558                     return;
559                 }
560                 const section = state.section.get(cm.sectionid);
561                 if (section === undefined) {
562                     return;
563                 }
565                 statemanager.setReadOnly(false);
567                 switch (action) {
568                     case 'delete':
569                         // Remove from section.
570                         section.cmlist = section.cmlist.reduce(
571                             (cmlist, current) => {
572                                 if (current != cmid) {
573                                     cmlist.push(current);
574                                 }
575                                 return cmlist;
576                             },
577                             []
578                         );
579                         // Delete form list.
580                         state.cm.delete(cmid);
581                         break;
583                     case 'hide':
584                     case 'show':
585                         cm.visible = (action === 'show') ? true : false;
586                         break;
588                     case 'duplicate':
589                         // Duplicate requires to get extra data from the server.
590                         courseeditor.dispatch('cmState', affectedids);
591                         break;
592                 }
593                 statemanager.setReadOnly(true);
594             },
595             legacySectionAction: function(statemanager, action, sectionid) {
597                 const state = statemanager.state;
598                 const section = state.section.get(sectionid);
599                 if (section === undefined) {
600                     return;
601                 }
603                 statemanager.setReadOnly(false);
605                 switch (action) {
606                     case 'setmarker':
607                         // Remove previous marker.
608                         state.section.forEach((current) => {
609                             if (current.id != sectionid) {
610                                 current.current = false;
611                             }
612                         });
613                         section.current = true;
614                         break;
616                     case 'removemarker':
617                         section.current = false;
618                         break;
619                 }
620                 statemanager.setReadOnly(true);
621             },
622         });
624         return /** @alias module:core_course/actions */ {
626             /**
627              * Initialises course page
628              *
629              * @method init
630              * @param {String} courseformat name of the current course format (for fetching strings)
631              */
632             initCoursePage: function(courseformat) {
634                 // Add a handler for course module actions.
635                 $('body').on('click keypress', SELECTOR.ACTIVITYLI + ' ' +
636                         SELECTOR.ACTIVITYACTION + '[data-action]', function(e) {
637                     if (e.type === 'keypress' && e.keyCode !== 13) {
638                         return;
639                     }
640                     var actionItem = $(this),
641                         moduleElement = actionItem.closest(SELECTOR.ACTIVITYLI),
642                         action = actionItem.attr('data-action'),
643                         moduleId = getModuleId(moduleElement);
644                     switch (action) {
645                         case 'moveleft':
646                         case 'moveright':
647                         case 'delete':
648                         case 'duplicate':
649                         case 'hide':
650                         case 'stealth':
651                         case 'show':
652                         case 'groupsseparate':
653                         case 'groupsvisible':
654                         case 'groupsnone':
655                             break;
656                         default:
657                             // Nothing to do here!
658                             return;
659                     }
660                     if (!moduleId) {
661                         return;
662                     }
663                     e.preventDefault();
664                     if (action === 'delete') {
665                         // Deleting requires confirmation.
666                         confirmDeleteModule(moduleElement, function() {
667                             editModule(moduleElement, moduleId, actionItem);
668                         });
669                     } else {
670                         editModule(moduleElement, moduleId, actionItem);
671                     }
672                 });
674                 // Add a handler for section show/hide actions.
675                 $('body').on('click keypress', SELECTOR.SECTIONLI + ' ' +
676                             SELECTOR.SECTIONACTIONMENU + '[data-sectionid] ' +
677                             'a[data-action]', function(e) {
678                     if (e.type === 'keypress' && e.keyCode !== 13) {
679                         return;
680                     }
681                     var actionItem = $(this),
682                         sectionElement = actionItem.closest(SELECTOR.SECTIONLI),
683                         sectionId = actionItem.closest(SELECTOR.SECTIONACTIONMENU).attr('data-sectionid');
684                     e.preventDefault();
685                     if (actionItem.attr('data-confirm')) {
686                         // Action requires confirmation.
687                         confirmEditSection(actionItem.attr('data-confirm'), function() {
688                             editSection(sectionElement, sectionId, actionItem, courseformat);
689                         });
690                     } else {
691                         editSection(sectionElement, sectionId, actionItem, courseformat);
692                     }
693                 });
695                 // The section and activity names are edited using inplace editable.
696                 // The "update" jQuery event must be captured in order to update the course state.
697                 $('body').on('updated', `${SELECTOR.SECTIONLI} [data-inplaceeditable]`, function(e) {
698                     if (e.ajaxreturn && e.ajaxreturn.itemid) {
699                         const state = courseeditor.state;
700                         const section = state.section.get(e.ajaxreturn.itemid);
701                         if (section !== undefined) {
702                             courseeditor.dispatch('sectionState', [e.ajaxreturn.itemid]);
703                         }
704                     }
705                 });
706                 $('body').on('updated', `${SELECTOR.ACTIVITYLI} [data-inplaceeditable]`, function(e) {
707                     if (e.ajaxreturn && e.ajaxreturn.itemid) {
708                         courseeditor.dispatch('cmState', [e.ajaxreturn.itemid]);
709                     }
710                 });
712                 // Add a handler for "Add sections" link to ask for a number of sections to add.
713                 str.get_string('numberweeks').done(function(strNumberSections) {
714                     var trigger = $(SELECTOR.ADDSECTIONS),
715                         modalTitle = trigger.attr('data-add-sections'),
716                         newSections = trigger.attr('data-new-sections');
717                     var modalBody = $('<div><label for="add_section_numsections"></label> ' +
718                         '<input id="add_section_numsections" type="number" min="1" max="' + newSections + '" value="1"></div>');
719                     modalBody.find('label').html(strNumberSections);
720                     ModalFactory.create({
721                         title: modalTitle,
722                         type: ModalFactory.types.SAVE_CANCEL,
723                         body: modalBody.html()
724                     }, trigger)
725                     .done(function(modal) {
726                         var numSections = $(modal.getBody()).find('#add_section_numsections'),
727                         addSections = function() {
728                             // Check if value of the "Number of sections" is a valid positive integer and redirect
729                             // to adding a section script.
730                             if ('' + parseInt(numSections.val()) === numSections.val() && parseInt(numSections.val()) >= 1) {
731                                 document.location = trigger.attr('href') + '&numsections=' + parseInt(numSections.val());
732                             }
733                         };
734                         modal.setSaveButtonText(modalTitle);
735                         modal.getRoot().on(ModalEvents.shown, function() {
736                             // When modal is shown focus and select the input and add a listener to keypress of "Enter".
737                             numSections.focus().select().on('keydown', function(e) {
738                                 if (e.keyCode === KeyCodes.enter) {
739                                     addSections();
740                                 }
741                             });
742                         });
743                         modal.getRoot().on(ModalEvents.save, function(e) {
744                             // When modal "Add" button is pressed.
745                             e.preventDefault();
746                             addSections();
747                         });
748                     });
749                 });
750             },
752             /**
753              * Replaces a section action menu item with another one (for example Show->Hide, Set marker->Remove marker)
754              *
755              * This method can be used by course formats in their listener to the coursesectionedited event
756              *
757              * @deprecated since Moodle 3.9
758              * @param {JQuery} sectionelement
759              * @param {String} selector CSS selector inside the section element, for example "a[data-action=show]"
760              * @param {String} image new image name ("i/show", "i/hide", etc.)
761              * @param {String} stringname new string for the action menu item
762              * @param {String} stringcomponent
763              * @param {String} newaction new value for data-action attribute of the link
764              */
765             replaceSectionActionItem: function(sectionelement, selector, image, stringname,
766                                                     stringcomponent, newaction) {
767                 log.debug('replaceSectionActionItem() is deprecated and will be removed.');
768                 var actionitem = sectionelement.find(SELECTOR.SECTIONACTIONMENU + ' ' + selector);
769                 replaceActionItem(actionitem, image, stringname, stringcomponent, newaction);
770             },
771             // Method to refresh a module.
772             refreshModule,
773         };
774     });