tooltip.js 15.3 KB
Newer Older
1
/* ========================================================================
Chris Rebert's avatar
Chris Rebert committed
2
 * Bootstrap: tooltip.js v3.3.4
3
 * http://getbootstrap.com/javascript/#tooltip
4
 * Inspired by the original jQuery.tipsy by Jason Frame
5
 * ========================================================================
Zlatan Vasović's avatar
Zlatan Vasović committed
6
 * Copyright 2011-2015 Twitter, Inc.
7
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
8
 * ======================================================================== */
Jacob Thornton's avatar
Jacob Thornton committed
9

10

11
12
13
14
15
16
17
+function ($) {
  'use strict';

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

  var Tooltip = function (element, options) {
18
19
20
21
22
    this.type       = null
    this.options    = null
    this.enabled    = null
    this.timeout    = null
    this.hoverState = null
23
24
25
26
27
    this.$element   = null

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

Chris Rebert's avatar
Chris Rebert committed
28
  Tooltip.VERSION  = '3.3.4'
29

30
31
  Tooltip.TRANSITION_DURATION = 150

32
33
34
35
36
37
38
39
40
41
42
43
44
  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
fat's avatar
fat committed
45
    }
46
  }
fat's avatar
fat committed
47

48
49
50
51
52
  Tooltip.prototype.init = function (type, element, options) {
    this.enabled   = true
    this.type      = type
    this.$element  = $(element)
    this.options   = this.getOptions(options)
53
    this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport))
54

55
    if (this.$element[0] instanceof document.constructor && !this.options.selector) {
56
      throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!')
57
58
    }

59
    var triggers = this.options.trigger.split(' ')
fat's avatar
fat committed
60

61
62
    for (var i = triggers.length; i--;) {
      var trigger = triggers[i]
fat's avatar
fat committed
63

64
65
66
67
68
      if (trigger == 'click') {
        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'
69

70
71
        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))
fat's avatar
fat committed
72
      }
73
74
    }

75
76
77
78
    this.options.selector ?
      (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
      this.fixTitle()
  }
79

80
81
82
  Tooltip.prototype.getDefaults = function () {
    return Tooltip.DEFAULTS
  }
83

84
85
  Tooltip.prototype.getOptions = function (options) {
    options = $.extend({}, this.getDefaults(), this.$element.data(), options)
Jacob Thornton's avatar
Jacob Thornton committed
86

87
88
89
90
91
    if (options.delay && typeof options.delay == 'number') {
      options.delay = {
        show: options.delay,
        hide: options.delay
      }
92
    }
93

94
95
    return options
  }
96

97
98
99
  Tooltip.prototype.getDelegateOptions = function () {
    var options  = {}
    var defaults = this.getDefaults()
100

101
102
103
    this._options && $.each(this._options, function (key, value) {
      if (defaults[key] != value) options[key] = value
    })
Jacob Thornton's avatar
Jacob Thornton committed
104

105
106
    return options
  }
107

108
109
110
  Tooltip.prototype.enter = function (obj) {
    var self = obj instanceof this.constructor ?
      obj : $(obj.currentTarget).data('bs.' + this.type)
111

112
113
114
115
116
    if (self && self.$tip && self.$tip.is(':visible')) {
      self.hoverState = 'in'
      return
    }

117
118
119
    if (!self) {
      self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
      $(obj.currentTarget).data('bs.' + this.type, self)
120
    }
fat's avatar
fat committed
121

122
    clearTimeout(self.timeout)
fat's avatar
fat committed
123

124
    self.hoverState = 'in'
125

126
    if (!self.options.delay || !self.options.delay.show) return self.show()
127

128
129
130
131
    self.timeout = setTimeout(function () {
      if (self.hoverState == 'in') self.show()
    }, self.options.delay.show)
  }
132

133
134
135
  Tooltip.prototype.leave = function (obj) {
    var self = obj instanceof this.constructor ?
      obj : $(obj.currentTarget).data('bs.' + this.type)
136

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

142
    clearTimeout(self.timeout)
143

144
    self.hoverState = 'out'
Yohn's avatar
Yohn committed
145

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

148
149
150
151
    self.timeout = setTimeout(function () {
      if (self.hoverState == 'out') self.hide()
    }, self.options.delay.hide)
  }
152

153
154
  Tooltip.prototype.show = function () {
    var e = $.Event('show.bs.' + this.type)
155

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

159
      var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0])
160
161
      if (e.isDefaultPrevented() || !inDom) return
      var that = this
162

163
      var $tip = this.tip()
fat's avatar
fat committed
164

165
      var tipId = this.getUID(this.type)
fat's avatar
fat committed
166

167
168
169
      this.setContent()
      $tip.attr('id', tipId)
      this.$element.attr('aria-describedby', tipId)
fat's avatar
fat committed
170

171
      if (this.options.animation) $tip.addClass('fade')
172

173
174
175
      var placement = typeof this.options.placement == 'function' ?
        this.options.placement.call(this, $tip[0], this.$element[0]) :
        this.options.placement
176

177
178
179
      var autoToken = /\s?auto?\s?/i
      var autoPlace = autoToken.test(placement)
      if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
180

181
182
183
184
185
      $tip
        .detach()
        .css({ top: 0, left: 0, display: 'block' })
        .addClass(placement)
        .data('bs.' + this.type, this)
fat's avatar
fat committed
186

187
      this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
Johann-S's avatar
Johann-S committed
188
      this.$element.trigger('inserted.bs.' + this.type)
189

190
191
192
      var pos          = this.getPosition()
      var actualWidth  = $tip[0].offsetWidth
      var actualHeight = $tip[0].offsetHeight
193

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

199
200
201
202
        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'  :
203
                    placement
204

205
206
207
        $tip
          .removeClass(orgPlacement)
          .addClass(placement)
208
      }
209

210
      var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
fat's avatar
fat committed
211

212
      this.applyPlacement(calculatedOffset, placement)
fat's avatar
fat committed
213

214
      var complete = function () {
215
        var prevHoverState = that.hoverState
216
217
        that.$element.trigger('shown.bs.' + that.type)
        that.hoverState = null
218
219

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

222
223
224
      $.support.transition && this.$tip.hasClass('fade') ?
        $tip
          .one('bsTransitionEnd', complete)
225
          .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
        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($tip[0], $.extend({
      using: function (props) {
        $tip.css({
          top: Math.round(props.top),
          left: Math.round(props.left)
        })
      }
    }, offset), 0)
Jacob Thornton's avatar
Jacob Thornton committed
256

257
    $tip.addClass('in')
258

259
260
261
    // 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
262

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

267
    var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight)
Jacob Thornton's avatar
Jacob Thornton committed
268

269
270
    if (delta.left) offset.left += delta.left
    else offset.top += delta.top
271

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

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

280
  Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) {
281
    this.arrow()
282
283
      .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%')
      .css(isVertical ? 'top' : 'left', '')
284
  }
285

286
287
288
  Tooltip.prototype.setContent = function () {
    var $tip  = this.tip()
    var title = this.getTitle()
fat's avatar
fat committed
289

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

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

299
300
    function complete() {
      if (that.hoverState != 'in') $tip.detach()
301
302
303
      that.$element
        .removeAttr('aria-describedby')
        .trigger('hidden.bs.' + that.type)
304
      callback && callback()
305
    }
306

307
    this.$element.trigger(e)
308

309
    if (e.isDefaultPrevented()) return
310

311
    $tip.removeClass('in')
312

313
    $.support.transition && $tip.hasClass('fade') ?
314
315
      $tip
        .one('bsTransitionEnd', complete)
316
        .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
317
      complete()
fat's avatar
fat committed
318

319
    this.hoverState = null
320

321
322
    return this
  }
323

324
325
326
327
  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', '')
328
    }
329
330
331
332
333
334
335
336
  }

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

  Tooltip.prototype.getPosition = function ($element) {
    $element   = $element || this.$element
337

338
339
    var el     = $element[0]
    var isBody = el.tagName == 'BODY'
340

341
    var elRect    = el.getBoundingClientRect()
342
343
344
345
    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 })
    }
346
347
    var elOffset  = isBody ? { top: 0, left: 0 } : $element.offset()
    var scroll    = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }
348
    var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null
349
350

    return $.extend({}, elRect, scroll, outerDims, elOffset)
351
352
353
  }

  Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
XhmikosR's avatar
XhmikosR committed
354
355
    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 } :
356
           placement == 'left'   ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
XhmikosR's avatar
XhmikosR committed
357
        /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382

  }

  Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) {
    var delta = { top: 0, left: 0 }
    if (!this.$viewport) return delta

    var viewportPadding = this.options.viewport && this.options.viewport.padding || 0
    var viewportDimensions = this.getPosition(this.$viewport)

    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
383
384
385
      }
    }

386
387
    return delta
  }
388

389
390
391
392
  Tooltip.prototype.getTitle = function () {
    var title
    var $e = this.$element
    var o  = this.options
393

394
395
    title = $e.attr('data-original-title')
      || (typeof o.title == 'function' ? o.title.call($e[0]) :  o.title)
396

397
398
    return title
  }
399

400
401
402
403
404
  Tooltip.prototype.getUID = function (prefix) {
    do prefix += ~~(Math.random() * 1000000)
    while (document.getElementById(prefix))
    return prefix
  }
405

406
407
408
  Tooltip.prototype.tip = function () {
    return (this.$tip = this.$tip || $(this.options.template))
  }
409

410
411
412
  Tooltip.prototype.arrow = function () {
    return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow'))
  }
413

414
415
416
  Tooltip.prototype.enable = function () {
    this.enabled = true
  }
417

418
419
420
  Tooltip.prototype.disable = function () {
    this.enabled = false
  }
421

422
423
424
  Tooltip.prototype.toggleEnabled = function () {
    this.enabled = !this.enabled
  }
425

426
427
428
429
430
431
432
433
  Tooltip.prototype.toggle = function (e) {
    var self = this
    if (e) {
      self = $(e.currentTarget).data('bs.' + this.type)
      if (!self) {
        self = new this.constructor(e.currentTarget, this.getDelegateOptions())
        $(e.currentTarget).data('bs.' + this.type, self)
      }
434
435
    }

436
437
    self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
  }
fat's avatar
fat committed
438

439
  Tooltip.prototype.destroy = function () {
440
    var that = this
441
    clearTimeout(this.timeout)
442
443
    this.hide(function () {
      that.$element.off('.' + that.type).removeData('bs.' + that.type)
444
445
446
447
448
449
      if (that.$tip) {
        that.$tip.detach()
      }
      that.$tip = null
      that.$arrow = null
      that.$viewport = null
450
    })
451
  }
452

453

454
455
  // TOOLTIP PLUGIN DEFINITION
  // =========================
456

457
458
  function Plugin(option) {
    return this.each(function () {
459
460
461
      var $this   = $(this)
      var data    = $this.data('bs.tooltip')
      var options = typeof option == 'object' && option
Jacob Thornton's avatar
Jacob Thornton committed
462

463
      if (!data && /destroy|hide/.test(option)) return
464
      if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
465
466
467
      if (typeof option == 'string') data[option]()
    })
  }
468

469
  var old = $.fn.tooltip
470

471
472
  $.fn.tooltip             = Plugin
  $.fn.tooltip.Constructor = Tooltip
473
474


475
476
  // TOOLTIP NO CONFLICT
  // ===================
477

478
479
480
481
  $.fn.tooltip.noConflict = function () {
    $.fn.tooltip = old
    return this
  }
482

483
}(jQuery);