MessageBox


MessageBox使用有三種方式:

MessageBox.alert(message, title);

MessageBox.confirm(message, title);

MessageBox.prompt(message, title);

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

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實例來使用。

下面是message-box.vue:

<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模態框蒙層。

下面是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:

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()出來。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM