BootStrap Tooltip插件源码解析


Posted in Javascript onDecember 27, 2016

Tooltip插件可以让你把要显示的内容以弹出框的形式来展示,如:

BootStrap Tooltip插件源码解析

因为自己在工作的过程中,用到了Tooltip这个插件,并且当时正想学习一下元素定位的问题,如:提示框显示的位置就是触发提示框元素的位置,可以配置在上、下、左、右等位置,所以就去看了源码。对于整个插件源码没有看全,但也学到了许多的知识点。能力有限,可能其中有认识错误的地方,以后再补充吧

1 使用方法不介绍 ,可以参照

Bootstrap 提示工具(Tooltip)插件

2 源码解析

+function ($) {
 'use strict';

 // TOOLTIP PUBLIC CLASS DEFINITION
 // ===============================


 var Tooltip = function (element, options) {
  this.type    =
  this.options  =
  this.enabled  =
  this.timeout  =
  this.hoverState =
  this.$element  = null

  this.init('tooltip', element, options)
 }

 Tooltip.VERSION = '3.3.0'

 Tooltip.TRANSITION_DURATION = 150

 //默认参数
 Tooltip.DEFAULTS = {
  animation: true,
  placement: 'top',
  selector: false,
  template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
  trigger: 'hover focus',
  title: '',
  delay: 0,
  html: false,
  container: false,
  viewport: {
   selector: 'body',
   padding: 0
  }
 }

 //初始化
 Tooltip.prototype.init = function (type, element, options) {
  this.enabled  = true
  this.type   = type
  this.$element = $(element)
  //初始化参数
  this.options  = this.getOptions(options)

  this.$viewport = this.options.viewport && $(this.options.viewport.selector || this.options.viewport)

  //多个触发器 即触发函数
  var triggers = this.options.trigger.split(' ')


  for (var i = triggers.length; i--;) {
   var trigger = triggers[i]

   if (trigger == 'click') {
    //绑定事件处理程序
    //事件命名空间 'click.tooltip'
    //触发时执行$.proxy(this.toggle, this)返回的函数
    this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
   } else if (trigger != 'manual') {
    var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin'
    var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'

    this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
    this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
   }
  }

  this.options.selector ?
   (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
   this.fixTitle()
 }

 Tooltip.prototype.getDefaults = function () {
  return Tooltip.DEFAULTS
 }

 //获得初始化参数
 Tooltip.prototype.getOptions = function (options) {
  // this.$element.data() data-key = value 形式的属性获取 
  // 返回一个对象{key:value}
  // options > 元素属性 > 默认 
  options = $.extend({}, this.getDefaults(), this.$element.data(), options)

  //delay 为数字 特殊处理
  //delay 为对象 delay:{ show: 500, hide: 100 }
  if (options.delay && typeof options.delay == 'number') {
   options.delay = {
    show: options.delay,
    hide: options.delay
   }
  }

  return options
 }

 Tooltip.prototype.getDelegateOptions = function () {
  var options = {}
  var defaults = this.getDefaults()

  this._options && $.each(this._options, function (key, value) {
   if (defaults[key] != value) options[key] = value
  })

  return options
 }

 //提示框显示
 Tooltip.prototype.enter = function (obj) {
  //obj是否是tooltip的实例
  var self = obj instanceof this.constructor ?
   obj : $(obj.currentTarget).data('bs.' + this.type)

  //$tip.is(':visible') 该div是否可见
  //可见时 只更新状态 然后直接返回
  if (self && self.$tip && self.$tip.is(':visible')) {
   self.hoverState = 'in'
   return
  }

  //self 不存在
  if (!self) {
   self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
   $(obj.currentTarget).data('bs.' + this.type, self)
  }

  //上次点击延时未显示 则取消显示
  clearTimeout(self.timeout)

  //设置状态
  self.hoverState = 'in'

  //delay: 数字 (默认 0) 或  对象 { show: 500, hide: 100 }
  //没有delay delay.show 时 直接显示 
  if (!self.options.delay || !self.options.delay.show) return self.show()

  //延时显示 self.options.delay.show:延时时间
  self.timeout = setTimeout(function () {
   if (self.hoverState == 'in') self.show()
  }, self.options.delay.show)
 }

 //提示框隐藏
 Tooltip.prototype.leave = function (obj) {
  var self = obj instanceof this.constructor ?
   obj : $(obj.currentTarget).data('bs.' + this.type)

  if (!self) {
   self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
   $(obj.currentTarget).data('bs.' + this.type, self)
  }

  clearTimeout(self.timeout)

  self.hoverState = 'out'

  if (!self.options.delay || !self.options.delay.hide) return self.hide()

  self.timeout = setTimeout(function () {
   if (self.hoverState == 'out') self.hide()
  }, self.options.delay.hide)
 }

 //提示框显示核心方法
 Tooltip.prototype.show = function () {
  console.info(this) 
  var e = $.Event('show.bs.' + this.type)

  if (this.hasContent() && this.enabled) {
   this.$element.trigger(e)

   //判断是否在根节点中
   //this.$element[0] 转换为DOM对象
   //.ownerDocument Document文档对象
   //.documentElement 根节点 HTML标签
   //$.contains 一个DOM节点是否包含另一个DOM节点。
   var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0])

   //调用preventDefault 或 不在根节点 return
   //事件对象中是否调用过 event.preventDefault()
   //event.preventDefault():阻止元素发生默认的行为
   //如 当点击提交按钮时阻止对表单的提交 、 阻止以下 URL 的链接
   if (e.isDefaultPrevented() || !inDom) return
   var that = this

   var $tip = this.tip()

   var tipId = this.getUID(this.type)
   //设置提示框的title
   this.setContent()
   $tip.attr('id', tipId)
   this.$element.attr('aria-describedby', tipId)
   //参数animation
   if (this.options.animation) $tip.addClass('fade')

   //参数placement 提示框的位置 top bottom left right
   var placement = typeof this.options.placement == 'function' ?
    this.options.placement.call(this, $tip[0], this.$element[0]) :
    this.options.placement

   // 判断placement是否包含"auto"
   var autoToken = /\s?auto?\s?/i
   var autoPlace = autoToken.test(placement)
   //包含 "auto"时
   if (autoPlace) placement = placement.replace(autoToken, '') || 'top'

   $tip
    //detach() 删除匹配的对象 与remove()不同的是,所有绑定的事件、附加的数据等都会保留下来
    .detach()
    .css({ top: 0, left: 0, display: 'block' })
    .addClass(placement)
    .data('bs.' + this.type, this)
   //参数 container 向指定元素添加该提示框
   //没有 添加到当前元素后面
   this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
   //位置对象pos pos = {top: , right: , bottom: , left: , width: ,scroll:}
   var pos     = this.getPosition()
   //提示框自身实际的 width height
   var actualWidth = $tip[0].offsetWidth
   var actualHeight = $tip[0].offsetHeight

   if (autoPlace) {
    var orgPlacement = placement
    var $container  = this.options.container ? $(this.options.container) : this.$element.parent()
    var containerDim = this.getPosition($container)

    placement = placement == 'bottom' && pos.bottom + actualHeight > containerDim.bottom ? 'top'  :
          placement == 'top'  && pos.top  - actualHeight < containerDim.top  ? 'bottom' :
          placement == 'right' && pos.right + actualWidth > containerDim.width ? 'left'  :
          placement == 'left'  && pos.left  - actualWidth < containerDim.left  ? 'right' :
          placement

    $tip
     .removeClass(orgPlacement)
     .addClass(placement)
   }

   //计算提示框的偏移量 {top: , left: }
   var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)

   //应用并设置偏移量
   this.applyPlacement(calculatedOffset, placement)

   var complete = function () {
    var prevHoverState = that.hoverState
    that.$element.trigger('shown.bs.' + that.type)
    that.hoverState = null

    if (prevHoverState == 'out') that.leave(that)
   }

   $.support.transition && this.$tip.hasClass('fade') ?
    $tip
     .one('bsTransitionEnd', complete)
     .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
    complete()
  }
 }

 Tooltip.prototype.applyPlacement = function (offset, placement) {
  var $tip  = this.tip()
  var width = $tip[0].offsetWidth
  var height = $tip[0].offsetHeight

  // manually read margins because getBoundingClientRect includes difference
  //获得提示框自身的外边距的值
  var marginTop = parseInt($tip.css('margin-top'), 10)
  var marginLeft = parseInt($tip.css('margin-left'), 10)

  // we must check for NaN for ie 8/9
  //是否是非数字
  if (isNaN(marginTop)) marginTop = 0
  if (isNaN(marginLeft)) marginLeft = 0

  //计算的偏移量 + 自身的外边距
  offset.top = offset.top + marginTop
  offset.left = offset.left + marginLeft

  // $.fn.offset doesn't round pixel values
  // so we use setOffset directly with our own function B-0
  // 应用了offset.setOffset方法,传入了using参数,因为offset设置值的时候,不能四舍五入
  $.offset.setOffset($tip[0], $.extend({
   using: function (props) {
    $tip.css({
     top: Math.round(props.top),
     left: Math.round(props.left)
    })
   }
  }, offset), 0)

  //添加 in class 让提示框显示
  $tip.addClass('in')

  // check to see if placing tip in new offset caused the tip to resize itself
  //获取显示后的提示框的宽和高 检查是否调整了自身的大小
  var actualWidth = $tip[0].offsetWidth
  var actualHeight = $tip[0].offsetHeight


  if (placement == 'top' && actualHeight != height) {
   offset.top = offset.top + height - actualHeight
  }

  var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight)

  if (delta.left) offset.left += delta.left
  else offset.top += delta.top

  var isVertical     = /top|bottom/.test(placement)
  var arrowDelta     = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight
  var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight'

  $tip.offset(offset)
  this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical)
 }

 Tooltip.prototype.replaceArrow = function (delta, dimension, isHorizontal) {
  this.arrow()
   .css(isHorizontal ? 'left' : 'top', 50 * (1 - delta / dimension) + '%')
   .css(isHorizontal ? 'top' : 'left', '')
 }

 //设置提示框的title
 Tooltip.prototype.setContent = function () {
  //div
  var $tip = this.tip()
  var title = this.getTitle()

  $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
  $tip.removeClass('fade in top bottom left right')
 }

 Tooltip.prototype.hide = function (callback) {
  var that = this
  var $tip = this.tip()
  var e  = $.Event('hide.bs.' + this.type)

  function complete() {
   if (that.hoverState != 'in') $tip.detach()
   that.$element
    .removeAttr('aria-describedby')
    .trigger('hidden.bs.' + that.type)
   callback && callback()
  }

  this.$element.trigger(e)

  if (e.isDefaultPrevented()) return

  $tip.removeClass('in')

  $.support.transition && this.$tip.hasClass('fade') ?
   $tip
    .one('bsTransitionEnd', complete)
    .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
   complete()

  this.hoverState = null

  return this
 }

 Tooltip.prototype.fixTitle = function () {
  var $e = this.$element
  if ($e.attr('title') || typeof ($e.attr('data-original-title')) != 'string') {
   $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
  }
 }

 Tooltip.prototype.hasContent = function () {
  return this.getTitle()
 }

 //获取位置
 Tooltip.prototype.getPosition = function ($element) {
  //不传入 则为当前点击元素
  $element  = $element || this.$element

  //转换为DOM对象
  var el   = $element[0]
  //是否是body元素
  var isBody = el.tagName == 'BODY'
  //获取元素各边与页面上边和左边的距离 left right top bottom height width
  var elRect  = el.getBoundingClientRect()
  //兼容IE8 没有 width height 则计算
  if (elRect.width == null) {
   // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093
   elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top })
  }
  //当前元素相对于文档的偏移量
  var elOffset = isBody ? { top: 0, left: 0 } : $element.offset()
  //垂直滚动条的距离
  //document.documentElement.scrollTop 和 document.body.scrollTop 浏览器兼容 
  var scroll  = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }
  var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null
  //合并 
  return $.extend({}, elRect, scroll, outerDims, elOffset)
 }

 //计算偏移量
 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  }

 }


 Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) {
  var delta = { top: 0, left: 0 }

  //默认参数
  //viewport: {
  // selector: 'body',
  // padding: 0
  //}
  //默认的话 this.$viewport = 'body'
  if (!this.$viewport) return delta

  var viewportPadding = this.options.viewport && this.options.viewport.padding || 0
  //this.$viewport = 'body' width height为 window的
  var viewportDimensions = this.getPosition(this.$viewport)
  console.info(viewportDimensions)
  if (/right|left/.test(placement)) {

   var topEdgeOffset  = pos.top - viewportPadding - viewportDimensions.scroll
   var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight
   if (topEdgeOffset < viewportDimensions.top) { // top overflow
    delta.top = viewportDimensions.top - topEdgeOffset
   } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow
    delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset
   }
  } else {

   var leftEdgeOffset = pos.left - viewportPadding
   var rightEdgeOffset = pos.left + viewportPadding + actualWidth
   if (leftEdgeOffset < viewportDimensions.left) { // left overflow
    delta.left = viewportDimensions.left - leftEdgeOffset
   } else if (rightEdgeOffset > viewportDimensions.width) { // right overflow
    delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset
   }
  }

  return delta
 }

 //提示框的title
 Tooltip.prototype.getTitle = function () {
  var title
  var $e = this.$element
  var o = this.options
  //先获取当前元素的data-original-title属性
  //否则获取参数title
  title = $e.attr('data-original-title')
   || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)

  return title
 }

 //随机生成唯一id 0 -- 100000 id值  
 Tooltip.prototype.getUID = function (prefix) {
  //~~ 去掉小数部分
  do prefix += ~~(Math.random() * 1000000)
  while (document.getElementById(prefix))
  return prefix
 }

 //生成提示框的div
 //不存在 则用模板
 //<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>
 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.enable = function () {
  this.enabled = true
 }

 Tooltip.prototype.disable = function () {
  this.enabled = false
 }

 Tooltip.prototype.toggleEnabled = function () {
  this.enabled = !this.enabled
 }

 //click 事件触发时执行的函数
 Tooltip.prototype.toggle = function (e) {
  //this tooltip实例对象
  var self = this
  if (e) {
   //e.currentTarget 返回注册该事件处理程序的元素
   self = $(e.currentTarget).data('bs.' + this.type)
   if (!self) {
    self = new this.constructor(e.currentTarget, this.getDelegateOptions())
    $(e.currentTarget).data('bs.' + this.type, self)
   }
  }
  //判断提示框当前的状态
  //true 当前显示 需要隐藏
  //false 相反
  self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
 }

 Tooltip.prototype.destroy = function () {
  var that = this
  clearTimeout(this.timeout)
  this.hide(function () {
   that.$element.off('.' + that.type).removeData('bs.' + that.type)
  })
 }


 // TOOLTIP PLUGIN DEFINITION
 // =========================

 function Plugin(option) {
  return this.each(function () {
   var $this  = $(this)
   //所选元素是否有 key为:bs.tooltip 对应的value值
   var data   = $this.data('bs.tooltip')
   //option为对象 options:option
   //option为字符串 则是方法 options:false 
   var options = typeof option == 'object' && option
   //option是对象 执行初始化 ,并提供了选择器
   var selector = options && options.selector

   //是destroy 方法 不执行
   if (!data && option == 'destroy') return
   //tooltip初始化
   if (selector) {
    //存在选择器,则将当前点击元素 bs.tooltip = {} 即去掉Tooltip实例
    if (!data) $this.data('bs.tooltip', (data = {}))
    //给选择器加上Tooltip实例
    if (!data[selector]) data[selector] = new Tooltip(this, options)

   } else {
    //没有value值时,则加上 bs.tooltip = data(new Tooltip(this, options)) 
    if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
   }

   //执行方法
   if (typeof option == 'string') data[option]()
  })
 }

 //缓存以前$.fn上的tooltip
 var old = $.fn.tooltip

 //添加到jQuery对象上
 $.fn.tooltip       = Plugin
 $.fn.tooltip.Constructor = Tooltip


 // TOOLTIP NO CONFLICT
 // ===================

 //tooltip冲突时 调用该方法
 //var other = $("#aa").tooltip().noConflict();
 $.fn.tooltip.noConflict = function () {
  $.fn.tooltip = old
  return this
 }

}(jQuery);

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
静态页面下用javascript操作ACCESS数据库(读增改删)的代码
May 14 Javascript
原生js事件的添加和删除的封装
Jul 01 Javascript
js 左右悬浮对联广告特效代码
Dec 12 Javascript
js实现刷新iframe的方法汇总
Apr 27 Javascript
常用的几个JQuery代码片段
Mar 13 Javascript
通过V8源码看一个关于JS数组排序的诡异问题
Aug 14 Javascript
jQuery实现的文字逐行向上间歇滚动效果示例
Sep 06 jQuery
Node.js+ELK日志规范的实现
May 23 Javascript
js实现全选和全不选
Jul 28 Javascript
vue组件开发之tab切换组件使用详解
Aug 21 Javascript
利用JS判断元素是否为数组的方法示例
Jan 08 Javascript
前端实现滑动按钮AJAX与后端交互的示例代码
Feb 24 Javascript
获取当前月(季度/年)的最后一天(set相关操作及应用)
Dec 27 #Javascript
javascript实现文字无缝滚动
Dec 27 #Javascript
JavaScript仿聊天室聊天记录
Dec 27 #Javascript
基于jQuery实现顶部导航栏功能
Dec 27 #Javascript
js正则表达式最长匹配(贪婪匹配)和最短匹配(懒惰匹配)用法分析
Dec 27 #Javascript
基于jQuery实现左侧菜单栏可折叠功能
Dec 27 #Javascript
JS正则表达式修饰符global(/g)用法分析
Dec 27 #Javascript
You might like
ASP知识讲座四
2006/10/09 PHP
攻克CakePHP系列二 表单数据显示
2008/10/22 PHP
解决php的“It is not safe to rely on the system’s timezone settings”问题
2015/10/08 PHP
PHP执行linux命令常用函数汇总
2016/02/02 PHP
PHP后台微信支付和支付宝支付开发
2017/04/28 PHP
使用SMB共享来绕过php远程文件包含的限制执行RFI的利用
2019/05/31 PHP
Centos7 Yum安装PHP7.2流程教程详解
2019/07/02 PHP
深入document.write()与HTML4.01的非成对标签的详解
2013/05/08 Javascript
Tab切换组件(选项卡功能)实例代码
2013/11/21 Javascript
回车直接实现点击某按钮的效果即触发单击事件
2014/02/27 Javascript
DOM节点删除函数removeChild()用法实例
2015/01/12 Javascript
JQuery控制radio选中和不选中方法总结
2015/04/15 Javascript
配置Grunt的Task时通配符支持和动态生成文件名问题
2015/09/06 Javascript
基于jQuery实现响应式圆形图片轮播特效
2015/11/25 Javascript
修改Jquery Dialog 位置的实现方法
2016/08/26 Javascript
浅谈ES6新增的数组方法和对象
2017/08/08 Javascript
jQuery实现表单动态加减、ajax表单提交功能
2018/06/08 jQuery
vue h5移动端禁止缩放代码
2019/10/28 Javascript
[56:20]LGD vs VP Supermajor 败者组决赛 BO3 第三场 6.10
2018/07/04 DOTA
python标准算法实现数组全排列的方法
2015/03/17 Python
Python编程实现删除VC临时文件及Debug目录的方法
2017/03/22 Python
python数据类型判断type与isinstance的区别实例解析
2017/10/31 Python
浅谈python的输入输出,注释,基本数据类型
2019/04/02 Python
Python3.7 新特性之dataclass装饰器
2019/05/27 Python
django 信号调度机制详解
2019/07/19 Python
Pytorch 抽取vgg各层并进行定制化处理的方法
2019/08/20 Python
韩国知名的家庭购物网站:CJmall
2016/08/01 全球购物
简述数组与指针的区别
2014/01/02 面试题
计算机应用专业毕业生求职信
2013/10/24 职场文书
工作自我评价分享
2013/12/01 职场文书
弘扬职业精神演讲稿
2014/03/20 职场文书
2014年行政工作总结
2014/11/19 职场文书
英语邀请函范文
2015/02/02 职场文书
2015年行政执法工作总结
2015/05/23 职场文书
2015年生活老师工作总结
2015/05/27 职场文书
JVM的类加载器和双亲委派模式你了解吗
2022/03/13 Java/Android