MessageBox

MessageBox使用有三種方式:css

MessageBox.alert(message, title);

MessageBox.confirm(message, title);

MessageBox.prompt(message, title);

引用的時候實際上是引入了message-box.js這個文件:html

var CONFIRM_TEXT = '肯定';
var CANCEL_TEXT = '取消';

//默認options參數
var defaults = {
    title: '提示',//標題
    message: '',//消息
    type: '',//消息類型
    showInput: false,//是否顯示輸入框
    showClose: true,
    modalFade: false,
    lockScroll: false,
    closeOnClickModal: true,//是否點擊遮罩的時候關閉提示框
    inputValue: null,//輸入框的值
    inputPlaceholder: '',//輸入框的佔位符
    inputPattern: null,
    inputValidator: null,
    inputErrorMessage: '',
    showConfirmButton: true,//是否顯示確認按鈕
    showCancelButton: false,//是否顯示取消按鈕
    confirmButtonPosition: 'right',//確認按鈕位置
    confirmButtonHighlight: false,//確認按鈕是否加粗顯示
    cancelButtonHighlight: false,//取消按鈕是否加粗顯示
    confirmButtonText: CONFIRM_TEXT,//確認按鈕文本
    cancelButtonText: CANCEL_TEXT,//取消按鈕文本
    confirmButtonClass: '',//確認按鈕類名
    cancelButtonClass: ''//取消按鈕類名
};

import Vue from 'vue';
import msgboxVue from './message-box.vue';

//merge函數用於合併傳遞給MessageBox函數的參數,alert,confirm和prompt三個方法最終都調用MessageBox函數
var merge = function(target) {
    for (var i = 1, j = arguments.length; i < j; i++) {
        var source = arguments[i];
        for (var prop in source) {
            if (source.hasOwnProperty(prop)) {
                var value = source[prop];
                if (value !== undefined) {
                    target[prop] = value;
                }
            }
        }
    }
    //第一個參數對象target做爲主參數,把後面的參數對象上的屬性都複製到target上面而後返回最終合併的target對象做爲MessageBox方法的參數
    return target;
};

var MessageBoxConstructor = Vue.extend(msgboxVue);
//使用Vue.extend()擴展一個message-box子類

var currentMsg, instance;
//currentMsg當前信息的參數,instance新message-box實例
var msgQueue = [];
//msgQueue消息序列數組,消息參數存放在裏面

//點擊消息盒子的confirm按鈕或者cancel按鈕後就會執行callback函數,傳入參數confirm或者cancel字符串
const defaultCallback = action => {
    if (currentMsg) {
        var callback = currentMsg.callback;
        if (typeof callback === 'function') {//若是當前消息有callback,那就用用戶自定義的callback
            if (instance.showInput) {
                callback(instance.inputValue, action);//prompt類型,參數一是輸入框的值,參數二是點擊的按鈕動做
            } else {
                callback(action);
            }
        }
        if (currentMsg.resolve) {//若是使用提供了promise的resolve函數,那就執行resolve,傳入相應參數
            var $type = currentMsg.options.$type;
            if ($type === 'confirm' || $type === 'prompt') {
                if (action === 'confirm') {
                    if (instance.showInput) {
                        currentMsg.resolve({ value: instance.inputValue, action });
                    } else {
                        currentMsg.resolve(action);
                    }
                } else if (action === 'cancel' && currentMsg.reject) {
                    currentMsg.reject(action);
                }
            } else {
                currentMsg.resolve(action);
            }
        }
    }
};

var initInstance = function() {
    instance = new MessageBoxConstructor({
        el: document.createElement('div')
    });//新實例掛載在一個新建div元素上面

    instance.callback = defaultCallback;
};
//initInstance初始化message-box實例,爲實例添加默認callback屬性

//MessageBox最終會調用showNwxtMsg方法執行顯示消息的操做
var showNextMsg = function() {
    if (!instance) {
        initInstance();
    }//若是是第一次調用,尚未新建message-box實例就新建一個

    if (!instance.value || instance.closeTimer) {//若是消息框當前沒有顯示說明沒有開啓使用
        if (msgQueue.length > 0) {
            currentMsg = msgQueue.shift();//從msgQueue的隊列頭部取出第一個做爲當前調用的參數

            var options = currentMsg.options;
            for (var prop in options) {
                if (options.hasOwnProperty(prop)) {
                    instance[prop] = options[prop];
                }
            }//把options裏的屬性複製到新message-box實例上
            if (options.callback === undefined) {//若是沒有callback,就用默認的
                instance.callback = defaultCallback;
            }
            ['modal', 'showClose', 'closeOnClickModal', 'closeOnPressEscape'].forEach(prop => {
                if (instance[prop] === undefined) {
                    instance[prop] = true;
                }
            });//modal,showClose,closeOnClickModal,closeOnPressEscape這幾個屬性若是沒有設置,那就設置爲true
            document.body.appendChild(instance.$el);//將新建的div添加到頁面裏

            Vue.nextTick(() => {//Dom更新以後執行
                instance.value = true;//message-box實例上的value屬性用來開啓mint-msgbox的顯示和隱藏,此處就是讓消息盒子顯示出來
            });
        }
    }
};

//alert,confirm和prompt三個方法最終都調用MessageBox函數
var MessageBox = function(options, callback) {
    if (typeof options === 'string') {
        options = {
            title: options
        };
        if (arguments[1]) {
            options.message = arguments[1];
        }
        if (arguments[2]) {
            options.type = arguments[2];
        }
        //第一個參數是title,第二個參數是message,第三個參數是type這種調用方式
    } else if (options.callback && !callback) {
        callback = options.callback;
    }

    if (typeof Promise !== 'undefined') {//若是支持Promise就返回一個Promise
        return new Promise(function(resolve, reject) { // eslint-disable-line
            msgQueue.push({
                options: merge({}, defaults, MessageBox.defaults || {}, options),
                callback: callback,
                resolve: resolve,
                reject: reject
            });

            showNextMsg();
        });
    } else {//若是不支持Promise就直接調用
        msgQueue.push({
            options: merge({}, defaults, MessageBox.defaults || {}, options),
            callback: callback
        });//msgQueue加入新的消息調用的參數

        showNextMsg();
    }
};

//設置默認參數
MessageBox.setDefaults = function(defaults) {
    MessageBox.defaults = defaults;
};

//alert形式消息調用
MessageBox.alert = function(message, title, options) {//message消息,title標題,options選項
    if (typeof title === 'object') {//若是第二個參數是對象,說明它是options,那就把它賦值給options,而title賦值爲空字符串
        options = title;
        title = '';
    }
    return MessageBox(merge({
        title: title,//標題
        message: message,//消息
        $type: 'alert',//alert類型消息
        closeOnPressEscape: false,
        closeOnClickModal: false
    }, options));//調用MessageBox方法,將message,title和options按照必定格式合併後傳遞過去
};

//confirm形式消息調用
MessageBox.confirm = function(message, title, options) {//message消息,title標題,options選項
    if (typeof title === 'object') {//若是第二個參數是對象,說明它是options,那就把它賦值給options,而title賦值爲空字符串
        options = title;
        title = '';
    }
    return MessageBox(merge({
        title: title,//標題
        message: message,//消息
        $type: 'confirm',//confirm類型消息
        showCancelButton: true//是否顯示取消按鈕
    }, options));
};

//prompt形式消息調用
MessageBox.prompt = function(message, title, options) {//message消息,title標題,options選項
    if (typeof title === 'object') {//若是第二個參數是對象,說明它是options,那就把它賦值給options,而title賦值爲空字符串
        options = title;
        title = '';
    }
    return MessageBox(merge({
        title: title,//標題
        message: message,//消息
        showCancelButton: true,//是否顯示取消按鈕
        showInput: true,//是否顯示一個輸入框
        $type: 'prompt'//propmt類型消息
    }, options));
};

//消息序列清空
MessageBox.close = function() {
    if (!instance) return;
    instance.value = false;
    msgQueue = [];
    currentMsg = null;
};

export default MessageBox;
export { MessageBox };

這個文件引入了message-box.vue,將message-box.vue經過Vue.extend()擴展爲Vue的一個子類,而後用new關鍵字來新建message-box實例來使用。vue

下面是message-box.vue:node

<template>
    <div class="mint-msgbox-wrapper">
        <transition name="msgbox-bounce">
            <!-- 爲消息盒子的顯示和隱藏添加動畫效果 -->
            <div class="mint-msgbox" v-show="value">
                <!-- 根據this.value的值來顯示或者隱藏整個消息盒子 -->
                <div class="mint-msgbox-header" v-if="title !== ''">
                    <!-- 消息盒子的標題,若是有標題就顯示,沒標題就隱藏 -->
                    <div class="mint-msgbox-title">{{ title }}</div>
                </div>
                <div class="mint-msgbox-content" v-if="message !== ''">
                    <!-- 消息盒子的內容,有則顯示,無則隱藏 -->
                    <div class="mint-msgbox-message" v-html="message"></div>
                    <div class="mint-msgbox-input" v-show="showInput">
                        <input v-model="inputValue" :placeholder="inputPlaceholder" ref="input">
                        <div class="mint-msgbox-errormsg" :style="{ visibility: !!editorErrorMessage ? 'visible' : 'hidden' }">{{ editorErrorMessage }}</div>
                    </div>
                    <!-- 當使用prompt的時候顯示輸入框,若是輸入驗證出錯就會顯示錯誤提示框 -->
                </div>
                <div class="mint-msgbox-btns">
                    <!-- 消息盒子的兩個按鈕,確認按鈕和取消按鈕,兩個按鈕的文字內容和樣式類名都是能夠自定義的 -->
                    <button :class="[ cancelButtonClasses ]" v-show="showCancelButton" @click="handleAction('cancel')">{{ cancelButtonText }}</button>
                    <button :class="[ confirmButtonClasses ]" v-show="showConfirmButton" @click="handleAction('confirm')">{{ confirmButtonText }}</button>
                </div>
            </div>
        </transition>
    </div>
</template>

<style lang="scss" scoped>
.mint-msgbox {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate3d(-50%, -50%, 0);
    background-color: #fff;
    width: 85%;
    border-radius: 3px;
    font-size: 16px;
    -webkit-user-select: none;//元素內的文字或者子元素可否被選中
    overflow: hidden;
    backface-visibility: hidden;//3d旋轉到元素背面時背面是否透明
    transition: .2s;
    .mint-msgbox-header {
        padding: 15px 0 0;
        .mint-msgbox-title {
            text-align: center;
            padding-left: 0;
            margin-bottom: 0;
            font-size: 16px;
            font-weight: bold;
            color: #333;
        }
    }
    .mint-msgbox-content {
        padding: 10px 20px 15px;
        border-bottom: 1px solid #ddd;
        min-height: 36px;
        position: relative;
        .mint-msgbox-input {
            padding-top: 15px;
            input {
                border: 1px solid #dedede;
                border-radius: 5px;
                padding: 4px 5px;
                width: 100%;
                appearance: none;
                outline: none;
            }
            input.invalid {
                border-color: #ff4949;
                &:focus {
                    border-color: #ff4949;
                }
            }
            .mint-msgbox-errormsg {
                color: red;
                font-size: 12px;
                min-height: 18px;
                margin-top: 2px;
            }
        }
        .mint-msgbox-message {
            color: #999;
            margin: 0;
            text-align: center;
            line-height: 36px;
        }
    }
    .mint-msgbox-btns {
        display: -webkit-box;
        display: -webkit-flex;
        display: -ms-flexbox;
        display: flex;
        height: 40px;
        line-height: 40px;
    }
    .mint-msgbox-btn {
        line-height: 35px;
        display: block;
        background-color: #fff;
        flex: 1;
        margin: 0;
        border: 0;
        &:focus {
            outline: none;
        }
        &:active {
            background-color: #fff;
        }
    }
    .mint-msgbox-cancel {
        width: 50%;
        border-right: 1px solid #ddd;
        &:active {
            color: #000;
        }
    }
    .mint-msgbox-confirm {
        color: #26a2ff;
        width: 50%;
        &:active {
            color: #26a2ff;
        }
    }
}
.msgbox-bounce-enter {
    opacity: 0;
    transform: translate3d(-50%, -50%, 0) scale(0.7);
}
.msgbox-bounce-leave-active {
    opacity: 0;
    transform: translate3d(-50%, -50%, 0) scale(0.9);
}
</style>
<style src="../style/popup.scss" lang="scss"></style>

<script>
let CONFIRM_TEXT = '肯定';
let CANCEL_TEXT = '取消';
import Popup from '../utils/popup';
export default {
    mixins: [ Popup ],//混入popup功能對象
    props: {
        modal: {
            default: true
        },
        showClose: {
            type: Boolean,
            default: true
        },
        lockScroll: {
            type: Boolean,
            default: false
        },
        closeOnClickModal: {
            default: true
        },
        closeOnPressEscape: {
            default: true
        },
        inputType: {
            type: String,
            default: 'text'
        }
    },
    computed: {
        confirmButtonClasses() {//生成新的確認按鈕類名,添加用戶自定義的類名或者開啓加粗顯示
            let classes = 'mint-msgbox-btn mint-msgbox-confirm ' + this.confirmButtonClass;
            if (this.confirmButtonHighlight) {
                classes += ' mint-msgbox-confirm-highlight';
            }
            return classes;
        },
        cancelButtonClasses() {//生成新的取消按鈕類名,添加用戶自定義的類名或者開啓加粗顯示
            let classes = 'mint-msgbox-btn mint-msgbox-cancel ' + this.cancelButtonClass;
            if (this.cancelButtonHighlight) {
                classes += ' mint-msgbox-cancel-highlight';
            }
            return classes;
        }
    },
    methods: {
        doClose() {
            this.value = false;
            this._closing = true;
            this.onClose && this.onClose();//若是實例有onClose屬性就直接執行
            setTimeout(() => {//若是設置了開啓彈層後鎖定滾動就將滾動狀態再恢復到初始狀態
                if (this.modal && this.bodyOverflow !== 'hidden') {
                    document.body.style.overflow = this.bodyOverflow;
                    document.body.style.paddingRight = this.bodyPaddingRight;
                }
                this.bodyOverflow = null;
                this.bodyPaddingRight = null;
            }, 200);
            this.opened = false;
            if (!this.transition) {
                this.doAfterClose();
            }
        },
        handleAction(action) {//處理確認按鈕和取消按鈕的點擊事件
            if (this.$type === 'prompt' && action === 'confirm' && !this.validate()) {//若prompt類型消息輸入框驗證未經過就不執行操做
                return;
            }
            var callback = this.callback;
            this.value = false;
            callback(action);//隱藏消息盒子而且執行實例上的回調callback
        },
        validate() {
            if (this.$type === 'prompt') {//若是是prompt類型消息,執行驗證
                var inputPattern = this.inputPattern;
                if (inputPattern && !inputPattern.test(this.inputValue || '')) {
                    //若是有自定義的正則inputPattern就用它來驗證輸入框內容,而後顯示錯誤提示信息,而且給input添加invalid類
                    this.editorErrorMessage = this.inputErrorMessage || '輸入的數據不合法!';
                    this.$refs.input.classList.add('invalid');
                    return false;
                }
                var inputValidator = this.inputValidator;//用自定義的inputValidator函數來驗證input輸入框內容,inputValidator返回布爾值或者錯誤信息
                if (typeof inputValidator === 'function') {
                    var validateResult = inputValidator(this.inputValue);
                    if (validateResult === false) {
                        this.editorErrorMessage = this.inputErrorMessage || '輸入的數據不合法!';
                        this.$refs.input.classList.add('invalid');
                        return false;
                    }
                    if (typeof validateResult === 'string') {
                        this.editorErrorMessage = validateResult;
                        return false;
                    }
                }
            }
            this.editorErrorMessage = '';//清空錯誤提示信息
            this.$refs.input.classList.remove('invalid');//若是驗證經過,就取出input的invalid 類
            return true;
        },
        handleInputType(val) {
            if (val === 'range' || !this.$refs.input) return;//若是input是range控件或者引用不到input,就不作操做直接返回
            this.$refs.input.type = val;//不然經過$refs引用找到input而後改變其type
        }
    },
    watch: {
        inputValue() {//prompt類型的時候,輸入框值發生變化就執行驗證函數
            if (this.$type === 'prompt') {
                this.validate();
            }
        },
        value(val) {//this.value用於顯示或者隱藏整個消息盒子
            this.handleInputType(this.inputType);//先處理一下input的類型
            if (val && this.$type === 'prompt') {//若是是prompt類型消息,就讓input輸入框獲取焦點
                setTimeout(() => {
                    if (this.$refs.input) {
                        this.$refs.input.focus();
                    }
                }, 500);
            }
        },
        inputType(val) {//輸入框類型有可能會由於用戶自定義設置而發生變化,就執行相應的處理函數
            this.handleInputType(val);
        }
    },
    data() {
        return {
            title: '',
            message: '',
            type: '',
            showInput: false,
            inputValue: null,//prompt類型消息輸入框的值
            inputPlaceholder: '',
            inputPattern: null,
            inputValidator: null,
            inputErrorMessage: '',
            showConfirmButton: true,
            showCancelButton: false,
            confirmButtonText: CONFIRM_TEXT,
            cancelButtonText: CANCEL_TEXT,
            confirmButtonClass: '',
            confirmButtonDisabled: false,
            cancelButtonClass: '',
            editorErrorMessage: null,
            callback: null
        };
    }
};
</script>

這時看到message-box.vue引入了一個文件popup,其實message-box的功能把彈出的效果和動畫還有後面的黑色透明蒙層都單獨封裝成了popup組件,這裏把popup組件的功能mixin了進來,就是爲了使用popup的modal模態框蒙層。web

下面是popup/index.js和popup/popup-manager.js:數組

import Vue from 'vue';
import merge from '@/ui/utils/merge';//複製後面的參數列表裏的對象屬性到第一個參數裏
import PopupManager from '@/ui/utils/popup/popup-manager';

let idSeed = 1;
const transitions = [];

//若是是直接使用popup組件,會傳遞一個pop-transition的選項用來選擇動畫效果,這裏的這個函數就是爲實例添加動畫鉤子函數,afterEnter和afterLeave
const hookTransition = (transition) => {
    if (transitions.indexOf(transition) !== -1) return;

    const getVueInstance = (element) => {
        let instance = element.__vue__;
        if (!instance) {
            const textNode = element.previousSibling;
            if (textNode.__vue__) {
                instance = textNode.__vue__;
            }
        }
        return instance;
    };

    Vue.transition(transition, {
        afterEnter(el) {
            const instance = getVueInstance(el);

            if (instance) {
                instance.doAfterOpen && instance.doAfterOpen();
            }
        },
        afterLeave(el) {
            const instance = getVueInstance(el);

            if (instance) {
                instance.doAfterClose && instance.doAfterClose();
            }
        }
    });
};

let scrollBarWidth;
//獲取瀏覽器右側垂直滾動條的寬度
const getScrollBarWidth = () => {
    if (Vue.prototype.$isServer) return;//若是當前實例在服務器端運行直接返回
    if (scrollBarWidth !== undefined) return scrollBarWidth;

    const outer = document.createElement('div');
    outer.style.visibility = 'hidden';
    outer.style.width = '100px';
    outer.style.position = 'absolute';
    outer.style.top = '-9999px';
    document.body.appendChild(outer);

    const widthNoScroll = outer.offsetWidth;//HTMLElement.offsetWidth包括了垂直方向滾動條的寬度,若是有的話
    outer.style.overflow = 'scroll';

    const inner = document.createElement('div');
    inner.style.width = '100%';
    outer.appendChild(inner);

    const widthWithScroll = inner.offsetWidth;
    outer.parentNode.removeChild(outer);

    return widthNoScroll - widthWithScroll;
};

//獲取dom元素
const getDOM = function(dom) {
    if (dom.nodeType === 3) {//若是是文本節點類型,就切換下一個兄弟節點繼續尋找
        dom = dom.nextElementSibling || dom.nextSibling;
        getDOM(dom);
    }
    return dom;
};

export default {
    props: {
        value: {//彈層的開啓或者關閉
            type: Boolean,
            default: false
        },
        transition: {//添加modal動畫效果,默認是淡入淡出效果
            type: String,
            default: ''
        },
        openDelay: {},//開啓延遲
        closeDelay: {},//關閉延遲
        zIndex: {},//掛載dom的z-index
        modal: {//是否開啓模態框
            type: Boolean,
            default: false
        },
        modalFade: {
            type: Boolean,
            default: true
        },
        modalClass: {
        },
        lockScroll: {//彈層開啓後是否鎖定滾動
            type: Boolean,
            default: true
        },
        closeOnPressEscape: {//鍵盤esc按鍵是否能夠關閉彈層
            type: Boolean,
            default: false
        },
        closeOnClickModal: {//點擊模態框後是否能夠關閉彈層
            type: Boolean,
            default: false
        }
    },

    created() {
        if (this.transition) {
            hookTransition(this.transition);
        }
    },

    beforeMount() {
        this._popupId = 'popup-' + idSeed++;//每一次的popup的時候都生成一個新id
        PopupManager.register(this._popupId, this);//用新popup的id註冊這次實例,存儲在instances對象裏
    },

    beforeDestroy() {
        PopupManager.deregister(this._popupId);//取消註冊popup的id對應的實例,從instances對象裏刪除實例
        PopupManager.closeModal(this._popupId);//關閉當前modal
        if (this.modal && this.bodyOverflow !== null && this.bodyOverflow !== 'hidden') {//恢復body的scroll狀態
            document.body.style.overflow = this.bodyOverflow;
            document.body.style.paddingRight = this.bodyPaddingRight;
        }
        this.bodyOverflow = null;
        this.bodyPaddingRight = null;
    },

    data() {
        return {
            opened: false,
            bodyOverflow: null,
            bodyPaddingRight: null,
            rendered: false//標記彈出層是否正在渲染中
        };
    },

    watch: {
        value(val) {//value用於判斷彈出層的顯示與隱藏
            if (val) {
                if (this._opening) return;//若是正在打開彈出層,就直接返回
                if (!this.rendered) {//若是並不是正在渲染中,就執行
                    this.rendered = true;//渲染中標記爲true
                    Vue.nextTick(() => {//下一次dom更新後執行this.open()
                        this.open();
                    });
                } else {
                    this.open();
                }
            } else {//false的時候關閉彈層
                this.close();
            }
        }
    },

    methods: {
        open(options) {//開啓彈出層
            if (!this.rendered) {//rendered參數標記開啓彈出層正在進行中
                this.rendered = true;
                this.$emit('input', true);
            }

            const props = merge({}, this, options, this.$props);

            if (this._closeTimer) {
                clearTimeout(this._closeTimer);
                this._closeTimer = null;
            }
            clearTimeout(this._openTimer);//初始化_closeTimer定時器

            const openDelay = Number(props.openDelay);//選項裏是否有openDelay開啓延遲參數,若是有就新建定時器延遲一段時間再開啓彈出層
            if (openDelay > 0) {
                this._openTimer = setTimeout(() => {
                    this._openTimer = null;
                    this.doOpen(props);
                }, openDelay);
            } else {
                this.doOpen(props);
            }
        },

        doOpen(props) {
            if (this.$isServer) return;
            if (this.willOpen && !this.willOpen()) return;
            if (this.opened) return;

            this._opening = true;//彈出層正在開啓標記爲true

            // 使用 vue-popup 的組件,若是須要和父組件通訊顯示的狀態,應該使用 value,它是一個 prop,
            // 這樣在父組件中用 v-model 便可;不然能夠使用 visible,它是一個 data
            this.visible = true;
            this.$emit('input', true);

            const dom = getDOM(this.$el);//獲取當前實例掛載的html元素

            const modal = props.modal;//布爾值,是否開啓一個陰影彈出層

            const zIndex = props.zIndex;//z-index值
            if (zIndex) {//若是有自定義的,就覆蓋默認z-index值
                PopupManager.zIndex = zIndex;
            }

            if (modal) {//若是使用模態框形式
                if (this._closing) {//若是正在執行關閉操做
                    PopupManager.closeModal(this._popupId);//關閉當前popup id的模態框
                    this._closing = false;//正在關閉標記變爲false
                }
                PopupManager.openModal(this._popupId, PopupManager.nextZIndex(), dom, props.modalClass, props.modalFade);//開啓新的modal
                if (props.lockScroll) {//若是有開啓彈層後鎖定滾動的選項就記錄下body的overflow值和padding-right值
                    if (!this.bodyOverflow) {
                        this.bodyPaddingRight = document.body.style.paddingRight;
                        this.bodyOverflow = document.body.style.overflow;
                    }
                    scrollBarWidth = getScrollBarWidth();//獲取右側垂直滾動條的寬度
                    let bodyHasOverflow = document.documentElement.clientHeight < document.body.scrollHeight;//若是html高度小於body內容高度就說明頁面有內滾動狀態
                    if (scrollBarWidth > 0 && bodyHasOverflow) {//若是body內容有滾動狀態並且右側滾動條有寬度就給body設置padding-right值和滾動條寬度同樣
                        document.body.style.paddingRight = scrollBarWidth + 'px';
                    }
                    document.body.style.overflow = 'hidden';//body的overflow設置爲hidden,鎖定滾動狀態
                }
            }

            if (getComputedStyle(dom).position === 'static') {
                dom.style.position = 'absolute';
            }
            //Window.getComputedStyle() 方法給出應用活動樣式表後的元素的全部CSS屬性的值,並解析這些值可能包含的任何基本計算。
            //獲取當前實例掛載的html元素的position樣式,若是是static,那就改變成absolute

            dom.style.zIndex = PopupManager.nextZIndex();
            //爲掛載html元素添加z-index,新的z-index加1,上面調用PopupManager.openModal傳遞的nextZIndex是先調用的,因此是2000給modal模態框添加的,此次給掛載dom添加的,是2001
            //掛載dom元素比modal模態框的z-index大1,全部模態框在下層顯示
            this.opened = true;//已經打開彈層opened標記爲true

            this.onOpen && this.onOpen();

            if (!this.transition) {//若是沒有過渡選項直接執行doAfterClose,若是有過渡選項會在動畫鉤子裏調用doAfterClose
                this.doAfterOpen();
            }
        },

        doAfterOpen() {
            this._opening = false;//已經開啓,正在開啓標記變爲false
        },

        close() {
            if (this.willClose && !this.willClose()) return;

            if (this._openTimer !== null) {//清空開啓彈層定時器
                clearTimeout(this._openTimer);
                this._openTimer = null;
            }
            clearTimeout(this._closeTimer);//清空關閉彈層定時器

            const closeDelay = Number(this.closeDelay);//關閉彈層延時

            if (closeDelay > 0) {//有延時就開啓定時器延時後再調用關閉函數
                this._closeTimer = setTimeout(() => {
                    this._closeTimer = null;
                    this.doClose();
                }, closeDelay);
            } else {//沒有延時直接調用關閉函數
                this.doClose();
            }
        },

        doClose() {
            this.visible = false;
            this.$emit('input', false);
            this._closing = true;//正在關閉標記變爲true

            this.onClose && this.onClose();//若是有onClose就直接調用

            if (this.lockScroll) {//若是設置了開啓彈層後鎖定滾動就將滾動狀態再恢復到初始狀態
                setTimeout(() => {
                    if (this.modal && this.bodyOverflow !== 'hidden') {
                        document.body.style.overflow = this.bodyOverflow;
                        document.body.style.paddingRight = this.bodyPaddingRight;
                    }
                    this.bodyOverflow = null;
                    this.bodyPaddingRight = null;
                }, 200);
            }

            this.opened = false;//已打開彈層標記爲false

            if (!this.transition) {//若是沒有過渡選項直接執行doAfterClose,若是有過渡選項會在動畫鉤子裏調用doAfterClose
                this.doAfterClose();
            }
        },

        doAfterClose() {
            PopupManager.closeModal(this._popupId);//關閉對應id的popup模態框
            this._closing = false;//正在關閉標記爲false
        }
    }
};

export { PopupManager };

下面是popup-manager.js:promise

import Vue from 'vue';
import { addClass, removeClass } from '@/ui/utils/dom';

let hasModal = false;

const getModal = function() {//獲取模態框DOM結構
    if (Vue.prototype.$isServer) return;
    let modalDom = PopupManager.modalDom;
    if (modalDom) {
        hasModal = true;//若是已經生成模態框dom,就把hasModal標記爲true
    } else {
        hasModal = false;
        modalDom = document.createElement('div');//建立div做爲模態框dom結構
        PopupManager.modalDom = modalDom;//PopupManager類上面的modalDom賦值爲這裏生成的div

        modalDom.addEventListener('touchmove', function(event) {//爲模態框添加touchmove事件,滑動的時候返回,不作操做
            event.preventDefault();
            event.stopPropagation();
        });

        modalDom.addEventListener('click', function() {//點擊模態框的時候,執行PopupManager.doOnModalClick()函數
            PopupManager.doOnModalClick && PopupManager.doOnModalClick();
        });
    }

    return modalDom;
};

const instances = {};

const PopupManager = {
    zIndex: 2000,

    modalFade: true,

    getInstance: function(id) {//用popup的id獲取已註冊的實例
        return instances[id];
    },

    register: function(id, instance) {//用新popup的id註冊這次實例,將當前popup對應的vue實例存儲在instances對象裏
        if (id && instance) {
            instances[id] = instance;
            //這個id存儲在instances的值就是對應的vue實例,這樣就能夠經過popup id來找到對應的實例,方便調用實例上的方法
        }
    },

    deregister: function(id) {//取消註冊popup的id對應的實例,從instances對象裏刪除實例
        if (id) {
            instances[id] = null;
            delete instances[id];
        }
    },

    nextZIndex: function() {//新的z-index加1
        return PopupManager.zIndex++;
    },

    modalStack: [],

    doOnModalClick: function() {//點擊模態框獲取modalStack中最後一個模態框,也就是z-index最大的那個,而後調用它對應實例的close方法來關閉彈層
        const topItem = PopupManager.modalStack[PopupManager.modalStack.length - 1];
        if (!topItem) return;

        const instance = PopupManager.getInstance(topItem.id);
        if (instance && instance.closeOnClickModal) {
            instance.close();
        }
    },

    openModal: function(id, zIndex, dom, modalClass, modalFade) {//打開模態框
        if (Vue.prototype.$isServer) return;//服務器端運行就直接返回
        if (!id || zIndex === undefined) return;//若是沒有傳遞popup id或者z-index值就返回
        this.modalFade = modalFade;//是否開啓動畫效果

        const modalStack = this.modalStack;//modal模態框的棧,數組,全部模態框信息都存裏面

        for (let i = 0, j = modalStack.length; i < j; i++) {//循環modalStack,若是已經存在,就返回
            const item = modalStack[i];
            if (item.id === id) {
                return;
            }
        }

        const modalDom = getModal();//生成模態框dom結構

        addClass(modalDom, 'v-modal');//爲模態框添加v-modal類,就是半透明黑色蒙層,fixed定位,佈滿整個頁面
        if (this.modalFade && !hasModal) {//添加動畫效果css類
            addClass(modalDom, 'v-modal-enter');
        }
        if (modalClass) {//爲模態框添加自定義類名
            let classArr = modalClass.trim().split(/\s+/);
            classArr.forEach(item => addClass(modalDom, item));
        }
        setTimeout(() => {
            removeClass(modalDom, 'v-modal-enter');
        }, 200);//去除漸入動畫效果類

        if (dom && dom.parentNode && dom.parentNode.nodeType !== 11) {//dom是傳入的實例掛載dom元素,若是存在且它的父級節點不是文檔片斷節點
            dom.parentNode.appendChild(modalDom);//將模態框加入頁面中
        } else {
            document.body.appendChild(modalDom);//若是dom的父級不存在,就加入body中
        }

        if (zIndex) {//爲模態框添加z-index
            modalDom.style.zIndex = zIndex;
        }
        modalDom.style.display = '';//模態框的display樣式置空

        this.modalStack.push({ id: id, zIndex: zIndex, modalClass: modalClass });//將當前模態框基本信息存入modalStack,以便關閉的時候使用
        //這個id存儲在instances的值就是對應的vue實例,這樣就能夠經過popup id來找到對應的實例,方便調用實例上的方法
    },

    closeModal: function(id) {
        const modalStack = this.modalStack;
        const modalDom = getModal();

        if (modalStack.length > 0) {//取最後一個modal,也就是最後打開的,z-index層級最高
            const topItem = modalStack[modalStack.length - 1];
            if (topItem.id === id) {
                if (topItem.modalClass) {//去除modalDom上的自定義類名
                    let classArr = topItem.modalClass.trim().split(/\s+/);
                    classArr.forEach(item => removeClass(modalDom, item));
                }

                modalStack.pop();//將對應modal從modalStack中刪除
                if (modalStack.length > 0) {//若是還有modal存在,就從新給z-index賦值
                    modalDom.style.zIndex = modalStack[modalStack.length - 1].zIndex;
                }
            } else {//若是id不對應就循環數組找到對應的modal,而後從modalStack中刪除
                for (let i = modalStack.length - 1; i >= 0; i--) {
                    if (modalStack[i].id === id) {
                        modalStack.splice(i, 1);
                        break;
                    }
                }
            }
        }

        if (modalStack.length === 0) {//modalStack清空後添加動畫效果真後完全清除dom結構
            if (this.modalFade) {
                addClass(modalDom, 'v-modal-leave');
            }
            setTimeout(() => {
                if (modalStack.length === 0) {
                    if (modalDom.parentNode) modalDom.parentNode.removeChild(modalDom);
                    modalDom.style.display = 'none';
                    PopupManager.modalDom = undefined;
                }
                removeClass(modalDom, 'v-modal-leave');
            }, 200);
        }
    }
};
!Vue.prototype.$isServer && window.addEventListener('keydown', function(event) {//鍵盤上escape按鈕也能夠關閉彈層
    if (event.keyCode === 27) { // ESC
        if (PopupManager.modalStack.length > 0) {
            const topItem = PopupManager.modalStack[PopupManager.modalStack.length - 1];
            if (!topItem) return;
            const instance = PopupManager.getInstance(topItem.id);
            if (instance.closeOnPressEscape) {
                instance.close();
            }
        }
    }
});

export default PopupManager;

popup的modal蒙層其實就是一個寬高100%,fixed定位的黑色透明層,它的z-index比message-box的z-index要小1,因此它顯示在message-box下方。瀏覽器

每個popup都生成id來存儲對應的vue實例,以方便找到對應vue實例調用它上面的方法。服務器

每個modal模態框的基本信息都存入modalStack棧,每一次開啓新的modal就push進去,每一次關閉modal就從最後面pop()出來。app

message-box也有一個msgQueue隊列,每次取隊列頭部的爲當前消息來顯示。

相關文章
相關標籤/搜索