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 无提示关闭窗口脚本
Aug 17 Javascript
javascript拖拽上传类库DropzoneJS使用方法
Dec 05 Javascript
jQuery实现的一个tab切换效果内部还嵌有切换
Aug 10 Javascript
简单实现兼容各大浏览器的js复制内容到剪切板
Sep 09 Javascript
javascript简易画板开发
Apr 12 Javascript
JavaScript中浅讲ajax图文详解
Nov 11 Javascript
Bootstrap笔记—折叠实例代码
Mar 13 Javascript
VsCode新建VueJs项目的详细步骤
Sep 23 Javascript
Angular实现点击按钮控制隐藏和显示功能示例
Dec 29 Javascript
Vue的watch和computed方法的使用及区别介绍
Sep 06 Javascript
9102年webpack4搭建vue项目的方法步骤
Feb 20 Javascript
Vue项目中如何封装axios(统一管理http请求)
May 02 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和js如何通过json互相传递数据相关问题探讨
2013/02/26 PHP
php中error与exception的区别及应用
2014/07/28 PHP
Ajax中的JSON格式与php传输过程全面解析
2017/11/14 PHP
PHP parse_ini_file函数的应用与扩展操作示例
2019/01/07 PHP
JavaScript效率调优经验
2009/06/04 Javascript
javascript获取隐藏元素(display:none)的高度和宽度的方法
2014/06/06 Javascript
jQuery支持动态参数将函数绑定到事件上的方法
2015/03/17 Javascript
用JavaScript实现对话框的教程
2015/06/04 Javascript
javascript截图 jQuery插件imgAreaSelect使用详解
2016/05/04 Javascript
详解jquery easyui之datagrid使用参考
2016/12/05 Javascript
javascript实现简单的ajax封装示例
2016/12/28 Javascript
Bootstrap导航中表单简单实现代码
2017/03/06 Javascript
微信小程序注册60s倒计时功能 使用JS实现注册60s倒计时功能
2017/08/16 Javascript
用Webpack构建Vue项目的实践
2017/11/07 Javascript
H5+C3+JS实现双人对战五子棋游戏(UI篇)
2020/05/28 Javascript
微信h5静默和非静默授权获取用户openId的方法和步骤
2020/06/08 Javascript
python使用cStringIO实现临时内存文件访问的方法
2015/03/26 Python
PyTorch快速搭建神经网络及其保存提取方法详解
2018/04/28 Python
Python多进程原理与用法分析
2018/08/21 Python
python按照多个条件排序的方法
2019/02/08 Python
Python二维数组实现求出3*3矩阵对角线元素的和示例
2019/11/29 Python
使用python实现下载我们想听的歌曲,速度超快
2020/07/09 Python
python smtplib发送多个email联系人的实现
2020/10/09 Python
一款利用html5和css3实现的3D立方体旋转效果教程
2016/04/26 HTML / CSS
HTML利用九宫格原理进行网页布局
2020/03/13 HTML / CSS
Bjorn Borg官方网上商店:国际运动时尚品牌
2016/08/27 全球购物
保密承诺书
2014/03/27 职场文书
环保标语大全
2014/06/12 职场文书
机电系毕业生求职信
2014/07/11 职场文书
合同权益转让协议书模板
2014/11/18 职场文书
考试作弊检讨书
2015/01/27 职场文书
统计员岗位职责
2015/02/11 职场文书
2016年学校“6﹒26国际禁毒日”宣传活动总结
2016/04/05 职场文书
Pytorch使用shuffle打乱数据的操作
2021/05/20 Python
Python实现生活常识解答机器人
2021/06/28 Python
SqlServer常用函数及时间处理小结
2023/05/08 SQL Server