bootstrap的tab.js封装了tab组件的相关功能,tab组件在网页里面非常常见,可以有限的空间里展示更多的网页内容。由于tab组件比之前的alert组件和button组件提供的功能相对高级一些,与之对应的html结构也比前两个组件复杂一些。在了解tab.js相关源码前,有必要先了解tab组件的html结构:
<div> <ul class="nav nav-tabs" > <li class=""> <a href="#home" data-toggle="tab" aria-expanded="false">Home</a> </li> <li class=""> <a href="#profile" data-toggle="tab" aria-expanded="false">Profile</a> </li> <li class="dropdown active"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false">Dropdown <span class="caret"></span> </a> <ul class="dropdown-menu"> <li class="active"> <a href="#dropdown1" data-toggle="tab" aria-expanded="true">@fat</a> </li> <li class=""> <a href="#dropdown2" data-toggle="tab" aria-expanded="false">@mdo</a> </li> </ul> </li> </ul> <div class="tab-content"> <div class="tab-pane fade" id="home" > <p>1</p> </div> <div class="tab-pane fade" id="profile" > <p>2</p> </div> <div class="tab-pane fade active in" id="dropdown1"> <p>3</p> </div> <div class="tab-pane fade" id="dropdown2"> <p>4</p> </div> </div> </div>
最外面的div是整个tab组件的容器,不可缺少。tab组件分成两部分,第一部分是导航部分,使用nav类;第二部分是内容部分,使用tab-content类。切换tab组件的关键元素是导航部分里面带有data-toggle="toggle"的a元素,相关事件都是由这些a元素的点击事件触发的。当前激活的tab组件,导航部分和内容部分相应的元素都会带有active的css类。比如以上html中就能看到导航部分的直接li子元素有一个是有active类的,内容部分里面某一个带有tab-pane类的div元素同时也有active的类。需要注意的是,bs的tab组件同时还支持导航部分里面使用下拉菜单组件,这样当点击某个下拉菜单相关的li时,并不会直接显示相应的tab-pane元素,而是会弹出下拉菜单,当点击下拉菜单里面的下拉项时,tab组件的内容部分会显示相应的tab-pane元素。如图:
接下来看tab.js的代码。
1. 先看plugin definition(略,比较简单,基本同button.js)
2. data-api
var clickHandler = function (e) { e.preventDefault() Plugin.call($(this), 'show') } $(document) .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler)
由这段代码可以看出两点:
一是引发tab组件切换的元素除了带有data-toggle="tab"属性的元素外,还有data-toggle="pill"的元素,这主要是因为bs的导航组件除了这种tab式的以外,还有胶囊式的,也成为nav-pills,如:
二是其实Plugin.call($(this), 'show')这种写法,还不如$(this).tab('show')来的直接。
3. 关键方法源码解析(分析内容直接写成注释了,这样更清晰,便于自己今后复习)
Tab.prototype.show = function () { var $this = this.element var $ul = $this.closest('ul:not(.dropdown-menu)')//这里必须加:not(.dropdown-menu),因为如果不加这个,在带有下拉菜单的tab组件中,点下拉菜单里面的data-toggle元素,获取到的ul元素不是tab组件导航部分的容器ul元素 var selector = $this.data('target')//从这里可以看出,可以在data-toggle元素上,通过data-target属性来指定相应的tab-pane元素,不过该属性必须是有效的jquery选择器字符串 if (!selector) { selector = $this.attr('href') selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 }//这个if的处理,是考虑那种没有配置data-target属性的时候,直接通过data-toggle元素的href属性值来获取相应的target选择器,从本文最顶部的html结构中就能看到相关的配置 if ($this.parent('li').hasClass('active')) return//当前激活的tab组件,再次点击,直接返回,不再处理 var $previous = $ul.find('.active:last a') var hideEvent = $.Event('hide.bs.tab', { relatedTarget: $this[0] }) var showEvent = $.Event('show.bs.tab', { relatedTarget: $previous[0] }) $previous.trigger(hideEvent)//对于上一个激活的元素来说,需要触发它的hide事件,并且把事件的relatedTarget设置成当前激活的元素 $this.trigger(showEvent)//对于当前激活的元素来说,需要触发它的show事件,并且把事件的relatedTarget设置成上一个激活的元素 //但是需要注意的是,bs的大部分组件都是带有动画过程的,所以大部分组件的显示隐藏都会由一对事件,如show和shown,体现出开始显示,显示动画和显示完成的一个过程 //上面两行代码,都是在动画显示前触发的事件,在动画结束后还会再触发一次事件,回调动画完成之后的处理 if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return var $target = $(selector) this.activate($this.closest('li'), $ul) this.activate($target, $target.parent(), function () { $previous.trigger({ type: 'hidden.bs.tab', relatedTarget: $this[0] }) $this.trigger({ type: 'shown.bs.tab', relatedTarget: $previous[0] }) }) //activate方法调用了两次,分别用来完成导航和内容部分的显示隐藏处理,相关代码分析如下: }
Tab.prototype.activate = function (element, container, callback) { var $active = container.find('> .active')//不管是导航部分还是内容部分,激活的元素都是带有active类的元素 var transition = callback && $.support.transition && (($active.length && $active.hasClass('fade')) || !!container.find('> .fade').length) //transition表示是否需要动画的条件 //这个是这样的,在show方法里,导航部分相关的activate调用并没有传递callback参数,所以导航部分是肯定不会使用动画的 //而内容部分是根据tab-pane元素是否具有fade属性来决定是否采用动画的 function next() { $active .removeClass('active') .find('> .dropdown-menu > .active') .removeClass('active') .end() .find('[data-toggle="tab"]') .attr('aria-expanded', false) //以上做的就是把上次激活的元素取消激活,不过额外添加了一些针对下拉菜单的特殊处理,如果上次激活的是一个下拉菜单,还得下拉菜单里面激活的菜单项取消激活,所以才有了这个调用: .find('> .dropdown-menu > .active').removeClass('active') element .addClass('active') .find('[data-toggle="tab"]') .attr('aria-expanded', true) //以上做的就是激活当前点击的元素或者相对应的tab-pane元素了 if (transition) { element[0].offsetWidth // reflow for transition,这句一直没弄明白啥意思,既不是复制,也不是函数调用,有可能纯粹是为了刷新dom的目的,因为offsetWidth是需要计算dom的 element.addClass('in')//动画结束,添加in类,设置opactity=1 } else { element.removeClass('fade')//不需要动画的,自然要fade类去掉,不然就变透明了 } if (element.parent('.dropdown-menu').length) { //下面这段是用来处理带有下拉菜单场景的 //当点击的是下拉菜单的菜单项时,element其实是下拉菜单dropdown-menu里面的li元素,而不是tab组件导航部分里面的li元素,所以要通过下面的代码给导航部分的元素加上active类 element .closest('li.dropdown') .addClass('active') .end() .find('[data-toggle="tab"]') .attr('aria-expanded', true) } callback && callback() } //下面这段的分析见transition.js $active.length && transition ? $active .one('bsTransitionEnd', next) .emulateTransitionEnd(Tab.TRANSITION_DURATION) : next() $active.removeClass('in')//in这个css类肯定是跟fade类一起搭配使用的,因为fade类配置了opacity=0,而in类配置了opacity=1,在动画运行之前,肯定要先把opacity还原成0,然后动画结束再把opacity设置成1即可,这样才能看到明显的动画效果。 //不过对于本来就没有fade属性的元素来说,这个代码就没有什么影响了 }