2 * Resource and activity toolbox class.
4 * This class is responsible for managing AJAX interactions with activities and resources
5 * when viewing a quiz in editing mode.
7 * @module mod_quiz-resource-toolbox
8 * @namespace M.mod_quiz.resource_toolbox
12 * Resource and activity toolbox class.
14 * This is a class extending TOOLBOX containing code specific to resources
16 * This class is responsible for managing AJAX interactions with activities and resources
17 * when viewing a quiz in editing mode.
21 * @extends M.course.toolboxes.toolbox
23 var RESOURCETOOLBOX = function() {
24 RESOURCETOOLBOX.superclass.constructor.apply(this, arguments);
27 Y.extend(RESOURCETOOLBOX, TOOLBOX, {
29 * An Array of events added when editing a max mark field.
30 * These should all be detached when editing is complete.
32 * @property editmaxmarkevents
37 editmaxmarkevents: [],
47 * Initialize the resource toolbox
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
55 * This function also creates a single event delegate to manage all AJAX actions for all activities on
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);
68 * Handles the delegation event. When this is fired someone has triggered an action.
70 * Note not all actions will result in an AJAX enhancement.
73 * @method handle_data_action
74 * @param {EventFacade} ev The event that was triggered.
77 handle_data_action: function(ev) {
78 // We need to get the anchor element that triggered this event.
80 if (!node.test('a')) {
81 node = node.ancestor(SELECTOR.ACTIVITYACTION);
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.
94 // Switch based upon the action and do the desired thing.
97 // The user wishes to edit the maxmark of the resource.
98 this.edit_maxmark(ev, node, activity, action);
101 // The user is deleting the activity.
102 this.delete_with_confirmation(ev, node, activity, action);
105 case 'removepagebreak':
106 // The user is adding or removing a page break.
107 this.update_page_break(ev, node, activity, action);
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);
115 // Nothing to do here!
121 * Add a loading icon to the specified activity.
123 * The icon is added within the action area.
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.
129 add_spinner: function(activity) {
130 var actionarea = activity.one(SELECTOR.ACTIONAREA);
132 return M.util.add_spinner(Y, actionarea);
138 * Deletes the given activity or resource after confirmation.
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.
147 delete_with_confirmation: function(ev, button, activity) {
148 // Prevent the default button action.
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)
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,
165 // If it is confirmed.
166 confirm.on('complete-yes', function() {
168 var spinner = this.add_spinner(element);
172 'id': Y.Moodle.mod_quiz.util.slot.getId(element)
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();
192 * Edit the maxmark for the resource
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.
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,
210 anchor = instancemaxmark,// Grab the anchor so that we can swap it with the edit form.
212 'class' : 'resource',
213 'field' : 'getmaxmark',
214 'id' : Y.Moodle.mod_quiz.util.slot.getId(activity)
217 // Prevent the default actions.
220 this.send_request(data, null, function(response) {
221 if (M.core.actionmenu && M.core.actionmenu.instance) {
222 M.core.actionmenu.instance.hideMenu();
225 // Try to retrieve the existing string from the server.
226 if (response.instancemaxmark) {
227 maxmarktext = response.instancemaxmark;
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',
239 'size' : parseInt(this.get('config').questiondecimalpoints, 10) + 2
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()) {
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);
273 * Handles the submit event when editing the activity or resources maxmark.
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.
281 edit_maxmark_submit : function(ev, activity, originalmaxmark) {
282 // We don't actually want to submit anything.
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) {
290 'class' : 'resource',
291 'field' : 'updatemaxmark',
292 'maxmark' : newmaxmark,
293 'id' : Y.Moodle.mod_quiz.util.slot.getId(activity)
295 this.send_request(data, spinner, function(response) {
296 if (response.instancemaxmark) {
297 activity.one(SELECTOR.INSTANCEMAXMARK).setContent(response.instancemaxmark);
304 * Handles the cancel event when editing the activity or resources maxmark.
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.
312 edit_maxmark_cancel : function(ev, activity, preventdefault) {
313 if (preventdefault) {
316 this.edit_maxmark_clear(activity);
320 * Handles clearing the editing UI and returning things to the original state they were in.
323 * @method edit_maxmark_clear
324 * @param {Node} activity The activity whose maxmark we were altering.
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');
333 editform.replace(editform.getData('anchor'));
336 instructions.remove();
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();
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">');
356 * Joins or separates the given slot with the page of the previous slot. Reorders the pages of
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.
367 update_page_break: function(ev, button, activity, action) {
368 // Prevent the default button action
371 var nextactivity = activity.next('li.activity.slot');
372 var spinner = this.add_spinner(nextactivity);
373 var value = action === 'removepagebreak' ? 1 : 2;
377 'field': 'updatepagebreak',
378 'id': Y.Moodle.mod_quiz.util.slot.getId(nextactivity),
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);
387 var page = activity.next(Y.Moodle.mod_quiz.util.page.SELECTORS.PAGE);
388 Y.Moodle.mod_quiz.util.page.remove(page, true);
390 this.reorganise_edit_page();
398 * Updates a slot to either require the question in the previous slot to
399 * have been answered, or not,
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.
409 update_dependency: function(ev, button, activity, action) {
410 // Prevent the default button action.
412 var spinner = this.add_spinner(activity);
416 'field': 'updatedependency',
417 'id': Y.Moodle.mod_quiz.util.slot.getId(activity),
418 'value': action === 'adddependency' ? 1 : 0
421 this.send_request(data, spinner, function(response) {
422 if (response.hasOwnProperty('requireprevious')) {
423 Y.Moodle.mod_quiz.util.slot.updateDependencyIcon(activity, response.requireprevious);
431 * Reorganise the UI after every edit action.
434 * @method reorganise_edit_page
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();
444 NAME : 'mod_quiz-resource-toolbox',
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;