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