diff --git a/mod/quiz/amd/build/modal_quiz_question_bank.min.js b/mod/quiz/amd/build/modal_quiz_question_bank.min.js new file mode 100644 index 00000000000..63b526ef271 --- /dev/null +++ b/mod/quiz/amd/build/modal_quiz_question_bank.min.js @@ -0,0 +1 @@ +define(["jquery","core/yui","core/notification","core/modal","core/modal_events","core/modal_registry","core/fragment"],function(a,b,c,d,e,f,g){var h=!1,i={ADD_TO_QUIZ_CONTAINER:"td.addtoquizaction",ANCHOR:"a[href]",PREVIEW_CONTAINER:"td.previewaction",SEARCH_OPTIONS:"#advancedsearch",DISPLAY_OPTIONS:"#displayoptions"},j=function(a){d.call(this,a),this.contextId=null,this.addOnPageId=null};return j.TYPE="mod_quiz-quiz-question-bank",j.prototype=Object.create(d.prototype),j.prototype.constructor=j,j.prototype.setContextId=function(a){this.contextId=a},j.prototype.getContextId=function(){return this.contextId},j.prototype.setAddOnPageId=function(a){this.addOnPageId=a},j.prototype.getAddOnPageId=function(){return this.addOnPageId},j.prototype.show=function(){return this.reloadBodyContent(window.location.search),d.prototype.show.call(this)},j.prototype.reloadBodyContent=function(a){var b=g.loadFragment("mod_quiz","quiz_question_bank",this.getContextId(),{querystring:a}).fail(c.exception);this.setBody(b)},j.prototype.handleAddToQuizEvent=function(a,b){var c=b.attr("href")+"&addonpage="+this.getAddOnPageId();b.attr("href",c)},j.prototype.handlePreviewContainerEvent=function(a,b){var c=["height=600","width=800","top=0","left=0","menubar=0","location=0","scrollbars","resizable","toolbar","status","directories=0","fullscreen=0","dependent"];window.openpopup(a,{url:b.attr("href"),name:"questionpreview",options:c.join(",")})},j.prototype.handleDisplayOptionFormEvent=function(b){b.stopPropagation(),b.preventDefault();var c=a(b.target).closest(i.DISPLAY_OPTIONS),d="?"+c.serialize();this.reloadBodyContent(d)},j.prototype.registerDisplayOptionListeners=function(){this.getModal().on("change",i.DISPLAY_OPTIONS,function(b){var c=a(b.target);c.attr("aria-autocomplete")||this.handleDisplayOptionFormEvent(b)}.bind(this)),this.getModal().on("submit",i.DISPLAY_OPTIONS,function(a){this.handleDisplayOptionFormEvent(a)}.bind(this))},j.prototype.registerEventListeners=function(){d.prototype.registerEventListeners.call(this),this.registerDisplayOptionListeners(),this.getModal().on("click",i.ANCHOR,function(b){var c=a(b.currentTarget);return c.closest(i.ADD_TO_QUIZ_CONTAINER).length?void this.handleAddToQuizEvent(b,c):c.closest(i.PREVIEW_CONTAINER).length?void this.handlePreviewContainerEvent(b,c):void(c.closest(i.SEARCH_OPTIONS).length||(b.preventDefault(),this.reloadBodyContent(c.prop("search"))))}.bind(this)),this.getRoot().on(e.bodyRendered,function(){b.use("moodle-core-formchangechecker",function(){M.core_formchangechecker.reset_form_dirty_state()})})},h||(f.register(j.TYPE,j,"core/modal"),h=!0),j}); \ No newline at end of file diff --git a/mod/quiz/amd/build/quizquestionbank.min.js b/mod/quiz/amd/build/quizquestionbank.min.js new file mode 100644 index 00000000000..4c3bfcca53b --- /dev/null +++ b/mod/quiz/amd/build/quizquestionbank.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/custom_interaction_events","core/modal_factory","mod_quiz/modal_quiz_question_bank"],function(a,b,c,d,e){var f={ADD_QUESTION_LINKS:'.menu [data-action="questionbank"]'};return{init:function(g){var h=a("body");d.create({type:e.TYPE,large:!0},[h,f.ADD_QUESTION_LINKS]).then(function(b){return b.setContextId(g),h.on(c.events.activate,f.ADD_QUESTION_LINKS,function(c){var d=a(c.target).closest(f.ADD_QUESTION_LINKS);b.setAddOnPageId(d.attr("data-addonpage")),b.setTitle(d.attr("data-header"))}),b}).fail(b.exception)}}}); \ No newline at end of file diff --git a/mod/quiz/amd/src/modal_quiz_question_bank.js b/mod/quiz/amd/src/modal_quiz_question_bank.js new file mode 100644 index 00000000000..618064583aa --- /dev/null +++ b/mod/quiz/amd/src/modal_quiz_question_bank.js @@ -0,0 +1,311 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Contain the logic for the question bank modal. + * + * @module mod_quiz/modal_quiz_question_bank + * @package mod_quiz + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define([ + 'jquery', + 'core/yui', + 'core/notification', + 'core/modal', + 'core/modal_events', + 'core/modal_registry', + 'core/fragment' +], +function( + $, + Y, + Notification, + Modal, + ModalEvents, + ModalRegistry, + Fragment +) { + + var registered = false; + var SELECTORS = { + ADD_TO_QUIZ_CONTAINER: 'td.addtoquizaction', + ANCHOR: 'a[href]', + PREVIEW_CONTAINER: 'td.previewaction', + SEARCH_OPTIONS: '#advancedsearch', + DISPLAY_OPTIONS: '#displayoptions', + }; + + /** + * Constructor for the Modal. + * + * @param {object} root The root jQuery element for the modal + */ + var ModalQuizQuestionBank = function(root) { + Modal.call(this, root); + + this.contextId = null; + this.addOnPageId = null; + }; + + ModalQuizQuestionBank.TYPE = 'mod_quiz-quiz-question-bank'; + ModalQuizQuestionBank.prototype = Object.create(Modal.prototype); + ModalQuizQuestionBank.prototype.constructor = ModalQuizQuestionBank; + + /** + * Save the Moodle context id that the question bank is being + * rendered in. + * + * @method setContextId + * @param {int} id + */ + ModalQuizQuestionBank.prototype.setContextId = function(id) { + this.contextId = id; + }; + + /** + * Retrieve the saved Moodle context id. + * + * @method getContextId + * @return {int} + */ + ModalQuizQuestionBank.prototype.getContextId = function() { + return this.contextId; + }; + + /** + * Set the id of the page that the question should be added to + * when the user clicks the add to quiz link. + * + * @method setAddOnPageId + * @param {int} id + */ + ModalQuizQuestionBank.prototype.setAddOnPageId = function(id) { + this.addOnPageId = id; + }; + + /** + * Returns the saved page id for the question to be added it. + * + * @method getAddOnPageId + * @return {int} + */ + ModalQuizQuestionBank.prototype.getAddOnPageId = function() { + return this.addOnPageId; + }; + + /** + * Override the parent show function. + * + * Reload the body contents when the modal is shown. The current + * window URL is used to inform the new content that should be + * displayed. + * + * @method show + * @return {void} + */ + ModalQuizQuestionBank.prototype.show = function() { + this.reloadBodyContent(window.location.search); + return Modal.prototype.show.call(this); + }; + + /** + * Replaces the current body contents with a new version of the question + * bank. + * + * The contents of the question bank are generated using the provided + * query string. + * + * @method reloadBodyContent + * @param {string} queryString URL encoded string. + */ + ModalQuizQuestionBank.prototype.reloadBodyContent = function(queryString) { + // Load the question bank fragment to be displayed in the modal. + var promise = Fragment.loadFragment( + 'mod_quiz', + 'quiz_question_bank', + this.getContextId(), + { + querystring: queryString + } + ).fail(Notification.exception); + + this.setBody(promise); + }; + + /** + * Update the URL of the anchor element that the user clicked on to make + * sure that the question is added to the correct page. + * + * @method handleAddToQuizEvent + * @param {event} e A JavaScript event + * @param {object} anchorElement The anchor element that was triggered + */ + ModalQuizQuestionBank.prototype.handleAddToQuizEvent = function(e, anchorElement) { + // If the user clicks the plus icon to add the question to the page + // directly then we need to intercept the click in order to adjust the + // href and include the correct add on page id before the page is + // redirected. + var href = anchorElement.attr('href') + '&addonpage=' + this.getAddOnPageId(); + anchorElement.attr('href', href); + }; + + /** + * Open a popup window to show the preview of the question. + * + * @method handlePreviewContainerEvent + * @param {event} e A JavaScript event + * @param {object} anchorElement The anchor element that was triggered + */ + ModalQuizQuestionBank.prototype.handlePreviewContainerEvent = function(e, anchorElement) { + var popupOptions = [ + 'height=600', + 'width=800', + 'top=0', + 'left=0', + 'menubar=0', + 'location=0', + 'scrollbars', + 'resizable', + 'toolbar', + 'status', + 'directories=0', + 'fullscreen=0', + 'dependent' + ]; + window.openpopup(e, { + url: anchorElement.attr('href'), + name: 'questionpreview', + options: popupOptions.join(',') + }); + }; + + /** + * Reload the modal body with the new display options the user has selected. + * + * A query string is built using the form elements to be used to generate the + * new body content. + * + * @method handleDisplayOptionFormEvent + * @param {event} e A JavaScript event + */ + ModalQuizQuestionBank.prototype.handleDisplayOptionFormEvent = function(e) { + // Stop propagation to prevent other wild event handlers + // from submitting the form on change. + e.stopPropagation(); + e.preventDefault(); + + var form = $(e.target).closest(SELECTORS.DISPLAY_OPTIONS); + var queryString = '?' + form.serialize(); + this.reloadBodyContent(queryString); + }; + + /** + * Listen for changes to the display options form. + * + * This handles the user changing: + * - The quiz category select box + * - The tags to filter by + * - Show/hide questions from sub categories + * - Show/hide old questions + * + * @method registerDisplayOptionListeners + */ + ModalQuizQuestionBank.prototype.registerDisplayOptionListeners = function() { + // Listen for changes to the display options form. + this.getModal().on('change', SELECTORS.DISPLAY_OPTIONS, function(e) { + // Get the element that was changed. + var modifiedElement = $(e.target); + if (modifiedElement.attr('aria-autocomplete')) { + // If the element that was change is the autocomplete + // input then we should ignore it because that is for + // display purposes only. + return; + } + + this.handleDisplayOptionFormEvent(e); + }.bind(this)); + + // Listen for the display options form submission because the tags + // filter will submit the form when it is changed. + this.getModal().on('submit', SELECTORS.DISPLAY_OPTIONS, function(e) { + this.handleDisplayOptionFormEvent(e); + }.bind(this)); + }; + + /** + * Set up all of the event handling for the modal. + * + * @method registerEventListeners + */ + ModalQuizQuestionBank.prototype.registerEventListeners = function() { + // Apply parent event listeners. + Modal.prototype.registerEventListeners.call(this); + + // Set up the event handlers for all of the display options. + this.registerDisplayOptionListeners(); + + this.getModal().on('click', SELECTORS.ANCHOR, function(e) { + var anchorElement = $(e.currentTarget); + + // If the anchor element was the add to quiz link. + if (anchorElement.closest(SELECTORS.ADD_TO_QUIZ_CONTAINER).length) { + this.handleAddToQuizEvent(e, anchorElement); + return; + } + + // If the anchor element was a preview question link. + if (anchorElement.closest(SELECTORS.PREVIEW_CONTAINER).length) { + this.handlePreviewContainerEvent(e, anchorElement); + return; + } + + // Click on expand/collaspse search-options. Has its own handler. + // We should not interfere. + if (anchorElement.closest(SELECTORS.SEARCH_OPTIONS).length) { + return; + } + + // Anything else means reload the pop-up contents. + e.preventDefault(); + this.reloadBodyContent(anchorElement.prop('search')); + }.bind(this)); + + // Disable the form change checker when the body is rendered. + this.getRoot().on(ModalEvents.bodyRendered, function() { + // Make sure the form change checker is disabled otherwise it'll + // stop the user from navigating away from the page once the modal + // is hidden. + Y.use('moodle-core-formchangechecker', function() { + M.core_formchangechecker.reset_form_dirty_state(); + }); + }); + }; + + // Automatically register with the modal registry the first time this module is + // imported so that you can create modals of this type using the modal factory. + if (!registered) { + ModalRegistry.register( + ModalQuizQuestionBank.TYPE, + ModalQuizQuestionBank, + 'core/modal' + ); + + registered = true; + } + + return ModalQuizQuestionBank; +}); diff --git a/mod/quiz/amd/src/quizquestionbank.js b/mod/quiz/amd/src/quizquestionbank.js new file mode 100644 index 00000000000..70014219406 --- /dev/null +++ b/mod/quiz/amd/src/quizquestionbank.js @@ -0,0 +1,77 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Initialise the question bank modal on the quiz page. + * + * @module mod_quiz/quizquestionbank + * @package mod_quiz + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( + [ + 'jquery', + 'core/notification', + 'core/custom_interaction_events', + 'core/modal_factory', + 'mod_quiz/modal_quiz_question_bank' + ], + function( + $, + Notification, + CustomEvents, + ModalFactory, + ModalQuizQuestionBank + ) { + + var SELECTORS = { + ADD_QUESTION_LINKS: '.menu [data-action="questionbank"]', + }; + + return { + init: function(contextId) { + var body = $('body'); + + // Create a question bank modal using the factory. + // The same modal will be used by all of the add question + // links on the page. The content of the modal will be + // changed depending on which link is clicked. + ModalFactory.create( + { + type: ModalQuizQuestionBank.TYPE, + large: true + }, + // Created a deligated listener rather than a single + // trigger element. + [body, SELECTORS.ADD_QUESTION_LINKS] + ).then(function(modal) { + // Save the Moodle context id that the modal is being rendered in. + modal.setContextId(contextId); + + body.on(CustomEvents.events.activate, SELECTORS.ADD_QUESTION_LINKS, function(e) { + // We need to listen for activations on the trigger elements because there are + // several on the page and we need to know which one was activated in order to + // set some relevant data on the modal. + var triggerElement = $(e.target).closest(SELECTORS.ADD_QUESTION_LINKS); + modal.setAddOnPageId(triggerElement.attr('data-addonpage')); + modal.setTitle(triggerElement.attr('data-header')); + }); + + return modal; + }).fail(Notification.exception); + } + }; +}); diff --git a/mod/quiz/classes/output/edit_renderer.php b/mod/quiz/classes/output/edit_renderer.php index 09350bbdc36..e12ae6250eb 100644 --- a/mod/quiz/classes/output/edit_renderer.php +++ b/mod/quiz/classes/output/edit_renderer.php @@ -100,10 +100,9 @@ class edit_renderer extends \plugin_renderer_base { if ($structure->can_be_edited()) { $popups = ''; - $popups .= $this->question_bank_loading(); - $this->page->requires->yui_module('moodle-mod_quiz-quizquestionbank', - 'M.mod_quiz.quizquestionbank.init', - array('class' => 'questionbank', 'cmid' => $structure->get_cmid())); + $this->page->requires->js_call_amd('mod_quiz/quizquestionbank', 'init', [ + $contexts->lowest()->id + ]); $popups .= $this->random_question_form($pageurl, $contexts, $pagevars); $this->page->requires->yui_module('moodle-mod_quiz-randomquestion',