5.6.3 源码分析
分析源码之前来思考一下,通过前面的两种用法可以看出,tooltip组件其实在HTML方面并没有特别的要求,可以定义data-toggle="tooltip",也可以什么都不定义,而使用jQuery的选择器来查找元素,然后应用tooltip函数。
在源码分析之前,一定要熟悉前面的9个属性,因为源码里针对这9个属性做了大量的代码工作。下面来一一分析一下。
步骤1 立即调用的函数。此步骤与Modal插件的步骤1一样,此处不再赘述。
步骤2 插件核心代码。tooltip的原型方法比较多,主要源码如下:
- var Tooltip = function (element, options) { // 实例化tooltip时,传入当前元素和options参数
- this.type =
- this.options =
- this.enabled =
- this.timeout =
- this.hoverState =
- this.$element = null
- // 根据传入的选项进行初始化事件绑定
- // 注意:这里传入tooltip,因为popover插件也使用了tooltip的原型方法,所以init提供了一个type参数
- this.init('tooltip', element, options)
- }
- Tooltip.DEFAULTS = {
- animation: true, // 是否开启动画
- placement: 'top', // 显示位置,默认在上方显示
- selector: false, // 触发器的选择符
- template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-
- inner"></div></div>', // tooltip显示的内容模板
- trigger: 'hover focus', // 设置触发tooltip的事件
- title: '', // 标题
- delay: 0, // 延迟
- html: false, // tooltip内容是否是html
- container: false // tooltip容器设置,如果有直接赋值,没有则默认false
- }
- // 初始化,事件绑定
- // 注意:这里传入的是tooltip,因为popover插件也使用了tooltip的原型方法,所以init提供了一个type参数
- // popover插件传入的就是popover
- Tooltip.prototype.init = function (type, element, options){};
- // 获取默认配置
- Tooltip.prototype.getDefaults = function (e){};
- // 获取配置参数(将传入的参数和默认参数合并)
- Tooltip.prototype.getOptions = function (options) {};
- // 手动触发的时候,添加额外的options参数
- Tooltip.prototype.getDelegateOptions = function (){};
- // 移除元素时的处理,主要是找到元素实例,然后调用show方法
- Tooltip.prototype.enter = function (obj) {};
- // 离开元素时的处理,主要是找到元素实例,然后调用hide方法
- Tooltip.prototype.leave = function (obj) {};
- // 显示tooltip提示语
- Tooltip.prototype.show = function (){};
- // 再次应用更新的placement样式和位置
- Tooltip.prototype.applyPlacement = function (offset, placement) {};
- // 更新小箭头的位置
- Tooltip.prototype.replaceArrow = function (delta, dimension, position) {};
- // 在模板里设置title内容,以便正式组装tooltip的内容
- Tooltip.prototype.setContent = function (){};
- // 关闭tooltip提示框
- Tooltip.prototype.hide = function (){};
- // 修复title提示,既有title,又有tooltip
- Tooltip.prototype.fixTitle = function (){};
- // 判断触发元素是拥有内容(及title值),其调用了getTitle
- Tooltip.prototype.hasContent = function (){};
- // 获取tooltip的当前位置
- Tooltip.prototype.getPosition = function (){};
- // 计算更新placement样式后的位置
- Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth,
- actualHeight) {};
- // 获取触发元素的title内容
- Tooltip.prototype.getTitle = function (){};
- // 获取tooltip的模板内容
- Tooltip.prototype.tip = function (){};
- // 查找小箭头元素
- Tooltip.prototype.arrow = function (){};
- // 验证元素释放合法
- Tooltip.prototype.validate = function (){};
- // 设置tooltip可用
- Tooltip.prototype.enable = function (){};
- // 设置tooltip不可用
- Tooltip.prototype.disable = function (){};
- // 反转可用状态
- Tooltip.prototype.toggleEnabled = function (){};
- // 反转tooltip显示/隐藏状态
- Tooltip.prototype.toggle = function (e) {};
- // 去除tooltip插件的绑定
- Tooltip.prototype.destroy = function (){};
上述源码,除了定义tooltip插件类函数和默认参数以外,其他的原型方法非常多,有的是用于显示、关闭、反转提示语的,还有对提示语进行位置计算、调整的,甚至还有调整提示语里的小箭头的以及获取和设置相关的内容的。所以读者在阅读这几页源码的时候,一定要静下心来,否则很容易混乱的。细节代码和注释如下:
- // 初始化,事件绑定
- // 注意:这里传入的是tooltip,因为popover插件也使用了tooltip的原型方法,所以init提供了
- // 一个type参数
- // popover插件传入的就是popover
- Tooltip.prototype.init = function (type, element, options) {
- this.enabled = true // 可用状态
- this.type = type // 默认是tooltip
- this.$element = $(element) // 当前元素
- this.options = this.getOptions(options) // 获取配置参数(将传入的参数和默认参数合并)
- var triggers = this.options.trigger.split(' ')
- // 将多个触发事件转换成数字,例如hover click
- for (var i = triggers.length; i--;) {
- var trigger = triggers[i] // 针对每个事件,单独处理如下的内容
- if (trigger == 'click') { // 如果是click事件
- // 则在click.tooltip上绑定toggle回调,即每单击一次,就会反转显示状态
- this.$element.on('click.' + this.type, this.options.selector, $.proxy
- (this.toggle, this))
- } else if (trigger != 'manual') { // 如果不是手动触发
- // 如果是hover事件,则进入事件是mouseenter,否则是focusin
- var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin'
- // 如果是hover事件,则移出事件是mouseleave,否则是focusout
- var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'
- // 给进入事件绑定enter回调,如mouseenter.tooltip
- this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy
- (this.enter, this))
- // 移出事件绑定leave回调,如focusout.tooltip
- this.$element.on(eventOut + '.' + this.type, this.options.selector,
- $.proxy(this.leave, this))
- }
- }
- // 如果指定了内部选择符
- // 则合并原来的options到一个新的_options对象上,并添加trigger和selector两个选项
- this.options.selector ?
- (this._options = $.extend({}, this.options, { trigger: 'manual',
- selector: '' })) :
- this.fixTitle() // 否则,修复title提示
- }
- // 获取默认配置
- Tooltip.prototype.getDefaults = function () {
- return Tooltip.DEFAULTS
- }
- // 获取配置参数(将传入的参数和默认参数合并)
- Tooltip.prototype.getOptions = function (options) {
- // 将传入的参数和默认参数合并
- options = $.extend({}, this.getDefaults(), this.$element.data(), options)
- // 如果传入的delay是数字,则表示show和hide的延迟时间都是该数字
- if (options.delay && typeof options.delay == 'number') {
- options.delay = {
- show: options.delay,
- hide: options.delay
- }
- }
- return options
- }
- // 手动触发的时候,添加额外的options参数
- Tooltip.prototype.getDelegateOptions = function () {
- var options = {}
- var defaults = this.getDefaults() // 默认配置参数
- this._options && $.each(this._options, function (key, value) {
- // 如果_options可用
- // 如果默认参数里指定key的值和_options指定key的值不一样,则将_options里的值赋
- // 值给options,也就是用新值
- if (defaults[key] != value) options[key] = value
- })
- return options // 返回更新后的选项参数
- }
- // 移除元素时的处理,主要是找到元素实例,然后调用show方法
- Tooltip.prototype.enter = function (obj) {
- // 如果obj是当前tooltip的实例就赋值给self
- // 否则就取该元素上的实例(data-bs.tooltip属性),即:// $('#id').tooltip(options).data
- // ('bs.tooltip')
- var self = obj instanceof this.constructor ?
- obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.'
- + this.type)
- clearTimeout(self.timeout) // 去除timeout
- self.hoverState = 'in' // 设置移入状态是in
- // 如果delay没提供,后者delay.show没提供,直接返回self.show
- if (!self.options.delay || !self.options.delay.show) return self.show()
- self.timeout = setTimeout(function () { // 重新设置timeout
- if (self.hoverState == 'in') self.show() // 如果移入状态是in,调用show方法
- }, self.options.delay.show)
- }
- // 离开元素时的处理,主要是找到元素实例,然后调用hide方法
- Tooltip.prototype.leave = function (obj) {
- // 如果obj是当前tooltip的实例就赋值给self
- // 否则就取该元素上的实例(data-bs.tooltip属性),即:// $('#id').tooltip(options).data
- // ('bs.tooltip')
- var self = obj instanceof this.constructor ?
- obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data
- ('bs.' + this.type)
- clearTimeout(self.timeout) // 去除timeout
- self.hoverState = 'out' // 设置移入状态是out
- // 如果delay没提供,后者delay.hide没提供,直接返回self.hide
- if (!self.options.delay || !self.options.delay.hide) return self.hide()
- self.timeout = setTimeout(function () { // 重新设置timeout
- if (self.hoverState == 'out') self.hide() // 如果移入状态是out,调用hide方法
- }, self.options.delay.hide)
- }
- // 显示tooltip提示语
- Tooltip.prototype.show = function () {
- var e = $.Event('show.bs.' + this.type) // 设置tooltip完全显示之前触发的show事件
- if (this.hasContent() && this.enabled) { // 如果有要显示的内容,并且tooltip可用
- this.$element.trigger(e) // 首先触发show事件
- if (e.isDefaultPrevented()) return // 如果show回调里阻止了继续操作,则返回
- var that = this;
- var $tip = this.tip() // 获取tooltip的模板内容
- this.setContent() // 在模板里设置title内容,以便正式组装tooltip的内容
- if (this.options.animation) $tip.addClass('fade')
- // 如果设置了动画,则在tooltip上添加fade样式
- // 如果显示位置参数设置的是function,则直接调用该函数,否则直接利用该位置,比如
- // left、right
- var placement = typeof this.options.placement == 'function' ?
- this.options.placement.call(this, $tip[0], this.$element[0]) :
- this.options.placement
- // 如果位置设置里有auto关键字,先删除auto关键字,比如只剩left;如果什么都没剩余,默认为top
- var autoToken = /\s?auto?\s?/i
- var autoPlace = autoToken.test(placement)
- if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
- $tip
- .detach()
- .css({ top: 0, left: 0, display: 'block' }) // 默认左上角块级显示(相对定位)
- .addClass(placement) // 设置显示方向是left、right、top、bottom中的一种
- // 如果指定了container容器,则将tooltip附加到该容器,否则附加到当前元素的最末尾(内部)
- this.options.container ? $tip.appendTo(this.options.container) : $tip.
- insertAfter(this.$element)
- var pos = this.getPosition() // 获取tooltip的当前位置
- var actualWidth = $tip[0].offsetWidth // 获取实际宽度
- var actualHeight = $tip[0].offsetHeight // 获取实际高度
- if (autoPlace) { // 如果定义了auto方向
- var $parent = this.$element.parent() // 获取触发元素的父元素
- var orgPlacement = placement // 将原来的方向先临时存到一个临时变量里
- // 获取当前页面的距离顶端的top高度
- var docScroll = document.documentElement.scrollTop || document.body.scrollTop
- // 获取父元素的整个宽度
- var parentWidth = this.options.container == 'body' ? window.innerWidth :
- $parent.outerWidth()
- // 获取父元素的整个高度
- var parentHeight = this.options.container == 'body' ? window.innerHeight :
- $parent.outerHeight()
- // 获取父元素的left值
- var parentLeft = this.options.container == 'body' ? 0 : $parent.offset().left
- // 如果位置是bottom,但是超出了父元素的高度,则使用top
- // 如果位置是top,但是超出了浏览器顶部,则使用bottom
- // 如果位置是right,但是超出了浏览器右边栏,则使用left
- // 如果位置是left,但是超出了浏览器左边栏,则使用right
- placement = placement == 'bottom' && pos.top + pos.height +
- actualHeight - docScroll > parentHeight ? 'top' :
- placement == 'top' && pos.top - docScroll -
- actualHeight < 0 ? 'bottom' :
- placement == 'right' && pos.right + actualWidth >
- parentWidth ? 'left' :
- placement == 'left' && pos.left - actualWidth <
- parentLeft ? 'right' :
- placement // 否则,还使用原来的位置
- $tip
- .removeClass(orgPlacement) // 删除原来设置的位置样式
- .addClass(placement) // 使用新计算的位置样式
- }
- // 计算更新placement样式后的位置
- var calculatedOffset = this.getCalculatedOffset(placement, pos,
- actualWidth, actualHeight)
- this.applyPlacement(calculatedOffset, placement)
- // 再次应用更新的placement样式和位置
- this.hoverState = null
- var complete = function () {
- that.$element.trigger('shown.bs.' + that.type)
- }
- $.support.transition && this.$tip.hasClass('fade') ?
- $tip
- .one($.support.transition.end, complete)
- .emulateTransitionEnd(150) :
- complete()
- }
- }
- // 再次应用更新的placement样式和位置
- Tooltip.prototype.applyPlacement = function (offset, placement) {
- var replace
- var $tip = this.tip()
- var width = $tip[0].offsetWidth
- var height = $tip[0].offsetHeight
- // 手动获取margin值,因为使用getBoundingClientRect在不同的浏览器不准
- var marginTop = parseInt($tip.css('margin-top'), 10)
- var marginLeft = parseInt($tip.css('margin-left'), 10)
- // 在IE8、IE9下必须要判断值为NaN的情况
- if (isNaN(marginTop)) marginTop = 0
- if (isNaN(marginLeft)) marginLeft = 0
- // 将margin值更新到offset
- offset.top = offset.top + marginTop
- offset.left = offset.left + marginLeft
- // 设置新的offset,但由于$.fn.offset不能彻底设置像素值,所以这里使用setOffset方法直接设置
- $.offset.setOffset($tip[0], $.extend({
- using: function (props) {
- $tip.css({
- top: Math.round(props.top),
- left: Math.round(props.left)
- })
- }
- }, offset), 0)
- $tip.addClass('in') // 并添加in样式,用于显示
- // 检查tooltip在重新应用后,是否又自己重绘并改变大小了
- var actualWidth = $tip[0].offsetWidth
- var actualHeight = $tip[0].offsetHeight
- // 如果选择的是top,并且高度改变了,则重新更新offset
- if (placement == 'top' && actualHeight != height) {
- replace = true
- offset.top = offset.top + height - actualHeight
- }
- if (/bottom|top/.test(placement)) { // 如果使用了bottom或top位置
- var delta = 0
- if (offset.left < 0) { // 如果超出了浏览器的左边框,则设置为最小值0
- delta = offset.left * -2
- offset.left = 0
- $tip.offset(offset) // 重新更新offset
- actualWidth = $tip[0].offsetWidth // 再次获取重绘以后的offset
- actualHeight = $tip[0].offsetHeight
- }
- this.replaceArrow(delta - width + actualWidth, actualWidth, 'left')
- // 更新小箭头的位置
- } else {
- this.replaceArrow(actualHeight - height, actualHeight, 'top')
- // 更新小箭头的位置
- }
- if (replace) $tip.offset(offset) // 重新应用offset
- }
- // 更新小箭头的位置
- Tooltip.prototype.replaceArrow = function (delta, dimension, position) {
- this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + '%') : '')
- }
- // 在模板里设置title内容,以便正式组装tooltip的内容
- Tooltip.prototype.setContent = function () {
- var $tip = this.tip()
- var title = this.getTitle()
- // 如果支持HTML,就设置HTML,否则设置text
- $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
- $tip.removeClass('fade in top bottom left right')
- // 如果有多余的样式,全部删除,后面会根据状态再添加
- }
- // 关闭tooltip提示框
- Tooltip.prototype.hide = function () {
- var that = this
- var $tip = this.tip()
- var e = $.Event('hide.bs.' + this.type) // 设置tooltip在开始关闭时触发的hide事件
- function complete() {
- if (that.hoverState != 'in') $tip.detach()
- // 如果当前元素没有in样式,直接移除tooltip
- that.$element.trigger('hidden.bs.' + that.type)
- }
- this.$element.trigger(e) // 触发hide事件
- if (e.isDefaultPrevented()) return // 如果hide回调里阻止了继续操作,则返回
- $tip.removeClass('in') // 删除in样式
- $.support.transition && this.$tip.hasClass('fade') ?
- // 如果设置了动画,则在tooltip上添加fade样式
- $tip
- .one($.support.transition.end, complete)
- // 设置动画以后,调用complete函数关闭tooltip
- .emulateTransitionEnd(150) : // 延迟150毫秒才开始动画
- complete() // 否则直接关闭
- this.hoverState = null
- return this
- }
- // 修复title提示,既有title,又有tooltip
- Tooltip.prototype.fixTitle = function () {
- var $e = this.$element
- // 触发元素有title值,或者data-original-title属性不是字符串时
- // 将title(或者空)赋值给data-original-title属性,然后再将title属性设置为空
- if ($e.attr('title') || typeof ($e.attr('data-original-title')) != 'string') {
- $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
- }
- }
- // 判断触发元素是拥有内容(及title值),其调用了getTitle
- Tooltip.prototype.hasContent = function () {
- return this.getTitle()
- }
- // 获取tooltip的当前位置
- Tooltip.prototype.getPosition = function () {
- var el = this.$element[0]
- return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.
- getBoundingClientRect() : {
- // 如果系统支持getBoundingClientRect函数,直接用该函数获取位置
- width: el.offsetWidth, // 否则使用el.offsetWidth和el.offsetHeight来获取位置
- height: el.offsetHeight
- }, this.$element.offset()) // 如果都获取不了,则返回默认的offset
- }
- // 计算更新placement样式后的位置
- Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth,
- actualHeight) {
- return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left +
- pos.width / 2 - actualWidth / 2 } :
- placement == 'top' ? { top: pos.top - actualHeight, left: pos.left +
- pos.width / 2 - actualWidth / 2 } :
- placement == 'left' ? { top: pos.top + pos.height / 2 -
- actualHeight / 2, left: pos.left - actualWidth } :
- /* placement == 'right' */ { top: pos.top + pos.height / 2 -
- actualHeight / 2, left: pos.left + pos.width }
- }
- // 获取触发元素的title内容
- Tooltip.prototype.getTitle = function () {
- var title
- var $e = this.$element
- var o = this.options
- // 第一优先级:data-original-title
- // 第二优先级:如果options.title传入的是function,将其调用结果作为title;如果不是直
- // 接返回options.title
- title = $e.attr('data-original-title')
- || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
- return title
- }
- // 获取tooltip的模板内容
- Tooltip.prototype.tip = function () {
- return this.$tip = this.$tip || $(this.options.template)
- }
- // 查找小箭头元素
- Tooltip.prototype.arrow = function () {
- return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')
- }
- // 验证元素释放合法
- Tooltip.prototype.validate = function () {
- if (!this.$element[0].parentNode) { // 如果元素没有父节点,比如document
- this.hide() // 隐藏该元素
- this.$element = null
- this.options = null
- }
- }
- // 设置tooltip可用
- Tooltip.prototype.enable = function () { this.enabled = true}
- // 设置tooltip不可用
- Tooltip.prototype.disable = function () { this.enabled = false}
- // 反转可用状态
- Tooltip.prototype.toggleEnabled = function () { this.enabled = !this.enabled}
- // 反转tooltip显示/隐藏状态
- Tooltip.prototype.toggle = function (e) {
- // 如果调用对象时触发元素,就查找该触发元素上的实例,否则就是this
- var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).
- data('bs.' + this.type) : this
- // 如果tip上有in样式,就触发移出操作(leave)关闭tooltip;否则触发移入操作(enter)开启tooltip
- self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
- }
- // 去除tooltip组件的绑定
- Tooltip.prototype.destroy = function () {
- clearTimeout(this.timeout)
- // 去除.tooltip命名空间中所有的事件绑定,并移除tooltip实例(data-bs.tooltip)
- this.hide().$element.off('.' + this.type).removeData('bs.' + this.type)
- }
步骤3 jQuery插件定义。在jQuery上定义$.fn.tooltip插件的代码,和前面几个插件几乎一样:遍历元素、实例化或触发动作。源码如下:
- var old = $.fn.tooltip
- // 保留其他库的$.fn.tooltip代码(如果定义的话),以便在noConflict之后,可以继续使用该老代码
- $.fn.tooltip = function (option) {
- return this.each(function () { // 根据选择器,遍历所有符合规则的元素
- var $this = $(this) // this赋值一个变量,防止作用域改变
- var data = $this.data('bs.tooltip') // 查询当前元素上是否已经有了tooltip实例
- // 如果option是对象,说明是参数集合,在new tooltip的时候传入
- var options = typeof option == 'object' && option
- if (!data && option == 'destroy') return
- // 如果没有实例,就初始化一个,并传入this
- if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
- // 如果option是字符串,说明传入的是一个方法,直接调用该方法
- if (typeof option == 'string') data[option]()
- })
- }
- $.fn.tooltip.Constructor = Tooltip // 重设插件构造器,可以通过该属性获取插件的真实类函数
步骤4 防冲突处理。此步骤与Modal插件的步骤4一样,此处不赘述。
步骤5 绑定触发事件。在前面我们已经说了,tooltip插件默认没有提供触发声明式的代码,所以也就没有此步代码了。