码迷,mamicode.com
首页 > Web开发 > 详细

Bootstrap源码:dropdown.js

时间:2015-07-09 23:01:05      阅读:483      评论:0      收藏:0      [点我收藏+]

标签:

bootstrap的dropdown.js,封装了一个非常灵活易用的下拉组件,在各种下拉场景中稍加变换,都能实现目标效果,还能跟其他的组件良好地结合,比如前面的tab.js,搭配完成更强大的组件功能。这个组件除了js之外,html的结构和css的配合更是精妙,我从这个组件里面学到了不少有用的经验和技巧,下面是它的html结构:

<div class="dropdown">
	<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="true">
		Dropdown
		<span class="caret"></span>
	</a>
	<ul class="dropdown-menu" aria-labelledby="drop3">
		<li><a href="#">Action</a></li>
		<li><a href="#">Another action</a></li>
		<li><a href="#">Something else here</a></li>
		<li role="separator" class="divider"></li>
		<li><a href="#">Separated link</a></li>
	</ul>
</div>

整个dropdown组件包裹在类为dropdown的容器div里面,内部由一个data-toggle="dropdown"的a元素和类为dropdown-menu的ul元素组成,a元素是触发下拉菜单显示隐藏的toggle元素,ul元素是下拉菜单的内容,这个组件灵活之处就是这里的a元素和ul元素并不是固定的,可以换成其他可用元素,只需要加上相应的css类即可。由此可看出,dropdown组件的html是十分简单的。为了实现下拉的效果,dropdown的css写的非常优美:

.dropdown-menu {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 1000;
  display: none;
  float: left;
  min-width: 160px;
  padding: 5px 0;
  margin: 2px 0 0;
  font-size: 14px;
  text-align: left;
  list-style: none;
  background-color: #fff;
  -webkit-background-clip: padding-box;
  background-clip: padding-box;
  border: 1px solid #ccc;
  border: 1px solid rgba(0, 0, 0, .15);
  border-radius: 4px;
  -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
  box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
}

对于一个下拉菜单来说,最重要的就是下拉区域的定位问题,bootstrap通过top:100%这个属性设置,轻松地完成下拉菜单的定位问题,不需要任何的js计算操作。 细心的话就会发现,只要控制下拉元素的display就可以简单地实现下拉的效果,不过bs为了兼容更多的场景,加入动画的效果,js的处理也并不简单。下面分析理解dropdown.js的源码。

1. 构造函数和插件定义十分简单

var backdrop = ‘.dropdown-backdrop‘
  var toggle   = ‘[data-toggle="dropdown"]‘
  var Dropdown = function (element) {
    $(element).on(‘click.bs.dropdown‘, this.toggle)
  }

  Dropdown.VERSION = ‘3.3.4‘

  function Plugin(option) {
    return this.each(function () {
      var $this = $(this)
      var data  = $this.data(‘bs.dropdown‘)

      if (!data) $this.data(‘bs.dropdown‘, (data = new Dropdown(this)))
      if (typeof option == ‘string‘) data[option].call($this)
    })
  }

  var old = $.fn.dropdown

  $.fn.dropdown             = Plugin
  $.fn.dropdown.Constructor = Dropdown


  // DROPDOWN NO CONFLICT
  // ====================

  $.fn.dropdown.noConflict = function () {
    $.fn.dropdown = old
    return this
  }

2. DATA API比前几个组件稍微复杂,作用都注明在注释里

$(document)
    .on(‘click.bs.dropdown.data-api‘, clearMenus)//目的是为了在展开下拉菜单后,再次点击页面任何区域都能隐藏之前已经展开的菜单,考虑的是一个页面中存在多个下拉菜单的情况,但是也有一个问题,就是点击展开的下拉菜单里面的菜单项,同样会隐藏菜单,这在不需要隐藏菜单的场景中将会是一个问题
    .on(‘click.bs.dropdown.data-api‘, ‘.dropdown form‘, function (e) { e.stopPropagation() })//假如下拉菜单里有表单元素时,通过冒泡阻止菜单的隐藏,就是阻止第一行代码里的监听器响应点击事件
    .on(‘click.bs.dropdown.data-api‘, toggle, Dropdown.prototype.toggle)//自动注册dropdown组件
    .on(‘keydown.bs.dropdown.data-api‘, toggle, Dropdown.prototype.keydown)//在toggle元素获取焦点后,允许按向下箭头展开菜单
    .on(‘keydown.bs.dropdown.data-api‘, ‘[role="menu"]‘, Dropdown.prototype.keydown)//允许在展开菜单后,可以通过向上向下箭头,切换菜单项
    .on(‘keydown.bs.dropdown.data-api‘, ‘[role="listbox"]‘, Dropdown.prototype.keydown)//允许在展开菜单后,可以通过向上向下箭头,切换菜单项

3. 方法解析,关键逻辑在注释里

    Dropdown.prototype.toggle = function (e) {
    var $this = $(this)

    if ($this.is(‘.disabled, :disabled‘)) return

    var $parent  = getParent($this)//getParent方法用来获取父元素,不过这个父元素很有可能不是dom结构上的父节点,而是通过data-target或者href指定的某个dom元素,所以才有专门的一个方法来写
    var isActive = $parent.hasClass(‘open‘)//父元素有open类,则说明菜单当前是已经展开的

    clearMenus()//先清空已经展开的所有菜单

    //只有在菜单未展开的时候才进行以下逻辑处理
    if (!isActive) {
      if (‘ontouchstart‘ in document.documentElement && !$parent.closest(‘.navbar-nav‘).length) {
        // if mobile we use a backdrop because click events don‘t delegate
        $(‘<div class="dropdown-backdrop"/>‘).insertAfter($(this)).on(‘click‘, clearMenus)
      }
      //以上代码的目的是为了在移动端里展开菜单后,点击页面任何区域都能隐藏菜单,因为bs的事件都是通过代理注册的监听器,而click事件在移动端里如果是通过代理注册的,不会执行相应的监听器,所以bs才用了backdrop这样的一个元素,替代实现隐藏菜单的功能。

      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
  }

  Dropdown.prototype.keydown = function (e) {
    if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return
    //如果按键不是向上向下箭头,空格和ESC键,或者按键是为了在文本控件里输入字符,就不做以下处理
    //注意按下回车键,会触发click事件!!!

    var $this = $(this)

    e.preventDefault()
    e.stopPropagation()

    if ($this.is(‘.disabled, :disabled‘)) return

    var $parent  = getParent($this)
    var isActive = $parent.hasClass(‘open‘)

    if ((!isActive && e.which != 27) || (isActive && e.which == 27)) {
      //实现的就是按向上向下和空格键展开菜单,再按esc键隐藏菜单。。。
      if (e.which == 27) $parent.find(toggle).trigger(‘focus‘)//这里需要用find(toggle)的原因是因为,$this有可能是[role=menu]的dom元素而不是data-toggle元素
      return $this.trigger(‘click‘)
    }

    var desc = ‘ li:not(.disabled):visible a‘
    var $items = $parent.find(‘[role="menu"]‘ + desc + ‘, [role="listbox"]‘ + desc)

    if (!$items.length) return

    var index = $items.index(e.target)

    if (e.which == 38 && index > 0)                 index--                        // 按向上键,index--
    if (e.which == 40 && index < $items.length - 1) index++                        // 按向下键,index++
    if (!~index)                                      index = 0
    //~index的作用:~3 = -4,~4=-5,~5=-6,~0=-1,~-1=0,~-2=1,~-3=2,其实没必要搞这种写法,尼玛。。。

    $items.eq(index).trigger(‘focus‘)
  }
  
  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 }

      if (!$parent.hasClass(‘open‘)) 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)
    })
  }

  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)

    return $parent && $parent.length ? $parent : $this.parent()
  }



Bootstrap源码:dropdown.js

标签:

原文地址:http://my.oschina.net/lyzg/blog/476787

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!