Modal(模态框)组件
一、简介
- 不支持多个模态框同时存在
- 一般作为文档body元素的直接子元素存在(不要受到其他元素的影响,假如把模态框元素放在一个具有
transform
属性的父元素里,模态框的position:fixed
属性就会被严重影响) autofocus
在模态框无作为
二、样式
此版本的Bootstrap大量使用了flex布局
- 在
.modal-content
中,使用了flex-direction: column
,使得.modal-header、.modal-body、.modal-footer
纵向排列 - 在
.modal-header
中,使用了display: flex; align-items:center
使得内容纵向居中,使用了justify-content:space-between
使得两端对齐,项目之间的间隔都相等(主要就是title以及关闭按钮) - 在
.modal-body
中,作为flex item使用了属性flex:1 1 auto
,第一个值为flex-grow
,在modal-content
中占据足够大的空间(因为header和footer的flex-grow
都为默认值0); 第二个值为flex-shrink
,默认都为1;第三个值为flex-basis
,定义在分配多余空间之前项目占据主轴(这里的.modal-content
的方向是 column,所以主轴是纵向)的空间 - 在
.modal-footer
中,使用了display: flex;align-items: center;
使得内容纵向居中,使用了justify-content: flex-end;
使得每个flex item 向右靠拢(默认放在footer的是一些按钮啥的)
三、脚本
Modal组件的脚本一般用于控制Modal的显示与隐藏,所以可以想象主要就是show和hide函数
下面是Modal组件的代码梗概:
class Modal {
constructor(element, config) {
// 配置属性,包括backdrop(是否有背景阴影层),keyboard(是否可用键盘控制esc键),show(是否需要在初始化的同时弹出Modal),focus(让_element元素focus?不理解)
this._config = this._getConfig(config)
// 这个元素并不是那个弹出来的框框, 而是modal-dialog类元素的更外一层, 即Modal的最外层(注意,也不是背景层, 背景层是动态添加在body底部的元素)
this._element = element
// 这个就是指弹出来的框框元素
this._dialog = $(element).find(Selector.DIALOG)[0]
// 背景阴影层元素
this._backdrop = null
// 是否弹框状态
this._isShown = false
// 原页面是否有滚动条
this._isBodyOverflowing = false
// 是否忽略_element的click事件(hide Modal)
this._ignoreBackdropClick = false
// 是否正在执行动画
this._isTransitioning = false
// 页面原始body的padding-right值
this._originalBodyPadding = 0
// 浏览器滚动条宽度
this._scrollbarWidth = 0
}
// public
// 弹出或是隐藏Modal
toggle(relatedTarget) {}
// 弹出
show(relatedTarget) {}
// 隐藏
hide(event) {}
// static
static _jQueryInterface(config, relatedTarget) {
return this.each(function () {
let data = $(this).data(DATA_KEY)
// 初始化的时候同时取默认属性值和按钮上的data-*属性和Modal元素的data-*属性
const _config = ...
if (!data)
data = new Modal(this, _config)
$(this).data(DATA_KEY, data)
}
// 如果是命令,那么执行对应的函数
if (typeof config === 'string') {
if (data[config] === undefined) {
throw new Error(`No method named "${config}"`)
}
data[config](relatedTarget)
} else if (_config.show) {
// 如果配置了 data-show="true",那么初始化的同时会打开Modal
data.show(relatedTarget)
}
})
}
}
// 触发click事件的按钮
$(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
// target表示按钮控制的Modal元素
let target
const selector = Util.getSelectorFromElement(this)
if (selector) {
target = $(selector)[0]
}
// 要么初始化,要么toggle
const config = $(target).data(DATA_KEY) ?
'toggle' : $.extend({}, $(target).data(), $(this).data())
// Modal的show事件触发
const $target = $(target).one(Event.SHOW, (showEvent) => {
if (showEvent.isDefaultPrevented()) {
// only register focus restorer if modal will actually get shown
return
}
// Modal在hidden事件触发后(Modal关闭后),触发此Modal出现的按钮会被focus
$target.one(Event.HIDDEN, () => {
if ($(this).is(':visible')) {
this.focus()
}
})
})
Modal._jQueryInterface.call($(target), config, this)
})
上述依旧是老套路,在Modal元素上依附实例,可以通过点击按钮初始化实例或是js调用来初始化实例,没有什么特别的地方
function show
show
函数用于弹出Modal,这里是贴出其执行的流程附加部分,这样可以更加便于理解整个执行的过程。流程如下:
- 如果有fade类, 设置_isTransitioning状态为true, 表明要执行动画
- 触发 show.bs.modal 事件,如果该事件回调过程中有执行preventdefault,那么此show函数不再继续执行
- 修改 isShown 状态为true,为将来注册事件做准备,表明将来是 show
通过document.body.clientWidth < window.innerWidth判断y轴是否有滚动条,判断结果保存在_isBodyOverflowing属性中,且通过添加删除一个
overflow:scroll
的元素, 获取此浏览器滚动条的宽度 offsetWidth - clientWidth_getScrollbarWidth() { const scrollDiv = document.createElement('div') scrollDiv.className = ClassName.SCROLLBAR_MEASURER document.body.appendChild(scrollDiv) const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth document.body.removeChild(scrollDiv) return scrollbarWidth }
- 如果页面存在滚动条,在下一步为body添加
overflow:hidden
属性后,页面缺失滚动条后变宽了,布局会变化,为了防止变化这一步添加了padding-right = 滚动条宽度,这样页面就不会有变化 - 为body添加
overflow:hidden
属性(为body添加open类) - 注册键盘ESC键触事件(关闭Modal,即调用hide函数)
随着浏览器窗口的变化,动态调整this._element的padding-left或是padding-right使得页面不发生调整
_adjustDialog() { // Modal弹出框的scrollHeight一般是与documentElement.clientHeight相同的,但是当documentElement.clientHeight过小时,Modal的scrollHeight会不变从而大于documentElement.clientHeight // 这个时候虽然body是overflow:hidden的,但Modal最外层是宽高100%且overflow-y:auto的,所以页面就会显示滚动条 // isModalOverflowing表示此时是否为Modal显示滚动条 const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight // 原来页面是没有滚动条的 // 这种情况是考虑突然出现滚动条 // 现在要为Modal添加滚动条,如果突然加上滚动条(相当于突然添加padding-right:17px)页面会发生变化, // 但是如果添加了padding-left:17px,此时和隐形的padding-right:17px(即滚动条)相抵,页面就不发生变化了 // 实验中发现:突然变小浏览器窗口的高度,Modal位置不会发生变化,但是高度再一次变大后Modal会移动(因为bootstrap没有移除padding-left:17px,但此时滚动条没了相当于只变了padding-right), // 再次变小又会移动(因为变小之前没有滚动条只有padding-left:17px, 而突然右边多了个滚动条,页面布局当然发生变化) if (!this._isBodyOverflowing && isModalOverflowing) { this._element.style.paddingLeft = `${this._scrollbarWidth}px` } // 原本整个页面body是有滚动条的(相当于padding-right:17px) // 这种情况是考虑突然没有了滚动条 // 原本是有滚动条的,但是窗口大小发生变化后突然没了滚动条,相当于padding-right突然为0,这样 // 会导致页面发生变化,此时如果加上padding-right的属性代替之前的滚动条,那么相当于页面没有变化 if (this._isBodyOverflowing && !isModalOverflowing) { this._element.style.paddingRight = `${this._scrollbarWidth}px` } }
- 为右上角的关闭按钮注册关闭事件
注册事件防止一种情况:在弹出框_dialog上鼠标点下去了,但是在外层_element上把鼠标松开来了,这种情况下即使配置属性backdrop是true,弹出框也不会hide
$(this._dialog).on(Event.MOUSEDOWN_DISMISS, () => { $(this._element).one(Event.MOUSEUP_DISMISS, (event) => { // 同时避免事件冒泡 if ($(event.target).is(this._element)) { this._ignoreBackdropClick = true } }) })
- 下面步骤即为显示阴影层(_backdrop元素)
- 将带有class为 ‘.modal-backdrop.fade’ 的div元素(即阴影层)插入body (注意:’.modal-backdrop’控制此全屏显示 (fixed, left,right,top,bottom:0))
为最外层的_element注册点击事件,如果配置属性backdrop是true,那么点击后弹出框会hide
$(this._element).on(Event.CLICK_DISMISS, (event) => { // 如果此次点击,mousedown发生在_dialog,而mouseup发生在_element,从而触发了click // 那么即使配置属性backdrop是true,也不会hide这个Modal if (this._ignoreBackdropClick) { this._ignoreBackdropClick = false return } // target一般指触发事件的元素,currentTarget(相当于this)一般则是事件冒泡上来触发函数的元素 // 避免事件冒泡触发此函数(这里由于是包含关系,所以必须避免这种情况) if (event.target !== event.currentTarget) { return } if (this._config.backdrop === 'static') { this._element.focus() } else { this.hide() } })
- 为背景阴影层添加show类,fade+show=(opacity:0.5)
- 下面步骤显示弹框(_element + _dialog)
- 为_element添加show类,fade + show = (opacity : 1)
- 触发shown事件
- 执行动画弹框或是不支持动画直接弹框
以上为整个Show函数的执行过程,虽然整个过程为了防止页面布局发生变化花了很多心思,但一些情况下依旧会变化,如果有时间,我希望能够来调整一番
function hide
hide事件与show事件恰好相反,但是要关注的细节明显少了许多,下面直接贴上代码以及注释
hide(event) {
// 触发hide事件
... ...
// 改变_isShown状态,应对接下来的事件处理
this._isShown = false
// 移除键盘ESC触发的事件监听
this._setEscapeEvent()
// 移除窗口变化的事件监听
this._setResizeEvent()
// 最外层移除show类, 即 opacity:0
$(this._element).removeClass(ClassName.SHOW)
// 移除点击非_dialog元素hide Modal的事件监听
$(this._element).off(Event.CLICK_DISMISS)
// 移除防止鼠标点击极端情况的事件监听
$(this._dialog).off(Event.MOUSEDOWN_DISMISS)
// 隐藏_element元素(_hideModal函数)
this._element.style.display = 'none'
// 触发hidden事件
... ...
}
四、细节
最后,这里讨论一下窗口变化事件( $(window).resize()
)注册的 _adjustDialog
回调,此事件影响了弹出框相对于浏览器窗口发生变化时页面做出的布局调整情况。
主要根据两个属性作出相应变化:
_isBodyOverflowing
: 原始页面是否有滚动条,相当于是否已经为body元素设置padding-right:?px
属性,?
表示滚动条宽度isModalOverflowing
: 弹出框是否有滚动条
根据代码,只能处理以下两种情况:
- 若body标签没有
padding-right:?px
属性,弹出框在窗口调整后需要滚动条,为_element元素添加属性paddingLeft: ?px
- 若body标签有
padding-right:?px
属性,弹出框在窗口调整后不再需要滚动条,为_element元素添加属性paddingRight: ?px
五、修改
对于上述处理方式,可能是我没能完全理解编写的初衷,或是有误解。但是为了达到浏览器高度变化不会导致弹出框发生调整的目的,我自己有了如下实现。
实现的主要思想是:
1. 当滚动条从无到有,那么设置_element的padding-left为滚动条宽度;
2. 当滚动条从有到无,设置_element的左右padding都为0
下面是在 _showElement
函数添加的代码,目的是在Modal弹出后获取当前是否显示滚动条:
_showElement(){
... ...
// 弹出动画结束后(如果有动画的话)的回调函数
const transitionComplete = () => {
if (this._config.focus) {
this._element.focus()
}
this._isTransitioning = false
$(this._element).trigger(shownEvent)
// 在shown钩子触发后,获取当前是否显示滚动条
this.prevModalOverFlow = this._element.scrollHeight > document.documentElement.clientHeight;
if(this.prevModalOverFlow){
// 如果在弹出Modal时发现是有显示滚动条的,那么按照上述思想设置padding-left的值
// 一开始就没滚动条那初始状态就是padding两边都为0
this._element.style.paddingLeft = this._scrollbarWidth + 'px';
}
}
}
下面是改变的原来 _adjustDialog
函数代码:
_adjustDialog() {
// 判断此次调整是否有显示滚动条,与之前一样
const isModalOverflowing =
this._element.scrollHeight > document.documentElement.clientHeight
// 下面代码也很直观
if(this.prevModalOverFlow && !isModalOverflowing){
this._element.style.paddingRight = '';
this._element.style.paddingLeft = '';
}
if(!this.prevModalOverFlow && isModalOverflowing){
this._element.style.paddingLeft = this._scrollbarWidth + 'px';
this._element.style.paddingRight = '';
}
this.prevModalOverFlow = isModalOverflowing
}
这样一来,浏览器的高度无论如何变化,Modal都不会左右移动。