MDL-62748 course: maxsections is a limit
[moodle.git] / course / amd / src / actions.js
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/>.
16 /**
17  * Various actions on modules and sections in the editing mode - hiding, duplicating, deleting, etc.
18  *
19  * @module     core_course/actions
20  * @package    core
21  * @copyright  2016 Marina Glancy
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  * @since      3.3
24  */
25 define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/url', 'core/yui',
26         'core/modal_factory', 'core/modal_events', 'core/key_codes'],
27     function($, ajax, templates, notification, str, url, Y, ModalFactory, ModalEvents, KeyCodes) {
28         var CSS = {
29             EDITINPROGRESS: 'editinprogress',
30             SECTIONDRAGGABLE: 'sectiondraggable',
31             EDITINGMOVE: 'editing_move'
32         };
33         var SELECTOR = {
34             ACTIVITYLI: 'li.activity',
35             ACTIONAREA: '.actions',
36             ACTIVITYACTION: 'a.cm-edit-action',
37             MENU: '.moodle-actionmenu[data-enhance=moodle-core-actionmenu]',
38             TOGGLE: '.toggle-display,.dropdown-toggle',
39             SECTIONLI: 'li.section',
40             SECTIONACTIONMENU: '.section_action_menu',
41             ADDSECTIONS: '#changenumsections [data-add-sections]'
42         };
44         Y.use('moodle-course-coursebase', function() {
45             var courseformatselector = M.course.format.get_section_selector();
46             if (courseformatselector) {
47                 SELECTOR.SECTIONLI = courseformatselector;
48             }
49         });
51         /**
52          * Wrapper for Y.Moodle.core_course.util.cm.getId
53          *
54          * @param {JQuery} element
55          * @returns {Integer}
56          */
57         var getModuleId = function(element) {
58             var id;
59             Y.use('moodle-course-util', function(Y) {
60                 id = Y.Moodle.core_course.util.cm.getId(Y.Node(element.get(0)));
61             });
62             return id;
63         };
65         /**
66          * Wrapper for Y.Moodle.core_course.util.cm.getName
67          *
68          * @param {JQuery} element
69          * @returns {String}
70          */
71         var getModuleName = function(element) {
72             var name;
73             Y.use('moodle-course-util', function(Y) {
74                 name = Y.Moodle.core_course.util.cm.getName(Y.Node(element.get(0)));
75             });
76             return name;
77         };
79         /**
80          * Wrapper for M.util.add_spinner for an activity
81          *
82          * @param {JQuery} activity
83          * @returns {Node}
84          */
85         var addActivitySpinner = function(activity) {
86             activity.addClass(CSS.EDITINPROGRESS);
87             var actionarea = activity.find(SELECTOR.ACTIONAREA).get(0);
88             if (actionarea) {
89                 var spinner = M.util.add_spinner(Y, Y.Node(actionarea));
90                 spinner.show();
91                 return spinner;
92             }
93             return null;
94         };
96         /**
97          * Wrapper for M.util.add_spinner for a section
98          *
99          * @param {JQuery} sectionelement
100          * @returns {Node}
101          */
102         var addSectionSpinner = function(sectionelement) {
103             sectionelement.addClass(CSS.EDITINPROGRESS);
104             var actionarea = sectionelement.find(SELECTOR.SECTIONACTIONMENU).get(0);
105             if (actionarea) {
106                 var spinner = M.util.add_spinner(Y, Y.Node(actionarea));
107                 spinner.show();
108                 return spinner;
109             }
110             return null;
111         };
113         /**
114          * Wrapper for M.util.add_lightbox
115          *
116          * @param {JQuery} sectionelement
117          * @returns {Node}
118          */
119         var addSectionLightbox = function(sectionelement) {
120             var lightbox = M.util.add_lightbox(Y, Y.Node(sectionelement.get(0)));
121             lightbox.show();
122             return lightbox;
123         };
125         /**
126          * Removes the spinner element
127          *
128          * @param {JQuery} element
129          * @param {Node} spinner
130          * @param {Number} delay
131          */
132         var removeSpinner = function(element, spinner, delay) {
133             window.setTimeout(function() {
134                 element.removeClass(CSS.EDITINPROGRESS);
135                 if (spinner) {
136                     spinner.hide();
137                 }
138             }, delay);
139         };
141         /**
142          * Removes the lightbox element
143          *
144          * @param {Node} lightbox lighbox YUI element returned by addSectionLightbox
145          * @param {Number} delay
146          */
147         var removeLightbox = function(lightbox, delay) {
148             if (lightbox) {
149                 window.setTimeout(function() {
150                     lightbox.hide();
151                 }, delay);
152             }
153         };
155         /**
156          * Initialise action menu for the element (section or module)
157          *
158          * @param {String} elementid CSS id attribute of the element
159          * @param {Boolean} openmenu whether to open menu - this can be used when re-initiating menu after indent action was pressed
160          */
161         var initActionMenu = function(elementid, openmenu) {
162             // Initialise action menu in the new activity.
163             Y.use('moodle-course-coursebase', function() {
164                 M.course.coursebase.invoke_function('setup_for_resource', '#' + elementid);
165             });
166             if (M.core.actionmenu && M.core.actionmenu.newDOMNode) {
167                 M.core.actionmenu.newDOMNode(Y.one('#' + elementid));
168             }
169             // Open action menu if the original element had data-keepopen.
170             if (openmenu) {
171                 // We must use YUI click simulate here so the toggle works in Clean theme. This toggle is not
172                 // needed in Boost because we use standard bootstrapbase action menu.
173                 var toggle = Y.one('#' + elementid + ' ' + SELECTOR.MENU).one(SELECTOR.TOGGLE);
174                 if (toggle && toggle.simulate) {
175                     toggle.simulate('click');
176                 }
177             }
178         };
180         /**
181          * Returns focus to the element that was clicked or "Edit" link if element is no longer visible.
182          *
183          * @param {String} elementId CSS id attribute of the element
184          * @param {String} action data-action property of the element that was clicked
185          */
186         var focusActionItem = function(elementId, action) {
187             var mainelement = $('#' + elementId);
188             var selector = '[data-action=' + action + ']';
189             if (action === 'groupsseparate' || action === 'groupsvisible' || action === 'groupsnone') {
190                 // New element will have different data-action.
191                 selector = '[data-action=groupsseparate],[data-action=groupsvisible],[data-action=groupsnone]';
192             }
193             if (mainelement.find(selector).is(':visible')) {
194                 mainelement.find(selector).focus();
195             } else {
196                 // Element not visible, focus the "Edit" link.
197                 mainelement.find(SELECTOR.MENU).find(SELECTOR.TOGGLE).focus();
198             }
199         };
201         /**
202          * Find next <a> after the element
203          *
204          * @param {JQuery} mainElement element that is about to be deleted
205          * @returns {JQuery}
206          */
207         var findNextFocusable = function(mainElement) {
208             var tabables = $("a:visible");
209             var isInside = false;
210             var foundElement = null;
211             tabables.each(function() {
212                 if ($.contains(mainElement[0], this)) {
213                     isInside = true;
214                 } else if (isInside) {
215                     foundElement = this;
216                     return false; // Returning false in .each() is equivalent to "break;" inside the loop in php.
217                 }
218             });
219             return foundElement;
220         };
222         /**
223          * Performs an action on a module (moving, deleting, duplicating, hiding, etc.)
224          *
225          * @param {JQuery} moduleElement activity element we perform action on
226          * @param {Number} cmid
227          * @param {JQuery} target the element (menu item) that was clicked
228          */
229         var editModule = function(moduleElement, cmid, target) {
230             var keepopen = target.attr('data-keepopen'),
231                     action = target.attr('data-action');
232             var spinner = addActivitySpinner(moduleElement);
233             var promises = ajax.call([{
234                 methodname: 'core_course_edit_module',
235                 args: {id: cmid,
236                     action: action,
237                     sectionreturn: target.attr('data-sectionreturn') ? target.attr('data-sectionreturn') : 0
238                 }
239             }], true);
241             var lightbox;
242             if (action === 'duplicate') {
243                 lightbox = addSectionLightbox(target.closest(SELECTOR.SECTIONLI));
244             }
245             $.when.apply($, promises)
246                 .done(function(data) {
247                     var elementToFocus = findNextFocusable(moduleElement);
248                     moduleElement.replaceWith(data);
249                     // Initialise action menu for activity(ies) added as a result of this.
250                     $('<div>' + data + '</div>').find(SELECTOR.ACTIVITYLI).each(function(index) {
251                         initActionMenu($(this).attr('id'), keepopen);
252                         if (index === 0) {
253                             focusActionItem($(this).attr('id'), action);
254                             elementToFocus = null;
255                         }
256                     });
257                     // In case of activity deletion focus the next focusable element.
258                     if (elementToFocus) {
259                         elementToFocus.focus();
260                     }
261                     // Remove spinner and lightbox with a delay.
262                     removeSpinner(moduleElement, spinner, 400);
263                     removeLightbox(lightbox, 400);
264                     // Trigger event that can be observed by course formats.
265                     moduleElement.trigger($.Event('coursemoduleedited', {ajaxreturn: data, action: action}));
266                 }).fail(function(ex) {
267                     // Remove spinner and lightbox.
268                     removeSpinner(moduleElement, spinner);
269                     removeLightbox(lightbox);
270                     // Trigger event that can be observed by course formats.
271                     var e = $.Event('coursemoduleeditfailed', {exception: ex, action: action});
272                     moduleElement.trigger(e);
273                     if (!e.isDefaultPrevented()) {
274                         notification.exception(ex);
275                     }
276                 });
277         };
279         /**
280          * Requests html for the module via WS core_course_get_module and updates the module on the course page
281          *
282          * Used after d&d of the module to another section
283          *
284          * @param {JQuery} activityElement
285          * @param {Number} cmid
286          * @param {Number} sectionreturn
287          */
288         var refreshModule = function(activityElement, cmid, sectionreturn) {
289             var spinner = addActivitySpinner(activityElement);
290             var promises = ajax.call([{
291                 methodname: 'core_course_get_module',
292                 args: {id: cmid, sectionreturn: sectionreturn}
293             }], true);
295             $.when.apply($, promises)
296                 .done(function(data) {
297                     removeSpinner(activityElement, spinner, 400);
298                     replaceActivityHtmlWith(data);
299                 }).fail(function() {
300                     removeSpinner(activityElement, spinner);
301                 });
302         };
304         /**
305          * Displays the delete confirmation to delete a module
306          *
307          * @param {JQuery} mainelement activity element we perform action on
308          * @param {function} onconfirm function to execute on confirm
309          */
310         var confirmDeleteModule = function(mainelement, onconfirm) {
311             var modtypename = mainelement.attr('class').match(/modtype_([^\s]*)/)[1];
312             var modulename = getModuleName(mainelement);
314             str.get_string('pluginname', modtypename).done(function(pluginname) {
315                 var plugindata = {
316                     type: pluginname,
317                     name: modulename
318                 };
319                 str.get_strings([
320                     {key: 'confirm'},
321                     {key: modulename === null ? 'deletechecktype' : 'deletechecktypename', param: plugindata},
322                     {key: 'yes'},
323                     {key: 'no'}
324                 ]).done(function(s) {
325                         notification.confirm(s[0], s[1], s[2], s[3], onconfirm);
326                     }
327                 );
328             });
329         };
331         /**
332          * Displays the delete confirmation to delete a section
333          *
334          * @param {String} message confirmation message
335          * @param {function} onconfirm function to execute on confirm
336          */
337         var confirmEditSection = function(message, onconfirm) {
338             str.get_strings([
339                 {key: 'confirm'}, // TODO link text
340                 {key: 'yes'},
341                 {key: 'no'}
342             ]).done(function(s) {
343                     notification.confirm(s[0], message, s[1], s[2], onconfirm);
344                 }
345             );
346         };
348         /**
349          * Replaces an action menu item with another one (for example Show->Hide, Set marker->Remove marker)
350          *
351          * @param {JQuery} actionitem
352          * @param {String} image new image name ("i/show", "i/hide", etc.)
353          * @param {String} stringname new string for the action menu item
354          * @param {String} stringcomponent
355          * @param {String} titlestr string for "title" attribute (if different from stringname)
356          * @param {String} titlecomponent
357          * @param {String} newaction new value for data-action attribute of the link
358          * @return {Promise} promise which is resolved when the replacement has completed
359          */
360         var replaceActionItem = function(actionitem, image, stringname,
361                                            stringcomponent, titlestr, titlecomponent, newaction) {
364             var stringRequests = [{key: stringname, component: stringcomponent}];
365             if (titlestr) {
366                 stringRequests.push({key: titlestr, component: titlecomponent});
367             }
369             return str.get_strings(stringRequests).then(function(strings) {
370                 actionitem.find('span.menu-action-text').html(strings[0]);
371                 actionitem.attr('title', strings[0]);
373                 var title = '';
374                 if (titlestr) {
375                     title = strings[1];
376                     actionitem.attr('title', title);
377                 }
378                 return templates.renderPix(image, 'core', title);
379             }).then(function(pixhtml) {
380                 actionitem.find('.icon').replaceWith(pixhtml);
381                 actionitem.attr('data-action', newaction);
382                 return;
383             }).catch(notification.exception);
384         };
386         /**
387          * Default post-processing for section AJAX edit actions.
388          *
389          * This can be overridden in course formats by listening to event coursesectionedited:
390          *
391          * $('body').on('coursesectionedited', 'li.section', function(e) {
392          *     var action = e.action,
393          *         sectionElement = $(e.target),
394          *         data = e.ajaxreturn;
395          *     // ... Do some processing here.
396          *     e.preventDefault(); // Prevent default handler.
397          * });
398          *
399          * @param {JQuery} sectionElement
400          * @param {JQuery} actionItem
401          * @param {Object} data
402          * @param {String} courseformat
403          */
404         var defaultEditSectionHandler = function(sectionElement, actionItem, data, courseformat) {
405             var action = actionItem.attr('data-action');
406             if (action === 'hide' || action === 'show') {
407                 if (action === 'hide') {
408                     sectionElement.addClass('hidden');
409                     replaceActionItem(actionItem, 'i/show',
410                         'showfromothers', 'format_' + courseformat, null, null, 'show');
411                 } else {
412                     sectionElement.removeClass('hidden');
413                     replaceActionItem(actionItem, 'i/hide',
414                         'hidefromothers', 'format_' + courseformat, null, null, 'hide');
415                 }
416                 // Replace the modules with new html (that indicates that they are now hidden or not hidden).
417                 if (data.modules !== undefined) {
418                     for (var i in data.modules) {
419                         replaceActivityHtmlWith(data.modules[i]);
420                     }
421                 }
422                 // Replace the section availability information.
423                 if (data.section_availability !== undefined) {
424                     sectionElement.find('.section_availability').first().replaceWith(data.section_availability);
425                 }
426             } else if (action === 'setmarker') {
427                 var oldmarker = $(SELECTOR.SECTIONLI + '.current'),
428                     oldActionItem = oldmarker.find(SELECTOR.SECTIONACTIONMENU + ' ' + 'a[data-action=removemarker]');
429                 oldmarker.removeClass('current');
430                 replaceActionItem(oldActionItem, 'i/marker',
431                     'highlight', 'core', 'markthistopic', 'core', 'setmarker');
432                 sectionElement.addClass('current');
433                 replaceActionItem(actionItem, 'i/marked',
434                     'highlightoff', 'core', 'markedthistopic', 'core', 'removemarker');
435             } else if (action === 'removemarker') {
436                 sectionElement.removeClass('current');
437                 replaceActionItem(actionItem, 'i/marker',
438                     'highlight', 'core', 'markthistopic', 'core', 'setmarker');
439             }
440         };
442         /**
443          * Replaces the course module with the new html (used to update module after it was edited or its visibility was changed).
444          *
445          * @param {String} activityHTML
446          */
447         var replaceActivityHtmlWith = function(activityHTML) {
448             $('<div>' + activityHTML + '</div>').find(SELECTOR.ACTIVITYLI).each(function() {
449                 // Extract id from the new activity html.
450                 var id = $(this).attr('id');
451                 // Find the existing element with the same id and replace its contents with new html.
452                 $(SELECTOR.ACTIVITYLI + '#' + id).replaceWith(activityHTML);
453                 // Initialise action menu.
454                 initActionMenu(id, false);
455             });
456         };
458         /**
459          * Performs an action on a module (moving, deleting, duplicating, hiding, etc.)
460          *
461          * @param {JQuery} sectionElement section element we perform action on
462          * @param {Nunmber} sectionid
463          * @param {JQuery} target the element (menu item) that was clicked
464          * @param {String} courseformat
465          */
466         var editSection = function(sectionElement, sectionid, target, courseformat) {
467             var action = target.attr('data-action'),
468                 sectionreturn = target.attr('data-sectionreturn') ? target.attr('data-sectionreturn') : 0;
469             var spinner = addSectionSpinner(sectionElement);
470             var promises = ajax.call([{
471                 methodname: 'core_course_edit_section',
472                 args: {id: sectionid, action: action, sectionreturn: sectionreturn}
473             }], true);
475             var lightbox = addSectionLightbox(sectionElement);
476             $.when.apply($, promises)
477                 .done(function(dataencoded) {
478                     var data = $.parseJSON(dataencoded);
479                     removeSpinner(sectionElement, spinner);
480                     removeLightbox(lightbox);
481                     sectionElement.find(SELECTOR.SECTIONACTIONMENU).find(SELECTOR.TOGGLE).focus();
482                     // Trigger event that can be observed by course formats.
483                     var e = $.Event('coursesectionedited', {ajaxreturn: data, action: action});
484                     sectionElement.trigger(e);
485                     if (!e.isDefaultPrevented()) {
486                         defaultEditSectionHandler(sectionElement, target, data, courseformat);
487                     }
488                 }).fail(function(ex) {
489                     // Remove spinner and lightbox.
490                     removeSpinner(sectionElement, spinner);
491                     removeLightbox(lightbox);
492                     // Trigger event that can be observed by course formats.
493                     var e = $.Event('coursesectioneditfailed', {exception: ex, action: action});
494                     sectionElement.trigger(e);
495                     if (!e.isDefaultPrevented()) {
496                         notification.exception(ex);
497                     }
498                 });
499         };
501         // Register a function to be executed after D&D of an activity.
502         Y.use('moodle-course-coursebase', function() {
503             M.course.coursebase.register_module({
504                 // Ignore camelcase eslint rule for the next line because it is an expected name of the callback.
505                 // eslint-disable-next-line camelcase
506                 set_visibility_resource_ui: function(args) {
507                     var mainelement = $(args.element.getDOMNode());
508                     var cmid = getModuleId(mainelement);
509                     if (cmid) {
510                         var sectionreturn = mainelement.find('.' + CSS.EDITINGMOVE).attr('data-sectionreturn');
511                         refreshModule(mainelement, cmid, sectionreturn);
512                     }
513                 }
514             });
515         });
517         return /** @alias module:core_course/actions */ {
519             /**
520              * Initialises course page
521              *
522              * @method init
523              * @param {String} courseformat name of the current course format (for fetching strings)
524              */
525             initCoursePage: function(courseformat) {
527                 // Add a handler for course module actions.
528                 $('body').on('click keypress', SELECTOR.ACTIVITYLI + ' ' +
529                         SELECTOR.ACTIVITYACTION + '[data-action]', function(e) {
530                     if (e.type === 'keypress' && e.keyCode !== 13) {
531                         return;
532                     }
533                     var actionItem = $(this),
534                         moduleElement = actionItem.closest(SELECTOR.ACTIVITYLI),
535                         action = actionItem.attr('data-action'),
536                         moduleId = getModuleId(moduleElement);
537                     switch (action) {
538                         case 'moveleft':
539                         case 'moveright':
540                         case 'delete':
541                         case 'duplicate':
542                         case 'hide':
543                         case 'stealth':
544                         case 'show':
545                         case 'groupsseparate':
546                         case 'groupsvisible':
547                         case 'groupsnone':
548                             break;
549                         default:
550                             // Nothing to do here!
551                             return;
552                     }
553                     if (!moduleId) {
554                         return;
555                     }
556                     e.preventDefault();
557                     if (action === 'delete') {
558                         // Deleting requires confirmation.
559                         confirmDeleteModule(moduleElement, function() {
560                             editModule(moduleElement, moduleId, actionItem);
561                         });
562                     } else {
563                         editModule(moduleElement, moduleId, actionItem);
564                     }
565                 });
567                 // Add a handler for section show/hide actions.
568                 $('body').on('click keypress', SELECTOR.SECTIONLI + ' ' +
569                             SELECTOR.SECTIONACTIONMENU + '[data-sectionid] ' +
570                             'a[data-action]', function(e) {
571                     if (e.type === 'keypress' && e.keyCode !== 13) {
572                         return;
573                     }
574                     var actionItem = $(this),
575                         sectionElement = actionItem.closest(SELECTOR.SECTIONLI),
576                         sectionId = actionItem.closest(SELECTOR.SECTIONACTIONMENU).attr('data-sectionid');
577                     e.preventDefault();
578                     if (actionItem.attr('data-confirm')) {
579                         // Action requires confirmation.
580                         confirmEditSection(actionItem.attr('data-confirm'), function() {
581                             editSection(sectionElement, sectionId, actionItem, courseformat);
582                         });
583                     } else {
584                         editSection(sectionElement, sectionId, actionItem, courseformat);
585                     }
586                 });
588                 // Add a handler for "Add sections" link to ask for a number of sections to add.
589                 str.get_string('numberweeks').done(function(strNumberSections) {
590                     var trigger = $(SELECTOR.ADDSECTIONS),
591                         modalTitle = trigger.attr('data-add-sections'),
592                         newSections = trigger.attr('new-sections');
593                     var modalBody = $('<div><label for="add_section_numsections"></label> ' +
594                         '<input id="add_section_numsections" type="number" min="1" max="' + newSections + '" value="1"></div>');
595                     modalBody.find('label').html(strNumberSections);
596                     ModalFactory.create({
597                         title: modalTitle,
598                         type: ModalFactory.types.SAVE_CANCEL,
599                         body: modalBody.html()
600                     }, trigger)
601                     .done(function(modal) {
602                         var numSections = $(modal.getBody()).find('#add_section_numsections'),
603                         addSections = function() {
604                             // Check if value of the "Number of sections" is a valid positive integer and redirect
605                             // to adding a section script.
606                             if ('' + parseInt(numSections.val()) === numSections.val() && parseInt(numSections.val()) >= 1) {
607                                 document.location = trigger.attr('href') + '&numsections=' + parseInt(numSections.val());
608                             }
609                         };
610                         modal.setSaveButtonText(modalTitle);
611                         modal.getRoot().on(ModalEvents.shown, function() {
612                             // When modal is shown focus and select the input and add a listener to keypress of "Enter".
613                             numSections.focus().select().on('keydown', function(e) {
614                                 if (e.keyCode === KeyCodes.enter) {
615                                     addSections();
616                                 }
617                             });
618                         });
619                         modal.getRoot().on(ModalEvents.save, function(e) {
620                             // When modal "Add" button is pressed.
621                             e.preventDefault();
622                             addSections();
623                         });
624                     });
625                 });
626             },
628             /**
629              * Replaces a section action menu item with another one (for example Show->Hide, Set marker->Remove marker)
630              *
631              * This method can be used by course formats in their listener to the coursesectionedited event
632              *
633              * @param {JQuery} sectionelement
634              * @param {String} selector CSS selector inside the section element, for example "a[data-action=show]"
635              * @param {String} image new image name ("i/show", "i/hide", etc.)
636              * @param {String} stringname new string for the action menu item
637              * @param {String} stringcomponent
638              * @param {String} titlestr string for "title" attribute (if different from stringname)
639              * @param {String} titlecomponent
640              * @param {String} newaction new value for data-action attribute of the link
641              */
642             replaceSectionActionItem: function(sectionelement, selector, image, stringname,
643                                                     stringcomponent, titlestr, titlecomponent, newaction) {
644                 var actionitem = sectionelement.find(SELECTOR.SECTIONACTIONMENU + ' ' + selector);
645                 replaceActionItem(actionitem, image, stringname, stringcomponent, titlestr, titlecomponent, newaction);
646             }
647         };
648     });