MDL-57431 mod_quiz: Quiz editing help icon fix
[moodle.git] / mod / quiz / yui / build / moodle-mod_quiz-toolboxes / moodle-mod_quiz-toolboxes-debug.js
CommitLineData
e1a2d0d9
CC
1YUI.add('moodle-mod_quiz-toolboxes', function (Y, NAME) {
2
ad3f8cd1 3/* eslint-disable no-unused-vars */
e1a2d0d9
CC
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 */
13
14// The CSS classes we use.
441d284a 15var CSS = {
3a0bc0fd
DP
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',
e1a2d0d9 25 JOIN: 'page_join',
3a0bc0fd
DP
26 MODINDENTCOUNT: 'mod-indent-',
27 MODINDENTHUGE: 'mod-indent-huge',
e1a2d0d9 28 PAGE: 'page',
3a0bc0fd
DP
29 SECTIONHIDDENCLASS: 'hidden',
30 SECTIONIDPREFIX: 'section-',
f37cffb6 31 SELECTMULTIPLE: 'select-multiple',
3a0bc0fd
DP
32 SLOT: 'slot',
33 SHOW: 'editing_show',
34 TITLEEDITOR: 'titleeditor'
e1a2d0d9
CC
35 },
36 // The CSS selectors we use.
37 SELECTOR = {
38 ACTIONAREA: '.actions',
3a0bc0fd
DP
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',
7d1709f9 49 DELETESECTIONICON: 'a.editing_delete .icon',
f37cffb6 50 DESELECTALL: '#questiondeselectall',
e1a2d0d9 51 EDITMAXMARK: 'a.editing_maxmark',
5d949702 52 EDITSECTION: 'a.editing_section',
8857c715 53 EDITSECTIONICON: 'a.editing_section .icon',
5d949702 54 EDITSHUFFLEQUESTIONSACTION: 'input.cm-edit-action[data-action]',
aa6b85d2 55 EDITSHUFFLEAREA: '.instanceshufflequestions .shuffle-progress .shuffle-help-tip',
3a0bc0fd
DP
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]',
f37cffb6
CC
70 SELECTMULTIPLEBUTTON: '#selectmultiplecommand',
71 SELECTMULTIPLECANCELBUTTON: '#selectmultiplecancelcommand',
72 SELECTMULTIPLECHECKBOX: '.select-multiple-checkbox',
73 SELECTMULTIPLEDELETEBUTTON: '#selectmultipledeletecommand',
74 SELECTALL: '#questionselectall',
3a0bc0fd
DP
75 SHOW: 'a.' + CSS.SHOW,
76 SLOTLI: 'li.slot',
77 SUMMARKS: '.mod_quiz_summarks'
e1a2d0d9
CC
78 },
79 BODY = Y.one(document.body);
80
81// Setup the basic namespace.
82M.mod_quiz = M.mod_quiz || {};
83
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 */
93var TOOLBOX = function() {
94 TOOLBOX.superclass.constructor.apply(this, arguments);
95};
96
97Y.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 }
f37cffb6 113
e1a2d0d9
CC
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 }
120
121 data.sesskey = M.cfg.sesskey;
122 data.courseid = this.get('courseid');
123 data.quizid = this.get('quizid');
124
125 var uri = M.cfg.wwwroot + this.get('ajaxurl');
126
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 }
3a0bc0fd
DP
139 } catch (e) {
140 // Ignore.
141 }
e1a2d0d9
CC
142
143 // Run the callback if we have one.
1a14f8a6 144 if (responsetext.hasOwnProperty('newsummarks')) {
e1a2d0d9
CC
145 Y.one(SELECTOR.SUMMARKS).setHTML(responsetext.newsummarks);
146 }
1a14f8a6 147 if (responsetext.hasOwnProperty('newnumquestions')) {
557f44d9
AN
148 Y.one(SELECTOR.NUMQUESTIONS).setHTML(
149 M.util.get_string('numquestionsx', 'quiz', responsetext.newnumquestions)
150 );
6375e98c 151 }
e1a2d0d9
CC
152 if (success_callback) {
153 Y.bind(success_callback, this, responsetext)();
154 }
155
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 };
171
172 // Apply optional config
173 if (optionalconfig) {
174 for (varname in optionalconfig) {
175 config[varname] = optionalconfig[varname];
176 }
177 }
178
179 if (statusspinner) {
180 statusspinner.show();
181 }
182
183 // Send the request
184 Y.io(uri, config);
185 return this;
186 }
187},
188{
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 },
201
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 }
232}
233);
ad3f8cd1
DP
234/* global TOOLBOX, BODY, SELECTOR */
235
e1a2d0d9
CC
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 */
245
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 */
258var RESOURCETOOLBOX = function() {
259 RESOURCETOOLBOX.superclass.constructor.apply(this, arguments);
260};
261
262Y.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: [],
273
274 /**
275 *
276 */
277 NODE_PAGE: 1,
278 NODE_SLOT: 2,
279 NODE_JOIN: 3,
280
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);
e1a2d0d9 298 Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this);
441d284a 299 Y.delegate('click', this.handle_data_action, BODY, SELECTOR.DEPENDENCY_LINK, this);
f37cffb6
CC
300 this.initialise_select_multiple();
301 },
302
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 });
317
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 });
323
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 });
329
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 });
335
336 // Disable delete multiple button by default.
337 Y.one(SELECTOR.SELECTMULTIPLEDELETEBUTTON).setAttribute('disabled', 'disabled');
338
339 // Assign the delete method to the delete multiple button.
340 Y.delegate('click', this.delete_multiple_with_confirmation, BODY, SELECTOR.SELECTMULTIPLEDELETEBUTTON, this);
341
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);
e1a2d0d9
CC
346 },
347
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 }
364
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);
369
370 if (!node.test('a') || !action || !activity) {
371 // It wasn't a valid action node.
372 return;
373 }
374
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;
a69f81f0
CC
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);
e1a2d0d9 389 break;
441d284a
TH
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;
e1a2d0d9
CC
395 default:
396 // Nothing to do here!
397 break;
398 }
399 },
400
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 },
417
f37cffb6
CC
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 },
433
e1a2d0d9
CC
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) {
a69f81f0 445 // Prevent the default button action.
e1a2d0d9
CC
446 ev.preventDefault();
447
a69f81f0 448 // Get the element we're working on.
3a0bc0fd 449 var element = activity,
e1a2d0d9
CC
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);
455
456 // Create the confirmation dialogue.
457 var confirm = new M.core.confirm({
458 question: confirmstring,
459 modal: true
460 });
461
462 // If it is confirmed.
463 confirm.on('complete-yes', function() {
a69f81f0 464 var spinner = this.add_spinner(element);
e1a2d0d9
CC
465 var data = {
466 'class': 'resource',
467 'action': 'DELETE',
468 'id': Y.Moodle.mod_quiz.util.slot.getId(element)
469 };
a69f81f0
CC
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) {
ccfb9b69 476 M.core.actionmenu.instance.hideMenu(ev);
a69f81f0 477 }
a69f81f0
CC
478 }
479 });
e1a2d0d9
CC
480
481 }, this);
e1a2d0d9
CC
482 },
483
f37cffb6
CC
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();
494
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');
504
505 // Do nothing if no slots are selected.
506 if (!slots || !slots.length) {
507 return;
508 }
509
510 // Create the confirmation dialogue.
511 var confirm = new M.core.confirm({
512 question: M.util.get_string('areyousureremoveselected', 'quiz'),
513 modal: true
514 });
515
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();
534
535 // Remove the select multiple options.
536 Y.one('body').removeClass(CSS.SELECTMULTIPLE);
537 }
538 });
539
540 }, this);
541 },
e1a2d0d9
CC
542
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 */
3a0bc0fd 554 edit_maxmark: function(ev, button, activity) {
e1a2d0d9 555 // Get the element we're working on
3a0bc0fd 556 var instancemaxmark = activity.one(SELECTOR.INSTANCEMAXMARK),
e1a2d0d9
CC
557 instance = activity.one(SELECTOR.ACTIVITYINSTANCE),
558 currentmaxmark = instancemaxmark.get('firstChild'),
559 oldmaxmark = currentmaxmark.get('data'),
560 maxmarktext = oldmaxmark,
561 thisevent,
3a0bc0fd 562 anchor = instancemaxmark, // Grab the anchor so that we can swap it with the edit form.
e1a2d0d9 563 data = {
3a0bc0fd
DP
564 'class': 'resource',
565 'field': 'getmaxmark',
566 'id': Y.Moodle.mod_quiz.util.slot.getId(activity)
e1a2d0d9
CC
567 };
568
569 // Prevent the default actions.
570 ev.preventDefault();
571
572 this.send_request(data, null, function(response) {
573 if (M.core.actionmenu && M.core.actionmenu.instance) {
ccfb9b69 574 M.core.actionmenu.instance.hideMenu(ev);
e1a2d0d9
CC
575 }
576
a69f81f0 577 // Try to retrieve the existing string from the server.
e1a2d0d9
CC
578 if (response.instancemaxmark) {
579 maxmarktext = response.instancemaxmark;
580 }
581
a69f81f0 582 // Create the editor and submit button.
e1a2d0d9
CC
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({
3a0bc0fd
DP
587 'value': maxmarktext,
588 'autocomplete': 'off',
589 'aria-describedby': 'id_editinstructions',
590 'maxLength': '12',
591 'size': parseInt(this.get('config').questiondecimalpoints, 10) + 2
e1a2d0d9
CC
592 });
593
a69f81f0 594 // Clear the existing content and put the editor in.
e1a2d0d9
CC
595 editform.appendChild(editor);
596 editform.setData('anchor', anchor);
597 instance.insert(editinstructions, 'before');
598 anchor.replace(editform);
599
e1a2d0d9
CC
600 // We hide various components whilst editing:
601 activity.addClass(CSS.EDITINGMAXMARK);
602
a69f81f0 603 // Focus and select the editor text.
e1a2d0d9
CC
604 editor.focus().select();
605
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);
611
612 // Handle form submission.
613 thisevent = editform.on('submit', this.edit_maxmark_submit, this, activity, oldmaxmark);
614 this.editmaxmarkevents.push(thisevent);
615 });
616 },
617
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 */
3a0bc0fd 627 edit_maxmark_submit: function(ev, activity, originalmaxmark) {
a69f81f0 628 // We don't actually want to submit anything.
e1a2d0d9
CC
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 = {
3a0bc0fd
DP
636 'class': 'resource',
637 'field': 'updatemaxmark',
638 'maxmark': newmaxmark,
639 'id': Y.Moodle.mod_quiz.util.slot.getId(activity)
e1a2d0d9
CC
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 },
648
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 */
3a0bc0fd 658 edit_maxmark_cancel: function(ev, activity, preventdefault) {
e1a2d0d9
CC
659 if (preventdefault) {
660 ev.preventDefault();
661 }
662 this.edit_maxmark_clear(activity);
663 },
664
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 */
3a0bc0fd 672 edit_maxmark_clear: function(activity) {
e1a2d0d9
CC
673 // Detach all listen events to prevent duplicate triggers
674 new Y.EventHandle(this.editmaxmarkevents).detach();
675
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 }
684
685 // Remove the editing class again to revert the display.
686 activity.removeClass(CSS.EDITINGMAXMARK);
687
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 });
692
f7986025 693 // TODO MDL-50768 This hack is to keep Behat happy until they release a version of
e1a2d0d9
CC
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 },
700
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
a69f81f0 706 * @method update_page_break
e1a2d0d9
CC
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.
441d284a 710 * @param {String} action The action, addpagebreak or removepagebreak.
e1a2d0d9
CC
711 * @chainable
712 */
a69f81f0 713 update_page_break: function(ev, button, activity, action) {
e1a2d0d9
CC
714 // Prevent the default button action
715 ev.preventDefault();
716
557f44d9 717 var nextactivity = activity.next('li.activity.slot');
441d284a 718 var spinner = this.add_spinner(nextactivity);
a69f81f0 719 var value = action === 'removepagebreak' ? 1 : 2;
e1a2d0d9
CC
720
721 var data = {
722 'class': 'resource',
a69f81f0 723 'field': 'updatepagebreak',
441d284a 724 'id': Y.Moodle.mod_quiz.util.slot.getId(nextactivity),
e1a2d0d9
CC
725 'value': value
726 };
727
e1a2d0d9 728 this.send_request(data, spinner, function(response) {
a69f81f0
CC
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();
441d284a
TH
737 }
738 });
739
740 return this;
741 },
742
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);
759
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 };
766
767 this.send_request(data, spinner, function(response) {
768 if (response.hasOwnProperty('requireprevious')) {
769 Y.Moodle.mod_quiz.util.slot.updateDependencyIcon(activity, response.requireprevious);
a69f81f0 770 }
e1a2d0d9
CC
771 });
772
773 return this;
774 },
e1a2d0d9 775
a69f81f0
CC
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();
5d949702 786 Y.Moodle.mod_quiz.util.slot.updateOneSlotSections();
441d284a 787 Y.Moodle.mod_quiz.util.slot.updateAllDependencyIcons();
e1a2d0d9
CC
788 },
789
3a0bc0fd
DP
790 NAME: 'mod_quiz-resource-toolbox',
791 ATTRS: {
792 courseid: {
793 'value': 0
e1a2d0d9 794 },
3a0bc0fd
DP
795 quizid: {
796 'value': 0
e1a2d0d9
CC
797 }
798 }
f37cffb6 799
e1a2d0d9
CC
800});
801
802M.mod_quiz.resource_toolbox = null;
803M.mod_quiz.init_resource_toolbox = function(config) {
804 M.mod_quiz.resource_toolbox = new RESOURCETOOLBOX(config);
805 return M.mod_quiz.resource_toolbox;
806};
ad3f8cd1
DP
807/* global TOOLBOX, BODY, SELECTOR */
808
e1a2d0d9 809/**
5d949702 810 * Section toolbox class.
e1a2d0d9 811 *
5d949702
K
812 * This class is responsible for managing AJAX interactions with sections
813 * when adding, editing, removing section headings.
e1a2d0d9
CC
814 *
815 * @module moodle-mod_quiz-toolboxes
816 * @namespace M.mod_quiz.toolboxes
817 */
818
819/**
820 * Section toolbox class.
821 *
822 * This class is responsible for managing AJAX interactions with sections
5d949702 823 * when adding, editing, removing section headings when editing a quiz.
e1a2d0d9
CC
824 *
825 * @class section
826 * @constructor
827 * @extends M.mod_quiz.toolboxes.toolbox
828 */
829var SECTIONTOOLBOX = function() {
830 SECTIONTOOLBOX.superclass.constructor.apply(this, arguments);
831};
832
833Y.extend(SECTIONTOOLBOX, TOOLBOX, {
5d949702
K
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: [],
844
e1a2d0d9
CC
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 */
5d949702 853 initializer: function() {
e1a2d0d9 854 M.mod_quiz.quizbase.register_module(this);
5d949702
K
855
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 },
860
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 }
877
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);
882
883 if ((!node.test('a') && !node.test('input[data-action]')) || !action || !activity) {
884 // It wasn't a valid action node.
885 return;
886 }
887
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 },
907
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();
921
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 });
927
928 // If it is confirmed.
929 confirm.on('complete-yes', function() {
930
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 });
942
943 }, this);
944 },
945
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-', ''),
3a0bc0fd 960 instancesection = activity.one(SELECTOR.INSTANCESECTION),
5d949702
K
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 };
968
969 // Prevent the default actions.
970 ev.preventDefault();
971
972 this.send_request(data, null, function(response) {
973 // Try to retrieve the existing string from the server.
974 var oldtext = response.instancesection;
975
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 });
986
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);
992
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 },
1005
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));
9f9d2caa
K
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 }
5d949702
K
1041 }
1042 });
1043 }
1044 },
1045
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 },
1061
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();
1072
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 }
1081
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 });
1086
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 },
1094
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 }
1113
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 };
1121
1122 // Prevent the default actions.
1123 ev.preventDefault();
1124
1125 // Send request.
1126 var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.EDITSHUFFLEAREA));
1127 this.send_request(data, spinner);
e1a2d0d9 1128 }
5d949702 1129
3a0bc0fd 1130}, {
5d949702
K
1131 NAME: 'mod_quiz-section-toolbox',
1132 ATTRS: {
1133 courseid: {
1134 'value': 0
e1a2d0d9 1135 },
5d949702
K
1136 quizid: {
1137 'value': 0
e1a2d0d9
CC
1138 }
1139 }
1140});
1141
1142M.mod_quiz.init_section_toolbox = function(config) {
1143 return new SECTIONTOOLBOX(config);
1144};
1145
1146
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});