1 YUI.add('moodle-mod_quiz-toolboxes', function (Y, NAME) {
4 * Resource and activity toolbox class.
6 * This class is responsible for managing AJAX interactions with activities and resources
7 * when viewing a course in editing mode.
9 * @module moodle-course-toolboxes
10 * @namespace M.course.toolboxes
13 // The CSS classes we use.
15 ACTIVITYINSTANCE : 'activityinstance',
16 AVAILABILITYINFODIV : 'div.availabilityinfo',
17 CONTENTWITHOUTLINK : 'contentwithoutlink',
18 CONDITIONALHIDDEN : 'conditionalhidden',
20 DIMMEDTEXT : 'dimmed_text',
21 EDITINSTRUCTIONS : 'editinstructions',
22 EDITINGMAXMARK: 'editor_displayed',
25 MODINDENTCOUNT : 'mod-indent-',
26 MODINDENTHUGE : 'mod-indent-huge',
27 MODULEIDPREFIX : 'slot-',
29 SECTIONHIDDENCLASS : 'hidden',
30 SECTIONIDPREFIX : 'section-',
32 SHOW : 'editing_show',
33 TITLEEDITOR : 'titleeditor'
35 // The CSS selectors we use.
37 ACTIONAREA: '.actions',
38 ACTIONLINKTEXT : '.actionlinktext',
39 ACTIVITYACTION : 'a.cm-edit-action[data-action], a.editing_maxmark',
40 ACTIVITYFORM : 'span.instancemaxmarkcontainer form',
41 ACTIVITYICON : 'img.activityicon',
42 ACTIVITYINSTANCE : '.' + CSS.ACTIVITYINSTANCE,
43 ACTIVITYLINK: '.' + CSS.ACTIVITYINSTANCE + ' > a',
44 ACTIVITYLI : 'li.activity',
45 ACTIVITYMAXMARK : 'input[name=maxmark]',
46 COMMANDSPAN : '.commands',
47 CONTENTAFTERLINK : 'div.contentafterlink',
48 CONTENTWITHOUTLINK : 'div.contentwithoutlink',
49 EDITMAXMARK: 'a.editing_maxmark',
50 HIDE : 'a.editing_hide',
51 HIGHLIGHT : 'a.editing_highlight',
52 INSTANCENAME : 'span.instancename',
53 INSTANCEMAXMARK : 'span.instancemaxmark',
54 MODINDENTDIV : '.mod-indent',
55 MODINDENTOUTER : '.mod-indent-outer',
56 NUMQUESTIONS : '.numberofquestions',
57 PAGECONTENT : 'div#page-content',
59 SECTIONUL : 'ul.section',
60 SHOW : 'a.' + CSS.SHOW,
61 SHOWHIDE : 'a.editing_showhide',
63 SUMMARKS : '.mod_quiz_summarks'
65 BODY = Y.one(document.body);
67 // Setup the basic namespace.
68 M.mod_quiz = M.mod_quiz || {};
71 * The toolbox class is a generic class which should never be directly
72 * instantiated. Please extend it instead.
79 var TOOLBOX = function() {
80 TOOLBOX.superclass.constructor.apply(this, arguments);
83 Y.extend(TOOLBOX, Y.Base, {
85 * Send a request using the REST API
87 * @method send_request
88 * @param {Object} data The data to submit with the AJAX request
89 * @param {Node} [statusspinner] A statusspinner which may contain a section loader
90 * @param {Function} success_callback The callback to use on success
91 * @param {Object} [optionalconfig] Any additional configuration to submit
94 send_request: function(data, statusspinner, success_callback, optionalconfig) {
95 // Default data structure
99 // Handle any variables which we must pass back through to
100 var pageparams = this.get('config').pageparams,
102 for (varname in pageparams) {
103 data[varname] = pageparams[varname];
106 data.sesskey = M.cfg.sesskey;
107 data.courseid = this.get('courseid');
108 data.quizid = this.get('quizid');
110 var uri = M.cfg.wwwroot + this.get('ajaxurl');
112 // Define the configuration to send with the request
113 var responsetext = [];
118 success: function(tid, response) {
120 responsetext = Y.JSON.parse(response.responseText);
121 if (responsetext.error) {
122 new M.core.ajaxException(responsetext);
126 // Run the callback if we have one.
127 if (responsetext.hasOwnProperty('newsummarks')) {
128 Y.one(SELECTOR.SUMMARKS).setHTML(responsetext.newsummarks);
130 if (responsetext.hasOwnProperty('newnumquestions')) {
131 Y.one(SELECTOR.NUMQUESTIONS).setHTML(M.util.get_string('numquestionsx', 'quiz', responsetext.newnumquestions));
133 if (success_callback) {
134 Y.bind(success_callback, this, responsetext)();
138 window.setTimeout(function() {
139 statusspinner.hide();
143 failure: function(tid, response) {
145 statusspinner.hide();
147 new M.core.ajaxException(response);
153 // Apply optional config
154 if (optionalconfig) {
155 for (varname in optionalconfig) {
156 config[varname] = optionalconfig[varname];
161 statusspinner.show();
170 NAME: 'mod_quiz-toolbox',
173 * The ID of the Moodle Course being edited.
175 * @attribute courseid
184 * The Moodle course format.
194 * The URL to use when submitting requests.
203 * Any additional configuration passed when creating the instance.
216 * Resource and activity toolbox class.
218 * This class is responsible for managing AJAX interactions with activities and resources
219 * when viewing a quiz in editing mode.
221 * @module mod_quiz-resource-toolbox
222 * @namespace M.mod_quiz.resource_toolbox
226 * Resource and activity toolbox class.
228 * This is a class extending TOOLBOX containing code specific to resources
230 * This class is responsible for managing AJAX interactions with activities and resources
231 * when viewing a quiz in editing mode.
235 * @extends M.course.toolboxes.toolbox
237 var RESOURCETOOLBOX = function() {
238 RESOURCETOOLBOX.superclass.constructor.apply(this, arguments);
241 Y.extend(RESOURCETOOLBOX, TOOLBOX, {
243 * An Array of events added when editing a max mark field.
244 * These should all be detached when editing is complete.
246 * @property editmaxmarkevents
251 editmaxmarkevents: [],
261 * Initialize the resource toolbox
263 * For each activity the commands are updated and a reference to the activity is attached.
264 * This way it doesn't matter where the commands are going to called from they have a reference to the
265 * activity that they relate to.
266 * This is essential as some of the actions are displayed in an actionmenu which removes them from the
269 * This function also creates a single event delegate to manage all AJAX actions for all activities on
272 * @method initializer
275 initializer: function() {
276 M.mod_quiz.quizbase.register_module(this);
277 BODY.delegate('key', this.handle_data_action, 'down:enter', SELECTOR.ACTIVITYACTION, this);
278 Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this);
282 * Handles the delegation event. When this is fired someone has triggered an action.
284 * Note not all actions will result in an AJAX enhancement.
287 * @method handle_data_action
288 * @param {EventFacade} ev The event that was triggered.
291 handle_data_action: function(ev) {
292 // We need to get the anchor element that triggered this event.
293 var node = ev.target;
294 if (!node.test('a')) {
295 node = node.ancestor(SELECTOR.ACTIVITYACTION);
298 // From the anchor we can get both the activity (added during initialisation) and the action being
299 // performed (added by the UI as a data attribute).
300 var action = node.getData('action'),
301 activity = node.ancestor(SELECTOR.ACTIVITYLI);
303 if (!node.test('a') || !action || !activity) {
304 // It wasn't a valid action node.
308 // Switch based upon the action and do the desired thing.
311 // The user wishes to edit the maxmark of the resource.
312 this.edit_maxmark(ev, node, activity, action);
315 // The user is deleting the activity.
316 this.delete_with_confirmation(ev, node, activity, action);
319 case 'removepagebreak':
320 // The user is adding or removing a page break.
321 this.update_page_break(ev, node, activity, action);
324 // Nothing to do here!
330 * Add a loading icon to the specified activity.
332 * The icon is added within the action area.
334 * @method add_spinner
335 * @param {Node} activity The activity to add a loading icon to
336 * @return {Node|null} The newly created icon, or null if the action area was not found.
338 add_spinner: function(activity) {
339 var actionarea = activity.one(SELECTOR.ACTIONAREA);
341 return M.util.add_spinner(Y, actionarea);
347 * Deletes the given activity or resource after confirmation.
350 * @method delete_with_confirmation
351 * @param {EventFacade} ev The event that was fired.
352 * @param {Node} button The button that triggered this action.
353 * @param {Node} activity The activity node that this action will be performed on.
356 delete_with_confirmation: function(ev, button, activity) {
357 // Prevent the default button action.
360 // Get the element we're working on.
361 var element = activity,
362 // Create confirm string (different if element has or does not have name)
364 qtypename = M.util.get_string('pluginname',
365 'qtype_' + element.getAttribute('class').match(/qtype_([^\s]*)/)[1]);
366 confirmstring = M.util.get_string('confirmremovequestion', 'quiz', qtypename);
368 // Create the confirmation dialogue.
369 var confirm = new M.core.confirm({
370 question: confirmstring,
374 // If it is confirmed.
375 confirm.on('complete-yes', function() {
377 var spinner = this.add_spinner(element);
381 'id': Y.Moodle.mod_quiz.util.slot.getId(element)
383 this.send_request(data, spinner, function(response) {
384 if (response.deleted) {
385 // Actually remove the element.
386 Y.Moodle.mod_quiz.util.slot.remove(element);
387 this.reorganise_edit_page();
388 if (M.core.actionmenu && M.core.actionmenu.instance) {
389 M.core.actionmenu.instance.hideMenu();
392 window.location.reload(true);
403 * Edit the maxmark for the resource
406 * @method edit_maxmark
407 * @param {EventFacade} ev The event that was fired.
408 * @param {Node} button The button that triggered this action.
409 * @param {Node} activity The activity node that this action will be performed on.
410 * @param {String} action The action that has been requested.
413 edit_maxmark : function(ev, button, activity) {
414 // Get the element we're working on
415 var activityid = Y.Moodle.mod_quiz.util.slot.getId(activity),
416 instancemaxmark = activity.one(SELECTOR.INSTANCEMAXMARK),
417 instance = activity.one(SELECTOR.ACTIVITYINSTANCE),
418 currentmaxmark = instancemaxmark.get('firstChild'),
419 oldmaxmark = currentmaxmark.get('data'),
420 maxmarktext = oldmaxmark,
422 anchor = instancemaxmark,// Grab the anchor so that we can swap it with the edit form.
424 'class' : 'resource',
425 'field' : 'getmaxmark',
429 // Prevent the default actions.
432 this.send_request(data, null, function(response) {
433 if (M.core.actionmenu && M.core.actionmenu.instance) {
434 M.core.actionmenu.instance.hideMenu();
437 // Try to retrieve the existing string from the server.
438 if (response.instancemaxmark) {
439 maxmarktext = response.instancemaxmark;
442 // Create the editor and submit button.
443 var editform = Y.Node.create('<form action="#" />');
444 var editinstructions = Y.Node.create('<span class="' + CSS.EDITINSTRUCTIONS + '" id="id_editinstructions" />')
445 .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
446 var editor = Y.Node.create('<input name="maxmark" type="text" class="' + CSS.TITLEEDITOR + '" />').setAttrs({
447 'value' : maxmarktext,
448 'autocomplete' : 'off',
449 'aria-describedby' : 'id_editinstructions',
451 'size' : parseInt(this.get('config').questiondecimalpoints, 10) + 2
454 // Clear the existing content and put the editor in.
455 editform.appendChild(editor);
456 editform.setData('anchor', anchor);
457 instance.insert(editinstructions, 'before');
458 anchor.replace(editform);
460 // Force the editing instruction to match the mod-indent position.
461 var padside = 'left';
462 if (right_to_left()) {
466 // We hide various components whilst editing:
467 activity.addClass(CSS.EDITINGMAXMARK);
469 // Focus and select the editor text.
470 editor.focus().select();
472 // Cancel the edit if we lose focus or the escape key is pressed.
473 thisevent = editor.on('blur', this.edit_maxmark_cancel, this, activity, false);
474 this.editmaxmarkevents.push(thisevent);
475 thisevent = editor.on('key', this.edit_maxmark_cancel, 'esc', this, activity, true);
476 this.editmaxmarkevents.push(thisevent);
478 // Handle form submission.
479 thisevent = editform.on('submit', this.edit_maxmark_submit, this, activity, oldmaxmark);
480 this.editmaxmarkevents.push(thisevent);
485 * Handles the submit event when editing the activity or resources maxmark.
488 * @method edit_maxmark_submit
489 * @param {EventFacade} ev The event that triggered this.
490 * @param {Node} activity The activity whose maxmark we are altering.
491 * @param {String} originalmaxmark The original maxmark the activity or resource had.
493 edit_maxmark_submit : function(ev, activity, originalmaxmark) {
494 // We don't actually want to submit anything.
496 var newmaxmark = Y.Lang.trim(activity.one(SELECTOR.ACTIVITYFORM + ' ' + SELECTOR.ACTIVITYMAXMARK).get('value'));
497 var spinner = this.add_spinner(activity);
498 this.edit_maxmark_clear(activity);
499 activity.one(SELECTOR.INSTANCEMAXMARK).setContent(newmaxmark);
500 if (newmaxmark !== null && newmaxmark !== "" && newmaxmark !== originalmaxmark) {
502 'class' : 'resource',
503 'field' : 'updatemaxmark',
504 'maxmark' : newmaxmark,
505 'id' : Y.Moodle.mod_quiz.util.slot.getId(activity)
507 this.send_request(data, spinner, function(response) {
508 if (response.instancemaxmark) {
509 activity.one(SELECTOR.INSTANCEMAXMARK).setContent(response.instancemaxmark);
516 * Handles the cancel event when editing the activity or resources maxmark.
519 * @method edit_maxmark_cancel
520 * @param {EventFacade} ev The event that triggered this.
521 * @param {Node} activity The activity whose maxmark we are altering.
522 * @param {Boolean} preventdefault If true we should prevent the default action from occuring.
524 edit_maxmark_cancel : function(ev, activity, preventdefault) {
525 if (preventdefault) {
528 this.edit_maxmark_clear(activity);
532 * Handles clearing the editing UI and returning things to the original state they were in.
535 * @method edit_maxmark_clear
536 * @param {Node} activity The activity whose maxmark we were altering.
538 edit_maxmark_clear : function(activity) {
539 // Detach all listen events to prevent duplicate triggers
540 new Y.EventHandle(this.editmaxmarkevents).detach();
542 var editform = activity.one(SELECTOR.ACTIVITYFORM),
543 instructions = activity.one('#id_editinstructions');
545 editform.replace(editform.getData('anchor'));
548 instructions.remove();
551 // Remove the editing class again to revert the display.
552 activity.removeClass(CSS.EDITINGMAXMARK);
554 // Refocus the link which was clicked originally so the user can continue using keyboard nav.
555 Y.later(100, this, function() {
556 activity.one(SELECTOR.EDITMAXMARK).focus();
559 // This hack is to keep Behat happy until they release a version of
560 // MinkSelenium2Driver that fixes
561 // https://github.com/Behat/MinkSelenium2Driver/issues/80.
562 if (!Y.one('input[name=maxmark')) {
563 Y.one('body').append('<input type="text" name="maxmark" style="display: none">');
568 * Joins or separates the given slot with the page of the previous slot. Reorders the pages of
572 * @method update_page_break
573 * @param {EventFacade} ev The event that was fired.
574 * @param {Node} button The button that triggered this action.
575 * @param {Node} activity The activity node that this action will be performed on.
578 update_page_break: function(ev, button, activity, action) {
579 // Prevent the default button action
582 nextactivity = activity.next('li.activity.slot');
583 var spinner = this.add_spinner(nextactivity),
585 var value = action === 'removepagebreak' ? 1 : 2;
589 'field': 'updatepagebreak',
594 slotid = Y.Moodle.mod_quiz.util.slot.getId(nextactivity);
596 data.id = Number(slotid);
598 this.send_request(data, spinner, function(response) {
599 if (response.slots) {
600 if (action === 'addpagebreak') {
601 Y.Moodle.mod_quiz.util.page.add(activity);
603 var page = activity.next(Y.Moodle.mod_quiz.util.page.SELECTORS.PAGE);
604 Y.Moodle.mod_quiz.util.page.remove(page, true);
606 this.reorganise_edit_page();
608 window.location.reload(true);
616 * Reorganise the UI after every edit action.
619 * @method reorganise_edit_page
621 reorganise_edit_page: function() {
622 Y.Moodle.mod_quiz.util.slot.reorderSlots();
623 Y.Moodle.mod_quiz.util.slot.reorderPageBreaks();
624 Y.Moodle.mod_quiz.util.page.reorderPages();
627 NAME : 'mod_quiz-resource-toolbox',
638 M.mod_quiz.resource_toolbox = null;
639 M.mod_quiz.init_resource_toolbox = function(config) {
640 M.mod_quiz.resource_toolbox = new RESOURCETOOLBOX(config);
641 return M.mod_quiz.resource_toolbox;
644 * Resource and activity toolbox class.
646 * This class is responsible for managing AJAX interactions with activities and resources
647 * when viewing a course in editing mode.
649 * @module moodle-mod_quiz-toolboxes
650 * @namespace M.mod_quiz.toolboxes
654 * Section toolbox class.
656 * This class is responsible for managing AJAX interactions with sections
657 * when viewing a course in editing mode.
661 * @extends M.mod_quiz.toolboxes.toolbox
663 var SECTIONTOOLBOX = function() {
664 SECTIONTOOLBOX.superclass.constructor.apply(this, arguments);
667 Y.extend(SECTIONTOOLBOX, TOOLBOX, {
669 * Initialize the section toolboxes module.
671 * Updates all span.commands with relevant handlers and other required changes.
673 * @method initializer
676 initializer : function() {
677 M.mod_quiz.quizbase.register_module(this);
679 // Section Highlighting.
680 Y.delegate('click', this.toggle_highlight, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.HIGHLIGHT, this);
682 // Section Visibility.
683 Y.delegate('click', this.toggle_hide_section, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.SHOWHIDE, this);
686 toggle_hide_section : function(e) {
687 // Prevent the default button action.
690 // Get the section we're working on.
691 var section = e.target.ancestor(M.mod_quiz.format.get_section_selector(Y)),
692 button = e.target.ancestor('a', true),
693 hideicon = button.one('img'),
695 // The value to submit
698 // The text for strings and images. Also determines the icon to display.
702 if (!section.hasClass(CSS.SECTIONHIDDENCLASS)) {
703 section.addClass(CSS.SECTIONHIDDENCLASS);
708 section.removeClass(CSS.SECTIONHIDDENCLASS);
714 var newstring = M.util.get_string(nextaction + 'fromothers', 'format_' + this.get('format'));
717 'src' : M.util.image_url('i/' + nextaction)
719 button.set('title', newstring);
721 // Change the highlight status
725 'id' : Y.Moodle.core_course.util.section.getId(section.ancestor(M.mod_quiz.edit.get_section_wrapper(Y), true)),
729 var lightbox = M.util.add_lightbox(Y, section);
732 this.send_request(data, lightbox, function(response) {
733 var activities = section.all(SELECTOR.ACTIVITYLI);
734 activities.each(function(node) {
736 if (node.one(SELECTOR.SHOW)) {
737 button = node.one(SELECTOR.SHOW);
739 button = node.one(SELECTOR.HIDE);
741 var activityid = Y.Moodle.mod_quiz.util.slot.getId(node);
743 // NOTE: resourcestotoggle is returned as a string instead
744 // of a Number so we must cast our activityid to a String.
745 if (Y.Array.indexOf(response.resourcestotoggle, "" + activityid) !== -1) {
746 M.mod_quiz.resource_toolbox.handle_resource_dim(button, node, action);
753 * Toggle highlighting the current section.
755 * @method toggle_highlight
756 * @param {EventFacade} e
758 toggle_highlight : function(e) {
759 // Prevent the default button action.
762 // Get the section we're working on.
763 var section = e.target.ancestor(M.mod_quiz.edit.get_section_selector(Y));
764 var button = e.target.ancestor('a', true);
765 var buttonicon = button.one('img');
767 // Determine whether the marker is currently set.
768 var togglestatus = section.hasClass('current');
771 // Set the current highlighted item text.
772 var old_string = M.util.get_string('markthistopic', 'moodle');
773 Y.one(SELECTOR.PAGECONTENT)
774 .all(M.mod_quiz.edit.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT)
775 .set('title', old_string);
776 Y.one(SELECTOR.PAGECONTENT)
777 .all(M.mod_quiz.edit.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT + ' img')
778 .set('alt', old_string)
779 .set('src', M.util.image_url('i/marker'));
781 // Remove the highlighting from all sections.
782 Y.one(SELECTOR.PAGECONTENT).all(M.mod_quiz.edit.get_section_selector(Y))
783 .removeClass('current');
785 // Then add it if required to the selected section.
787 section.addClass('current');
788 value = Y.Moodle.core_course.util.section.getId(section.ancestor(M.mod_quiz.edit.get_section_wrapper(Y), true));
789 var new_string = M.util.get_string('markedthistopic', 'moodle');
791 .set('title', new_string);
793 .set('alt', new_string)
794 .set('src', M.util.image_url('i/marked'));
797 // Change the highlight status.
803 var lightbox = M.util.add_lightbox(Y, section);
805 this.send_request(data, lightbox);
808 NAME : 'mod_quiz-section-toolbox',
822 M.mod_quiz.init_section_toolbox = function(config) {
823 return new SECTIONTOOLBOX(config);
834 "moodle-mod_quiz-quizbase",
835 "moodle-mod_quiz-util-slot",
836 "moodle-core-notification-ajaxexception"