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 相关文章推荐
jQuery 事件队列调整方法
Sep 18 Javascript
js跨域访问示例(客户端/服务端)
May 19 Javascript
jquery+ajax请求且带返回值的代码
Aug 12 Javascript
javascript计时器编写过程与实现方法
Feb 29 Javascript
webpack打包后直接访问页面图片路径错误的解决方法
Jun 17 Javascript
Node.js中 __dirname 的使用介绍
Jun 19 Javascript
Easyui使用Dialog行内按钮布局的实例
Jul 27 Javascript
vue2 全局变量的设置方法
Mar 09 Javascript
微信小程序学习笔记之本地数据缓存功能详解
Mar 29 Javascript
Vue+elementui 实现复杂表头和动态增加列的二维表格功能
Sep 23 Javascript
JavaScript交换变量常用4种方法解析
Sep 02 Javascript
vue+element table表格实现动态列筛选的示例代码
Jan 14 Vue.js
获取当前月(季度/年)的最后一天(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
PHP下编码转换函数mb_convert_encoding与iconv的使用说明
2009/12/16 PHP
PHP+iFrame实现页面无需刷新的异步文件上传
2014/09/16 PHP
ThinkPHP连接Oracle数据库
2016/04/22 PHP
获取offsetTop和offsetLeft值的js代码(兼容)
2013/04/16 Javascript
javascript获取隐藏dom的宽高 具体实现
2013/07/14 Javascript
js css后面所带参数含义介绍
2013/08/18 Javascript
Jquery uploadify图片上传插件无法上传的解决方法
2013/12/16 Javascript
javascript使用smipleChart实现简单图表
2015/01/02 Javascript
JavaScript通过join函数连接数组里所有元素的方法
2015/03/20 Javascript
JS清除文本框内容离开在恢复及鼠标离开文本框时触发js的方法
2016/01/12 Javascript
jQuery表单验证插件解析(推荐)
2016/07/21 Javascript
详细谈谈AngularJS的子级作用域问题
2016/09/05 Javascript
vue.js事件处理器是什么
2017/03/20 Javascript
JavaScript中的遍历详解(多种遍历)
2017/04/07 Javascript
JS简单判断字符在另一个字符串中出现次数的2种常用方法
2017/04/20 Javascript
vue2.0 自定义组件的方法(vue组件的封装)
2018/06/05 Javascript
学习React中ref的两个demo示例
2018/08/14 Javascript
bootstrap与pagehelper实现分页效果
2018/12/29 Javascript
[55:18]Liquid vs Chaos 2019国际邀请赛小组赛 BO2 第一场 8.15
2019/08/16 DOTA
[01:14:41]DOTA2-DPC中国联赛定级赛 iG vs Magma BO3第一场 1月8日
2021/03/11 DOTA
python读写文件操作示例程序
2013/12/02 Python
Python的Django REST框架中的序列化及请求和返回
2016/04/11 Python
简单实现python收发邮件功能
2018/01/05 Python
使用pycharm和pylint检查python代码规范操作
2020/06/09 Python
python批量合成bilibili的m4s缓存文件为MP4格式 ver2.5
2020/12/01 Python
CSS3自定义滚动条样式的示例代码
2017/08/21 HTML / CSS
Jeep牧马人、切诺基和自由人零配件:4 Wheel Drive Hardware
2017/07/02 全球购物
奥巴马英文演讲稿
2014/05/15 职场文书
给校长的建议书400字
2014/05/15 职场文书
五四青年节演讲稿
2014/05/26 职场文书
家庭贫困证明书(3篇)
2014/09/15 职场文书
2015年客房服务员工作总结
2015/05/15 职场文书
2015年党务公开工作总结
2015/05/19 职场文书
聘任协议书(挂靠)
2015/09/21 职场文书
python tqdm用法及实例详解
2021/06/16 Python
python 实现图片特效处理
2022/04/03 Python