fewieden/MMM-Modal

View on GitHub
MMM-Modal.js

Summary

Maintainability
B
4 hrs
Test Coverage
/**
 * @file MMM-Modal.js
 *
 * @author fewieden
 * @license MIT
 *
 * @see  https://github.com/fewieden/MMM-Modal
 */

/* global Module Log */

/**
 * @external Module
 * @see https://github.com/MichMich/MagicMirror/blob/master/js/module.js
 */

/**
 * @external Log
 * @see https://github.com/MichMich/MagicMirror/blob/master/js/logger.js
 */

/**
 * @module MMM-Modal
 * @description Frontend of the MagicMirror² module.
 *
 * @requires external:Module
 * @requires external:Log
 */
Module.register('MMM-Modal', {
    /**
     * @member {string} requiresVersion - Defines the required minimum version of the MagicMirror framework in order to run this verion of the module.
     */
    requiresVersion: '2.15.0',

    /**
     * @member {Object} defaults - Defines the default config values.
     * @property {boolean|number} timer - Flag to disable timer or seconds to show modal.
     * @property {boolean} touch - Flag to en-/disable touch support.
     * @property {boolean} showModuleName - Flag to show/hide the module name of the modal.
     */
    defaults: {
        timer: 15 * 1000,
        touch: false,
        showModuleName: true
    },

    /**
     * @member {string} defaultTemplate - Path to fallback of inner modal template.
     */
    defaultTemplate: 'templates/InnerTemplate.njk',

    /**
     * @member {object|null} modal - Modal with template path, data and options.
     * @property {string} template - Path to modal template.
     * @property {string} senderName - Name of the module which sent the modal.
     * @property {object} data - Dynamic values for displaying in the modal.
     * @property {object} options - Options for displaying in the modal.
     */
    modal: null,

    /**
     * @member {Object} voice - Defines the default mode and commands of this module.
     * @property {string} mode - Voice mode of this module.
     * @property {string[]} sentences - List of voice commands of this module.
     */
    voice: {
        mode: 'MODAL',
        sentences: [
            'OPEN HELP',
            'CLOSE HELP',
            'CLOSE MODAL',
            'CANCEL MODAL',
            'CONFIRM MODAL',
        ]
    },

    /**
     * @function createTimer
     * @description Creates a timeout to close modal after specified time.
     *
     * @returns {void}
     */
    createTimer() {
        if (this.config.timer) {
            clearTimeout(this.timer);
            this.timer = setTimeout(() => this.closeModal(false), this.config.timer);
        }
    },

    /**
     * @function suspend
     * @description Closes the current modal when the module gets suspended.
     * @override
     */
    suspend() {
        this.closeModal(false);
        Log.log(`${this.name} is suspended.`);
    },

    /**
     * @function notificationReceived
     * @description Handles incoming broadcasts from other modules or the MagicMirror² core.
     * @override
     *
     * @param {string} notification - Notification name
     * @param {*} payload - Detailed payload of the notification.
     * @param {Object} sender - Module that sent the notification or undefined for MagicMirror² core.
     */
    notificationReceived(notification, payload, sender) {
        if (notification === 'ALL_MODULES_STARTED') {
            this.sendNotification('REGISTER_VOICE_MODULE', this.voice);
        } else if (notification === `VOICE_${this.voice.mode}` && sender.name === 'MMM-voice') {
            this.handleModals(payload);
        } else if (notification === 'VOICE_MODE_CHANGED' && sender.name === 'MMM-voice'
            && payload.old === this.voice.mode) {
            this.closeModal(false);
        } else if (notification === 'OPEN_MODAL') {
            this.handleModals(notification, payload, sender);
        } else if (this.isDialogAction(notification, sender)) {
            this.handleModals(notification, payload, sender);
        } else if (notification === 'CLOSE_MODAL' || notification === 'DOM_OBJECTS_CREATED') {
            this.closeModal(false);
        }
    },

    /**
     * @function isDialogAction
     * @description Checks if the modal contains a dialog action and the sender is owner of the modal.
     *
     * @param {string} notification - Notification received from other module.
     * @param {string} sender - Module which send the notification.
     *
     * @returns {boolean} Is the notification a valid dialog action?
     */
    isDialogAction(notification, sender) {
        const identifier = sender ? sender.identifier : this.identifier;

        return this.modal && identifier === this.modal.identifier && this.modal.options && this.modal.options.isDialog === true
            && (notification === 'CANCEL_MODAL' || notification === 'CONFIRM_MODAL');
    },

    /**
     * @function getStyles
     * @description Style dependencies for this module.
     * @override
     *
     * @returns {string[]} List of the style dependency filepaths.
     */
    getStyles() {
        return [`${this.name}.css`];
    },

    /**
     * @function getTranslations
     * @description Translations for this module.
     * @override
     *
     * @returns {Object.<string, string>} Available translations for this module (key: language code, value: filepath).
     */
    getTranslations() {
        return {
            en: 'translations/en.json',
            de: 'translations/de.json'
        };
    },

    /**
     * @function getTemplate
     * @description Nunjuck template.
     * @override
     *
     * @returns {string} Path to nunjuck template.
     */
    getTemplate() {
        return `${this.name}/templates/${this.name}.njk`;
    },

    /**
     * @function getTemplateData
     * @description Data that gets rendered in the nunjuck template.
     * @override
     *
     * @returns {object} Data for the nunjuck template.
     */
    getTemplateData() {
        const senderName = this.modal.template ? this.modal.senderName : this.name;

        let innerTemplate = this.defaultTemplate;
        if (this.modal.template) {
            innerTemplate = `${senderName}/${this.modal.template}`;
        }

        return {
            innerTemplate,
            senderName,
            config: this.config,
            data: this.modal.data,
            options: this.modal.options
        };
    },

    /**
     * @function handleModals
     * @description Hide/show modals based on voice commands or module notifications.
     *
     * @param {string} command - Command for open and closing the modal.
     * @param {*} payload - Detailed payload of the notification.
     * @param {object} sender - Contains name and identifier of the module, which sent the command.
     *
     * @returns {void}
     */
    handleModals(command, payload, sender) {
        if (/CLOSE/g.test(command) && !/OPEN/g.test(command)) {
            this.closeModal(false);
        } else if (/CANCEL/g.test(command) && !/CONFIRM/g.test(command)) {
            this.closeModal(false);
        } else if (/CONFIRM/g.test(command) && !/CANCEL/g.test(command)) {
            this.closeModal(true);
        } else if (/OPEN/g.test(command) && !/CLOSE/g.test(command)) {
            this.notifyModule(false);

            let modal = payload;

            if (!sender) {
                modal = {
                    identifier: this.identifier,
                    senderName: this.name,
                    template: 'templates/HelpModal.njk',
                    data: this.voice,
                    options: {}
                }
            } else {
                modal.senderName = sender.name;
                modal.identifier = sender.identifier;
                modal.options = modal.options || {};
                modal.data = modal.data || {};
            }

            this.modal = modal;

            this.openModal();
        }
    },

    /**
     * @function nunjuckPath
     * @description Path to modules directory for nunjuck loader.
     *
     * @returns {string} File path.
     */
    nunjuckPath() {
        return this.data.path.replace(this.name, '').replace('//', '/');
    },

    /**
     * @function getDom
     * @description Generates the DOM of the module. Called by the MagicMirror² core.
     * @override
     *
     * @returns {Element} The DOM to display.
     */
    getDom() {
        const wrapper = document.createElement('div');

        if (!this.modal) {
            return wrapper;
        }

        this.nunjucksEnvironment().render(this.getTemplate(), this.getTemplateData(), (err, res) => {
            wrapper.innerHTML = res;

            if (this.config.touch) {
                const actions = [
                    {name: 'close', confirmed: false},
                    {name: 'cancel', confirmed: false},
                    {name: 'confirm', confirmed: true},
                ];

                for (const {name, confirmed} of actions) {
                    const element = wrapper.querySelector(`.btn-${name}`);

                    if (element) {
                        element.addEventListener('click', () => {
                            this.closeModal(confirmed);
                        });
                    }
                }
            }

            if (err) {
                if (this.modal.options.callback) {
                    this.modal.options.callback(err);
                }

                this.closeModal(false);
            }
        });

        return wrapper;
    },

    /**
     * @function file
     * @description Retrieve the path to a module file.
     * @override
     *
     * @param {string} file - File name
     *
     * @returns {string} File path.
     */
    file(file) {
        if (file === '') {
            return this.nunjuckPath();
        }

        return `${this.data.path}/${file}`.replace('//', '/');
    },

    /**
     * @function openModal
     * @description Displays the modal.
     *
     * @returns {void}
     */
    openModal() {
        this.show(0, () => {
            this.createTimer();
            this.toggleBlur();

            this.updateDom(300);
            setTimeout(() => {
                if (this.modal && this.modal.options.callback) {
                    this.modal.options.callback(null);
                }
            }, 300);
        }, {
            lockString: this.identifier,
            onError: error => {
                Log.error('Could not show module because of', error);

                if (this.modal && this.modal.options.callback) {
                    this.modal.options.callback(error);
                }
            }
        });
    },

    /**
     * @function notifyModule
     * @description Notify other module about dialog result.
     *
     * @param {boolean} confirmed - Was the dialog of the modal confirmed or not.
     *
     * @returns {void}
     */
    notifyModule(confirmed) {
        if (this.modal && this.modal.identifier !== this.identifier) {
            this.sendNotification('MODAL_CLOSED', {
                identifier: this.modal.identifier,
                confirmed
            });
        }
    },

    /**
     * @function closeModal
     * @description Close the current modal.
     *
     * @param {boolean} confirmed - Was the dialog of the modal confirmed or not.
     *
     * @returns {void}
     */
    closeModal(confirmed) {
        if (!this.modal) {
            return;
        }

        this.notifyModule(confirmed);

        clearTimeout(this.timer);
        this.modal = null;
        this.updateDom(300);
        this.hide(0, {lockString: this.identifier});
        this.toggleBlur();
    },

    /**
     * @function toggleBlur
     * @description Toggles blur over all modules. The DOM is addressed directly.
     *
     * @returns {void}
     */
    toggleBlur() {
        const modules = document.querySelectorAll('.module');
        for (let i = 0; i < modules.length; i += 1) {
            if (!modules[i].classList.contains('MMM-Modal')) {
                if (this.modal) {
                    modules[i].classList.add('MMM-Modal-blur');
                } else {
                    modules[i].classList.remove('MMM-Modal-blur');
                }
            }
        }
    }
});