diff options
| author | fat <[email protected]> | 2015-01-03 13:58:44 -0800 |
|---|---|---|
| committer | fat <[email protected]> | 2015-02-11 11:29:43 -0800 |
| commit | 834220ea20ce5b7cd31edfb624a28b4bf8b29a6a (patch) | |
| tree | 19bfdadb0c140df437e7b7034e5e1f5ce58343cc /js/tooltip.js | |
| parent | aed1cd31218113d67d2eca3296edf5d1700b19b8 (diff) | |
| download | bootstrap-834220ea20ce5b7cd31edfb624a28b4bf8b29a6a.tar.xz bootstrap-834220ea20ce5b7cd31edfb624a28b4bf8b29a6a.zip | |
bootstrap onto closure
Diffstat (limited to 'js/tooltip.js')
| -rw-r--r-- | js/tooltip.js | 1086 |
1 files changed, 741 insertions, 345 deletions
diff --git a/js/tooltip.js b/js/tooltip.js index 8663bb767..ced0e9282 100644 --- a/js/tooltip.js +++ b/js/tooltip.js @@ -1,472 +1,868 @@ -/* ======================================================================== - * Bootstrap: tooltip.js v3.3.2 +/** ======================================================================= + * Bootstrap: tooltip.js v4.0.0 * http://getbootstrap.com/javascript/#tooltip - * Inspired by the original jQuery.tipsy by Jason Frame * ======================================================================== * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ + * ======================================================================== + * @fileoverview - Bootstrap's tooltip plugin. + * (Inspired by jQuery.tipsy by Jason Frame) + * + * Public Methods & Properties: + * + * + $.tooltip + * + $.tooltip.noConflict + * + $.tooltip.Constructor + * + $.tooltip.Constructor.VERSION + * + $.tooltip.Constructor.Defaults + * + $.tooltip.Constructor.Defaults.container + * + $.tooltip.Constructor.Defaults.animation + * + $.tooltip.Constructor.Defaults.placement + * + $.tooltip.Constructor.Defaults.selector + * + $.tooltip.Constructor.Defaults.template + * + $.tooltip.Constructor.Defaults.trigger + * + $.tooltip.Constructor.Defaults.title + * + $.tooltip.Constructor.Defaults.delay + * + $.tooltip.Constructor.Defaults.html + * + $.tooltip.Constructor.Defaults.viewport + * + $.tooltip.Constructor.Defaults.viewport.selector + * + $.tooltip.Constructor.Defaults.viewport.padding + * + $.tooltip.Constructor.prototype.enable + * + $.tooltip.Constructor.prototype.disable + * + $.tooltip.Constructor.prototype.destroy + * + $.tooltip.Constructor.prototype.toggleEnabled + * + $.tooltip.Constructor.prototype.toggle + * + $.tooltip.Constructor.prototype.show + * + $.tooltip.Constructor.prototype.hide + * + * ======================================================================== + */ +'use strict'; -+function ($) { - 'use strict'; - // TOOLTIP PUBLIC CLASS DEFINITION - // =============================== +/** + * Our tooltip class. + * @param {Element!} element + * @param {Object=} opt_config + * @constructor + */ +var Tooltip = function (element, opt_config) { - var Tooltip = function (element, options) { - this.type = - this.options = - this.enabled = - this.timeout = - this.hoverState = - this.$element = null + /** @private {boolean} */ + this._isEnabled = true - this.init('tooltip', element, options) - } + /** @private {number} */ + this._timeout = 0 - Tooltip.VERSION = '3.3.2' - - 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 - } - } + /** @private {string} */ + this._hoverState = '' - 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) + /** @protected {Element} */ + this.element = element - var triggers = this.options.trigger.split(' ') + /** @protected {Object} */ + this.config = this._getConfig(opt_config) - for (var i = triggers.length; i--;) { - var trigger = triggers[i] + /** @protected {Element} */ + this.tip = null - 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' + /** @protected {Element} */ + this.arrow = null - 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)) - } - } + if (this.config['viewport']) { - this.options.selector ? - (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : - this.fixTitle() - } + /** @private {Element} */ + this._viewport = $(this.config['viewport']['selector'] || this.config['viewport'])[0] - Tooltip.prototype.getDefaults = function () { - return Tooltip.DEFAULTS } - Tooltip.prototype.getOptions = function (options) { - options = $.extend({}, this.getDefaults(), this.$element.data(), options) + this._setListeners() +} + + +/** + * @const + * @type {string} + */ +Tooltip['VERSION'] = '4.0.0' + + +/** + * @const + * @type {Object} + */ +Tooltip['Defaults'] = { + 'container' : false, + '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, + 'viewport': { + 'selector': 'body', + 'padding' : 0 + } +} + + +/** + * @const + * @enum {string} + * @protected + */ +Tooltip.Direction = { + TOP: 'top', + LEFT: 'left', + RIGHT: 'right', + BOTTOM: 'bottom' +} + + +/** + * @const + * @type {string} + * @private + */ +Tooltip._NAME = 'tooltip' + + +/** + * @const + * @type {string} + * @private + */ +Tooltip._DATA_KEY = 'bs.tooltip' + + +/** + * @const + * @type {number} + * @private + */ +Tooltip._TRANSITION_DURATION = 150 + + +/** + * @const + * @enum {string} + * @private + */ +Tooltip._HoverState = { + IN: 'in', + OUT: 'out' +} + + +/** + * @const + * @enum {string} + * @private + */ +Tooltip._Event = { + HIDE : 'hide.bs.tooltip', + HIDDEN : 'hidden.bs.tooltip', + SHOW : 'show.bs.tooltip', + SHOWN : 'shown.bs.tooltip' +} + + +/** + * @const + * @enum {string} + * @private + */ +Tooltip._ClassName = { + FADE : 'fade', + IN : 'in' +} + + +/** + * @const + * @enum {string} + * @private + */ +Tooltip._Selector = { + TOOLTIP : '.tooltip', + TOOLTIP_INNER : '.tooltip-inner', + TOOLTIP_ARROW : '.tooltip-arrow' +} + + +/** + * @const + * @type {Function} + * @private + */ +Tooltip._JQUERY_NO_CONFLICT = $.fn[Tooltip._NAME] + + +/** + * @param {Object=} opt_config + * @this {jQuery} + * @return {jQuery} + * @private + */ +Tooltip._jQueryInterface = function (opt_config) { + return this.each(function () { + var data = $(this).data(Tooltip._DATA_KEY) + var config = typeof opt_config == 'object' ? opt_config : null + + if (!data && opt_config == 'destroy') { + return + } - if (options.delay && typeof options.delay == 'number') { - options.delay = { - show: options.delay, - hide: options.delay - } + if (!data) { + data = new Tooltip(this, config) + $(this).data(Tooltip._DATA_KEY, data) } - return options + if (typeof opt_config === 'string') { + data[opt_config]() + } + }) +} + + +/** + * Enable tooltip + */ +Tooltip.prototype['enable'] = function () { + this._isEnabled = true +} + + +/** + * Disable tooltip + */ +Tooltip.prototype['disable'] = function () { + this._isEnabled = false +} + + +/** + * Toggle the tooltip enable state + */ +Tooltip.prototype['toggleEnabled'] = function () { + this._isEnabled = !this._isEnabled +} + +/** + * Toggle the tooltips display + * @param {Event} opt_event + */ +Tooltip.prototype['toggle'] = function (opt_event) { + var context = this + var dataKey = this.getDataKey() + + if (opt_event) { + context = $(opt_event.currentTarget).data(dataKey) + + if (!context) { + context = new this.constructor(opt_event.currentTarget, this._getDelegateConfig()) + $(opt_event.currentTarget).data(dataKey, context) + } } - Tooltip.prototype.getDelegateOptions = function () { - var options = {} - var defaults = this.getDefaults() + $(context.getTipElement()).hasClass(Tooltip._ClassName.IN) ? + context._leave(null, context) : + context._enter(null, context) +} - this._options && $.each(this._options, function (key, value) { - if (defaults[key] != value) options[key] = value - }) - return options - } +/** + * Remove tooltip functionality + */ +Tooltip.prototype['destroy'] = function () { + clearTimeout(this._timeout) + this['hide'](function () { + $(this.element) + .off(Tooltip._Selector.TOOLTIP) + .removeData(this.getDataKey()) + }.bind(this)) +} + + +/** + * Show the tooltip + * todo (fat): ~fuck~ this is a big function - refactor out all of positioning logic + * and replace with external lib + */ +Tooltip.prototype['show'] = function () { + var showEvent = $.Event(this.getEventObject().SHOW) + + if (this.isWithContent() && this._isEnabled) { + $(this.element).trigger(showEvent) - Tooltip.prototype.enter = function (obj) { - var self = obj instanceof this.constructor ? - obj : $(obj.currentTarget).data('bs.' + this.type) + var isInTheDom = $.contains(this.element.ownerDocument.documentElement, this.element) - if (self && self.$tip && self.$tip.is(':visible')) { - self.hoverState = 'in' + if (showEvent.isDefaultPrevented() || !isInTheDom) { return } - if (!self) { - self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) - $(obj.currentTarget).data('bs.' + this.type, self) + var tip = this.getTipElement() + var tipId = Bootstrap.getUID(this.getName()) + + tip.setAttribute('id', tipId) + this.element.setAttribute('aria-describedby', tipId) + + this.setContent() + + if (this.config['animation']) { + $(tip).addClass(Tooltip._ClassName.FADE) } - clearTimeout(self.timeout) + var placement = typeof this.config['placement'] == 'function' ? + this.config['placement'].call(this, tip, this.element) : + this.config['placement'] - self.hoverState = 'in' + var autoToken = /\s?auto?\s?/i + var isWithAutoPlacement = autoToken.test(placement) - if (!self.options.delay || !self.options.delay.show) return self.show() + if (isWithAutoPlacement) { + placement = placement.replace(autoToken, '') || Tooltip.Direction.TOP + } - self.timeout = setTimeout(function () { - if (self.hoverState == 'in') self.show() - }, self.options.delay.show) - } + if (tip.parentNode && tip.parentNode.nodeType == Node.ELEMENT_NODE) { + tip.parentNode.removeChild(tip) + } + + tip.style.top = 0 + tip.style.left = 0 + tip.style.display = 'block' + + $(tip).addClass(Tooltip._NAME + '-' + placement) - Tooltip.prototype.leave = function (obj) { - var self = obj instanceof this.constructor ? - obj : $(obj.currentTarget).data('bs.' + this.type) + $(tip).data(this.getDataKey(), this) - if (!self) { - self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) - $(obj.currentTarget).data('bs.' + this.type, self) + if (this.config['container']) { + $(this.config['container'])[0].appendChild(tip) + } else { + this.element.parentNode.insertBefore(tip, this.element.nextSibling) } - clearTimeout(self.timeout) + var position = this._getPosition() + var actualWidth = tip.offsetWidth + var actualHeight = tip.offsetHeight + + var calculatedPlacement = this._getCalculatedAutoPlacement(isWithAutoPlacement, placement, position, actualWidth, actualHeight) + var calculatedOffset = this._getCalculatedOffset(calculatedPlacement, position, actualWidth, actualHeight) - self.hoverState = 'out' + this._applyCalculatedPlacement(calculatedOffset, calculatedPlacement) - if (!self.options.delay || !self.options.delay.hide) return self.hide() + var complete = function () { + var prevHoverState = this.hoverState + $(this.element).trigger(this.getEventObject().SHOWN) + this.hoverState = null - self.timeout = setTimeout(function () { - if (self.hoverState == 'out') self.hide() - }, self.options.delay.hide) + if (prevHoverState == 'out') this._leave(null, this) + }.bind(this) + + Bootstrap.transition && $(this._tip).hasClass(Tooltip._ClassName.FADE) ? + $(this._tip) + .one(Bootstrap.TRANSITION_END, complete) + .emulateTransitionEnd(Tooltip._TRANSITION_DURATION) : + complete() } +} - Tooltip.prototype.show = function () { - var e = $.Event('show.bs.' + this.type) - if (this.hasContent() && this.enabled) { - this.$element.trigger(e) +/** + * Hide the tooltip breh + */ +Tooltip.prototype['hide'] = function (callback) { + var tip = this.getTipElement() + var hideEvent = $.Event(this.getEventObject().HIDE) - var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) - if (e.isDefaultPrevented() || !inDom) return - var that = this + var complete = function () { + if (this._hoverState != Tooltip._HoverState.IN) { + tip.parentNode.removeChild(tip) + } - var $tip = this.tip() + this.element.removeAttribute('aria-describedby') + $(this.element).trigger(this.getEventObject().HIDDEN) + + if (callback) { + callback() + } + }.bind(this) - var tipId = this.getUID(this.type) + $(this.element).trigger(hideEvent) - this.setContent() - $tip.attr('id', tipId) - this.$element.attr('aria-describedby', tipId) + if (hideEvent.isDefaultPrevented()) return - if (this.options.animation) $tip.addClass('fade') + $(tip).removeClass(Tooltip._ClassName.IN) - var placement = typeof this.options.placement == 'function' ? - this.options.placement.call(this, $tip[0], this.$element[0]) : - this.options.placement + if (Bootstrap.transition && $(this._tip).hasClass(Tooltip._ClassName.FADE)) { + $(tip) + .one(Bootstrap.TRANSITION_END, complete) + .emulateTransitionEnd(Tooltip._TRANSITION_DURATION) + } else { + complete() + } - var autoToken = /\s?auto?\s?/i - var autoPlace = autoToken.test(placement) - if (autoPlace) placement = placement.replace(autoToken, '') || 'top' + this._hoverState = '' +} - $tip - .detach() - .css({ top: 0, left: 0, display: 'block' }) - .addClass(this.type + '-' + placement) - .data('bs.' + this.type, this) - this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) +/** + * @return {string} + */ +Tooltip.prototype['getHoverState'] = function (callback) { + return this._hoverState +} - var pos = this.getPosition() - var actualWidth = $tip[0].offsetWidth - var actualHeight = $tip[0].offsetHeight - if (autoPlace) { - var origPlacement = placement - var $container = this.options.container ? $(this.options.container) : this.$element.parent() - var containerDim = this.getPosition($container) +/** + * @return {string} + * @protected + */ +Tooltip.prototype.getName = function () { + return Tooltip._NAME +} - 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(this.type + '-' + origPlacement) - .addClass(this.type + '-' + placement) - } +/** + * @return {string} + * @protected + */ +Tooltip.prototype.getDataKey = function () { + return Tooltip._DATA_KEY +} - var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) - this.applyPlacement(calculatedOffset, placement) +/** + * @return {Object} + * @protected + */ +Tooltip.prototype.getEventObject = function () { + return Tooltip._Event +} - var complete = function () { - var prevHoverState = that.hoverState - that.$element.trigger('shown.bs.' + that.type) - that.hoverState = null - if (prevHoverState == 'out') that.leave(that) - } +/** + * @return {string} + * @protected + */ +Tooltip.prototype.getTitle = function () { + var title = this.element.getAttribute('data-original-title') - $.support.transition && this.$tip.hasClass('fade') ? - $tip - .one('bsTransitionEnd', complete) - .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : - complete() - } + if (!title) { + title = typeof this.config['title'] === 'function' ? + this.config['title'].call(this.element) : + this.config['title'] } - 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 IE9 - 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) - - $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 - } + return /** @type {string} */ (title) +} - var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) - if (delta.left) offset.left += delta.left - else offset.top += delta.top +/** + * @return {Element} + * @protected + */ +Tooltip.prototype.getTipElement = function () { + return (this._tip = this._tip || $(this.config['template'])[0]) +} - 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) - } +/** + * @return {Element} + * @protected + */ +Tooltip.prototype.getArrowElement = function () { + return (this.arrow = this.arrow || $(this.getTipElement()).find(Tooltip._Selector.TOOLTIP_ARROW)[0]) +} - Tooltip.prototype.replaceArrow = function (delta, dimension, isHorizontal) { - this.arrow() - .css(isHorizontal ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') - .css(isHorizontal ? 'top' : 'left', '') - } - Tooltip.prototype.setContent = function () { - var $tip = this.tip() - var title = this.getTitle() +/** + * @return {boolean} + * @protected + */ +Tooltip.prototype.isWithContent = function () { + return !!this.getTitle() +} - $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) - $tip.removeClass('fade in tooltip-top tooltip-bottom tooltip-left tooltip-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() - } +/** + * @protected + */ +Tooltip.prototype.setContent = function () { + var tip = this.getTipElement() + var title = this.getTitle() - this.$element.trigger(e) + $(tip).find(Tooltip._Selector.TOOLTIP_INNER)[0][this.config['html'] ? 'innerHTML' : 'innerText'] = title - if (e.isDefaultPrevented()) return + $(tip) + .removeClass(Tooltip._ClassName.FADE) + .removeClass(Tooltip._ClassName.IN) - $tip.removeClass('in') + for (var direction in Tooltip.Direction) { + $(tip).removeClass(Tooltip._NAME + '-' + direction) + } +} - $.support.transition && this.$tip.hasClass('fade') ? - $tip - .one('bsTransitionEnd', complete) - .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : - complete() - this.hoverState = null +/** + * @private + */ +Tooltip.prototype._setListeners = function () { + var triggers = this.config['trigger'].split(' ') - return this - } + triggers.forEach(function (trigger) { + if (trigger == 'click') { + $(this.element).on('click.bs.tooltip', this.config['selector'], this['toggle'].bind(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', '') + } else if (trigger != 'manual') { + var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' + var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' + + $(this.element) + .on(eventIn + '.bs.tooltip', this.config['selector'], this._enter.bind(this)) + .on(eventOut + '.bs.tooltip', this.config['selector'], this._leave.bind(this)) } - } + }.bind(this)) - Tooltip.prototype.hasContent = function () { - return this.getTitle() + if (this.config['selector']) { + this.config = $.extend({}, this.config, { 'trigger': 'manual', 'selector': '' }) + } else { + this._fixTitle() } +} - Tooltip.prototype.getPosition = function ($element) { - $element = $element || this.$element - var el = $element[0] - var isBody = el.tagName == 'BODY' +/** + * @param {Object=} opt_config + * @return {Object} + * @private + */ +Tooltip.prototype._getConfig = function (opt_config) { + var config = $.extend({}, this.constructor['Defaults'], $(this.element).data(), opt_config) - 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 }) + if (config['delay'] && typeof config['delay'] == 'number') { + config['delay'] = { + 'show': config['delay'], + 'hide': config['delay'] } - 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 - - 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 } + return config +} - } - 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 - } +/** + * @return {Object} + * @private + */ +Tooltip.prototype._getDelegateConfig = function () { + var config = {} + var defaults = this.constructor['Defaults'] + + if (this.config) { + for (var key in this.config) { + var value = this.config[key] + if (defaults[key] != value) config[key] = value } + } + + return config +} + + + +/** + * @param {boolean} isWithAutoPlacement + * @param {string} placement + * @param {Object} position + * @param {number} actualWidth + * @param {number} actualHeight + * @return {string} + * @private + */ +Tooltip.prototype._getCalculatedAutoPlacement = function (isWithAutoPlacement, placement, position, actualWidth, actualHeight) { + if (isWithAutoPlacement) { + var originalPlacement = placement + var container = this.config['container'] ? $(this.config['container'])[0] : this.element.parentNode + var containerDim = this._getPosition(/** @type {Element} */ (container)) + + placement = placement == Tooltip.Direction.BOTTOM && position.bottom + actualHeight > containerDim.bottom ? Tooltip.Direction.TOP : + placement == Tooltip.Direction.TOP && position.top - actualHeight < containerDim.top ? Tooltip.Direction.BOTTOM : + placement == Tooltip.Direction.RIGHT && position.right + actualWidth > containerDim.width ? Tooltip.Direction.LEFT : + placement == Tooltip.Direction.LEFT && position.left - actualWidth < containerDim.left ? Tooltip.Direction.RIGHT : + placement + + $(this._tip) + .removeClass(Tooltip._NAME + '-' + originalPlacement) + .addClass(Tooltip._NAME + '-' + placement) + } + return placement +} + + +/** + * @param {string} placement + * @param {Object} position + * @param {number} actualWidth + * @param {number} actualHeight + * @return {{left: number, top: number}} + * @private + */ +Tooltip.prototype._getCalculatedOffset = function (placement, position, actualWidth, actualHeight) { + return placement == Tooltip.Direction.BOTTOM ? { top: position.top + position.height, left: position.left + position.width / 2 - actualWidth / 2 } : + placement == Tooltip.Direction.TOP ? { top: position.top - actualHeight, left: position.left + position.width / 2 - actualWidth / 2 } : + placement == Tooltip.Direction.LEFT ? { top: position.top + position.height / 2 - actualHeight / 2, left: position.left - actualWidth } : + /* placement == Tooltip.Direction.RIGHT */ { top: position.top + position.height / 2 - actualHeight / 2, left: position.left + position.width } +} + + +/** + * @param {string} placement + * @param {Object} position + * @param {number} actualWidth + * @param {number} actualHeight + * @return {Object} + * @private + */ +Tooltip.prototype._getViewportAdjustedDelta = function (placement, position, actualWidth, actualHeight) { + var delta = { top: 0, left: 0 } + + if (!this._viewport) { return delta } - Tooltip.prototype.getTitle = function () { - var title - var $e = this.$element - var o = this.options + var viewportPadding = this.config['viewport'] && this.config['viewport']['padding'] || 0 + var viewportDimensions = this._getPosition(this._viewport) - title = $e.attr('data-original-title') - || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) + if (placement === Tooltip.Direction.RIGHT || placement === Tooltip.Direction.LEFT) { + var topEdgeOffset = position.top - viewportPadding - viewportDimensions.scroll + var bottomEdgeOffset = position.top + viewportPadding - viewportDimensions.scroll + actualHeight - return title + 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 = position.left - viewportPadding + var rightEdgeOffset = position.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 + } } - Tooltip.prototype.getUID = function (prefix) { - do prefix += ~~(Math.random() * 1000000) - while (document.getElementById(prefix)) - return prefix + return delta +} + + +/** + * @param {Element=} opt_element + * @return {Object} + * @private + */ +Tooltip.prototype._getPosition = function (opt_element) { + var element = opt_element || this.element + var isBody = element.tagName == 'BODY' + var rect = element.getBoundingClientRect() + var offset = isBody ? { top: 0, left: 0 } : $(element).offset() + var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : this.element.scrollTop } + var outerDims = isBody ? { width: window.innerWidth, height: window.innerHeight } : null + + return $.extend({}, rect, scroll, outerDims, offset) +} + + +/** + * @param {{left: number, top: number}} offset + * @param {string} placement + * @private + */ +Tooltip.prototype._applyCalculatedPlacement = function (offset, placement) { + var tip = this.getTipElement() + var width = tip.offsetWidth + var height = tip.offsetHeight + + // manually read margins because getBoundingClientRect includes difference + var marginTop = parseInt(tip.style.marginTop, 10) + var marginLeft = parseInt(tip.style.marginLeft, 10) + + // we must check for NaN for ie 8/9 + if (isNaN(marginTop)) { + marginTop = 0 + } + if (isNaN(marginLeft)) { + marginLeft = 0 } - Tooltip.prototype.tip = function () { - return (this.$tip = this.$tip || $(this.options.template)) + 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, $.extend({ + using: function (props) { + tip.style.top = Math.round(props.top) + 'px' + tip.style.left = Math.round(props.left) + 'px' + } + }, offset), 0) + + $(tip).addClass(Tooltip._ClassName.IN) + + // check to see if placing tip in new offset caused the tip to resize itself + var actualWidth = tip.offsetWidth + var actualHeight = tip.offsetHeight + + if (placement == Tooltip.Direction.TOP && actualHeight != height) { + offset.top = offset.top + height - actualHeight } - Tooltip.prototype.arrow = function () { - return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) + var delta = this._getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) + + if (delta.left) { + offset.left += delta.left + } else { + offset.top += delta.top } - Tooltip.prototype.enable = function () { - this.enabled = true + var isVertical = placement === Tooltip.Direction.TOP || placement === Tooltip.Direction.BOTTOM + 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[arrowOffsetPosition], isVertical) +} + + +/** + * @param {number} delta + * @param {number} dimension + * @param {boolean} isHorizontal + * @private + */ +Tooltip.prototype._replaceArrow = function (delta, dimension, isHorizontal) { + var arrow = this.getArrowElement() + + arrow.style[isHorizontal ? 'left' : 'top'] = 50 * (1 - delta / dimension) + '%' + arrow.style[isHorizontal ? 'top' : 'left'] = '' +} + + + +/** + * @private + */ +Tooltip.prototype._fixTitle = function () { + if (this.element.getAttribute('title') || typeof this.element.getAttribute('data-original-title') != 'string') { + this.element.setAttribute('data-original-title', this.element.getAttribute('title') || '') + this.element.setAttribute('title', '') } +} + - Tooltip.prototype.disable = function () { - this.enabled = false +/** + * @param {Event=} opt_event + * @param {Object=} opt_context + * @private + */ +Tooltip.prototype._enter = function (opt_event, opt_context) { + var dataKey = this.getDataKey() + var context = opt_context || $(opt_event.currentTarget).data(dataKey) + + if (context && context._tip && context._tip.offsetWidth) { + context._hoverState = Tooltip._HoverState.IN + return } - Tooltip.prototype.toggleEnabled = function () { - this.enabled = !this.enabled + if (!context) { + context = new this.constructor(opt_event.currentTarget, this._getDelegateConfig()) + $(opt_event.currentTarget).data(dataKey, context) } - 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) - } - } + clearTimeout(context._timeout) - self.tip().hasClass('in') ? self.leave(self) : self.enter(self) - } + context._hoverState = Tooltip._HoverState.IN - Tooltip.prototype.destroy = function () { - var that = this - clearTimeout(this.timeout) - this.hide(function () { - that.$element.off('.' + that.type).removeData('bs.' + that.type) - }) + if (!context.config['delay'] || !context.config['delay']['show']) { + context['show']() + return } + context._timeout = setTimeout(function () { + if (context._hoverState == Tooltip._HoverState.IN) { + context['show']() + } + }, context.config['delay']['show']) +} + + +/** + * @param {Event=} opt_event + * @param {Object=} opt_context + * @private + */ +Tooltip.prototype._leave = function (opt_event, opt_context) { + var dataKey = this.getDataKey() + var context = opt_context || $(opt_event.currentTarget).data(dataKey) + + if (!context) { + context = new this.constructor(opt_event.currentTarget, this._getDelegateConfig()) + $(opt_event.currentTarget).data(dataKey, context) + } - // TOOLTIP PLUGIN DEFINITION - // ========================= + clearTimeout(context._timeout) - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.tooltip') - var options = typeof option == 'object' && option + context._hoverState = Tooltip._HoverState.OUT - if (!data && option == 'destroy') return - if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) - if (typeof option == 'string') data[option]() - }) + if (!context.config['delay'] || !context.config['delay']['hide']) { + context['hide']() + return } - var old = $.fn.tooltip + context._timeout = setTimeout(function () { + if (context._hoverState == Tooltip._HoverState.OUT) { + context['hide']() + } + }, context.config['delay']['hide']) +} - $.fn.tooltip = Plugin - $.fn.tooltip.Constructor = Tooltip - // TOOLTIP NO CONFLICT - // =================== +/** + * ------------------------------------------------------------------------ + * jQuery Interface + noConflict implementaiton + * ------------------------------------------------------------------------ + */ + +/** + * @const + * @type {Function} + */ +$.fn[Tooltip._NAME] = Tooltip._jQueryInterface + + +/** + * @const + * @type {Function} + */ +$.fn[Tooltip._NAME]['Constructor'] = Tooltip - $.fn.tooltip.noConflict = function () { - $.fn.tooltip = old - return this - } -}(jQuery); +/** + * @const + * @type {Function} + */ +$.fn[Tooltip._NAME]['noConflict'] = function () { + $.fn[Tooltip._NAME] = Tooltip._JQUERY_NO_CONFLICT + return this +} |
