tooltip.js 15.1 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
53
  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)
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)
188

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

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

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

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

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

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

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

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

221
222
223
      $.support.transition && this.$tip.hasClass('fade') ?
        $tip
          .one('bsTransitionEnd', complete)
224
          .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
225
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
        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
255

256
    $tip.addClass('in')
257

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

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

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

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

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

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

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

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

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

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

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

306
    this.$element.trigger(e)
307

308
    if (e.isDefaultPrevented()) return
309

310
    $tip.removeClass('in')
311

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

318
    this.hoverState = null
319

320
321
    return this
  }
322

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

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

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

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

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

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

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

  }

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

385
386
    return delta
  }
387

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

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

396
397
    return title
  }
398

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

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

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

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

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

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

425
426
427
428
429
430
431
432
  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)
      }
433
434
    }

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

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

452

453
454
  // TOOLTIP PLUGIN DEFINITION
  // =========================
455

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

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

468
  var old = $.fn.tooltip
469

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


474
475
  // TOOLTIP NO CONFLICT
  // ===================
476

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

482
}(jQuery);