nw-skeleton

Source: app-wrapper/js/helper/modalHelper.js

/**
 * @fileOverview ModalHelper class file
 * @author Dino Ivankov <dinoivankov@gmail.com>
 * @version 1.3.1
 */

const _ = require('lodash');
const AppBaseClass = require('../lib/appBase').AppBaseClass;

var _appWrapper;
var appState;

/**
 * ModalHelper class - handles and manages modal dialogs
 *
 * @class
 * @extends {appWrapper.AppBaseClass}
 * @memberof appWrapper.helpers
 */
class ModalHelper extends AppBaseClass {

    /**
     * Creates ModalHelper instance
     *
     * @constructor
     * @return {ModalHelper}              Instance of ModalHelper class
     */
    constructor() {
        super();

        _appWrapper = this.getAppWrapper();
        appState = this.getAppState();

        this.boundMethods = {
            confirmResolve: null,
            queryResolve: null,
            queryReject: null,
        };

        this.timeouts = {
            autoClose: null
        };

        this.intervals = {
            autoClose: null
        };

        return this;
    }

    /**
     * Initializes ModalHelper object instance
     *
     * @async
     * @return {ModalHelper}    Instance of ModalHelper class
     */
    async initialize () {
        await super.initialize();
        appState.modalData.currentModal = this.getModalObject('defaultModal', {});
        return this;
    }

    /**
     * Sets modal dialog status to busy using message from argument (or default one if no message is passed)
     *
     * @param  {string} message Optional message to display in modal
     * @return {undefined}
     */
    modalBusy (message) {
        let cm = appState.modalData.currentModal;
        if (!cm.busy){
            if (!message){
                message = this.translate('Please wait...');
            }
            appState.modalData.currentModal.busyText = message;
            appState.modalData.currentModal.busy = true;
        }
    }

    /**
     * Sets modal dialog status to not busy
     *
     * @return {undefined}
     */
    modalNotBusy () {
        appState.modalData.currentModal.busy = false;
    }

    /**
     * Sets modal dialog ready status to true
     *
     * @return {undefined}
     */
    modalReady () {
        appState.modalData.currentModal.ready = true;
    }

    /**
     * Resets modal dialog ready status to false
     *
     * @return {undefined}
     */
    modalNotReady () {
        appState.modalData.currentModal.ready = false;
    }

    /**
     * Closes currently visible modal dialog
     *
     * @param  {Boolean} force Flag to force modal closing even if modal is busy
     * @return {undefined}
     */
    closeCurrentModal (force){
        let md = appState.modalData;
        let cm = md.currentModal;

        if (!cm.busy || force === true) {
            this.log('Closing current modal.', 'info', []);
            this.setModalVars({
                opening: false,
                closing: true
            });
            if (force === true){
                md.fadeModal = 'none';
            }
            this.stopAutoCloseModal();
            this.resetModalActions();
            if (cm.animateSize){
                this.modalBusy();
                this.modalNotReady();
            } else {
                md.modalVisible = false;
            }

            cm.autoCloseTime = null;
            md.modalElement = null;

            appState.status.noHandlingKeys = false;
        } else {
            this.log('Can not close modal because it is busy', 'warning', []);
        }
    }

    /**
     * Closes current modal dialog with delay, setting busy status and message grom argument
     *
     * @param  {Integer} delay      Delay in milliseconds
     * @param  {string} busyText    Busy message to show
     * @param  {Boolean} force      Flag to force modal closing even if modal is busy
     * @return {undefined}
     */
    closeCurrentModalDelayed (delay, busyText, force){
        if (!delay){
            delay = 1000;
        }
        if (!appState.modalData.currentModal.busy || force) {
            this.modalNotReady();
            this.modalBusy(busyText);
            this.setModalVars({
                opening: false,
                closing: true
            });

            setTimeout( () => {
                this.emptyModal();
                this.modalNotBusy();
                this.closeCurrentModal();
            }, delay);
        } else {
            this.log('Can not delayed close modal because it is already busy', 'warning', []);
        }
    }

    /**
     * Opens current modal dialog
     *
     * @return {undefined}
     */
    openCurrentModal () {
        let fadeModal = appState.modalData.fadeModal;
        let duration = parseInt(parseFloat(_appWrapper.getHelper('style').getCssVarValue('--long-animation-duration'), 10) * 1000, 10);
        let md = appState.modalData;
        let cm = md.currentModal;

        this.setModalVars({
            opening: true,
            closing: false
        });

        this.log('Opening current modal.', 'info', []);

        if (cm.autoCloseTime) {
            cm.autoCloseTimeText = parseInt(cm.autoCloseTime / 1000, 10);
        }

        cm.messages = [];
        cm.currentMessageIndex = -1;
        md.modalVisible = true;
        md.modalElement = document.querySelector('.modal-dialog');

        this.stopAutoCloseModal();

        if (cm.autoCloseTime) {
            cm.autoCloseTimeText = _appWrapper.getHelper('format').formatDurationCustom(cm.autoCloseTime / 1000);
        }

        if (cm.noHandlingKeys) {
            appState.status.noHandlingKeys = true;
        }

        if (!cm.showContentImmediately){
            setTimeout(() => {
                if (cm.bodyComponent != cm.defaultBodyComponent){
                    cm.bodyComponent = cm.defaultBodyComponent;
                }
                this.modalNotBusy();
                this.modalReady();
            }, duration);
        } else {
            md.fadeModal = 'none';
            if (cm.bodyComponent != cm.defaultBodyComponent){
                cm.bodyComponent = cm.defaultBodyComponent;
            }
            this.modalNotBusy();
            this.modalReady();
            md.fadeModal = fadeModal;
        }
        if (cm.autoCloseTime) {
            this.autoCloseModal();
        }
    }

    /**
     * Opens simple modal dialog using basic parameters from method arguments
     *
     * @async
     * @param  {string}     title         Modal title
     * @param  {string}     text          Modal text
     * @param  {Object}     options       Modal option overrides
     * @param  {Function}   confirmAction Confirm modal callback
     * @param  {Function}   cancelAction  Cancel / close modal callback
     * @return {undefined}
     */
    async openSimpleModal(title, text, options, confirmAction, cancelAction) {
        this.closeCurrentModal(true);
        this.log('Opening simple modal.', 'info', []);
        appState.modalData.currentModal = _.cloneDeep(appState.appModals.defaultModal);
        if (options && _.isObject(options)){
            appState.modalData.currentModal = _.merge(appState.modalData.currentModal, options);
        }
        appState.modalData.currentModal.title = title;
        appState.modalData.currentModal.body = text;
        if (confirmAction){
            _appWrapper._confirmModalAction = confirmAction;
        }
        if (cancelAction){
            _appWrapper._cancelModalAction = cancelAction;
        }
        this.openCurrentModal();
    }

    /**
     * Resolve method for confirm modal promise
     *
     * @async
     * @param  {Event} e Event that triggered the method
     * @return {undefined}
     */
    async confirmResolve (e) {
        if (e && e.preventDefault && _.isFunction(e.preventDefault)){
            e.preventDefault();
        }
        this.closeCurrentModal();
    }

    /**
     * Opens confirm modal dialog using basic parameters from method arguments
     *
     * @async
     * @param  {string}     title               Modal title
     * @param  {string}     text                Modal text
     * @param  {string}     confirmButtonText   Confirm button text
     * @param  {string}     cancelButtonText    Cancel button text
     * @param  {Function}   confirmAction       Modal confirm callback
     * @return {undefined}
     */
    async confirm (title, text, confirmButtonText, cancelButtonText, confirmAction) {
        this.log('Opening confirm modal.', 'info', []);
        appState.modalData.currentModal = _.cloneDeep(appState.appModals.defaultModal);

        if (!text){
            text = '';
        }

        if (!confirmButtonText){
            confirmButtonText = this.translate('Confirm');
        }

        if (!cancelButtonText){
            cancelButtonText = this.translate('Cancel');
        }

        appState.modalData.currentModal = _.cloneDeep(appState.appModals.defaultModal);

        appState.modalData.currentModal.bodyComponent = 'modal-body';
        appState.modalData.currentModal.title = title;
        appState.modalData.currentModal.body = text;
        appState.modalData.currentModal.confirmButtonText = confirmButtonText;
        appState.modalData.currentModal.cancelButtonText = cancelButtonText;
        appState.modalData.currentModal.modalClassName = 'confirm-modal';
        appState.modalData.currentModal.cancelSelected = true;
        appState.modalData.currentModal.confirmSelected = false;

        this.modalBusy();
        if (confirmAction){
            _appWrapper._confirmModalAction = confirmAction;
        } else {
            _appWrapper._confirmModalAction = this.boundMethods.confirmResolve;
        }
        this.openCurrentModal();
    }

    /**
     * Shows inline confirm modal dialog (preserving original modal content) using basic parameters from method arguments
     *
     * @async
     * @param  {string}     title               Modal title
     * @param  {string}     text                Modal text
     * @param  {string}     confirmButtonText   Confirm button text
     * @param  {string}     cancelButtonText    Cancel button text
     * @param  {Function}   confirmAction       Modal confirm callback
     * @return {undefined}
     */
    async inlineConfirm (title, text, confirmButtonText, cancelButtonText, confirmAction) {

        let returnPromise;
        let resolveReference;
        returnPromise = new Promise((resolve) => {
            resolveReference = resolve;
        });

        let icd = appState.modalData.currentModal.inlineConfirmData;

        if (!text){
            text = '';
        }

        if (!confirmButtonText){
            confirmButtonText = this.translate('Confirm');
        }

        if (!cancelButtonText){
            cancelButtonText = this.translate('Cancel');
        }

        icd.title = title;
        icd.body = text;
        icd.confirmButtonText = confirmButtonText;
        icd.cancelButtonText = cancelButtonText;
        icd.confirmAction = async (result) => {
            if (confirmAction){
                confirmAction();
            }
            resolveReference(result);
        };
        await _appWrapper.nextTick();
        appState.modalData.currentModal.inlineConfirm = true;

        return returnPromise;
    }

    /**
     * Resolve method for query modal
     *
     * @async
     * @param  {Event} e Event that triggered the method
     * @return {undefined}
     */
    async queryResolve (e) {
        if (e && e.preventDefault && _.isFunction(e.preventDefault)){
            e.preventDefault();
        }
        this.closeCurrentModal();
    }

    /**
     * Reject method for query modal
     *
     * @async
     * @param  {Event} e Event that triggered the method
     * @return {undefined}
     */
    async queryReject (e) {
        if (e && e.preventDefault && _.isFunction(e.preventDefault)){
            e.preventDefault();
        }
        this.closeCurrentModal();
    }

    /**
     * Opens query modal dialog
     *
     * @async
     * @param  {Function} confirmAction Modal confirm callback
     * @param  {Function} cancelAction  Modal cancel/close callback
     * @return {undefined}
     */
    async queryModal (confirmAction, cancelAction) {
        this.log('Opening query modal.', 'info', []);
        this.modalBusy();
        if (confirmAction){
            _appWrapper._confirmModalAction = confirmAction;
        } else {
            _appWrapper._confirmModalAction = this.boundMethods.queryResolve;
        }
        if (cancelAction){
            _appWrapper._cancelModalAction = cancelAction;
        } else {
            _appWrapper._cancelModalAction = this.boundMethods.queryReject;
        }
        this.openCurrentModal();
    }

    /**
     * Adds modal message to currently visible modal
     *
     * @param {Object} messageObject Message object
     * @return {undefined}
     */
    addModalMessage (messageObject) {
        if (appState.modalData.currentModal && appState.modalData.modalVisible){
            if (appState.modalData.currentModal.messages){
                if (_.isArray(appState.modalData.currentModal.messages)){
                    let feApp = window.getFeApp();
                    let modalDialogMessagesComponent = _.get(feApp, '$refs.modalDialog.$refs.modalDialogContents.$refs.modalDialogMessages');
                    if (modalDialogMessagesComponent){
                        modalDialogMessagesComponent.addModalMessage(messageObject);
                    } else {
                        appState.modalData.currentModal.messages.push(messageObject);
                        appState.modalData.currentModal.currentMessageIndex = appState.modalData.currentModal.messages.length - 1;
                    }
                }
            }
        }
    }

    /**
     * Clears modal messages from currently visible modal
     *
     * @return {undefined}
     */
    clearModalMessages () {
        if (appState.modalData.currentModal && _.isObject(appState.modalData.currentModal)){
            this.setModalVar('messages', []);
        }
    }

    /**
     * Gets modal data object by its name, applying optional option overrides
     *
     * @param  {string} modalName Name of modal object
     * @param  {Object} options   Modal option overrides
     * @return {(Object|Boolean)} Modal object or false if no modal found
     */
    getModalObject(modalName, options){
        let modalObj = false;
        if (!_.isUndefined(appState.appModals[modalName])){
            modalObj = _.cloneDeep(appState.appModals.defaultModal);
            _.merge(modalObj, _.cloneDeep(appState.appModals[modalName]));
            if (options && _.isObject(options)){
                modalObj = _.merge(modalObj, options);
            }
        }
        if (!modalObj){
            this.log('Can not find modal object with name "{1}"!', 'error', [modalName]);
        }
        return modalObj;
    }

    /**
     * Opens modal dialog by its name, applying optional option overrides
     *
     * @param  {string} modalName Name of modal object
     * @param  {Object} options   Modal option overrides
     * @return {undefined}
     */
    openModal(modalName, options){
        let modalObj = this.getModalObject(modalName, options);
        if (modalObj){
            appState.modalData.currentModal = modalObj;
            this.openCurrentModal();
        }
    }

    /**
     * Automatically closes modal after timeout from modal options
     *
     * @return {undefined}
     */
    autoCloseModal () {
        this.log('Setting up modal auto-close.', 'info', []);
        let cm = appState.modalData.currentModal;
        let seconds = parseInt(cm.autoCloseTime / 1000, 10);

        this.stopAutoCloseModal();
        this.setModalVar('autoClosing', true);

        this.timeouts.autoClose = setTimeout(() => {
            this.log('Auto-closing modal.', 'info', []);
            this.stopAutoCloseModal();
            if (!cm.animateSize){
                if (cm.cancelOnClose && cm.onCancel && _.isFunction(cm.onCancel)){
                    this.log('Calling current modal onCancel...', 'info', []);
                    cm.onCancel();
                }
                if (cm.onBeforeClose && _.isFunction(cm.onBeforeClose)){
                    this.log('Calling current modal onBeforeClose...', 'info', []);
                    cm.onBeforeClose();
                }
                if (cm.onClose && _.isFunction(cm.onClose)){
                    this.log('Calling current modal onClose...', 'info', []);
                    cm.onClose();
                }
            }
            this.closeCurrentModal();
        }, +cm.autoCloseTime);

        this.intervals.autoClose = setInterval(() => {
            seconds--;
            if (seconds < 0){
                seconds = 0;
            }
            this.updateAutoCloseTimer(seconds);
        }, +cm.autoCloseTimeIntervalDuration);
    }

    /**
     * Updates auto close timer in modal dialog (for auto-closing modals)
     *
     * @param  {Integer} seconds Remaining time in seconds
     * @return {undefined}
     */
    updateAutoCloseTimer (seconds) {
        let md = appState.modalData;
        let cm = md.currentModal;

        let remainingTime = _appWrapper.getHelper('format').formatDurationCustom(seconds);

        if (!cm.originalConfirmButtonText){
            cm.originalConfirmButtonText = cm.confirmButtonText;
        }
        if (!cm.originalCancelButtonText){
            cm.originalCancelButtonText = cm.cancelButtonText;
        }

        if (cm.showCloseLink){
            if (!cm.busy){
                let closeLink = md.modalElement.querySelector('.modal-dialog-close-contents');
                if (closeLink){
                    let highlightClass;
                    if (+seconds <= parseInt(cm.autoCloseTimeExpireNotify / 1000, 10)){
                        highlightClass = 'modal-dialog-close-contents-highlighted-expiring';
                    } else if (+seconds <= parseInt(cm.autoCloseTimeNotify / 1000, 10)){
                        highlightClass = 'modal-dialog-close-contents-highlighted';
                    }
                    if (highlightClass){
                        closeLink.addClass(highlightClass);
                    }
                    cm.autoCloseTimeText = remainingTime;
                    setTimeout( () => {
                        if (highlightClass){
                            closeLink.removeClass(highlightClass);
                        }
                    }, 100);
                }
            } else {
                cm.autoCloseTimeText = '';
            }
        } else if (cm.showConfirmButton){
            if (!cm.busy){
                cm.confirmButtonText = cm.originalConfirmButtonText + ' (' + remainingTime + ')';
            } else {
                cm.confirmButtonText = cm.originalConfirmButtonText;
            }
        } else if (cm.showCancelButton) {
            if (!cm.busy){
                cm.cancelButtonText = cm.originalCancelButtonText + ' (' + remainingTime + ')';
            } else {
                cm.cancelButtonText = cm.originalCancelButtonText;
            }
        }
    }

    /**
     * Stops auto closing modal countdown
     *
     * @return {undefined}
     */
    stopAutoCloseModal () {
        let cm = appState.modalData.currentModal;
        if (cm.autoClosing){
            this.log('Stopping modal auto-close.', 'info', []);
            cm.autoClosing = false;
        }
        clearInterval(this.intervals.autoClose);
        clearTimeout(this.timeouts.autoClose);
    }

    /**
     * Resets callbacks for current modal object
     *
     *
     * @return {undefined}
     */
    resetCurrentCallbacks () {
        appState.modalData.currentModal.onBeforeOpen = null;
        appState.modalData.currentModal.onOpen = null;
        appState.modalData.currentModal.onBeforeClose = null;
        appState.modalData.currentModal.onClose = null;
    }

    /**
     * Empties current modal, reverting it to default values
     *
     * @return {undefined}
     */
    emptyModal () {
        appState.modalData.currentModal.title = '';
        appState.modalData.currentModal.body = '';
        appState.modalData.currentModal.showConfirmButton = false;
        appState.modalData.currentModal.showCancelButton = false;
        appState.modalData.currentModal.bodyComponent = 'modal-body';
    }

    /**
     * Resets modal action listeners to their defaults
     *
     * @return {undefined}
     */
    resetModalActions () {
        _appWrapper._confirmModalAction = _appWrapper.__confirmModalAction;
        _appWrapper._cancelModalAction = _appWrapper.__cancelModalAction;
    }

    /**
     * Sets modal variable for current modal
     *
     * @param {string} name  Name of the variable
     * @param {Object} value Value of the variable
     * @return {undefined}
     */
    setModalVar(name, value){
        appState.modalData.currentModal[name] = value;
    }

    /**
     * Sets modal variables using data argument
     *
     * @param {Object} data Data with properties and values for variables tos et
     * @return {undefined}
     */
    setModalVars(data){
        appState.modalData.currentModal = _.merge(appState.modalData.currentModal, data);
    }

    /**
     * Gets current modal variable value by name
     *
     * @param  {string} name  Name of the variable
     * @return {Object} value Value of the variable
     */
    getModalVar(name){
        return _.get(appState.modalData.currentModal, name);
    }

    /**
     * Sets modal data using argument
     *
     * @param {Object}  data      Modal data to set
     * @param {Boolean} overwrite Flag to indicate data overwriting instead of merging with existing data
     * @return {undefined}
     */
    setModalData(data, overwrite){
        if (!overwrite){
            _.merge(appState.modalData.currentModal, data);
        } else {
            appState.modalData.currentModal = data;
        }
    }
}

exports.ModalHelper = ModalHelper;