Bootstrap源碼:dropdown.js

jopen 9年前發布 | 73K 次閱讀 Bootstrap Ajax框架

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()
  }

來自:http://my.oschina.net/lyzg/blog/476787

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!