tooltip.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. /* ========================================================================
  2. * Bootstrap: tooltip.js v3.3.4
  3. * http://getbootstrap.com/javascript/#tooltip
  4. * Inspired by the original jQuery.tipsy by Jason Frame
  5. * ========================================================================
  6. * Copyright 2011-2015 Twitter, Inc.
  7. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  8. * ======================================================================== */
  9. +function ($) {
  10. 'use strict';
  11. // TOOLTIP PUBLIC CLASS DEFINITION
  12. // ===============================
  13. var Tooltip = function (element, options) {
  14. this.type = null
  15. this.options = null
  16. this.enabled = null
  17. this.timeout = null
  18. this.hoverState = null
  19. this.$element = null
  20. this.init('tooltip', element, options)
  21. }
  22. Tooltip.VERSION = '3.3.4'
  23. Tooltip.TRANSITION_DURATION = 150
  24. Tooltip.DEFAULTS = {
  25. animation: true,
  26. placement: 'top',
  27. selector: false,
  28. template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
  29. trigger: 'hover focus',
  30. title: '',
  31. delay: 0,
  32. html: false,
  33. container: false,
  34. viewport: {
  35. selector: 'body',
  36. padding: 0
  37. }
  38. }
  39. Tooltip.prototype.init = function (type, element, options) {
  40. this.enabled = true
  41. this.type = type
  42. this.$element = $(element)
  43. this.options = this.getOptions(options)
  44. this.$viewport = this.options.viewport && $(this.options.viewport.selector || this.options.viewport)
  45. if (this.$element[0] instanceof document.constructor && !this.options.selector) {
  46. throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!')
  47. }
  48. var triggers = this.options.trigger.split(' ')
  49. for (var i = triggers.length; i--;) {
  50. var trigger = triggers[i]
  51. if (trigger == 'click') {
  52. this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
  53. } else if (trigger != 'manual') {
  54. var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin'
  55. var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'
  56. this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
  57. this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
  58. }
  59. }
  60. this.options.selector ?
  61. (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
  62. this.fixTitle()
  63. }
  64. Tooltip.prototype.getDefaults = function () {
  65. return Tooltip.DEFAULTS
  66. }
  67. Tooltip.prototype.getOptions = function (options) {
  68. options = $.extend({}, this.getDefaults(), this.$element.data(), options)
  69. if (options.delay && typeof options.delay == 'number') {
  70. options.delay = {
  71. show: options.delay,
  72. hide: options.delay
  73. }
  74. }
  75. return options
  76. }
  77. Tooltip.prototype.getDelegateOptions = function () {
  78. var options = {}
  79. var defaults = this.getDefaults()
  80. this._options && $.each(this._options, function (key, value) {
  81. if (defaults[key] != value) options[key] = value
  82. })
  83. return options
  84. }
  85. Tooltip.prototype.enter = function (obj) {
  86. var self = obj instanceof this.constructor ?
  87. obj : $(obj.currentTarget).data('bs.' + this.type)
  88. if (self && self.$tip && self.$tip.is(':visible')) {
  89. self.hoverState = 'in'
  90. return
  91. }
  92. if (!self) {
  93. self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
  94. $(obj.currentTarget).data('bs.' + this.type, self)
  95. }
  96. clearTimeout(self.timeout)
  97. self.hoverState = 'in'
  98. if (!self.options.delay || !self.options.delay.show) return self.show()
  99. self.timeout = setTimeout(function () {
  100. if (self.hoverState == 'in') self.show()
  101. }, self.options.delay.show)
  102. }
  103. Tooltip.prototype.leave = function (obj) {
  104. var self = obj instanceof this.constructor ?
  105. obj : $(obj.currentTarget).data('bs.' + this.type)
  106. if (!self) {
  107. self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
  108. $(obj.currentTarget).data('bs.' + this.type, self)
  109. }
  110. clearTimeout(self.timeout)
  111. self.hoverState = 'out'
  112. if (!self.options.delay || !self.options.delay.hide) return self.hide()
  113. self.timeout = setTimeout(function () {
  114. if (self.hoverState == 'out') self.hide()
  115. }, self.options.delay.hide)
  116. }
  117. Tooltip.prototype.show = function () {
  118. var e = $.Event('show.bs.' + this.type)
  119. if (this.hasContent() && this.enabled) {
  120. this.$element.trigger(e)
  121. var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0])
  122. if (e.isDefaultPrevented() || !inDom) return
  123. var that = this
  124. var $tip = this.tip()
  125. var tipId = this.getUID(this.type)
  126. this.setContent()
  127. $tip.attr('id', tipId)
  128. this.$element.attr('aria-describedby', tipId)
  129. if (this.options.animation) $tip.addClass('fade')
  130. var placement = typeof this.options.placement == 'function' ?
  131. this.options.placement.call(this, $tip[0], this.$element[0]) :
  132. this.options.placement
  133. var autoToken = /\s?auto?\s?/i
  134. var autoPlace = autoToken.test(placement)
  135. if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
  136. $tip
  137. .detach()
  138. .css({ top: 0, left: 0, display: 'block' })
  139. .addClass(placement)
  140. .data('bs.' + this.type, this)
  141. this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
  142. var pos = this.getPosition()
  143. var actualWidth = $tip[0].offsetWidth
  144. var actualHeight = $tip[0].offsetHeight
  145. if (autoPlace) {
  146. var orgPlacement = placement
  147. var $container = this.options.container ? $(this.options.container) : this.$element.parent()
  148. var containerDim = this.getPosition($container)
  149. placement = placement == 'bottom' && pos.bottom + actualHeight > containerDim.bottom ? 'top' :
  150. placement == 'top' && pos.top - actualHeight < containerDim.top ? 'bottom' :
  151. placement == 'right' && pos.right + actualWidth > containerDim.width ? 'left' :
  152. placement == 'left' && pos.left - actualWidth < containerDim.left ? 'right' :
  153. placement
  154. $tip
  155. .removeClass(orgPlacement)
  156. .addClass(placement)
  157. }
  158. var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
  159. this.applyPlacement(calculatedOffset, placement)
  160. var complete = function () {
  161. var prevHoverState = that.hoverState
  162. that.$element.trigger('shown.bs.' + that.type)
  163. that.hoverState = null
  164. if (prevHoverState == 'out') that.leave(that)
  165. }
  166. $.support.transition && this.$tip.hasClass('fade') ?
  167. $tip
  168. .one('bsTransitionEnd', complete)
  169. .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
  170. complete()
  171. }
  172. }
  173. Tooltip.prototype.applyPlacement = function (offset, placement) {
  174. var $tip = this.tip()
  175. var width = $tip[0].offsetWidth
  176. var height = $tip[0].offsetHeight
  177. // manually read margins because getBoundingClientRect includes difference
  178. var marginTop = parseInt($tip.css('margin-top'), 10)
  179. var marginLeft = parseInt($tip.css('margin-left'), 10)
  180. // we must check for NaN for ie 8/9
  181. if (isNaN(marginTop)) marginTop = 0
  182. if (isNaN(marginLeft)) marginLeft = 0
  183. offset.top = offset.top + marginTop
  184. offset.left = offset.left + marginLeft
  185. // $.fn.offset doesn't round pixel values
  186. // so we use setOffset directly with our own function B-0
  187. $.offset.setOffset($tip[0], $.extend({
  188. using: function (props) {
  189. $tip.css({
  190. top: Math.round(props.top),
  191. left: Math.round(props.left)
  192. })
  193. }
  194. }, offset), 0)
  195. $tip.addClass('in')
  196. // check to see if placing tip in new offset caused the tip to resize itself
  197. var actualWidth = $tip[0].offsetWidth
  198. var actualHeight = $tip[0].offsetHeight
  199. if (placement == 'top' && actualHeight != height) {
  200. offset.top = offset.top + height - actualHeight
  201. }
  202. var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight)
  203. if (delta.left) offset.left += delta.left
  204. else offset.top += delta.top
  205. var isVertical = /top|bottom/.test(placement)
  206. var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight
  207. var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight'
  208. $tip.offset(offset)
  209. this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical)
  210. }
  211. Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) {
  212. this.arrow()
  213. .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%')
  214. .css(isVertical ? 'top' : 'left', '')
  215. }
  216. Tooltip.prototype.setContent = function () {
  217. var $tip = this.tip()
  218. var title = this.getTitle()
  219. $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
  220. $tip.removeClass('fade in top bottom left right')
  221. }
  222. Tooltip.prototype.hide = function (callback) {
  223. var that = this
  224. var $tip = $(this.$tip)
  225. var e = $.Event('hide.bs.' + this.type)
  226. function complete() {
  227. if (that.hoverState != 'in') $tip.detach()
  228. that.$element
  229. .removeAttr('aria-describedby')
  230. .trigger('hidden.bs.' + that.type)
  231. callback && callback()
  232. }
  233. this.$element.trigger(e)
  234. if (e.isDefaultPrevented()) return
  235. $tip.removeClass('in')
  236. $.support.transition && $tip.hasClass('fade') ?
  237. $tip
  238. .one('bsTransitionEnd', complete)
  239. .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
  240. complete()
  241. this.hoverState = null
  242. return this
  243. }
  244. Tooltip.prototype.fixTitle = function () {
  245. var $e = this.$element
  246. if ($e.attr('title') || typeof ($e.attr('data-original-title')) != 'string') {
  247. $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
  248. }
  249. }
  250. Tooltip.prototype.hasContent = function () {
  251. return this.getTitle()
  252. }
  253. Tooltip.prototype.getPosition = function ($element) {
  254. $element = $element || this.$element
  255. var el = $element[0]
  256. var isBody = el.tagName == 'BODY'
  257. var elRect = el.getBoundingClientRect()
  258. if (elRect.width == null) {
  259. // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093
  260. elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top })
  261. }
  262. var elOffset = isBody ? { top: 0, left: 0 } : $element.offset()
  263. var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }
  264. var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null
  265. return $.extend({}, elRect, scroll, outerDims, elOffset)
  266. }
  267. Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
  268. return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } :
  269. placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } :
  270. placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
  271. /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }
  272. }
  273. Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) {
  274. var delta = { top: 0, left: 0 }
  275. if (!this.$viewport) return delta
  276. var viewportPadding = this.options.viewport && this.options.viewport.padding || 0
  277. var viewportDimensions = this.getPosition(this.$viewport)
  278. if (/right|left/.test(placement)) {
  279. var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll
  280. var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight
  281. if (topEdgeOffset < viewportDimensions.top) { // top overflow
  282. delta.top = viewportDimensions.top - topEdgeOffset
  283. } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow
  284. delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset
  285. }
  286. } else {
  287. var leftEdgeOffset = pos.left - viewportPadding
  288. var rightEdgeOffset = pos.left + viewportPadding + actualWidth
  289. if (leftEdgeOffset < viewportDimensions.left) { // left overflow
  290. delta.left = viewportDimensions.left - leftEdgeOffset
  291. } else if (rightEdgeOffset > viewportDimensions.width) { // right overflow
  292. delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset
  293. }
  294. }
  295. return delta
  296. }
  297. Tooltip.prototype.getTitle = function () {
  298. var title
  299. var $e = this.$element
  300. var o = this.options
  301. title = $e.attr('data-original-title')
  302. || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
  303. return title
  304. }
  305. Tooltip.prototype.getUID = function (prefix) {
  306. do prefix += ~~(Math.random() * 1000000)
  307. while (document.getElementById(prefix))
  308. return prefix
  309. }
  310. Tooltip.prototype.tip = function () {
  311. return (this.$tip = this.$tip || $(this.options.template))
  312. }
  313. Tooltip.prototype.arrow = function () {
  314. return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow'))
  315. }
  316. Tooltip.prototype.enable = function () {
  317. this.enabled = true
  318. }
  319. Tooltip.prototype.disable = function () {
  320. this.enabled = false
  321. }
  322. Tooltip.prototype.toggleEnabled = function () {
  323. this.enabled = !this.enabled
  324. }
  325. Tooltip.prototype.toggle = function (e) {
  326. var self = this
  327. if (e) {
  328. self = $(e.currentTarget).data('bs.' + this.type)
  329. if (!self) {
  330. self = new this.constructor(e.currentTarget, this.getDelegateOptions())
  331. $(e.currentTarget).data('bs.' + this.type, self)
  332. }
  333. }
  334. self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
  335. }
  336. Tooltip.prototype.destroy = function () {
  337. var that = this
  338. clearTimeout(this.timeout)
  339. this.hide(function () {
  340. that.$element.off('.' + that.type).removeData('bs.' + that.type)
  341. })
  342. }
  343. // TOOLTIP PLUGIN DEFINITION
  344. // =========================
  345. function Plugin(option) {
  346. return this.each(function () {
  347. var $this = $(this)
  348. var data = $this.data('bs.tooltip')
  349. var options = typeof option == 'object' && option
  350. if (!data && /destroy|hide/.test(option)) return
  351. if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
  352. if (typeof option == 'string') data[option]()
  353. })
  354. }
  355. var old = $.fn.tooltip
  356. $.fn.tooltip = Plugin
  357. $.fn.tooltip.Constructor = Tooltip
  358. // TOOLTIP NO CONFLICT
  359. // ===================
  360. $.fn.tooltip.noConflict = function () {
  361. $.fn.tooltip = old
  362. return this
  363. }
  364. }(jQuery);