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的让textarea自适应高度的插件
Aug 03 Javascript
Js冒泡事件详解及阻止示例
Mar 21 Javascript
IE6-8中Date不支持toISOString的修复方法
May 04 Javascript
jquery 设置style:display的方法
Jan 29 Javascript
jQuery实现类似老虎机滚动抽奖效果
Aug 06 Javascript
JavaScript检测原始值、引用值、属性
Jun 20 Javascript
jQuery设置图片等比例缩小的方法
Apr 29 jQuery
基于jquery的on和click的区别详解
Jan 15 jQuery
关于vue的npm run dev和npm run build的区别介绍
Jan 14 Javascript
微信小程序实现页面跳转传递参数(实体,对象)
Aug 12 Javascript
简单了解Vue + ElementUI后台管理模板
Apr 07 Javascript
vue中后端做Excel导出功能返回数据流前端的处理操作
Sep 08 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
如何选购合适的收音机
2021/03/01 无线电
windows下PHP APACHE MYSQ完整配置
2007/01/02 PHP
php启用zlib压缩文件的配置方法
2013/06/12 PHP
浅析php中三个等号(===)和两个等号(==)的区别
2013/08/06 PHP
PHP随机获取未被微信屏蔽的域名(微信域名检测)
2017/03/19 PHP
JQuery 学习笔记 选择器之五
2009/07/23 Javascript
js parsefloat parseint 转换函数
2010/01/21 Javascript
jquery attr 设定src中含有&amp;(宏)符号问题的解决方法
2011/07/26 Javascript
JS 对输入框进行限制(常用的都有)
2013/07/30 Javascript
兼容主流浏览器的jQuery+CSS 实现遮罩层的简单代码
2014/10/14 Javascript
jQuery中[attribute=value]选择器用法实例
2014/12/31 Javascript
jQuery实现鼠标单击网页文字后在文本框显示的方法
2015/05/06 Javascript
JS实现快速的导航下拉菜单动画效果附源码下载
2016/11/01 Javascript
jQuery设置Easyui校验规则(推荐)
2016/11/21 Javascript
javascript中活灵活现的Array对象详解
2016/11/30 Javascript
JS中正则表达式全局匹配模式 /g用法详解
2017/04/01 Javascript
jQuery扇形定时器插件pietimer使用方法详解
2017/07/18 jQuery
vue 使用html2canvas将DOM转化为图片的方法
2018/09/11 Javascript
javascript中toFixed()四舍五入使用方法详解
2018/09/28 Javascript
JavaScript实现的开关灯泡点击切换特效示例
2019/07/08 Javascript
深入浅析ng-bootstrap 组件集中 tabset 组件的实现分析
2019/07/19 Javascript
vue 地区选择器v-distpicker的常用功能
2019/07/23 Javascript
将RGB值转换为灰度值的简单算法
2019/10/09 Javascript
JavaScript布尔运算符原理使用解析
2020/05/06 Javascript
基于Electron实现桌面应用开发代码实例
2020/07/07 Javascript
解决Vue大括号字符换行踩的坑
2020/11/09 Javascript
python 设置文件编码格式的实现方法
2017/12/21 Python
Django Celery异步任务队列的实现
2019/07/24 Python
Django 后台获取文件列表 InMemoryUploadedFile的例子
2019/08/07 Python
浅谈pytorch torch.backends.cudnn设置作用
2020/02/20 Python
Python中的全局变量如何理解
2020/06/04 Python
纯css3显示隐藏一个div特效的具体实现
2014/02/10 HTML / CSS
AmazeUI底部导航栏与分享按钮的示例代码
2020/08/18 HTML / CSS
通信工程求职信
2014/07/16 职场文书
幼儿园综治宣传月活动总结
2015/05/07 职场文书
python中Tkinter 窗口之输入框和文本框的实现
2021/04/12 Python