MDL-40988 quiz: ability to break quizzes into sections
[moodle.git] / mod / quiz / yui / src / toolboxes / js / resource.js
blob843ad5c5e31cf8916f4e623aeb3177ef23df1cc4
1 /**
2  * Resource and activity toolbox class.
3  *
4  * This class is responsible for managing AJAX interactions with activities and resources
5  * when viewing a quiz in editing mode.
6  *
7  * @module mod_quiz-resource-toolbox
8  * @namespace M.mod_quiz.resource_toolbox
9  */
11 /**
12  * Resource and activity toolbox class.
13  *
14  * This is a class extending TOOLBOX containing code specific to resources
15  *
16  * This class is responsible for managing AJAX interactions with activities and resources
17  * when viewing a quiz in editing mode.
18  *
19  * @class resources
20  * @constructor
21  * @extends M.course.toolboxes.toolbox
22  */
23 var RESOURCETOOLBOX = function() {
24     RESOURCETOOLBOX.superclass.constructor.apply(this, arguments);
27 Y.extend(RESOURCETOOLBOX, TOOLBOX, {
28     /**
29      * An Array of events added when editing a max mark field.
30      * These should all be detached when editing is complete.
31      *
32      * @property editmaxmarkevents
33      * @protected
34      * @type Array
35      * @protected
36      */
37     editmaxmarkevents: [],
39     /**
40      *
41      */
42     NODE_PAGE: 1,
43     NODE_SLOT: 2,
44     NODE_JOIN: 3,
46     /**
47      * Initialize the resource toolbox
48      *
49      * For each activity the commands are updated and a reference to the activity is attached.
50      * This way it doesn't matter where the commands are going to called from they have a reference to the
51      * activity that they relate to.
52      * This is essential as some of the actions are displayed in an actionmenu which removes them from the
53      * page flow.
54      *
55      * This function also creates a single event delegate to manage all AJAX actions for all activities on
56      * the page.
57      *
58      * @method initializer
59      * @protected
60      */
61     initializer: function() {
62         M.mod_quiz.quizbase.register_module(this);
63         Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this);
64         Y.delegate('click', this.handle_data_action, BODY, SELECTOR.DEPENDENCY_LINK, this);
65     },
67     /**
68      * Handles the delegation event. When this is fired someone has triggered an action.
69      *
70      * Note not all actions will result in an AJAX enhancement.
71      *
72      * @protected
73      * @method handle_data_action
74      * @param {EventFacade} ev The event that was triggered.
75      * @returns {boolean}
76      */
77     handle_data_action: function(ev) {
78         // We need to get the anchor element that triggered this event.
79         var node = ev.target;
80         if (!node.test('a')) {
81             node = node.ancestor(SELECTOR.ACTIVITYACTION);
82         }
84         // From the anchor we can get both the activity (added during initialisation) and the action being
85         // performed (added by the UI as a data attribute).
86         var action = node.getData('action'),
87             activity = node.ancestor(SELECTOR.ACTIVITYLI);
89         if (!node.test('a') || !action || !activity) {
90             // It wasn't a valid action node.
91             return;
92         }
94         // Switch based upon the action and do the desired thing.
95         switch (action) {
96             case 'editmaxmark':
97                 // The user wishes to edit the maxmark of the resource.
98                 this.edit_maxmark(ev, node, activity, action);
99                 break;
100             case 'delete':
101                 // The user is deleting the activity.
102                 this.delete_with_confirmation(ev, node, activity, action);
103                 break;
104             case 'addpagebreak':
105             case 'removepagebreak':
106                 // The user is adding or removing a page break.
107                 this.update_page_break(ev, node, activity, action);
108                 break;
109             case 'adddependency':
110             case 'removedependency':
111                 // The user is adding or removing a dependency between questions.
112                 this.update_dependency(ev, node, activity, action);
113                 break;
114             default:
115                 // Nothing to do here!
116                 break;
117         }
118     },
120     /**
121      * Add a loading icon to the specified activity.
122      *
123      * The icon is added within the action area.
124      *
125      * @method add_spinner
126      * @param {Node} activity The activity to add a loading icon to
127      * @return {Node|null} The newly created icon, or null if the action area was not found.
128      */
129     add_spinner: function(activity) {
130         var actionarea = activity.one(SELECTOR.ACTIONAREA);
131         if (actionarea) {
132             return M.util.add_spinner(Y, actionarea);
133         }
134         return null;
135     },
137     /**
138      * Deletes the given activity or resource after confirmation.
139      *
140      * @protected
141      * @method delete_with_confirmation
142      * @param {EventFacade} ev The event that was fired.
143      * @param {Node} button The button that triggered this action.
144      * @param {Node} activity The activity node that this action will be performed on.
145      * @chainable
146      */
147     delete_with_confirmation: function(ev, button, activity) {
148         // Prevent the default button action.
149         ev.preventDefault();
151         // Get the element we're working on.
152         var element   = activity,
153             // Create confirm string (different if element has or does not have name)
154             confirmstring = '',
155             qtypename = M.util.get_string('pluginname',
156                         'qtype_' + element.getAttribute('class').match(/qtype_([^\s]*)/)[1]);
157         confirmstring = M.util.get_string('confirmremovequestion', 'quiz', qtypename);
159         // Create the confirmation dialogue.
160         var confirm = new M.core.confirm({
161             question: confirmstring,
162             modal: true
163         });
165         // If it is confirmed.
166         confirm.on('complete-yes', function() {
168             var spinner = this.add_spinner(element);
169             var data = {
170                 'class': 'resource',
171                 'action': 'DELETE',
172                 'id': Y.Moodle.mod_quiz.util.slot.getId(element)
173             };
174             this.send_request(data, spinner, function(response) {
175                 if (response.deleted) {
176                     // Actually remove the element.
177                     Y.Moodle.mod_quiz.util.slot.remove(element);
178                     this.reorganise_edit_page();
179                     if (M.core.actionmenu && M.core.actionmenu.instance) {
180                         M.core.actionmenu.instance.hideMenu();
181                     }
182                 }
183             });
185         }, this);
187         return this;
188     },
191     /**
192      * Edit the maxmark for the resource
193      *
194      * @protected
195      * @method edit_maxmark
196      * @param {EventFacade} ev The event that was fired.
197      * @param {Node} button The button that triggered this action.
198      * @param {Node} activity The activity node that this action will be performed on.
199      * @param {String} action The action that has been requested.
200      * @return Boolean
201      */
202     edit_maxmark : function(ev, button, activity) {
203         // Get the element we're working on
204         var instancemaxmark  = activity.one(SELECTOR.INSTANCEMAXMARK),
205             instance = activity.one(SELECTOR.ACTIVITYINSTANCE),
206             currentmaxmark = instancemaxmark.get('firstChild'),
207             oldmaxmark = currentmaxmark.get('data'),
208             maxmarktext = oldmaxmark,
209             thisevent,
210             anchor = instancemaxmark,// Grab the anchor so that we can swap it with the edit form.
211             data = {
212                 'class'   : 'resource',
213                 'field'   : 'getmaxmark',
214                 'id'      : Y.Moodle.mod_quiz.util.slot.getId(activity)
215             };
217         // Prevent the default actions.
218         ev.preventDefault();
220         this.send_request(data, null, function(response) {
221             if (M.core.actionmenu && M.core.actionmenu.instance) {
222                 M.core.actionmenu.instance.hideMenu();
223             }
225             // Try to retrieve the existing string from the server.
226             if (response.instancemaxmark) {
227                 maxmarktext = response.instancemaxmark;
228             }
230             // Create the editor and submit button.
231             var editform = Y.Node.create('<form action="#" />');
232             var editinstructions = Y.Node.create('<span class="' + CSS.EDITINSTRUCTIONS + '" id="id_editinstructions" />')
233                 .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
234             var editor = Y.Node.create('<input name="maxmark" type="text" class="' + CSS.TITLEEDITOR + '" />').setAttrs({
235                 'value' : maxmarktext,
236                 'autocomplete' : 'off',
237                 'aria-describedby' : 'id_editinstructions',
238                 'maxLength' : '12',
239                 'size' : parseInt(this.get('config').questiondecimalpoints, 10) + 2
240             });
242             // Clear the existing content and put the editor in.
243             editform.appendChild(editor);
244             editform.setData('anchor', anchor);
245             instance.insert(editinstructions, 'before');
246             anchor.replace(editform);
248             // Force the editing instruction to match the mod-indent position.
249             var padside = 'left';
250             if (window.right_to_left()) {
251                 padside = 'right';
252             }
254             // We hide various components whilst editing:
255             activity.addClass(CSS.EDITINGMAXMARK);
257             // Focus and select the editor text.
258             editor.focus().select();
260             // Cancel the edit if we lose focus or the escape key is pressed.
261             thisevent = editor.on('blur', this.edit_maxmark_cancel, this, activity, false);
262             this.editmaxmarkevents.push(thisevent);
263             thisevent = editor.on('key', this.edit_maxmark_cancel, 'esc', this, activity, true);
264             this.editmaxmarkevents.push(thisevent);
266             // Handle form submission.
267             thisevent = editform.on('submit', this.edit_maxmark_submit, this, activity, oldmaxmark);
268             this.editmaxmarkevents.push(thisevent);
269         });
270     },
272     /**
273      * Handles the submit event when editing the activity or resources maxmark.
274      *
275      * @protected
276      * @method edit_maxmark_submit
277      * @param {EventFacade} ev The event that triggered this.
278      * @param {Node} activity The activity whose maxmark we are altering.
279      * @param {String} originalmaxmark The original maxmark the activity or resource had.
280      */
281     edit_maxmark_submit : function(ev, activity, originalmaxmark) {
282         // We don't actually want to submit anything.
283         ev.preventDefault();
284         var newmaxmark = Y.Lang.trim(activity.one(SELECTOR.ACTIVITYFORM + ' ' + SELECTOR.ACTIVITYMAXMARK).get('value'));
285         var spinner = this.add_spinner(activity);
286         this.edit_maxmark_clear(activity);
287         activity.one(SELECTOR.INSTANCEMAXMARK).setContent(newmaxmark);
288         if (newmaxmark !== null && newmaxmark !== "" && newmaxmark !== originalmaxmark) {
289             var data = {
290                 'class'   : 'resource',
291                 'field'   : 'updatemaxmark',
292                 'maxmark'   : newmaxmark,
293                 'id'      : Y.Moodle.mod_quiz.util.slot.getId(activity)
294             };
295             this.send_request(data, spinner, function(response) {
296                 if (response.instancemaxmark) {
297                     activity.one(SELECTOR.INSTANCEMAXMARK).setContent(response.instancemaxmark);
298                 }
299             });
300         }
301     },
303     /**
304      * Handles the cancel event when editing the activity or resources maxmark.
305      *
306      * @protected
307      * @method edit_maxmark_cancel
308      * @param {EventFacade} ev The event that triggered this.
309      * @param {Node} activity The activity whose maxmark we are altering.
310      * @param {Boolean} preventdefault If true we should prevent the default action from occuring.
311      */
312     edit_maxmark_cancel : function(ev, activity, preventdefault) {
313         if (preventdefault) {
314             ev.preventDefault();
315         }
316         this.edit_maxmark_clear(activity);
317     },
319     /**
320      * Handles clearing the editing UI and returning things to the original state they were in.
321      *
322      * @protected
323      * @method edit_maxmark_clear
324      * @param {Node} activity  The activity whose maxmark we were altering.
325      */
326     edit_maxmark_clear : function(activity) {
327         // Detach all listen events to prevent duplicate triggers
328         new Y.EventHandle(this.editmaxmarkevents).detach();
330         var editform = activity.one(SELECTOR.ACTIVITYFORM),
331             instructions = activity.one('#id_editinstructions');
332         if (editform) {
333             editform.replace(editform.getData('anchor'));
334         }
335         if (instructions) {
336             instructions.remove();
337         }
339         // Remove the editing class again to revert the display.
340         activity.removeClass(CSS.EDITINGMAXMARK);
342         // Refocus the link which was clicked originally so the user can continue using keyboard nav.
343         Y.later(100, this, function() {
344             activity.one(SELECTOR.EDITMAXMARK).focus();
345         });
347         // This hack is to keep Behat happy until they release a version of
348         // MinkSelenium2Driver that fixes
349         // https://github.com/Behat/MinkSelenium2Driver/issues/80.
350         if (!Y.one('input[name=maxmark')) {
351             Y.one('body').append('<input type="text" name="maxmark" style="display: none">');
352         }
353     },
355     /**
356      * Joins or separates the given slot with the page of the previous slot. Reorders the pages of
357      * the other slots
358      *
359      * @protected
360      * @method update_page_break
361      * @param {EventFacade} ev The event that was fired.
362      * @param {Node} button The button that triggered this action.
363      * @param {Node} activity The activity node that this action will be performed on.
364      * @param {String} action The action, addpagebreak or removepagebreak.
365      * @chainable
366      */
367     update_page_break: function(ev, button, activity, action) {
368         // Prevent the default button action
369         ev.preventDefault();
371         var nextactivity = activity.next('li.activity.slot');
372         var spinner = this.add_spinner(nextactivity);
373         var value = action === 'removepagebreak' ? 1 : 2;
375         var data = {
376             'class': 'resource',
377             'field': 'updatepagebreak',
378             'id':    Y.Moodle.mod_quiz.util.slot.getId(nextactivity),
379             'value': value
380         };
382         this.send_request(data, spinner, function(response) {
383             if (response.slots) {
384                 if (action === 'addpagebreak') {
385                     Y.Moodle.mod_quiz.util.page.add(activity);
386                 } else {
387                     var page = activity.next(Y.Moodle.mod_quiz.util.page.SELECTORS.PAGE);
388                     Y.Moodle.mod_quiz.util.page.remove(page, true);
389                 }
390                 this.reorganise_edit_page();
391             }
392         });
394         return this;
395     },
397     /**
398      * Updates a slot to either require the question in the previous slot to
399      * have been answered, or not,
400      *
401      * @protected
402      * @method update_page_break
403      * @param {EventFacade} ev The event that was fired.
404      * @param {Node} button The button that triggered this action.
405      * @param {Node} activity The activity node that this action will be performed on.
406      * @param {String} action The action, adddependency or removedependency.
407      * @chainable
408      */
409     update_dependency: function(ev, button, activity, action) {
410         // Prevent the default button action.
411         ev.preventDefault();
412         var spinner = this.add_spinner(activity);
414         var data = {
415             'class': 'resource',
416             'field': 'updatedependency',
417             'id':    Y.Moodle.mod_quiz.util.slot.getId(activity),
418             'value': action === 'adddependency' ? 1 : 0
419         };
421         this.send_request(data, spinner, function(response) {
422             if (response.hasOwnProperty('requireprevious')) {
423                 Y.Moodle.mod_quiz.util.slot.updateDependencyIcon(activity, response.requireprevious);
424             }
425         });
427         return this;
428     },
430     /**
431      * Reorganise the UI after every edit action.
432      *
433      * @protected
434      * @method reorganise_edit_page
435      */
436     reorganise_edit_page: function() {
437         Y.Moodle.mod_quiz.util.slot.reorderSlots();
438         Y.Moodle.mod_quiz.util.slot.reorderPageBreaks();
439         Y.Moodle.mod_quiz.util.page.reorderPages();
440         Y.Moodle.mod_quiz.util.slot.updateOneSlotSections();
441         Y.Moodle.mod_quiz.util.slot.updateAllDependencyIcons();
442     },
444     NAME : 'mod_quiz-resource-toolbox',
445     ATTRS : {
446         courseid : {
447             'value' : 0
448         },
449         quizid : {
450             'value' : 0
451         }
452     }
455 M.mod_quiz.resource_toolbox = null;
456 M.mod_quiz.init_resource_toolbox = function(config) {
457     M.mod_quiz.resource_toolbox = new RESOURCETOOLBOX(config);
458     return M.mod_quiz.resource_toolbox;