Semantic-UI Dropdown Modal 组件源码分析和 JS 事件冒泡学习
背景
Git@OSC 页面使用的是 Semantic-UI 前端框架,在 Github 上看到 2.0 以上版本才支持多选下拉框 issue #847,但是现在使用的版本不支持多选下拉框。也就是 Dropdown 组件不支持 Multiple Selection 。现在码云的下拉框希望统一使用 Semantic-UI 的多选下拉框,去除其他第三方插件,所以把 Dropdown 组件单独升级到最新版本。
但是,当多选下拉框应用的 Semantic-UI 的 modal 组件中,会出现问题。现象是:多选下拉框是 modal 中的内容,为了删除某个选项,当点击多选框中某选项时,整个 modal 都会被隐藏。
寻找原因
在最新的 Semantic-UI 演示发现没有此问题,在 jsfiddle 的例子中
当点击取消某个已选项时,modal 并不会隐藏,一切都是正常的。
在 Semantic-UI modal 中,只有 Forcing a Choice 模式才不会模式才不会隐藏 modal ,其他情况譬如点击关闭图标、触发 esc 键盘事件、approve 和 deny (modal 中确定或者取消按钮),以及页面中 modal 以外的其他区域被点击都会触发隐藏 modal 事件。那么通过以上分析,在触发隐藏 modal 事件的情景中,查看码云所使用的 modal 组件源码:
event: {
close: function() {
module.verbose('Closing element pressed');
if( $(this).is(selector.approve) ) {
if($.proxy(settings.onApprove, element)() !== false) {
module.hide();
}
else {
module.verbose('Approve callback returned false cancelling hide');
}
}
else if( $(this).is(selector.deny) ) {
if($.proxy(settings.onDeny, element)() !== false) {
module.hide();
}
else {
module.verbose('Deny callback returned false cancelling hide');
}
}
else {
module.hide();
}
},
.......
close 是 event 的对象方法,$close.on('click' + eventNamespace, module.event.close);
在 $close 选择器上绑定了点击事件,同时 event.close 作为事件处理函数。
bind: {
events: function() {
$close.on('click' + eventNamespace, module.event.close);
$window.on('resize' + elementNamespace, module.event.resize);
}
},
$close 选择器在 $close = $module.find(selector.close)
,由于 selector.close 为'.close, .actions .button'
,看出来 .close
表示关闭图标,.button
是 approve,deny 和 modal 这些 selectors 都会绑定到点击事件上, 其处理程序是 module.event.close
。bind.events 的调用处,events 函数在 modal 初始化时就被调用了,如下所示:
module = {
initialize: function() {
module.verbose('Initializing dimmer', $context);
module.create.id();
module.create.dimmer();
module.refreshModals();
module.verbose('Attaching close events', $close);
module.bind.events();
module.observeChanges();
module.instantiate();
},
我们再来看看 selector :
selector : {
close : '.close, .actions .button',
approve : '.actions .positive, .actions .approve, .actions .ok',
deny : '.actions .negative, .actions .deny, .actions .cancel',
modal : '.ui.modal'
},
继续回头看看 event 对象的close 方法 判断是否隐藏的条件为 selector 是否是 approve, deny,所以这些元素被点击之后都会导致 modal 被隐藏。同时取消被选项按钮的元素结构为 <i class="delete icon"></i>
元素的点击事件并没有在 selector.close 选择器内,所以 $close.on('click' + eventNamespace, module.event.close)
这个点击时间并没有被触发。那么又是哪里的点击事件导致了 modal 隐藏呢。
接着我们看 modal 模块另外的绑定点击事件$dimmer.on('click' + elementNamespace, module.event.click)
。当在 module.showModal
对象方法调用 set.clickaway() 函数被调用时,该事件被注册。沿上追溯,module.show 调用了 showModal 函数。那么使用 $('xxxx'),modal('show')
show 函数就被调用了。$dimmer 是 modal弹出框页面整个遮罩层,监听了整个页面的点击事件,除了 selector.close 选择器内的元素外,都监听了页面所有元素的点击事件。接着我们看事件处理程序 module.event.click
click: function(event) {
if( $(event.target).closest($module).size() === 0 ) {
module.debug('Dimmer clicked, hiding all modals');
if( module.is.active() ) {
module.remove.clickaway();
if(settings.allowMultiple) {
module.hide();
}
else {
module.hideAll();
}
}
}
}
可以看出,并没有过滤掉删除多选项的图标元素,而是所有在页面的元素点击之后都会触发隐藏 modal
解决方法以及过程
上一节从源码层面研究了 Semantic-UI dropdown 多选项被点击之后,modal 被隐藏的前因后果,而实际上不希望 modal 被隐藏,那么这一节只要讨论如何解决该问题。
有两种方式:
- 阻止 dropdown 多选项被点击之后点击事件冒泡,以此 modal 组件就不会捕获到
<i class="delete icon"></i>
冒泡出来的点击事件。 - 在 modal 组件中过滤掉 modal 弹出框除了 close, approve, deny selector 之外的元素点击事件。在本篇文章讨论例子的场景中,
<i class="delete icon"></i>
元素位于 modal 弹出框之内,其点击事件如果不阻止冒泡,会被 modal 弹出框监听到从而导致 modal 被隐藏。如果监听对其冒泡出来的点击事件过滤,则会解决问题。
阻止 dropdown 多选项点击事件冒泡
这部分内容我们着重看 dropdown 组件的源码。
首先找到组件初始化函数initialize函数
中的事件的绑定module.bind.events();
,然后在 events 函数中看到鼠标事件 module.bind.mouseEvents()
,跟进去 mouseEvents 函数。
看到这样一个判断条件
if(module.is.multiple()) {
$module
.on('click' + eventNamespace, selector.label, module.event.label.click)
.on('click' + eventNamespace, selector.remove, module.event.remove.click)
;
}
在 selector 中找到 label 和 remove 分别代表什么。
label : '> .label',
remove : '> .label > .delete.icon',
可以看出多选项的删除图标正是 remove 表示,于是我们重点放在判断条件中委托绑定在 selector.remove
上的点击事件,事实上我们就是需要在这里阻止点击事件冒泡,然而通过 on 函数第三个参数 module.event.remove.click
,我们接下来定位到事件处理程序:
remove: {
click: function() {
var
$label = $(this).parent()
;
if( $label.hasClass(className.active) ) {
// remove all selected labels
module.remove.activeLabels();
}
else {
// remove this label only
module.remove.activeLabels( $label );
}
}
},
最终我们定位到了解决问题的关键地方,我们只要在事件处理函数中阻止事件冒泡即可。那么使用 jquery 的 event.stopPropagation() API 即可。修改源码结果:
remove: {
click: function(event) {
var
$label = $(this).parent()
;
if( $label.hasClass(className.active) ) {
// remove all selected labels
module.remove.activeLabels();
}
else {
// remove this label only
module.remove.activeLabels( $label );
}
event.stopPropagation();
}
}
modal 组件过滤弹出框内非 close, approve, deny 元素的的遮罩层点击事件
Semantic-UI modal 组件,点击事件的绑定在 2.0.0以后做了很大的改变。
# < 2.0.0
bind: {
events: function() {
$close.on('click' + eventNamespace, module.event.close)
}
}
# > 2.0.0
bind: {
events: function() {
module.verbose('Attaching events');
$module
.on('click' + eventNamespace, selector.close, module.event.close)
.on('click' + eventNamespace, selector.approve, module.event.approve)
.on('click' + eventNamespace, selector.deny, module.event.deny);
}
}
通过上面的代码可以看出,在 2.0.0 以前,所有 selectors(包括 close, approve, deny) 的点击都在 event.close 处理程序中处理。而 2.0.0 以后,selector.close ,selector.approve, selector.deny 分别被委托以对应的处理程序。然而这三个点击事件的绑定在这里不早研究讨论。
重要研究 modal 弹出框以后,遮罩层的点击事件。我们看其时间处理程序的代码(2.0.0 以上版本的源码)
click: function(event) {
var
$target = $(event.target),
isInModal = ($target.closest(selector.modal).length > 0),
isInDOM = $.contains(document.documentElement, event.target)
;
if(!isInModal && isInDOM) {
module.debug('Dimmer clicked, hiding all modals');
if( module.is.active() ) {
module.remove.clickaway();
if(settings.allowMultiple) {
module.hide();
}
else {
module.hideAll();
}
}
}
}
多了个 isInModal
标识,判断元素在弹出框中。除了 selector 以外,弹出框中的元素被点击会被过滤掉,没有做隐藏 modal 处理。
事实上,Semantic-UI 就是通过这种方式处理问题的,而不是使用阻止事件冒泡。
总结
- 初步掌握了分析前端框架的方法
- 深入学习 Semantic-UI Dropdown 和 Modal 组件内点击事件机制
- 掌握 javascript 事件冒泡