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 的例子中

Paste_Image.png

当点击取消某个已选项时,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();
  }
}

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 事件冒泡
0 条评论
您想说点什么吗?