使用方法

有含有"dropdown"类的容器里面包含下拉触发器(按钮,或链接等),和下拉框的内容
  • 下拉触发器必须包含"data-toggle=dropdown"的属性。可以用data-target或href指定包含下拉框的容器,如果没有,则从其父元素内找
  • 下拉框内容必须有"dropdown-menu"的类
<div class="dropdown">
  <button data-toggle="dropdown">点击</button>
  <ul class="dropdown-menu">
    <li>item1</li>
    <li>item1</li>
    <li>item3</li>
  </ul>
</div>

核心思想

  1. 页面完成载入后,为documen和data-toggle="dropdown"绑定点击处理程序
  2. 点击document,则清除页面内所有下拉框的父元素的"open"类,从而隐藏下拉框
  3. 点击触发下拉框的元素,为其父元素增加"open"类,同时return false阻止冒泡,防止冒泡到document上调用隐藏下拉框的处理函数

初始化

在页面加载完就绑定document即对应下拉框触发器的点击事件
// 事件触发顺序是,如果指定事件目标,则先触发目标上的绑定事件处理程序,然后再冒泡到document
$(document)
  .on('click.bs.dropdown.data-api', clearMenus) // 绑定document点击时,调用clearMenus事件处理程序
  .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) // 事件代理,在dropdown form上点击会阻止冒泡
  .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) // 在含有data-toggle="dropdown"的元素除点击,会调用toggle方法。这里会先触发
  .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown)
  .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown)

结构分析

// 绑定触发元素的点击事件
var Dropdown = function (element) {};
// 先调用clearMenu隐藏所有下拉框,在展示当前的下拉框
Dropdown.prototype.toggle = function (e) {};
// 清除全部容器的open类
function clearMenus(e) {};
// 获得触发元素指定的容器或其父元素
function getParent($this) {};

具体分析

  • 构造函数
/**
 * 只是对传进来的元素绑定点击事件
 */
var Dropdown = function (element) {
  $(element).on('click.bs.dropdown', this.toggle)
}
  • DomContentLoaded事件内的变量方法
/** @type {String} 用于匹配backdrop类的字符串 */
var backdrop = '.dropdown-backdrop'
/** @type {String} 用于匹配含有data-toggle触发器的元素的字符串 */
var toggle   = '[data-toggle="dropdown"]'
/**
 * 先需找在触发器通过data-target或href指定的容器,若没有则默认是其父容器
 * @param  {jquery对象} $this 有data-toggle的触发器
 * @return {jquery对象}       包含触发器的容器
 */
function getParent($this) {
  var selector = $this.attr('data-target')
  if (!selector) {
    selector = $this.attr('href')
    selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
  }
  var $parent = selector && $(selector)
  // 可以在触发器上用data-target或href来自定义父容器
  // 如果没有自定义容器,则默认是触发器的父元素
  return $parent && $parent.length ? $parent : $this.parent()
}
/**
 * each遍历,把所有容器的open类去除,以此保证只能有一个出现
 * @param  {e} e 事件对象
 * @return {undefined}   无返回
 */
function clearMenus(e) {
  if (e && e.which === 3) return
  $(backdrop).remove()
  $(toggle).each(function () {
    var $this         = $(this)
    var $parent       = getParent($this)
    var relatedTarget = { relatedTarget: this }
    // 如果容器没有open,就不继续
    if (!$parent.hasClass('open')) return
    // 例外条件
    if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return

    $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget))

    if (e.isDefaultPrevented()) return
    // 不明白干什么的
    $this.attr('aria-expanded', 'false')
    // 隐藏下拉框
    $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget)
  })
}
  • toggle方法
/**
 * 下拉框的toggle,并返回false阻止冒泡到document上
 * @param  {object} e 事件对象
 * @return {boolean}   false
 */
Dropdown.prototype.toggle = function (e) {
  var $this = $(this)
  if ($this.is('.disabled, :disabled')) return
  // 包含下拉框的容器
  var $parent  = getParent($this)
  // 通过容器切换open类,来控制下拉框的显示与隐藏
  var isActive = $parent.hasClass('open')
  // 先隐藏页面的所有下拉框
  clearMenus()
  // 如果点击元素的容器没有.open(即下拉框隐藏)
  if (!isActive) {
    // 用ontouchstart来判断是否移动端
    if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) {
      // if mobile we use a backdrop because click events don't delegate
      // 移动端的click事件不会冒泡到document?实验证明,没有这一段,也能实现点击下拉框外关闭的效果
      // 移动端插入一个dropdown-backdrop元素(全屏但z-index比下拉框小),做点击下拉框外时隐藏的效果
      $(document.createElement('div'))
        .addClass('dropdown-backdrop')
        .insertAfter($(this))
        .on('click', clearMenus)
    }

    var relatedTarget = { relatedTarget: this }
    $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget))

    if (e.isDefaultPrevented()) return

    $this
      .trigger('focus')
      .attr('aria-expanded', 'true')

    $parent
      .toggleClass('open')
      .trigger('shown.bs.dropdown', relatedTarget)
  }
  // 在这里return false阻止冒泡到document上,导致下拉框隐藏
  return false
}
  • keydown方法太烂了,多数情况下都没有用,暂不分析

总结:

  1. 事件处理函数的调用顺序:先调用目标元素上的,再调用冒泡到的
  2. 可以使用'ontouchstart' in document.documentElement来判断是否移动端
  3. $(document).on(click, clearMenu)实现点击下拉框外隐藏下拉框(其实$el.cloest()也可以实现)
  4. 使用toggle类比增加样式更优雅
  5. CSS
    • 使用一个比下拉框的z-index低的dropdown-backdrop也能实现点击下拉框外隐藏下拉框
    • 设置position: absolute;top: 100%实现在父元素的下方出现下拉框
      1. postion定位后的top|bottom|left|right单位为百分比时,是相对于其父元素的宽度和高度
      2. margin的top|bottom等单位为百分比时,也是相对于其父元素的宽度和高度
      3. transform里的translate:(50%, 50%)是相对于本身的宽度和高度




Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐