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',