lib/amd/src/modal_factory.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/>.

/**
 * Create a modal.
 *
 * @module     core/modal_factory
 * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
        'core/modal_save_cancel', 'core/modal_cancel', 'core/local/modal/alert',
        'core/templates', 'core/notification', 'core/custom_interaction_events',
        'core/pending'],
    function($, ModalEvents, ModalRegistry, Modal, ModalSaveCancel,
        ModalCancel, ModalAlert, Templates, Notification, CustomEvents, Pending) {

    // The templates for each type of modal.
    var TEMPLATES = {
        DEFAULT: 'core/modal',
        SAVE_CANCEL: 'core/modal_save_cancel',
        CANCEL: 'core/modal_cancel',
        ALERT: 'core/local/modal/alert',
    };

    /**
     * The available types of modals.
     *
     * @constant
     * @static
     * @public
     * @property {String} DEFAULT The default modal
     * @property {String} SAVE_CANCEL A modal which can be used to either save, or cancel.
     * @property {String} CANCEL A modal which displayed a cancel button
     * @property {String} ALERT An information modal which only displays information.
     */
    var TYPES = {
        DEFAULT: 'DEFAULT',
        SAVE_CANCEL: 'SAVE_CANCEL',
        CANCEL: 'CANCEL',
        ALERT: 'ALERT',
    };

    // Register the common set of modals.
    ModalRegistry.register(TYPES.DEFAULT, Modal, TEMPLATES.DEFAULT);
    ModalRegistry.register(TYPES.SAVE_CANCEL, ModalSaveCancel, TEMPLATES.SAVE_CANCEL);
    ModalRegistry.register(TYPES.CANCEL, ModalCancel, TEMPLATES.CANCEL);
    ModalRegistry.register(TYPES.ALERT, ModalAlert, TEMPLATES.ALERT);

    /**
     * Set up the events required to show the modal and return focus when the modal
     * is closed.
     *
     * @method setUpTrigger
     * @private
     * @param {Promise} modalPromise The modal instance
     * @param {object} triggerElement The jQuery element to open the modal
     * @param {object} modalConfig The modal configuration given to the factory
     */
    var setUpTrigger = function(modalPromise, triggerElement, modalConfig) {
        // The element that actually shows the modal.
        var actualTriggerElement = null;
        // Check if the client has provided a callback function to be called
        // before the modal is displayed.
        var hasPreShowCallback = (typeof modalConfig.preShowCallback == 'function');
        // Function to handle the trigger element being activated.
        var triggeredCallback = function(e, data) {
            var pendingPromise = new Pending('core/modal_factory:setUpTrigger:triggeredCallback');
            actualTriggerElement = $(e.currentTarget);
            modalPromise.then(function(modal) {
                if (hasPreShowCallback) {
                    // If the client provided a pre-show callback then execute
                    // it now before showing the modal.
                    modalConfig.preShowCallback(actualTriggerElement, modal);
                }

                modal.show();

                return modal;
            })
            .then(pendingPromise.resolve);
            data.originalEvent.preventDefault();
        };

        // The trigger element can either be a single element or it can be an
        // element + selector pair to create a delegated event handler to trigger
        // the modal.
        if (Array.isArray(triggerElement)) {
            var selector = triggerElement[1];
            triggerElement = triggerElement[0];

            CustomEvents.define(triggerElement, [CustomEvents.events.activate]);
            triggerElement.on(CustomEvents.events.activate, selector, triggeredCallback);
        } else {
            CustomEvents.define(triggerElement, [CustomEvents.events.activate]);
            triggerElement.on(CustomEvents.events.activate, triggeredCallback);
        }

        modalPromise.then(function(modal) {
            modal.getRoot().on(ModalEvents.hidden, function() {
                // Focus on the trigger element that actually launched the modal.
                if (actualTriggerElement !== null) {
                    actualTriggerElement.focus();
                }
            });

            return modal;
        });
    };

    /**
     * Create the correct instance of a modal based on the givem type. Sets up
     * the trigger between the modal and the trigger element.
     *
     * @method createFromElement
     * @private
     * @param {object} registryConf A config from the ModalRegistry
     * @param {object} modalElement The modal HTML jQuery object
     * @return {object} Modal instance
     */
    var createFromElement = function(registryConf, modalElement) {
        modalElement = $(modalElement);
        var Module = registryConf.module;
        var modal = new Module(modalElement);

        return modal;
    };

    /**
     * Create the correct modal instance for the given type, including loading
     * the correct template.
     *
     * @method createFromType
     * @private
     * @param {object} registryConf A config from the ModalRegistry
     * @param {object} templateContext The context to render the template with
     * @returns {promise} Resolved with a Modal instance
     */
    var createFromType = function(registryConf, templateContext) {
        var templateName = registryConf.template;

        var modalPromise = Templates.render(templateName, templateContext)
            .then(function(html) {
                var modalElement = $(html);
                return createFromElement(registryConf, modalElement);
            })
            .fail(Notification.exception);

        return modalPromise;
    };

    /**
     * Create a Modal instance.
     *
     * @method create
     * @param {object} modalConfig The configuration to create the modal instance
     * @param {object} triggerElement The trigger HTML jQuery object
     * @return {promise} Resolved with a Modal instance
     */
    var create = function(modalConfig, triggerElement) {
        var type = modalConfig.type || TYPES.DEFAULT;
        var isLarge = modalConfig.large ? true : false;
        // If 'scrollable' is not configured, set the modal to be scrollable by default.
        var isScrollable = modalConfig.hasOwnProperty('scrollable') ? modalConfig.scrollable : true;
        var registryConf = null;
        var templateContext = {};

        registryConf = ModalRegistry.get(type);

        if (!registryConf) {
            Notification.exception({message: 'Unable to find modal of type: ' + type});
        }

        if (typeof modalConfig.templateContext != 'undefined') {
            templateContext = modalConfig.templateContext;
        }

        var pendingModalPromise = new Pending('core/modal_factory:create');
        var modalPromise = createFromType(registryConf, templateContext)
            .then(function(modal) {
                if (typeof modalConfig.title != 'undefined') {
                    modal.setTitle(modalConfig.title);
                }

                if (typeof modalConfig.body != 'undefined') {
                    modal.setBody(modalConfig.body);
                }

                if (typeof modalConfig.footer != 'undefined') {
                    modal.setFooter(modalConfig.footer);
                }

                if (modalConfig.buttons) {
                    Object.entries(modalConfig.buttons).forEach(function([key, value]) {
                        modal.setButtonText(key, value);
                    });
                }

                if (isLarge) {
                    modal.setLarge();
                }

                if (typeof modalConfig.removeOnClose !== 'undefined') {
                    // If configured remove the modal when hiding it.
                    modal.setRemoveOnClose(modalConfig.removeOnClose);
                }

                modal.setScrollable(isScrollable);
                pendingModalPromise.resolve();
                return modal;
            });

        if (typeof triggerElement != 'undefined') {
            setUpTrigger(modalPromise, triggerElement, modalConfig);
        }

        return modalPromise;
    };

    return {
        create: create,
        types: TYPES,
    };
});