mod/quiz/amd/src/modal_add_random_question.js

// 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 <http://www.gnu.org/licenses/>.

/**
 * Contain the logic for the add random question modal.
 *
 * @module     mod_quiz/modal_add_random_question
 * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

import $ from 'jquery';
import Modal from './add_question_modal';
import * as Notification from 'core/notification';
import * as Fragment from 'core/fragment';
import * as Templates from 'core/templates';
import * as FormChangeChecker from 'core_form/changechecker';
import {call as fetchMany} from 'core/ajax';
import Pending from 'core/pending';

const SELECTORS = {
    ANCHOR: 'a[href]',
    EXISTING_CATEGORY_CONTAINER: '[data-region="existing-category-container"]',
    EXISTING_CATEGORY_TAB: '#id_existingcategoryheader',
    NEW_CATEGORY_CONTAINER: '[data-region="new-category-container"]',
    NEW_CATEGORY_TAB: '#id_newcategoryheader',
    TAB_CONTENT: '[data-region="tab-content"]',
    ADD_ON_PAGE_FORM_ELEMENT: '[name="addonpage"]',
    ADD_RANDOM_BUTTON: 'input[type="submit"][name="addrandom"]',
    ADD_NEW_CATEGORY_BUTTON: 'input[type="submit"][name="newcategory"]',
    SUBMIT_BUTTON_ELEMENT: 'input[type="submit"][name="addrandom"], input[type="submit"][name="newcategory"]',
    FORM_HEADER: 'legend',
    SELECT_NUMBER_TO_ADD: '#menurandomcount',
    NEW_CATEGORY_ELEMENT: '#categoryname',
    PARENT_CATEGORY_ELEMENT: '#parentcategory',
    FILTER_CONDITION_ELEMENT: '[data-filtercondition]',
    FORM_ELEMENT: '#add_random_question_form',
    MESSAGE_INPUT: '[name="message"]',
    SWITCH_TO_OTHER_BANK: 'button[data-action="switch-question-bank"]',
    NEW_BANKMOD_ID: 'data-newmodid',
    BANK_SEARCH: '#searchbanks',
    GO_BACK_BUTTON: 'button[data-action="go-back"]',
};

export default class ModalAddRandomQuestion extends Modal {
    static TYPE = 'mod_quiz-quiz-add-random-question';
    static TEMPLATE = 'mod_quiz/modal_add_random_question';

    /**
     * Create the add random question modal.
     *
     * @param  {Number} contextId Current context id.
     * @param  {Number} bankCmId Current question bank course module id.
     * @param  {string} category Category id and category context id comma separated.
     * @param  {string} returnUrl URL to return to after form submission.
     * @param  {Number} quizCmId Current quiz course module id.
     * @param  {boolean} showNewCategory Display the New category tab when selecting random questions.
     */
    static init(
        contextId,
        bankCmId,
        category,
        returnUrl,
        quizCmId,
        showNewCategory = true
    ) {
        const selector = '.menu [data-action="addarandomquestion"]';
        document.addEventListener('click', (e) => {
            const trigger = e.target.closest(selector);
            if (!trigger) {
                return;
            }
            e.preventDefault();

            ModalAddRandomQuestion.create({
                contextId,
                bankCmId,
                category,
                returnUrl,
                quizCmId,
                showNewCategory,
                title: trigger.dataset.header,
                addOnPage: trigger.dataset.addonpage,

                templateContext: {
                    hidden: showNewCategory,
                },
            });
        });
    }

    /**
     * Constructor for the Modal.
     *
     * @param {object} root The root jQuery element for the modal
     */
    constructor(root) {
        super(root);
        this.category = null;
        this.returnUrl = null;
        this.quizCmId = null;
        this.loadedForm = false;
    }

    configure(modalConfig) {
        modalConfig.removeOnClose = true;

        this.setCategory(modalConfig.category);
        this.setReturnUrl(modalConfig.returnUrl);
        this.showNewCategory = modalConfig.showNewCategory;

        super.configure(modalConfig);
    }

    /**
     * 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
     */
    setAddOnPageId(id) {
        super.setAddOnPageId(id);
        this.getBody().find(SELECTORS.ADD_ON_PAGE_FORM_ELEMENT).val(id);
    }

    /**
     * Set the category for this form. The category is a comma separated
     * category id and category context id.
     *
     * @method setCategory
     * @param {string} category
     */
    setCategory(category) {
        this.category = category;
    }

    /**
     * Returns the saved category.
     *
     * @method getCategory
     * @return {string}
     */
    getCategory() {
        return this.category;
    }

    /**
     * Set the return URL for the form.
     *
     * @method setReturnUrl
     * @param {string} url
     */
    setReturnUrl(url) {
        this.returnUrl = url;
    }

    /**
     * Returns the return URL for the form.
     *
     * @method getReturnUrl
     * @return {string}
     */
    getReturnUrl() {
        return this.returnUrl;
    }

    /**
     * Moves a given form element inside (a child of) a given tab element.
     *
     * Hides the 'legend' (e.g. header) element of the form element because the
     * tab has the name.
     *
     * Moves the submit button into a footer element at the bottom of the form
     * element for styling purposes.
     *
     * @method moveContentIntoTab
     * @param  {jquery} tabContent The form element to move into the tab.
     * @param  {jquey} tabElement The tab element for the form element to move into.
     */
    moveContentIntoTab(tabContent, tabElement) {
        // Hide the header because the tabs show us which part of the form we're
        // looking at.
        tabContent.find(SELECTORS.FORM_HEADER).addClass('hidden');
        // Move the element inside a tab.
        tabContent.wrap(tabElement);
    }

    /**
     * Empty the tab content container and move all tabs from the form into the
     * tab container element.
     *
     * @method moveTabsIntoTabContent
     * @param  {jquery} form The form element.
     */
    moveTabsIntoTabContent(form) {
        // Empty it to remove the loading icon.
        const tabContent = this.getBody().find(SELECTORS.TAB_CONTENT).empty();
        // Make sure all tabs are inside the tab content element.
        form.find('[role="tabpanel"]').wrapAll(tabContent);
    }

    /**
     * Make sure all of the tabs have a cancel button in their fotter to sit along
     * side the submit button.
     *
     * @method moveCancelButtonToTabs
     * @param  {jquey} form The form element.
     */
    moveCancelButtonToTabs(form) {
        const cancelButton = form.find(SELECTORS.CANCEL_BUTTON_ELEMENT).addClass('ms-1');
        const tabFooters = form.find('[data-region="footer"]');
        // Remove the buttons container element.
        cancelButton.closest(SELECTORS.BUTTON_CONTAINER).remove();
        cancelButton.clone().appendTo(tabFooters);
    }

    /**
     * Load the add random question form in a fragement and perform some transformation
     * on the HTML to convert it into tabs for rendering in the modal.
     *
     * @method loadForm
     * @return {promise} Resolved with form HTML and JS.
     */
    loadForm() {
        const addonpage = this.getAddOnPageId();
        const returnurl = this.getReturnUrl();
        const quizcmid = this.quizCmId;
        const bankcmid = this.bankCmId;

        return Fragment.loadFragment(
            'mod_quiz',
            'add_random_question_form',
            this.getContextId(),
            {
                addonpage,
                returnurl,
                quizcmid,
                bankcmid,
            }
        )
            .then((html, js) => {
                const form = $(html);
                const existingCategoryTabContent = form.find(SELECTORS.EXISTING_CATEGORY_TAB);
                const existingCategoryTab = this.getBody().find(SELECTORS.EXISTING_CATEGORY_CONTAINER);
                const newCategoryTabContent = form.find(SELECTORS.NEW_CATEGORY_TAB);
                const newCategoryTab = this.getBody().find(SELECTORS.NEW_CATEGORY_CONTAINER);

                // Transform the form into tabs for better rendering in the modal.
                this.moveContentIntoTab(existingCategoryTabContent, existingCategoryTab);
                this.moveContentIntoTab(newCategoryTabContent, newCategoryTab);
                this.moveTabsIntoTabContent(form);

                Templates.replaceNode(this.getBody().find(SELECTORS.TAB_CONTENT), form, js);
                return;
            })
            .then(() => {
                // 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.
                FormChangeChecker.disableAllChecks();

                // Add question to quiz.
                this.getBody()[0].addEventListener('click', (e) => {
                    const button = e.target.closest(SELECTORS.SUBMIT_BUTTON_ELEMENT);
                    if (!button) {
                        return;
                    }
                    e.preventDefault();

                    // Intercept the submission to adjust the POST params so that the quiz mod id is set and not the bank module id.
                    document.querySelector('#questionscontainer input[name="cmid"]').setAttribute('name', this.quizCmId);

                    // Add Random questions if the add random button was clicked.
                    const addRandomButton = e.target.closest(SELECTORS.ADD_RANDOM_BUTTON);
                    if (addRandomButton) {
                        const randomcount = document.querySelector(SELECTORS.SELECT_NUMBER_TO_ADD).value;
                        const filtercondition = document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition;

                        this.addQuestions(quizcmid, addonpage, randomcount, filtercondition, '', '');
                        return;
                    }
                    // Add new category if the add category button was clicked.
                    const addCategoryButton = e.target.closest(SELECTORS.ADD_NEW_CATEGORY_BUTTON);
                    if (addCategoryButton) {
                        this.addQuestions(
                            quizcmid,
                            addonpage,
                            1,
                            '',
                            document.querySelector(SELECTORS.NEW_CATEGORY_ELEMENT).value,
                            document.querySelector(SELECTORS.PARENT_CATEGORY_ELEMENT).value
                        );
                        return;
                    }
                });

                this.getModal().on('click', SELECTORS.SWITCH_TO_OTHER_BANK, () => {
                    this.handleSwitchBankContentReload(SELECTORS.BANK_SEARCH)
                        .then(function(ModalQuizQuestionBank) {
                            $(SELECTORS.BANK_SEARCH)?.on('change', (e) => {
                                const bankCmId = $(e.currentTarget).val();
                                // Have to recreate the modal as we have already used the body for the switch bank content.
                                if (bankCmId > 0) {
                                    ModalAddRandomQuestion.create({
                                        'contextId': ModalQuizQuestionBank.getContextId(),
                                        'bankCmId': bankCmId,
                                        'category': ModalQuizQuestionBank.getCategory(),
                                        'returnUrl': ModalQuizQuestionBank.getReturnUrl(),
                                        'quizCmId': ModalQuizQuestionBank.quizCmId,
                                        'title': ModalQuizQuestionBank.originalTitle,
                                        'addOnPage': ModalQuizQuestionBank.getAddOnPageId(),
                                        'templateContext': {hidden: ModalQuizQuestionBank.showNewCategory},
                                        'showNewCategory': ModalQuizQuestionBank.showNewCategory,
                                    }).catch(Notification.exception);

                                    return ModalQuizQuestionBank;
                                }
                            });
                        })
                        .then((ModalQuizQuestionBank) => ModalQuizQuestionBank.destroy());
                });

                this.getModal().on('click', SELECTORS.GO_BACK_BUTTON, (e) => {
                    const anchorElement = $(e.currentTarget);
                    // Have to recreate the modal as we have already used the body for the switch bank content.
                    ModalAddRandomQuestion.create({
                        'contextId': this.getContextId(),
                        'bankCmId': anchorElement.attr('value'),
                        'category': this.getCategory(),
                        'returnUrl': this.getReturnUrl(),
                        'quizCmId': this.quizCmId,
                        'title': this.originalTitle,
                        'addOnPage': this.getAddOnPageId(),
                        'templateContext': {hidden: this.showNewCategory},
                        'showNewCategory': this.showNewCategory,
                    }).then(this.destroy()).catch(Notification.exception);
                });

                this.getModal().on('click', SELECTORS.ANCHOR, (e) => {
                    const anchorElement = $(e.currentTarget);
                    // Have to recreate the modal as we have already used the body for the switch bank content.
                    if (anchorElement.closest('a[' + SELECTORS.NEW_BANKMOD_ID + ']').length) {
                        ModalAddRandomQuestion.create({
                            'contextId': this.getContextId(),
                            'bankCmId': anchorElement.attr(SELECTORS.NEW_BANKMOD_ID),
                            'category': this.getCategory(),
                            'returnUrl': this.getReturnUrl(),
                            'quizCmId': this.quizCmId,
                            'title': this.originalTitle,
                            'addOnPage': this.getAddOnPageId(),
                            'templateContext': {hidden: this.showNewCategory},
                            'showNewCategory': this.showNewCategory,
                        }).then(this.destroy()).catch(Notification.exception);
                    }
                });
            })
            .catch(Notification.exception);
    }

    /**
     * Call web service function to add random questions
     *
     * @param {number} quizcmid the course module id of the quiz to add questions to.
     * @param {number} addonpage the page where random questions will be added to
     * @param {number} randomcount Number of random questions
     * @param {string} filtercondition Filter condition
     * @param {string} newcategory add new category
     * @param {string} parentcategory parent category of new category
     */
    async addQuestions(
        quizcmid,
        addonpage,
        randomcount,
        filtercondition,
        newcategory,
        parentcategory
    ) {
        // We do not need to resolve this Pending because the form submission will result in a page redirect.
        new Pending('mod-quiz/modal_add_random_questions');
        const call = {
            methodname: 'mod_quiz_add_random_questions',
            args: {
                cmid: quizcmid,
                addonpage,
                randomcount,
                filtercondition,
                newcategory,
                parentcategory,
            }
        };
        try {
            const response = await fetchMany([call])[0];
            const form = document.querySelector(SELECTORS.FORM_ELEMENT);
            const messageInput = form.querySelector(SELECTORS.MESSAGE_INPUT);
            messageInput.value = response.message;
            form.submit();
        } catch (e) {
            Notification.exception(e);
        }
    }

    /**
     * Override the modal show function to load the form when this modal is first
     * shown.
     *
     * @method show
     */
    show() {
        super.show(this);

        if (!this.loadedForm) {
            this.tabHtml = this.getBody();
            this.loadForm(window.location.search);
            this.loadedForm = true;
        }
    }
}

ModalAddRandomQuestion.registerModalType();