5.3.3 源码分析

分析源码之前,我们先来回顾一下下拉菜单的HTML布局规则的定义。示例如下:

  1. <div class="dropdown">
  2. <a class="dropdown-toggle" data-toggle="dropdown" href="#">Dropdown trigger</a>
  3. <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
  4. ...
  5. </ul>
  6. </div>

上述示例可以看出下拉菜单的几个重要属性:触发属性(data-toggle="dropdown")、目标父容器属性(href="#")、按钮元素容器(带dropdown-menu样式的<ul>)。

步骤1 立即调用的函数。此步骤与Modal插件的步骤1一样,此处不赘述。

步骤2 插件核心代码。Dropdown插件的核心源码如下:

  1. var backdrop = '.dropdown-backdrop' // 弹出下拉菜单时的背景样式
  2. var toggle = '[data-toggle=dropdown]' // dropdown触发元素的自定义属性
  3. var Dropdown = function (element) {
  4. var $el = $(element).on('click.bs.dropdown', this.toggle)
  5. // 插件类函数定义,一旦触发,就在click事件上绑定toggle,所以不能再用自定义代码进行toggle了
  6. }
  7. // 控制下拉菜单的打开、关闭操作
  8. Dropdown.prototype.toggle = function (e){};
  9. // 利用箭头控制下拉菜单(例如,按向下箭头的时候,打开下拉菜单)
  10. Dropdown.prototype.keydown = function (e){};
  11. // 关闭所有的下拉菜单
  12. function clearMenus(){};
  13. // 获取下拉菜单的父元素容器
  14. function getParent($this) {};

上述源码,除了定义Dropdown插件类函数外,还定义了两个原型方法和两个内部函数。原型方法toggle和keydown分别是处理下拉菜单的展开、关闭操作,以及支持按键对下拉菜单的选择操作的功能。clearMenus函数用于在打开特定的下拉菜单之前,关闭所有其他的下拉菜单;getParent函数则用于查找目标元素(即下拉菜单的父元素用于在上面判断是否有open样式,不过通常触发元素和菜单项都同属于一个父元素容器,就像示例中的代码一样)。代码如下:

  1. // 控制下拉菜单的打开、关闭操作
  2. Dropdown.prototype.toggle = function (e) {
  3. var $this = $(this)
  4.  
  5. if ($this.is('.disabled, :disabled')) return // 如果有禁用标记,则不处理
  6.  
  7. var $parent = getParent($this) // 获取触发元素的父元素
  8. var isActive = $parent.hasClass('open') // 判断父元素是否有open样式,有,
  9. // 则代表当前下拉菜单正在打开状态
  10.  
  11. clearMenus() // 关闭所有其他的下拉菜单,即同一个页面,只允许同时出现一个下拉菜单
  12.  
  13. // 判断:单击时当前下拉菜单是否是打开状态
  14. // 如果是,在clearMenus阶段就已经关闭了,所以就不需要再次关闭了
  15. // 如果不是,说明默认是关闭状态,则需要展开下拉菜单
  16. if (!isActive) {
  17. // 如果是移动设备,则使用dropdown-backdrop样式,因为移动设备不支持click单击委托
  18. if ('ontouchstart' in document.documentElement && !$parent.closest
  19. ('.navbar-nav').length) {
  20. $('<div class="dropdown-backdrop"/>').insertAfter($(this)).on
  21. ('click', clearMenus)
  22. }
  23.  
  24. var relatedTarget = { relatedTarget: this }
  25. $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget))
  26. // 展开下拉菜单前,触发show事件
  27.  
  28. if (e.isDefaultPrevented()) return // 如果已经阻止了默认行为,就不再处理了
  29.  
  30. $parent
  31. .toggleClass('open') // 添加open样式,打开下拉菜单,因为
  32. // dropdown-menu在open样式内会自动显示
  33. .trigger('shown.bs.dropdown', relatedTarget)
  34. // 展开下拉菜单后,触发shown事件
  35.  
  36. $this.focus() // 给当前触发元素加上焦点
  37. }
  38.  
  39. return false // 阻止该元素后续的默认行为
  40. }
  41. // 利用箭头控制下拉菜单(例如,按向下箭头的时候,打开下拉菜单)
  42. Dropdown.prototype.keydown = function (e) {
  43. // 如果按键不是Esc、或上下方向箭头,则忽略处理
  44. if (!/(38|40|27)/.test(e.keyCode)) return
  45.  
  46. var $this = $(this)
  47. e.preventDefault() // 阻止默认行为
  48. e.stopPropagation() // 阻止冒泡
  49.  
  50. if ($this.is('.disabled, :disabled')) return // 如果有禁用标记,则不做处理
  51.  
  52. var $parent = getParent($this) // 获取触发元素的父元素
  53. var isActive = $parent.hasClass('open')
  54. // 判断父元素是否有open样式,有,则代表当前下拉菜单正在打开状态
  55.  
  56. // 如果有open样式,或者没有open样式(但按键是向下箭头的话),也打开下拉菜单
  57. if (!isActive || (isActive && e.keyCode == 27)) {
  58. if (e.which == 27) $parent.find(toggle).focus()
  59. // 如果按了向下箭头,则给触发元素加上焦点
  60. return $this.click() // 默认单击事件,打开下拉菜单
  61. }
  62. // 返回可以利用箭头选择的下拉菜单项
  63. // 打开下拉菜单时,上下箭头只操作(选中)设置了role = menu(或role=listbox)的链接项
  64. // 必须是可见的a链接,并且不包括分隔符
  65. var desc = ' li:not(.divider):visible a'
  66. var $items = $parent.find('[role=menu]' + desc + ', [role=listbox]' + desc)
  67.  
  68. if (!$items.length) return // 如果没有,则不做处理
  69. var index = $items.index($items.filter(':focus'))
  70. // 找出当前处于焦点状态的第一个下拉菜单项的索引
  71.  
  72. if (e.keyCode == 38 && index > 0) index-- // 按向上箭头的话,index减1
  73. if (e.keyCode == 40 && index < $items.length - 1) index++
  74. // 按向下箭头的话,index加1
  75. if (!~index) index = 0 // 特殊意外情况,设置为第一个菜单项
  76.  
  77. $items.eq(index).focus() // 给所选择的菜单项设置焦点
  78. }
  79. // 关闭所有的下拉菜单
  80. function clearMenus(e) {
  81. $(backdrop).remove() // 删除用于移动设备的背景
  82. $(toggle).each(function () { // 根据选择器,遍历所有的dropdown标记,然后全部关闭
  83. var $parent = getParent($(this))
  84. var relatedTarget = { relatedTarget: this }
  85. if (!$parent.hasClass('open')) return
  86. $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget))
  87. // 关闭前,触发hide事件
  88. if (e.isDefaultPrevented()) return
  89. $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget)
  90. // 关闭后,触发hidden事件
  91. })
  92. }
  93. // 获取下拉菜单的父元素容器
  94. function getParent($this) {
  95. var selector = $this.attr('data-target')
  96. // 如果设置了target,就针对该traget元素进行处理
  97. if (!selector) {
  98. selector = $this.attr('href') // 如果没有target,则查找href里设置的值
  99. selector = selector && /#[A-Za-z]/.test(selector) && selector.replace
  100. (/.*(?=#[^\s]*$)/, '')
  101. }
  102. var $parent = selector && $(selector) // 如果上述两个步骤满足其一,设置其为
  103. parent元素(下拉菜单的容器元素)
  104. // 如果都不满足,就使用当前触发元素($this)的父元素
  105. return $parent && $parent.length ? $parent : $this.parent()
  106. }

步骤3 jQuery插件定义。在jQuery上定义$.fn.dropdown插件的代码和Modal插件类似,也是遍历元素、实例化或触发动作。源码如下:

  1. var old = $.fn.dropdown
  2. // 保留其他库的$.fn.dropdown代码(如果定义的话),以便在noConflict之后,可以继续使用该老代码
  3. $.fn.dropdown = function (option) {
  4. return this.each(function () { // 根据选择器,遍历所有符合规则的元素
  5. var $this = $(this)
  6. var data = $this.data('bs.dropdown') // 获取自定义属性dropdown的值
  7.  
  8. // 如果值不存在,则将Dropdown实例设置为data-dropdown的值
  9. if (!data) $this.data('bs.dropdown', (data = new Dropdown(this)))
  10. // 如果option传递了string,则表示要执行某个方法
  11. // 比如传入了toggle,则要执行Dropdown实例的toggle方法,data["toggle"]相当于
  12. data.toggle();
  13. if (typeof option == 'string') data[option].call($this)
  14. })
  15. }
  16. $.fn.dropdown.Constructor = Dropdown // 重设插件构造器,可以通过该属性获取插件的真实类函数

步骤4 防冲突处理。源码如下:

  1. // 防冲突处理
  2. $.fn.dropdown.noConflict = function () {
  3. $.fn.dropdown = old // 恢复以前的旧代码
  4. return this // 将$.fn.dropdown.noConflict()设置为Bootstrap的dropdown插件
  5. }

步骤5 绑定触发事件。源码如下:

  1. // 绑定触发事件
  2. $(document)
  3. // 为声明式的HTML绑定单击事件,在单击以后先关闭左右的下拉菜单
  4. .on('click.bs.dropdown.data-api', clearMenus)
  5. // 如果内部有form元素,则阻止冒泡,不做处理
  6. .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
  7. // 默认行为,一般都要进行toggle操作(打开或关闭)
  8. .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle)
  9. // 为触发元素和下拉菜单项绑定keydown按钮事件,以便可以进行打开或选择操作
  10. // toggle = [data-toggle=dropdown]表示所有带有自定义属性data-toggle="dropdown"的元素
  11. .on('keydown.bs.dropdown.data-api', toggle + ', [role=menu], [role=listbox]',
  12. Dropdown.prototype.keydown)

在绑定触发事件上,dropdown插件做了很多操作,首先是确保关闭所有的下拉菜单,如果有form元素,则进行特殊忽略处理,接着才绑定真正的单击事件所要触发的内容(Dropdown.prototype.toggle),最后也提供了keydown事件的绑定,用于让键盘方向键也可以选择下拉菜单项。