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