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队列,每次取队列头部的为当前消息来显示。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架