Bootstrap源码:button.js

bootrap的button.js可以提供三个方面的功能:

1. 设置按钮的状态:比如在按钮在做ajax操作的时候,我们可以先把这个按钮设置为loading状态,等到ajax回调的时候再把按钮状态恢复

2. 所谓的按钮toggle,点一下,按钮选中,始终保持focus的样式,再点一下,按钮取消选中,:

3. 结合checkbox或radio和btn等相关按钮的样式,实现按钮式风格的多选或单选控件

多选:

单选:

开始分析源码。

先看plugin definition:

function Plugin(option) {
    return this.each(function () {
      var $this   = $(this)
      var data    = $this.data('bs.button')
      var options = typeof option == 'object' && option
      if (!data) $this.data('bs.button', (data = new Button(this, options)))
      if (option == 'toggle') data.toggle()
      else if (option) data.setState(option)
    })
  }

基本逻辑跟alert.js差不多,区别是最后两行代码,当我们调用$('.btn').button('toggle'),实际上调用的是Button类的toggle方法,用来实现第2、3个功能;当我们调用$('.btn').button('loading'),实际上调用的是Button类的setState方法,用来实现第1个功能。从这段可以看出,Button组件用了一个类实现了两个不相关的功能。

第二看data api:

$(document)
    .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) {
      var $btn = $(e.target)
      if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
      Plugin.call($btn, 'toggle')
      e.preventDefault()
    })
    .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) {
      $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type))
    })

从这段代码可以看出,toggle的功能是默认就初始化的,只要html元素上有data-toggle=button的属性,而设置状态的功能是没有默认就初始化的,需要根据实际情况手工调用,这主要是考虑到实际应用中各个按钮的功能逻辑太过多样性,不可能完全标准化起来,所以才没有统一处理。

先看第一个跟click相关的代码,这种带命名空间的事件绑定方式可以很方便地触发同一元素同一标准事件如click的部分事件监听器(假如该元素的某一标准事件注册了多个监听器的情况下)和移除同一元素同一标准事件的部分选择器,比如说再对[data-toggle^="button"]这个选择器的所有元素,注册一个新的命名空间的事件监听器:

$(document)
    .on('click.operation.log', '[data-toggle^="button"]', (function(){
        var clickCount = 0;
        return function(e){
            console.log('一共点击了' + (++ clickCount) + '下按钮!');
        }
    })())

当:

$('[data-toggle^="button"]').click();

时,上面定义的两个事件监听器都会被触发;

当:

$('[data-toggle^="button"]').trigger('click!');

时,上面定义的两个事件监听器都不会被触发;

当:

$('[data-toggle^="button"]').trigger('click.operation.log');

时,第一个click事件不会被触发;

当:

$('[data-toggle^="button"]').off('operation.log');

时,第一个click事件监听器不会被解除绑定。

再来看这行代码:

Plugin.call($btn, 'toggle')

跟下面的代码作用是一样的,只是上面的显得比较高级:

$btn.button('toggle')

再看这行代码:

$(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type))

为什么要调用closest方法,这得看一下使用button组件时对应的html结构都是怎样的:

#对应第一个功能的html
<button type="button" id="myButton" data-loading-text="Loading..." class="btn btn-primary" autocomplete="off">
  Loading state
</button>

#对应第二个功能的html
<button type="button" class="btn btn-primary" data-toggle="button" aria-pressed="false" autocomplete="off">
  Single toggle
</button>

#最后的两个都对应第三个功能的html
<div class="btn-group" data-toggle="buttons">
  <label class="btn btn-primary active">
    <input type="checkbox" autocomplete="off" checked> Checkbox 1 (pre-checked)
  </label>
  <label class="btn btn-primary">
    <input type="checkbox" autocomplete="off"> Checkbox 2
  </label>
  <label class="btn btn-primary">
    <input type="checkbox" autocomplete="off"> Checkbox 3
  </label>
</div>

<div class="btn-group" data-toggle="buttons">
  <label class="btn btn-primary active">
    <input type="radio" name="options" id="option1" autocomplete="off" checked> Radio 1 (preselected)
  </label>
  <label class="btn btn-primary">
    <input type="radio" name="options" id="option2" autocomplete="off"> Radio 2
  </label>
  <label class="btn btn-primary">
    <input type="radio" name="options" id="option3" autocomplete="off"> Radio 3
  </label>
</div>

从最后两个html结构可以看出,data-toggle对应的元素只是按钮元素的容器,而且按钮元素下面还有checkbox radio元素,如果点击的是这些checkbox radio,那么通过e.target获取到的就不是按钮元素;不过当点击的不是checkbox radio,直接就是按钮元素的时候,e.target获取到的就是按钮元素了,所以通过closet这个方法可以很方便地获取到想要的元素:

closest会首先检查当前元素是否匹配,如果匹配则直接返回元素本身。如果不匹配则向上查找父元素,一层一层往上,直到找到匹配选择器的元素。如果什么都没找到则返回一个空的jQuery对象。
closest和parents的主要区别是:1,前者从当前元素开始匹配寻找,后者从父元素开始匹配寻找;2,前者逐级向上查找,直到发现匹配的元素后就停止了,后者一直向上查找直到根元素,然后把这些元素放进一个临时集合中,再用给定的选择器表达式去过滤;3,前者返回0或1个元素,后者可能包含0个,1个,或者多个元素。

最后的toggleClass: 

toggleClass('focus', /^focus(in)?$/.test(e.type))

首先得知道不是所有元素都能触发focus跟blur事件,一般只有表单元素,链接,按钮才支持,当这些元素获取焦点时,会在该元素上触发focus事件,但是focus事件不会冒泡到这些元素的父元素,父元素要想捕获自己的内部元素是否获取到了焦点时,可以通过focusin事件来注册监听器:

$( "p" ).focusin(function() {
  $( this ).find( "span" ).css( "display", "inline" ).fadeOut( 1000 );
});

但是在jquery里面,当一个不支持focus事件的元素的内部元素获取到了焦点时,父元素同样可以通过focus事件注册监听器,只是传递给监听器的事件类型是focusin而不是focus,所以才有了这种处理方式:

.on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) {
      $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type))
    })

这样就能统一对[data-toggle^="button"]元素的focus事件的处理,只要在监听器里判断e.type即可。当[data-toggle^="button"]本身就是按钮元素时,监听器里的type就是focus,当[data-toggle^="button"]是包裹元素时如btn-group,监听器里的type就是focusin。

blur与focusout事件的关系同理:利用jquery,可以为不支持blur事件的元素注册blur事件的监听器,当内部元素触发blur事件时,父元素依然能捕获到这个事件,只是事件的type是focusout。

toggleClass的这个使用方式也很特别:

toggleClass('focus', /^focus(in)?$/.test(e.type))

作用是只有在第二个参数为true时,才给元素添加样式,其他情况都是移除样式。

这一行代码的整体作用是:当data-toggle=button的元素或其内部获得焦点时,就给相应的按钮元素加上focus类,否则就移除该类。之所以加上focus类目的,主要还是为了统一bootstrap整个框架的风格,bootstrap使用了大量的伪类如:active,:focus等来定义元素在激活和获取焦点等事件时的样式,而这些样式并不会一直保留,当焦点转移之后,样式就没了,对于button这个组件来说,toggle的作用需要保持组件的状态,不能受到焦点事件的影响,所以才定义了focus这样的类,类似的还有.active类,这样当点击button组件之后,鼠标再点击别的地方,button组件还是看起来像获取了焦点的状态一样。。。但真正情况并不是这样的:

1. singgle的按钮元素虽然能触发focus.bs.button.data-api blur.bs.button.data-api这两个事件,但是由于blur的影响,focus类在焦点转移之后就没了。。。而且按钮元素本身就是支持focus事件的button元素,所以加focus也没必要,当然它会加

2. checkbox radio的按钮组,由于按钮元素并不是真正的button,只是一个label,所以理论上,当label内部获取到焦点的时候,加上focus类,让它看上去更像一个按钮,听起来不错,事实是由于label内部的checkbox和radio根本没显示出来,所以用户连点它们的机会都没有,这两个事件怎么会触发跟捕获了。

看起来这几句的处理是有点多余了,唯一派的上用场的只有当按钮元素内部有可见的可以触发focus事件的元素才行了。

最后看Button类的定义

构造函数和setState方法:
var Button = function (element, options) {
    this.$element  = $(element)
    this.options   = $.extend({}, Button.DEFAULTS, options)
    this.isLoading = false
  }
  Button.VERSION  = '3.3.4'
  Button.DEFAULTS = {
    loadingText: 'loading...'
  }
  Button.prototype.setState = function (state) {
    var d    = 'disabled'
    var $el  = this.$element
    var val  = $el.is('input') ? 'val' : 'html'
    var data = $el.data()
    state = state + 'Text'
    if (data.resetText == null) $el.data('resetText', $el[val]())
    // push to event loop to allow forms to submit
    setTimeout($.proxy(function () {
      $el[val](data[state] == null ? this.options[state] : data[state])
      if (state == 'loadingText') {
        this.isLoading = true
        $el.addClass(d).attr(d, d)
      } else if (this.isLoading) {
        this.isLoading = false
        $el.removeClass(d).removeAttr(d)
      }
    }, this), 0)
  }

button组件默认提供了两种状态,loading和reset,loading状态时按钮上显示的文字为loading...,可以通过data-loading-text或者在options中增加loadingText属性来自定义该文本。只有loading状态,才会给组件加禁用的样式和disabled属性,其他状态都会移除禁用样式和disabled属性。所有使用loading和reset之外的状态没有什么特殊的意义,就只能用来显示不同的按钮文本。需要注意的是,由于button组件可以加在任何元素上,所以在setState方法里,根据元素的类型,用val定义了接下来设置组件文本时的方法:

    var val  = $el.is('input') ? 'val' : 'html'

这对应的就是jquery的val方法和html方法。利用了[]操作符调用方法的特性。这段代码不难理解,不过还有一个写法值得借鉴,就是setTimeout延时0ms的用法,看起来跟直接调用没有区别,实际上这种调用是有特殊的作用的:setTimeout延时0ms的作用。

再看看toggle功能相关的代码:

  Button.prototype.toggle = function () {
    var changed = true
    var $parent = this.$element.closest('[data-toggle="buttons"]')
    if ($parent.length) {
      var $input = this.$element.find('input')
      if ($input.prop('type') == 'radio') {
        if ($input.prop('checked') && this.$element.hasClass('active')) changed = false
        else $parent.find('.active').removeClass('active')
      }
      if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change')
    } else {
      this.$element.attr('aria-pressed', !this.$element.hasClass('active'))
    }
    if (changed) this.$element.toggleClass('active')
  }

首先得理解这段代码实现的功能,对于单纯的toggle状态切换按钮,点击后按钮被选中,即使失去焦点,选中状态也会保留,再次点击才会取消;对于多选按钮,跟单纯的toggle按钮差不多;唯一不同的是,单选按钮的toggle,因为在一个按钮组中,选中某一个按钮后必须取消选中其他按钮,并且点击两次同一按钮,不会取消按钮的选中状态,因为单选控件是不能取消选中的,而多选则可以。接下来看代码:

$parent指向button组件的容器;$element指向带有.btn类的按钮元素;该方法默认每次调用都会改变按钮状态,所以changed变量初始化为true,假如没有单选和和复选,最后一行代码就可以实现singgle toggle的功能。假如是单选和复选,则对单选进行特殊处理,如果单选控件本身已选中(checked)并且按钮元素已经有了active的样式,则表示该按钮已经点过一次,则changed置为false,以后多次点击都会执行这个功能。如果下次点击的不是同一按钮元素,则会通过以下代码清除上次点击按钮的选中状态:

$parent.find('.active').removeClass('active')

单选框怎么没有取消?表单内多个单选框如果name相同,点击后只有一个单选框会选中,之前选中的会自动取消,这是html的标准。

if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change')

这个代码就比较简单了。复选跟单选都需要它来设置复选框和单选框的选中状态,同时会触发复选框和单选框上面绑定的事件。

你可能感兴趣的:(bootstrap,bootstrap源码)