tooltip.js 16 KB
Newer Older
fat's avatar
fat committed
1
2
/* ========================================================================
 * Bootstrap: tooltip.js v3.3.4
3
 * http://getbootstrap.com/javascript/#tooltip
fat's avatar
fat committed
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)
fat's avatar
fat committed
8
 * ======================================================================== */
9

10

fat's avatar
fat committed
11
12
+function ($) {
  'use strict';
13

fat's avatar
fat committed
14
15
  // TOOLTIP PUBLIC CLASS DEFINITION
  // ===============================
16

fat's avatar
fat committed
17
18
19
20
21
22
23
24
  var Tooltip = function (element, options) {
    this.type       = null
    this.options    = null
    this.enabled    = null
    this.timeout    = null
    this.hoverState = null
    this.$element   = null
    this.inState    = null
25

fat's avatar
fat committed
26
27
    this.init('tooltip', element, options)
  }
28

fat's avatar
fat committed
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
  Tooltip.VERSION  = '3.3.4'

  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
    }
  }
fat's avatar
fat committed
48

fat's avatar
fat committed
49
50
51
52
53
54
55
  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 && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport))
    this.inState   = { click: false, hover: false, focus: false }
fat's avatar
fat committed
56

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

fat's avatar
fat committed
61
    var triggers = this.options.trigger.split(' ')
62

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

fat's avatar
fat committed
66
67
68
69
70
      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'
71

fat's avatar
fat committed
72
73
74
        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
75
    }
Jacob Thornton's avatar
Jacob Thornton committed
76

fat's avatar
fat committed
77
78
79
    this.options.selector ?
      (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
      this.fixTitle()
80
  }
81

fat's avatar
fat committed
82
83
  Tooltip.prototype.getDefaults = function () {
    return Tooltip.DEFAULTS
84
85
  }

fat's avatar
fat committed
86
87
  Tooltip.prototype.getOptions = function (options) {
    options = $.extend({}, this.getDefaults(), this.$element.data(), options)
88

fat's avatar
fat committed
89
90
91
92
93
    if (options.delay && typeof options.delay == 'number') {
      options.delay = {
        show: options.delay,
        hide: options.delay
      }
94
95
    }

fat's avatar
fat committed
96
97
    return options
  }
fat's avatar
fat committed
98

fat's avatar
fat committed
99
100
101
  Tooltip.prototype.getDelegateOptions = function () {
    var options  = {}
    var defaults = this.getDefaults()
fat's avatar
fat committed
102

fat's avatar
fat committed
103
104
105
    this._options && $.each(this._options, function (key, value) {
      if (defaults[key] != value) options[key] = value
    })
fat's avatar
fat committed
106

fat's avatar
fat committed
107
108
    return options
  }
fat's avatar
fat committed
109

fat's avatar
fat committed
110
111
112
  Tooltip.prototype.enter = function (obj) {
    var self = obj instanceof this.constructor ?
      obj : $(obj.currentTarget).data('bs.' + this.type)
113

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

fat's avatar
fat committed
119
120
    if (obj instanceof $.Event) {
      self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true
fat's avatar
fat committed
121
122
    }

fat's avatar
fat committed
123
124
125
    if (self.tip().hasClass('in') || self.hoverState == 'in') {
      self.hoverState = 'in'
      return
126
    }
127

fat's avatar
fat committed
128
    clearTimeout(self.timeout)
Yohn's avatar
Yohn committed
129

fat's avatar
fat committed
130
    self.hoverState = 'in'
131

fat's avatar
fat committed
132
    if (!self.options.delay || !self.options.delay.show) return self.show()
fat's avatar
fat committed
133

fat's avatar
fat committed
134
135
136
    self.timeout = setTimeout(function () {
      if (self.hoverState == 'in') self.show()
    }, self.options.delay.show)
137
  }
138

fat's avatar
fat committed
139
140
141
  Tooltip.prototype.isInStateTrue = function () {
    for (var key in this.inState) {
      if (this.inState[key]) return true
fat's avatar
fat committed
142
    }
143

fat's avatar
fat committed
144
    return false
fat's avatar
fat committed
145
  }
146

fat's avatar
fat committed
147
148
149
  Tooltip.prototype.leave = function (obj) {
    var self = obj instanceof this.constructor ?
      obj : $(obj.currentTarget).data('bs.' + this.type)
fat's avatar
fat committed
150

fat's avatar
fat committed
151
152
153
154
    if (!self) {
      self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
      $(obj.currentTarget).data('bs.' + this.type, self)
    }
155

fat's avatar
fat committed
156
157
158
    if (obj instanceof $.Event) {
      self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false
    }
159

fat's avatar
fat committed
160
    if (self.isInStateTrue()) return
161

fat's avatar
fat committed
162
    clearTimeout(self.timeout)
163

fat's avatar
fat committed
164
    self.hoverState = 'out'
165

fat's avatar
fat committed
166
    if (!self.options.delay || !self.options.delay.hide) return self.hide()
fat's avatar
fat committed
167

fat's avatar
fat committed
168
169
170
171
    self.timeout = setTimeout(function () {
      if (self.hoverState == 'out') self.hide()
    }, self.options.delay.hide)
  }
fat's avatar
fat committed
172

fat's avatar
fat committed
173
174
  Tooltip.prototype.show = function () {
    var e = $.Event('show.bs.' + this.type)
175

fat's avatar
fat committed
176
177
    if (this.hasContent() && this.enabled) {
      this.$element.trigger(e)
178

fat's avatar
fat committed
179
180
181
      var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0])
      if (e.isDefaultPrevented() || !inDom) return
      var that = this
182

fat's avatar
fat committed
183
      var $tip = this.tip()
184

fat's avatar
fat committed
185
      var tipId = this.getUID(this.type)
Jacob Thornton's avatar
Jacob Thornton committed
186

fat's avatar
fat committed
187
188
189
      this.setContent()
      $tip.attr('id', tipId)
      this.$element.attr('aria-describedby', tipId)
190

fat's avatar
fat committed
191
      if (this.options.animation) $tip.addClass('fade')
192

fat's avatar
fat committed
193
194
195
      var placement = typeof this.options.placement == 'function' ?
        this.options.placement.call(this, $tip[0], this.$element[0]) :
        this.options.placement
196

fat's avatar
fat committed
197
198
199
      var autoToken = /\s?auto?\s?/i
      var autoPlace = autoToken.test(placement)
      if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
200

fat's avatar
fat committed
201
202
203
204
205
      $tip
        .detach()
        .css({ top: 0, left: 0, display: 'block' })
        .addClass(placement)
        .data('bs.' + this.type, this)
fat's avatar
fat committed
206

fat's avatar
fat committed
207
208
      this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
      this.$element.trigger('inserted.bs.' + this.type)
209

fat's avatar
fat committed
210
211
212
      var pos          = this.getPosition()
      var actualWidth  = $tip[0].offsetWidth
      var actualHeight = $tip[0].offsetHeight
213

fat's avatar
fat committed
214
215
216
      if (autoPlace) {
        var orgPlacement = placement
        var viewportDim = this.getPosition(this.$viewport)
217

fat's avatar
fat committed
218
219
220
221
222
        placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top'    :
                    placement == 'top'    && pos.top    - actualHeight < viewportDim.top    ? 'bottom' :
                    placement == 'right'  && pos.right  + actualWidth  > viewportDim.width  ? 'left'   :
                    placement == 'left'   && pos.left   - actualWidth  < viewportDim.left   ? 'right'  :
                    placement
223

fat's avatar
fat committed
224
225
226
227
        $tip
          .removeClass(orgPlacement)
          .addClass(placement)
      }
228

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

fat's avatar
fat committed
231
      this.applyPlacement(calculatedOffset, placement)
232

fat's avatar
fat committed
233
234
235
236
      var complete = function () {
        var prevHoverState = that.hoverState
        that.$element.trigger('shown.bs.' + that.type)
        that.hoverState = null
237

fat's avatar
fat committed
238
239
        if (prevHoverState == 'out') that.leave(that)
      }
fat's avatar
fat committed
240

fat's avatar
fat committed
241
242
243
244
245
      $.support.transition && this.$tip.hasClass('fade') ?
        $tip
          .one('bsTransitionEnd', complete)
          .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
        complete()
246
    }
247
  }
248

fat's avatar
fat committed
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
  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  += marginTop
    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)

    $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
284
    }
285

fat's avatar
fat committed
286
    var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight)
287

fat's avatar
fat committed
288
289
    if (delta.left) offset.left += delta.left
    else offset.top += delta.top
290

fat's avatar
fat committed
291
292
293
    var isVertical          = /top|bottom/.test(placement)
    var arrowDelta          = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight
    var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight'
fat's avatar
fat committed
294

fat's avatar
fat committed
295
296
    $tip.offset(offset)
    this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical)
fat's avatar
fat committed
297
298
  }

fat's avatar
fat committed
299
300
301
302
  Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) {
    this.arrow()
      .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%')
      .css(isVertical ? 'top' : 'left', '')
fat's avatar
fat committed
303
  }
304

fat's avatar
fat committed
305
306
307
308
309
310
  Tooltip.prototype.setContent = function () {
    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')
311
  }
312

fat's avatar
fat committed
313
314
315
316
317
318
319
320
321
322
323
324
  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()
    }
325

fat's avatar
fat committed
326
    this.$element.trigger(e)
327

fat's avatar
fat committed
328
    if (e.isDefaultPrevented()) return
fat's avatar
fat committed
329

fat's avatar
fat committed
330
    $tip.removeClass('in')
fat's avatar
fat committed
331

fat's avatar
fat committed
332
333
334
335
336
    $.support.transition && $tip.hasClass('fade') ?
      $tip
        .one('bsTransitionEnd', complete)
        .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
      complete()
fat's avatar
fat committed
337

fat's avatar
fat committed
338
    this.hoverState = null
fat's avatar
fat committed
339

fat's avatar
fat committed
340
    return this
341
  }
342

fat's avatar
fat committed
343
344
345
346
347
  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', '')
    }
348
  }
349

fat's avatar
fat committed
350
351
352
  Tooltip.prototype.hasContent = function () {
    return this.getTitle()
  }
fat's avatar
fat committed
353

fat's avatar
fat committed
354
355
  Tooltip.prototype.getPosition = function ($element) {
    $element   = $element || this.$element
fat's avatar
fat committed
356

fat's avatar
fat committed
357
358
    var el     = $element[0]
    var isBody = el.tagName == 'BODY'
fat's avatar
fat committed
359

fat's avatar
fat committed
360
361
362
363
364
365
366
367
    var elRect    = el.getBoundingClientRect()
    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()
    var scroll    = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }
    var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null
fat's avatar
fat committed
368

fat's avatar
fat committed
369
    return $.extend({}, elRect, scroll, outerDims, elOffset)
370
  }
371

fat's avatar
fat committed
372
373
374
375
376
  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 }
fat's avatar
fat committed
377

378
  }
379

fat's avatar
fat committed
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
  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.right) { // right overflow
        delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset
      }
    }
fat's avatar
fat committed
404

fat's avatar
fat committed
405
406
    return delta
  }
fat's avatar
fat committed
407

fat's avatar
fat committed
408
409
410
411
  Tooltip.prototype.getTitle = function () {
    var title
    var $e = this.$element
    var o  = this.options
fat's avatar
fat committed
412

fat's avatar
fat committed
413
414
    title = $e.attr('data-original-title')
      || (typeof o.title == 'function' ? o.title.call($e[0]) :  o.title)
fat's avatar
fat committed
415

fat's avatar
fat committed
416
    return title
417
  }
fat's avatar
fat committed
418

fat's avatar
fat committed
419
420
421
422
  Tooltip.prototype.getUID = function (prefix) {
    do prefix += ~~(Math.random() * 1000000)
    while (document.getElementById(prefix))
    return prefix
423
  }
424

fat's avatar
fat committed
425
426
427
428
429
430
431
432
  Tooltip.prototype.tip = function () {
    if (!this.$tip) {
      this.$tip = $(this.options.template)
      if (this.$tip.length != 1) {
        throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!')
      }
    }
    return this.$tip
433
  }
434

fat's avatar
fat committed
435
436
437
  Tooltip.prototype.arrow = function () {
    return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow'))
  }
438

fat's avatar
fat committed
439
440
441
  Tooltip.prototype.enable = function () {
    this.enabled = true
  }
fat's avatar
fat committed
442

fat's avatar
fat committed
443
444
  Tooltip.prototype.disable = function () {
    this.enabled = false
445
  }
446

fat's avatar
fat committed
447
448
  Tooltip.prototype.toggleEnabled = function () {
    this.enabled = !this.enabled
fat's avatar
fat committed
449
  }
450

fat's avatar
fat committed
451
452
453
454
455
456
457
458
459
  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)
      }
    }
460

fat's avatar
fat committed
461
462
463
464
465
466
467
468
    if (e) {
      self.inState.click = !self.inState.click
      if (self.isInStateTrue()) self.enter(self)
      else self.leave(self)
    } else {
      self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
    }
  }
Jacob Thornton's avatar
Jacob Thornton committed
469

fat's avatar
fat committed
470
471
472
473
474
475
476
477
478
479
480
481
  Tooltip.prototype.destroy = function () {
    var that = this
    clearTimeout(this.timeout)
    this.hide(function () {
      that.$element.off('.' + that.type).removeData('bs.' + that.type)
      if (that.$tip) {
        that.$tip.detach()
      }
      that.$tip = null
      that.$arrow = null
      that.$viewport = null
    })
482
  }
483

484

fat's avatar
fat committed
485
486
  // TOOLTIP PLUGIN DEFINITION
  // =========================
487

fat's avatar
fat committed
488
489
490
491
492
  function Plugin(option) {
    return this.each(function () {
      var $this   = $(this)
      var data    = $this.data('bs.tooltip')
      var options = typeof option == 'object' && option
493

fat's avatar
fat committed
494
495
496
497
498
      if (!data && /destroy|hide/.test(option)) return
      if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
      if (typeof option == 'string') data[option]()
    })
  }
fat's avatar
fat committed
499

fat's avatar
fat committed
500
  var old = $.fn.tooltip
fat's avatar
fat committed
501

fat's avatar
fat committed
502
503
  $.fn.tooltip             = Plugin
  $.fn.tooltip.Constructor = Tooltip
fat's avatar
fat committed
504

505

fat's avatar
fat committed
506
507
508
509
510
511
512
  // TOOLTIP NO CONFLICT
  // ===================

  $.fn.tooltip.noConflict = function () {
    $.fn.tooltip = old
    return this
  }
513

fat's avatar
fat committed
514
}(jQuery);