e1af1d1f76a8362c33ed5aa9ec5527de16626e7f
[moodle.git] / mod / quiz / yui / build / moodle-mod_quiz-toolboxes / moodle-mod_quiz-toolboxes-debug.js
1 YUI.add('moodle-mod_quiz-toolboxes', function (Y, NAME) {
3 /* eslint-disable no-unused-vars */
4 /**
5  * Resource and activity toolbox class.
6  *
7  * This class is responsible for managing AJAX interactions with activities and resources
8  * when viewing a course in editing mode.
9  *
10  * @module moodle-course-toolboxes
11  * @namespace M.course.toolboxes
12  */
14 // The CSS classes we use.
15 var CSS = {
16         ACTIVITYINSTANCE: 'activityinstance',
17         AVAILABILITYINFODIV: 'div.availabilityinfo',
18         CONTENTWITHOUTLINK: 'contentwithoutlink',
19         CONDITIONALHIDDEN: 'conditionalhidden',
20         DIMCLASS: 'dimmed',
21         DIMMEDTEXT: 'dimmed_text',
22         EDITINSTRUCTIONS: 'editinstructions',
23         EDITINGMAXMARK: 'editor_displayed',
24         HIDE: 'hide',
25         JOIN: 'page_join',
26         MODINDENTCOUNT: 'mod-indent-',
27         MODINDENTHUGE: 'mod-indent-huge',
28         PAGE: 'page',
29         SECTIONHIDDENCLASS: 'hidden',
30         SECTIONIDPREFIX: 'section-',
31         SELECTMULTIPLE: 'select-multiple',
32         SLOT: 'slot',
33         SHOW: 'editing_show',
34         TITLEEDITOR: 'titleeditor'
35     },
36     // The CSS selectors we use.
37     SELECTOR = {
38         ACTIONAREA: '.actions',
39         ACTIONLINKTEXT: '.actionlinktext',
40         ACTIVITYACTION: 'a.cm-edit-action[data-action], a.editing_maxmark, a.editing_section, input.shuffle_questions',
41         ACTIVITYFORM: 'span.instancemaxmarkcontainer form',
42         ACTIVITYINSTANCE: '.' + CSS.ACTIVITYINSTANCE,
43         SECTIONINSTANCE: '.sectioninstance',
44         ACTIVITYLI: 'li.activity, li.section',
45         ACTIVITYMAXMARK: 'input[name=maxmark]',
46         COMMANDSPAN: '.commands',
47         CONTENTAFTERLINK: 'div.contentafterlink',
48         CONTENTWITHOUTLINK: 'div.contentwithoutlink',
49         DELETESECTIONICON: 'a.editing_delete .icon',
50         DESELECTALL: '#questiondeselectall',
51         EDITMAXMARK: 'a.editing_maxmark',
52         EDITSECTION: 'a.editing_section',
53         EDITSECTIONICON: 'a.editing_section .icon',
54         EDITSHUFFLEQUESTIONSACTION: 'input.cm-edit-action[data-action]',
55         EDITSHUFFLEAREA: '.instanceshufflequestions .shuffle-progress',
56         HIDE: 'a.editing_hide',
57         HIGHLIGHT: 'a.editing_highlight',
58         INSTANCENAME: 'span.instancename',
59         INSTANCEMAXMARK: 'span.instancemaxmark',
60         INSTANCESECTION: 'span.instancesection',
61         INSTANCESECTIONAREA: 'div.section-heading',
62         MODINDENTDIV: '.mod-indent',
63         MODINDENTOUTER: '.mod-indent-outer',
64         NUMQUESTIONS: '.numberofquestions',
65         PAGECONTENT: 'div#page-content',
66         PAGELI: 'li.page',
67         SECTIONUL: 'ul.section',
68         SECTIONFORM: '.instancesectioncontainer form',
69         SECTIONINPUT: 'input[name=section]',
70         SELECTMULTIPLEBUTTON: '#selectmultiplecommand',
71         SELECTMULTIPLECANCELBUTTON: '#selectmultiplecancelcommand',
72         SELECTMULTIPLECHECKBOX: '.select-multiple-checkbox',
73         SELECTMULTIPLEDELETEBUTTON: '#selectmultipledeletecommand',
74         SELECTALL: '#questionselectall',
75         SHOW: 'a.' + CSS.SHOW,
76         SLOTLI: 'li.slot',
77         SUMMARKS: '.mod_quiz_summarks'
78     },
79     BODY = Y.one(document.body);
81 // Setup the basic namespace.
82 M.mod_quiz = M.mod_quiz || {};
84 /**
85  * The toolbox class is a generic class which should never be directly
86  * instantiated. Please extend it instead.
87  *
88  * @class toolbox
89  * @constructor
90  * @protected
91  * @extends Base
92  */
93 var TOOLBOX = function() {
94     TOOLBOX.superclass.constructor.apply(this, arguments);
95 };
97 Y.extend(TOOLBOX, Y.Base, {
98     /**
99      * Send a request using the REST API
100      *
101      * @method send_request
102      * @param {Object} data The data to submit with the AJAX request
103      * @param {Node} [statusspinner] A statusspinner which may contain a section loader
104      * @param {Function} success_callback The callback to use on success
105      * @param {Object} [optionalconfig] Any additional configuration to submit
106      * @chainable
107      */
108     send_request: function(data, statusspinner, success_callback, optionalconfig) {
109         // Default data structure
110         if (!data) {
111             data = {};
112         }
114         // Handle any variables which we must pass back through to
115         var pageparams = this.get('config').pageparams,
116             varname;
117         for (varname in pageparams) {
118             data[varname] = pageparams[varname];
119         }
121         data.sesskey = M.cfg.sesskey;
122         data.courseid = this.get('courseid');
123         data.quizid = this.get('quizid');
125         var uri = M.cfg.wwwroot + this.get('ajaxurl');
127         // Define the configuration to send with the request
128         var responsetext = [];
129         var config = {
130             method: 'POST',
131             data: data,
132             on: {
133                 success: function(tid, response) {
134                     try {
135                         responsetext = Y.JSON.parse(response.responseText);
136                         if (responsetext.error) {
137                             new M.core.ajaxException(responsetext);
138                         }
139                     } catch (e) {
140                         // Ignore.
141                     }
143                     // Run the callback if we have one.
144                     if (responsetext.hasOwnProperty('newsummarks')) {
145                         Y.one(SELECTOR.SUMMARKS).setHTML(responsetext.newsummarks);
146                     }
147                     if (responsetext.hasOwnProperty('newnumquestions')) {
148                         Y.one(SELECTOR.NUMQUESTIONS).setHTML(
149                                 M.util.get_string('numquestionsx', 'quiz', responsetext.newnumquestions)
150                             );
151                     }
152                     if (success_callback) {
153                         Y.bind(success_callback, this, responsetext)();
154                     }
156                     if (statusspinner) {
157                         window.setTimeout(function() {
158                             statusspinner.hide();
159                         }, 400);
160                     }
161                 },
162                 failure: function(tid, response) {
163                     if (statusspinner) {
164                         statusspinner.hide();
165                     }
166                     new M.core.ajaxException(response);
167                 }
168             },
169             context: this
170         };
172         // Apply optional config
173         if (optionalconfig) {
174             for (varname in optionalconfig) {
175                 config[varname] = optionalconfig[varname];
176             }
177         }
179         if (statusspinner) {
180             statusspinner.show();
181         }
183         // Send the request
184         Y.io(uri, config);
185         return this;
186     }
187 },
189     NAME: 'mod_quiz-toolbox',
190     ATTRS: {
191         /**
192          * The ID of the Moodle Course being edited.
193          *
194          * @attribute courseid
195          * @default 0
196          * @type Number
197          */
198         courseid: {
199             'value': 0
200         },
202         /**
203          * The Moodle course format.
204          *
205          * @attribute format
206          * @default 'topics'
207          * @type String
208          */
209         quizid: {
210             'value': 0
211         },
212         /**
213          * The URL to use when submitting requests.
214          * @attribute ajaxurl
215          * @default null
216          * @type String
217          */
218         ajaxurl: {
219             'value': null
220         },
221         /**
222          * Any additional configuration passed when creating the instance.
223          *
224          * @attribute config
225          * @default {}
226          * @type Object
227          */
228         config: {
229             'value': {}
230         }
231     }
233 );
234 /* global TOOLBOX, BODY, SELECTOR */
236 /**
237  * Resource and activity toolbox class.
238  *
239  * This class is responsible for managing AJAX interactions with activities and resources
240  * when viewing a quiz in editing mode.
241  *
242  * @module mod_quiz-resource-toolbox
243  * @namespace M.mod_quiz.resource_toolbox
244  */
246 /**
247  * Resource and activity toolbox class.
248  *
249  * This is a class extending TOOLBOX containing code specific to resources
250  *
251  * This class is responsible for managing AJAX interactions with activities and resources
252  * when viewing a quiz in editing mode.
253  *
254  * @class resources
255  * @constructor
256  * @extends M.course.toolboxes.toolbox
257  */
258 var RESOURCETOOLBOX = function() {
259     RESOURCETOOLBOX.superclass.constructor.apply(this, arguments);
260 };
262 Y.extend(RESOURCETOOLBOX, TOOLBOX, {
263     /**
264      * An Array of events added when editing a max mark field.
265      * These should all be detached when editing is complete.
266      *
267      * @property editmaxmarkevents
268      * @protected
269      * @type Array
270      * @protected
271      */
272     editmaxmarkevents: [],
274     /**
275      *
276      */
277     NODE_PAGE: 1,
278     NODE_SLOT: 2,
279     NODE_JOIN: 3,
281     /**
282      * Initialize the resource toolbox
283      *
284      * For each activity the commands are updated and a reference to the activity is attached.
285      * This way it doesn't matter where the commands are going to called from they have a reference to the
286      * activity that they relate to.
287      * This is essential as some of the actions are displayed in an actionmenu which removes them from the
288      * page flow.
289      *
290      * This function also creates a single event delegate to manage all AJAX actions for all activities on
291      * the page.
292      *
293      * @method initializer
294      * @protected
295      */
296     initializer: function() {
297         M.mod_quiz.quizbase.register_module(this);
298         Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this);
299         Y.delegate('click', this.handle_data_action, BODY, SELECTOR.DEPENDENCY_LINK, this);
300         this.initialise_select_multiple();
301     },
303     /**
304      * Initialize the select multiple options
305      *
306      * Add actions to the buttons that enable multiple slots to be selected and managed at once.
307      *
308      * @method initialise_select_multiple
309      * @protected
310      */
311     initialise_select_multiple: function() {
312         // Click select multiple button to show the select all options.
313         Y.one(SELECTOR.SELECTMULTIPLEBUTTON).on('click', function(e) {
314             e.preventDefault();
315             Y.one('body').addClass(CSS.SELECTMULTIPLE);
316         });
318         // Click cancel button to show the select all options.
319         Y.one(SELECTOR.SELECTMULTIPLECANCELBUTTON).on('click', function(e) {
320             e.preventDefault();
321             Y.one('body').removeClass(CSS.SELECTMULTIPLE);
322         });
324         // Click select all link to check all the checkboxes.
325         Y.one(SELECTOR.SELECTALL).on('click', function(e) {
326             e.preventDefault();
327             Y.all(SELECTOR.SELECTMULTIPLECHECKBOX).set('checked', 'checked');
328         });
330         // Click deselect all link to show the select all checkboxes.
331         Y.one(SELECTOR.DESELECTALL).on('click', function(e) {
332             e.preventDefault();
333             Y.all(SELECTOR.SELECTMULTIPLECHECKBOX).set('checked', '');
334         });
336         // Disable delete multiple button by default.
337         Y.one(SELECTOR.SELECTMULTIPLEDELETEBUTTON).setAttribute('disabled', 'disabled');
339         // Assign the delete method to the delete multiple button.
340         Y.delegate('click', this.delete_multiple_with_confirmation, BODY, SELECTOR.SELECTMULTIPLEDELETEBUTTON, this);
342         // Enable the delete all button only when at least one slot is selected.
343         Y.delegate('click', this.toggle_select_all_buttons_enabled, BODY, SELECTOR.SELECTMULTIPLECHECKBOX, this);
344         Y.delegate('click', this.toggle_select_all_buttons_enabled, BODY, SELECTOR.SELECTALL, this);
345         Y.delegate('click', this.toggle_select_all_buttons_enabled, BODY, SELECTOR.DESELECTALL, this);
346     },
348     /**
349      * Handles the delegation event. When this is fired someone has triggered an action.
350      *
351      * Note not all actions will result in an AJAX enhancement.
352      *
353      * @protected
354      * @method handle_data_action
355      * @param {EventFacade} ev The event that was triggered.
356      * @returns {boolean}
357      */
358     handle_data_action: function(ev) {
359         // We need to get the anchor element that triggered this event.
360         var node = ev.target;
361         if (!node.test('a')) {
362             node = node.ancestor(SELECTOR.ACTIVITYACTION);
363         }
365         // From the anchor we can get both the activity (added during initialisation) and the action being
366         // performed (added by the UI as a data attribute).
367         var action = node.getData('action'),
368             activity = node.ancestor(SELECTOR.ACTIVITYLI);
370         if (!node.test('a') || !action || !activity) {
371             // It wasn't a valid action node.
372             return;
373         }
375         // Switch based upon the action and do the desired thing.
376         switch (action) {
377             case 'editmaxmark':
378                 // The user wishes to edit the maxmark of the resource.
379                 this.edit_maxmark(ev, node, activity, action);
380                 break;
381             case 'delete':
382                 // The user is deleting the activity.
383                 this.delete_with_confirmation(ev, node, activity, action);
384                 break;
385             case 'addpagebreak':
386             case 'removepagebreak':
387                 // The user is adding or removing a page break.
388                 this.update_page_break(ev, node, activity, action);
389                 break;
390             case 'adddependency':
391             case 'removedependency':
392                 // The user is adding or removing a dependency between questions.
393                 this.update_dependency(ev, node, activity, action);
394                 break;
395             default:
396                 // Nothing to do here!
397                 break;
398         }
399     },
401     /**
402      * Add a loading icon to the specified activity.
403      *
404      * The icon is added within the action area.
405      *
406      * @method add_spinner
407      * @param {Node} activity The activity to add a loading icon to
408      * @return {Node|null} The newly created icon, or null if the action area was not found.
409      */
410     add_spinner: function(activity) {
411         var actionarea = activity.one(SELECTOR.ACTIONAREA);
412         if (actionarea) {
413             return M.util.add_spinner(Y, actionarea);
414         }
415         return null;
416     },
418     /**
419      * If a select multiple checkbox is checked enable the buttons in the select multiple
420      * toolbar otherwise disable it.
421      *
422      * @method toggle_select_all_buttons_enabled
423      */
424     toggle_select_all_buttons_enabled: function() {
425         var checked = Y.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked');
426         var deletebutton = Y.one(SELECTOR.SELECTMULTIPLEDELETEBUTTON);
427         if (checked && !checked.isEmpty()) {
428             deletebutton.removeAttribute('disabled');
429         } else {
430             deletebutton.setAttribute('disabled', 'disabled');
431         }
432     },
434     /**
435      * Deletes the given activity or resource after confirmation.
436      *
437      * @protected
438      * @method delete_with_confirmation
439      * @param {EventFacade} ev The event that was fired.
440      * @param {Node} button The button that triggered this action.
441      * @param {Node} activity The activity node that this action will be performed on.
442      * @chainable
443      */
444     delete_with_confirmation: function(ev, button, activity) {
445         // Prevent the default button action.
446         ev.preventDefault();
448         // Get the element we're working on.
449         var element = activity,
450             // Create confirm string (different if element has or does not have name)
451             confirmstring = '',
452             qtypename = M.util.get_string('pluginname',
453                         'qtype_' + element.getAttribute('class').match(/qtype_([^\s]*)/)[1]);
454         confirmstring = M.util.get_string('confirmremovequestion', 'quiz', qtypename);
456         // Create the confirmation dialogue.
457         var confirm = new M.core.confirm({
458             question: confirmstring,
459             modal: true
460         });
462         // If it is confirmed.
463         confirm.on('complete-yes', function() {
464             var spinner = this.add_spinner(element);
465             var data = {
466                 'class': 'resource',
467                 'action': 'DELETE',
468                 'id': Y.Moodle.mod_quiz.util.slot.getId(element)
469             };
470             this.send_request(data, spinner, function(response) {
471                 if (response.deleted) {
472                     // Actually remove the element.
473                     Y.Moodle.mod_quiz.util.slot.remove(element);
474                     this.reorganise_edit_page();
475                     if (M.core.actionmenu && M.core.actionmenu.instance) {
476                         M.core.actionmenu.instance.hideMenu(ev);
477                     }
478                 }
479             });
481         }, this);
482     },
484     /**
485      * Deletes the given activities or resources after confirmation.
486      *
487      * @protected
488      * @method delete_multiple_with_confirmation
489      * @param {EventFacade} ev The event that was fired.
490      * @chainable
491      */
492     delete_multiple_with_confirmation: function(ev) {
493         ev.preventDefault();
495         var ids = '';
496         var slots = [];
497         Y.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked').each(function(node) {
498             var slot = Y.Moodle.mod_quiz.util.slot.getSlotFromComponent(node);
499             ids += ids === '' ? '' : ',';
500             ids += Y.Moodle.mod_quiz.util.slot.getId(slot);
501             slots.push(slot);
502         });
503         var element = Y.one('div.mod-quiz-edit-content');
505         // Do nothing if no slots are selected.
506         if (!slots || !slots.length) {
507             return;
508         }
510         // Create the confirmation dialogue.
511         var confirm = new M.core.confirm({
512             question: M.util.get_string('areyousureremoveselected', 'quiz'),
513             modal: true
514         });
516         // If it is confirmed.
517         confirm.on('complete-yes', function() {
518             var spinner = this.add_spinner(element);
519             var data = {
520                 'class': 'resource',
521                 field: 'deletemultiple',
522                 ids: ids
523             };
524             // Delete items on server.
525             this.send_request(data, spinner, function(response) {
526                 // Delete locally if deleted on server.
527                 if (response.deleted) {
528                     // Actually remove the element.
529                     Y.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked').each(function(node) {
530                         Y.Moodle.mod_quiz.util.slot.remove(node.ancestor('li.activity'));
531                     });
532                     // Update the page numbers and sections.
533                     this.reorganise_edit_page();
535                     // Remove the select multiple options.
536                     Y.one('body').removeClass(CSS.SELECTMULTIPLE);
537                 }
538             });
540         }, this);
541     },
543     /**
544      * Edit the maxmark for the resource
545      *
546      * @protected
547      * @method edit_maxmark
548      * @param {EventFacade} ev The event that was fired.
549      * @param {Node} button The button that triggered this action.
550      * @param {Node} activity The activity node that this action will be performed on.
551      * @param {String} action The action that has been requested.
552      * @return Boolean
553      */
554     edit_maxmark: function(ev, button, activity) {
555         // Get the element we're working on
556         var instancemaxmark = activity.one(SELECTOR.INSTANCEMAXMARK),
557             instance = activity.one(SELECTOR.ACTIVITYINSTANCE),
558             currentmaxmark = instancemaxmark.get('firstChild'),
559             oldmaxmark = currentmaxmark.get('data'),
560             maxmarktext = oldmaxmark,
561             thisevent,
562             anchor = instancemaxmark, // Grab the anchor so that we can swap it with the edit form.
563             data = {
564                 'class': 'resource',
565                 'field': 'getmaxmark',
566                 'id': Y.Moodle.mod_quiz.util.slot.getId(activity)
567             };
569         // Prevent the default actions.
570         ev.preventDefault();
572         this.send_request(data, null, function(response) {
573             if (M.core.actionmenu && M.core.actionmenu.instance) {
574                 M.core.actionmenu.instance.hideMenu(ev);
575             }
577             // Try to retrieve the existing string from the server.
578             if (response.instancemaxmark) {
579                 maxmarktext = response.instancemaxmark;
580             }
582             // Create the editor and submit button.
583             var editform = Y.Node.create('<form action="#" />');
584             var editinstructions = Y.Node.create('<span class="' + CSS.EDITINSTRUCTIONS + '" id="id_editinstructions" />')
585                 .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
586             var editor = Y.Node.create('<input name="maxmark" type="text" class="' + CSS.TITLEEDITOR + '" />').setAttrs({
587                 'value': maxmarktext,
588                 'autocomplete': 'off',
589                 'aria-describedby': 'id_editinstructions',
590                 'maxLength': '12',
591                 'size': parseInt(this.get('config').questiondecimalpoints, 10) + 2
592             });
594             // Clear the existing content and put the editor in.
595             editform.appendChild(editor);
596             editform.setData('anchor', anchor);
597             instance.insert(editinstructions, 'before');
598             anchor.replace(editform);
600             // We hide various components whilst editing:
601             activity.addClass(CSS.EDITINGMAXMARK);
603             // Focus and select the editor text.
604             editor.focus().select();
606             // Cancel the edit if we lose focus or the escape key is pressed.
607             thisevent = editor.on('blur', this.edit_maxmark_cancel, this, activity, false);
608             this.editmaxmarkevents.push(thisevent);
609             thisevent = editor.on('key', this.edit_maxmark_cancel, 'esc', this, activity, true);
610             this.editmaxmarkevents.push(thisevent);
612             // Handle form submission.
613             thisevent = editform.on('submit', this.edit_maxmark_submit, this, activity, oldmaxmark);
614             this.editmaxmarkevents.push(thisevent);
615         });
616     },
618     /**
619      * Handles the submit event when editing the activity or resources maxmark.
620      *
621      * @protected
622      * @method edit_maxmark_submit
623      * @param {EventFacade} ev The event that triggered this.
624      * @param {Node} activity The activity whose maxmark we are altering.
625      * @param {String} originalmaxmark The original maxmark the activity or resource had.
626      */
627     edit_maxmark_submit: function(ev, activity, originalmaxmark) {
628         // We don't actually want to submit anything.
629         ev.preventDefault();
630         var newmaxmark = Y.Lang.trim(activity.one(SELECTOR.ACTIVITYFORM + ' ' + SELECTOR.ACTIVITYMAXMARK).get('value'));
631         var spinner = this.add_spinner(activity);
632         this.edit_maxmark_clear(activity);
633         activity.one(SELECTOR.INSTANCEMAXMARK).setContent(newmaxmark);
634         if (newmaxmark !== null && newmaxmark !== "" && newmaxmark !== originalmaxmark) {
635             var data = {
636                 'class': 'resource',
637                 'field': 'updatemaxmark',
638                 'maxmark': newmaxmark,
639                 'id': Y.Moodle.mod_quiz.util.slot.getId(activity)
640             };
641             this.send_request(data, spinner, function(response) {
642                 if (response.instancemaxmark) {
643                     activity.one(SELECTOR.INSTANCEMAXMARK).setContent(response.instancemaxmark);
644                 }
645             });
646         }
647     },
649     /**
650      * Handles the cancel event when editing the activity or resources maxmark.
651      *
652      * @protected
653      * @method edit_maxmark_cancel
654      * @param {EventFacade} ev The event that triggered this.
655      * @param {Node} activity The activity whose maxmark we are altering.
656      * @param {Boolean} preventdefault If true we should prevent the default action from occuring.
657      */
658     edit_maxmark_cancel: function(ev, activity, preventdefault) {
659         if (preventdefault) {
660             ev.preventDefault();
661         }
662         this.edit_maxmark_clear(activity);
663     },
665     /**
666      * Handles clearing the editing UI and returning things to the original state they were in.
667      *
668      * @protected
669      * @method edit_maxmark_clear
670      * @param {Node} activity  The activity whose maxmark we were altering.
671      */
672     edit_maxmark_clear: function(activity) {
673         // Detach all listen events to prevent duplicate triggers
674         new Y.EventHandle(this.editmaxmarkevents).detach();
676         var editform = activity.one(SELECTOR.ACTIVITYFORM),
677             instructions = activity.one('#id_editinstructions');
678         if (editform) {
679             editform.replace(editform.getData('anchor'));
680         }
681         if (instructions) {
682             instructions.remove();
683         }
685         // Remove the editing class again to revert the display.
686         activity.removeClass(CSS.EDITINGMAXMARK);
688         // Refocus the link which was clicked originally so the user can continue using keyboard nav.
689         Y.later(100, this, function() {
690             activity.one(SELECTOR.EDITMAXMARK).focus();
691         });
693         // TODO MDL-50768 This hack is to keep Behat happy until they release a version of
694         // MinkSelenium2Driver that fixes
695         // https://github.com/Behat/MinkSelenium2Driver/issues/80.
696         if (!Y.one('input[name=maxmark')) {
697             Y.one('body').append('<input type="text" name="maxmark" style="display: none">');
698         }
699     },
701     /**
702      * Joins or separates the given slot with the page of the previous slot. Reorders the pages of
703      * the other slots
704      *
705      * @protected
706      * @method update_page_break
707      * @param {EventFacade} ev The event that was fired.
708      * @param {Node} button The button that triggered this action.
709      * @param {Node} activity The activity node that this action will be performed on.
710      * @param {String} action The action, addpagebreak or removepagebreak.
711      * @chainable
712      */
713     update_page_break: function(ev, button, activity, action) {
714         // Prevent the default button action
715         ev.preventDefault();
717         var nextactivity = activity.next('li.activity.slot');
718         var spinner = this.add_spinner(nextactivity);
719         var value = action === 'removepagebreak' ? 1 : 2;
721         var data = {
722             'class': 'resource',
723             'field': 'updatepagebreak',
724             'id':    Y.Moodle.mod_quiz.util.slot.getId(nextactivity),
725             'value': value
726         };
728         this.send_request(data, spinner, function(response) {
729             if (response.slots) {
730                 if (action === 'addpagebreak') {
731                     Y.Moodle.mod_quiz.util.page.add(activity);
732                 } else {
733                     var page = activity.next(Y.Moodle.mod_quiz.util.page.SELECTORS.PAGE);
734                     Y.Moodle.mod_quiz.util.page.remove(page, true);
735                 }
736                 this.reorganise_edit_page();
737             }
738         });
740         return this;
741     },
743     /**
744      * Updates a slot to either require the question in the previous slot to
745      * have been answered, or not,
746      *
747      * @protected
748      * @method update_page_break
749      * @param {EventFacade} ev The event that was fired.
750      * @param {Node} button The button that triggered this action.
751      * @param {Node} activity The activity node that this action will be performed on.
752      * @param {String} action The action, adddependency or removedependency.
753      * @chainable
754      */
755     update_dependency: function(ev, button, activity, action) {
756         // Prevent the default button action.
757         ev.preventDefault();
758         var spinner = this.add_spinner(activity);
760         var data = {
761             'class': 'resource',
762             'field': 'updatedependency',
763             'id':    Y.Moodle.mod_quiz.util.slot.getId(activity),
764             'value': action === 'adddependency' ? 1 : 0
765         };
767         this.send_request(data, spinner, function(response) {
768             if (response.hasOwnProperty('requireprevious')) {
769                 Y.Moodle.mod_quiz.util.slot.updateDependencyIcon(activity, response.requireprevious);
770             }
771         });
773         return this;
774     },
776     /**
777      * Reorganise the UI after every edit action.
778      *
779      * @protected
780      * @method reorganise_edit_page
781      */
782     reorganise_edit_page: function() {
783         Y.Moodle.mod_quiz.util.slot.reorderSlots();
784         Y.Moodle.mod_quiz.util.slot.reorderPageBreaks();
785         Y.Moodle.mod_quiz.util.page.reorderPages();
786         Y.Moodle.mod_quiz.util.slot.updateOneSlotSections();
787         Y.Moodle.mod_quiz.util.slot.updateAllDependencyIcons();
788     },
790     NAME: 'mod_quiz-resource-toolbox',
791     ATTRS: {
792         courseid: {
793             'value': 0
794         },
795         quizid: {
796             'value': 0
797         }
798     }
800 });
802 M.mod_quiz.resource_toolbox = null;
803 M.mod_quiz.init_resource_toolbox = function(config) {
804     M.mod_quiz.resource_toolbox = new RESOURCETOOLBOX(config);
805     return M.mod_quiz.resource_toolbox;
806 };
807 /* global TOOLBOX, BODY, SELECTOR */
809 /**
810  * Section toolbox class.
811  *
812  * This class is responsible for managing AJAX interactions with sections
813  * when adding, editing, removing section headings.
814  *
815  * @module moodle-mod_quiz-toolboxes
816  * @namespace M.mod_quiz.toolboxes
817  */
819 /**
820  * Section toolbox class.
821  *
822  * This class is responsible for managing AJAX interactions with sections
823  * when adding, editing, removing section headings when editing a quiz.
824  *
825  * @class section
826  * @constructor
827  * @extends M.mod_quiz.toolboxes.toolbox
828  */
829 var SECTIONTOOLBOX = function() {
830     SECTIONTOOLBOX.superclass.constructor.apply(this, arguments);
831 };
833 Y.extend(SECTIONTOOLBOX, TOOLBOX, {
834     /**
835      * An Array of events added when editing a max mark field.
836      * These should all be detached when editing is complete.
837      *
838      * @property editsectionevents
839      * @protected
840      * @type Array
841      * @protected
842      */
843     editsectionevents: [],
845     /**
846      * Initialize the section toolboxes module.
847      *
848      * Updates all span.commands with relevant handlers and other required changes.
849      *
850      * @method initializer
851      * @protected
852      */
853     initializer: function() {
854         M.mod_quiz.quizbase.register_module(this);
856         BODY.delegate('key', this.handle_data_action, 'down:enter', SELECTOR.ACTIVITYACTION, this);
857         Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this);
858         Y.delegate('change', this.handle_data_action, BODY, SELECTOR.EDITSHUFFLEQUESTIONSACTION, this);
859     },
861     /**
862      * Handles the delegation event. When this is fired someone has triggered an action.
863      *
864      * Note not all actions will result in an AJAX enhancement.
865      *
866      * @protected
867      * @method handle_data_action
868      * @param {EventFacade} ev The event that was triggered.
869      * @returns {boolean}
870      */
871     handle_data_action: function(ev) {
872         // We need to get the anchor element that triggered this event.
873         var node = ev.target;
874         if (!node.test('a') && !node.test('input[data-action]')) {
875             node = node.ancestor(SELECTOR.ACTIVITYACTION);
876         }
878         // From the anchor we can get both the activity (added during initialisation) and the action being
879         // performed (added by the UI as a data attribute).
880         var action = node.getData('action'),
881             activity = node.ancestor(SELECTOR.ACTIVITYLI);
883         if ((!node.test('a') && !node.test('input[data-action]')) || !action || !activity) {
884             // It wasn't a valid action node.
885             return;
886         }
888         // Switch based upon the action and do the desired thing.
889         switch (action) {
890             case 'edit_section_title':
891                 // The user wishes to edit the section headings.
892                 this.edit_section_title(ev, node, activity, action);
893                 break;
894             case 'shuffle_questions':
895                 // The user wishes to edit the shuffle questions of the section (resource).
896                 this.edit_shuffle_questions(ev, node, activity, action);
897                 break;
898             case 'deletesection':
899                 // The user is deleting the activity.
900                 this.delete_section_with_confirmation(ev, node, activity, action);
901                 break;
902             default:
903                 // Nothing to do here!
904                 break;
905         }
906     },
908     /**
909      * Deletes the given section heading after confirmation.
910      *
911      * @protected
912      * @method delete_section_with_confirmation
913      * @param {EventFacade} ev The event that was fired.
914      * @param {Node} button The button that triggered this action.
915      * @param {Node} activity The activity node that this action will be performed on.
916      * @chainable
917      */
918     delete_section_with_confirmation: function(ev, button, activity) {
919         // Prevent the default button action.
920         ev.preventDefault();
922         // Create the confirmation dialogue.
923         var confirm = new M.core.confirm({
924             question: M.util.get_string('confirmremovesectionheading', 'quiz', activity.get('aria-label')),
925             modal: true
926         });
928         // If it is confirmed.
929         confirm.on('complete-yes', function() {
931             var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.ACTIONAREA));
932             var data = {
933                 'class':  'section',
934                 'action': 'DELETE',
935                 'id':     activity.get('id').replace('section-', '')
936             };
937             this.send_request(data, spinner, function(response) {
938                 if (response.deleted) {
939                     window.location.reload(true);
940                 }
941             });
943         }, this);
944     },
946     /**
947      * Edit the edit section title for the section
948      *
949      * @protected
950      * @method edit_section_title
951      * @param {EventFacade} ev The event that was fired.
952      * @param {Node} button The button that triggered this action.
953      * @param {Node} activity The activity node that this action will be performed on.
954      * @param {String} action The action that has been requested.
955      * @return Boolean
956      */
957     edit_section_title: function(ev, button, activity) {
958         // Get the element we're working on
959         var activityid = activity.get('id').replace('section-', ''),
960             instancesection = activity.one(SELECTOR.INSTANCESECTION),
961             thisevent,
962             anchor = instancesection, // Grab the anchor so that we can swap it with the edit form.
963             data = {
964                 'class': 'section',
965                 'field': 'getsectiontitle',
966                 'id':    activityid
967             };
969         // Prevent the default actions.
970         ev.preventDefault();
972         this.send_request(data, null, function(response) {
973             // Try to retrieve the existing string from the server.
974             var oldtext = response.instancesection;
976             // Create the editor and submit button.
977             var editform = Y.Node.create('<form action="#" />');
978             var editinstructions = Y.Node.create('<span class="' + CSS.EDITINSTRUCTIONS + '" id="id_editinstructions" />')
979                 .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
980             var editor = Y.Node.create('<input name="section" type="text" />').setAttrs({
981                 'value': oldtext,
982                 'autocomplete': 'off',
983                 'aria-describedby': 'id_editinstructions',
984                 'maxLength': '255' // This is the maxlength in DB.
985             });
987             // Clear the existing content and put the editor in.
988             editform.appendChild(editor);
989             editform.setData('anchor', anchor);
990             instancesection.insert(editinstructions, 'before');
991             anchor.replace(editform);
993             // Focus and select the editor text.
994             editor.focus().select();
995             // Cancel the edit if we lose focus or the escape key is pressed.
996             thisevent = editor.on('blur', this.edit_section_title_cancel, this, activity, false);
997             this.editsectionevents.push(thisevent);
998             thisevent = editor.on('key', this.edit_section_title_cancel, 'esc', this, activity, true);
999             this.editsectionevents.push(thisevent);
1000             // Handle form submission.
1001             thisevent = editform.on('submit', this.edit_section_title_submit, this, activity, oldtext);
1002             this.editsectionevents.push(thisevent);
1003         });
1004     },
1006     /**
1007      * Handles the submit event when editing section heading.
1008      *
1009      * @protected
1010      * @method edit_section_title_submiy
1011      * @param {EventFacade} ev The event that triggered this.
1012      * @param {Node} activity The activity whose maxmark we are altering.
1013      * @param {String} oldtext The original maxmark the activity or resource had.
1014      */
1015     edit_section_title_submit: function(ev, activity, oldtext) {
1016          // We don't actually want to submit anything.
1017         ev.preventDefault();
1018         var newtext = Y.Lang.trim(activity.one(SELECTOR.SECTIONFORM + ' ' + SELECTOR.SECTIONINPUT).get('value'));
1019         var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.INSTANCESECTIONAREA));
1020         this.edit_section_title_clear(activity);
1021         if (newtext !== null && newtext !== oldtext) {
1022             activity.one(SELECTOR.INSTANCESECTION).setContent(newtext);
1023             var data = {
1024                 'class':      'section',
1025                 'field':      'updatesectiontitle',
1026                 'newheading': newtext,
1027                 'id':         activity.get('id').replace('section-', '')
1028             };
1029             this.send_request(data, spinner, function(response) {
1030                 if (response) {
1031                     activity.one(SELECTOR.INSTANCESECTION).setContent(response.instancesection);
1032                     activity.one(SELECTOR.EDITSECTIONICON).set('title',
1033                             M.util.get_string('sectionheadingedit', 'quiz', response.instancesection));
1034                     activity.one(SELECTOR.EDITSECTIONICON).set('alt',
1035                             M.util.get_string('sectionheadingedit', 'quiz', response.instancesection));
1036                     var deleteicon = activity.one(SELECTOR.DELETESECTIONICON);
1037                     if (deleteicon) {
1038                         deleteicon.set('title', M.util.get_string('sectionheadingremove', 'quiz', response.instancesection));
1039                         deleteicon.set('alt', M.util.get_string('sectionheadingremove', 'quiz', response.instancesection));
1040                     }
1041                 }
1042             });
1043         }
1044     },
1046     /**
1047      * Handles the cancel event when editing the activity or resources maxmark.
1048      *
1049      * @protected
1050      * @method edit_maxmark_cancel
1051      * @param {EventFacade} ev The event that triggered this.
1052      * @param {Node} activity The activity whose maxmark we are altering.
1053      * @param {Boolean} preventdefault If true we should prevent the default action from occuring.
1054      */
1055     edit_section_title_cancel: function(ev, activity, preventdefault) {
1056         if (preventdefault) {
1057             ev.preventDefault();
1058         }
1059         this.edit_section_title_clear(activity);
1060     },
1062     /**
1063      * Handles clearing the editing UI and returning things to the original state they were in.
1064      *
1065      * @protected
1066      * @method edit_maxmark_clear
1067      * @param {Node} activity  The activity whose maxmark we were altering.
1068      */
1069     edit_section_title_clear: function(activity) {
1070         // Detach all listen events to prevent duplicate triggers
1071         new Y.EventHandle(this.editsectionevents).detach();
1073         var editform = activity.one(SELECTOR.SECTIONFORM),
1074             instructions = activity.one('#id_editinstructions');
1075         if (editform) {
1076             editform.replace(editform.getData('anchor'));
1077         }
1078         if (instructions) {
1079             instructions.remove();
1080         }
1082         // Refocus the link which was clicked originally so the user can continue using keyboard nav.
1083         Y.later(100, this, function() {
1084             activity.one(SELECTOR.EDITSECTION).focus();
1085         });
1087         // This hack is to keep Behat happy until they release a version of
1088         // MinkSelenium2Driver that fixes
1089         // https://github.com/Behat/MinkSelenium2Driver/issues/80.
1090         if (!Y.one('input[name=section]')) {
1091             Y.one('body').append('<input type="text" name="section" style="display: none">');
1092         }
1093     },
1095     /**
1096      * Edit the edit shuffle questions for the section
1097      *
1098      * @protected
1099      * @method edit_shuffle_questions
1100      * @param {EventFacade} ev The event that was fired.
1101      * @param {Node} button The button that triggered this action.
1102      * @param {Node} activity The activity node that this action will be performed on.
1103      * @param {String} action The action that has been requested.
1104      * @return Boolean
1105      */
1106     edit_shuffle_questions: function(ev, button, activity) {
1107         var newvalue;
1108         if (activity.one(SELECTOR.EDITSHUFFLEQUESTIONSACTION).get('checked')) {
1109             newvalue = 1;
1110         } else {
1111             newvalue = 0;
1112         }
1114         // Get the element we're working on
1115         var data = {
1116             'class': 'section',
1117             'field': 'updateshufflequestions',
1118             'id': activity.get('id').replace('section-', ''),
1119             'newshuffle': newvalue
1120         };
1122         // Prevent the default actions.
1123         ev.preventDefault();
1125         // Send request.
1126         var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.EDITSHUFFLEAREA));
1127         this.send_request(data, spinner);
1128     }
1130 }, {
1131     NAME: 'mod_quiz-section-toolbox',
1132     ATTRS: {
1133         courseid: {
1134             'value': 0
1135         },
1136         quizid: {
1137             'value': 0
1138         }
1139     }
1140 });
1142 M.mod_quiz.init_section_toolbox = function(config) {
1143     return new SECTIONTOOLBOX(config);
1144 };
1147 }, '@VERSION@', {
1148     "requires": [
1149         "base",
1150         "node",
1151         "event",
1152         "event-key",
1153         "io",
1154         "moodle-mod_quiz-quizbase",
1155         "moodle-mod_quiz-util-slot",
1156         "moodle-core-notification-ajaxexception"
1157     ]
1158 });