813 lines
28 KiB
JavaScript
813 lines
28 KiB
JavaScript
/**
|
|
* angular-strap
|
|
* @version v2.2.1 - 2015-03-10
|
|
* @link http://mgcrea.github.io/angular-strap
|
|
* @author Olivier Louvignes (olivier@mg-crea.com)
|
|
* @license MIT License, http://www.opensource.org/licenses/MIT
|
|
*/
|
|
'use strict';
|
|
|
|
angular.module('mgcrea.ngStrap.tooltip', ['mgcrea.ngStrap.helpers.dimensions'])
|
|
|
|
.provider('$tooltip', function() {
|
|
|
|
var defaults = this.defaults = {
|
|
animation: 'am-fade',
|
|
customClass: '',
|
|
prefixClass: 'tooltip',
|
|
prefixEvent: 'tooltip',
|
|
container: false,
|
|
target: false,
|
|
placement: 'top',
|
|
template: 'tooltip/tooltip.tpl.html',
|
|
contentTemplate: false,
|
|
trigger: 'hover focus',
|
|
keyboard: false,
|
|
html: false,
|
|
show: false,
|
|
title: '',
|
|
type: '',
|
|
delay: 0,
|
|
autoClose: false,
|
|
bsEnabled: true,
|
|
viewport: {
|
|
selector: 'body',
|
|
padding: 0
|
|
}
|
|
};
|
|
|
|
this.$get = ["$window", "$rootScope", "$compile", "$q", "$templateCache", "$http", "$animate", "$sce", "dimensions", "$$rAF", "$timeout", function($window, $rootScope, $compile, $q, $templateCache, $http, $animate, $sce, dimensions, $$rAF, $timeout) {
|
|
|
|
var trim = String.prototype.trim;
|
|
var isTouch = 'createTouch' in $window.document;
|
|
var htmlReplaceRegExp = /ng-bind="/ig;
|
|
var $body = angular.element($window.document);
|
|
|
|
function TooltipFactory(element, config) {
|
|
|
|
var $tooltip = {};
|
|
|
|
// Common vars
|
|
var nodeName = element[0].nodeName.toLowerCase();
|
|
var options = $tooltip.$options = angular.extend({}, defaults, config);
|
|
$tooltip.$promise = fetchTemplate(options.template);
|
|
var scope = $tooltip.$scope = options.scope && options.scope.$new() || $rootScope.$new();
|
|
if(options.delay && angular.isString(options.delay)) {
|
|
var split = options.delay.split(',').map(parseFloat);
|
|
options.delay = split.length > 1 ? {show: split[0], hide: split[1]} : split[0];
|
|
}
|
|
|
|
// store $id to identify the triggering element in events
|
|
// give priority to options.id, otherwise, try to use
|
|
// element id if defined
|
|
$tooltip.$id = options.id || element.attr('id') || '';
|
|
|
|
// Support scope as string options
|
|
if(options.title) {
|
|
scope.title = $sce.trustAsHtml(options.title);
|
|
}
|
|
|
|
// Provide scope helpers
|
|
scope.$setEnabled = function(isEnabled) {
|
|
scope.$$postDigest(function() {
|
|
$tooltip.setEnabled(isEnabled);
|
|
});
|
|
};
|
|
scope.$hide = function() {
|
|
scope.$$postDigest(function() {
|
|
$tooltip.hide();
|
|
});
|
|
};
|
|
scope.$show = function() {
|
|
scope.$$postDigest(function() {
|
|
$tooltip.show();
|
|
});
|
|
};
|
|
scope.$toggle = function() {
|
|
scope.$$postDigest(function() {
|
|
$tooltip.toggle();
|
|
});
|
|
};
|
|
// Publish isShown as a protected var on scope
|
|
$tooltip.$isShown = scope.$isShown = false;
|
|
|
|
// Private vars
|
|
var timeout, hoverState;
|
|
|
|
// Support contentTemplate option
|
|
if(options.contentTemplate) {
|
|
$tooltip.$promise = $tooltip.$promise.then(function(template) {
|
|
var templateEl = angular.element(template);
|
|
return fetchTemplate(options.contentTemplate)
|
|
.then(function(contentTemplate) {
|
|
var contentEl = findElement('[ng-bind="content"]', templateEl[0]);
|
|
if(!contentEl.length) contentEl = findElement('[ng-bind="title"]', templateEl[0]);
|
|
contentEl.removeAttr('ng-bind').html(contentTemplate);
|
|
return templateEl[0].outerHTML;
|
|
});
|
|
});
|
|
}
|
|
|
|
// Fetch, compile then initialize tooltip
|
|
var tipLinker, tipElement, tipTemplate, tipContainer, tipScope;
|
|
$tooltip.$promise.then(function(template) {
|
|
if(angular.isObject(template)) template = template.data;
|
|
if(options.html) template = template.replace(htmlReplaceRegExp, 'ng-bind-html="');
|
|
template = trim.apply(template);
|
|
tipTemplate = template;
|
|
tipLinker = $compile(template);
|
|
$tooltip.init();
|
|
});
|
|
|
|
$tooltip.init = function() {
|
|
|
|
// Options: delay
|
|
if (options.delay && angular.isNumber(options.delay)) {
|
|
options.delay = {
|
|
show: options.delay,
|
|
hide: options.delay
|
|
};
|
|
}
|
|
|
|
// Replace trigger on touch devices ?
|
|
// if(isTouch && options.trigger === defaults.trigger) {
|
|
// options.trigger.replace(/hover/g, 'click');
|
|
// }
|
|
|
|
// Options : container
|
|
if(options.container === 'self') {
|
|
tipContainer = element;
|
|
} else if(angular.isElement(options.container)) {
|
|
tipContainer = options.container;
|
|
} else if(options.container) {
|
|
tipContainer = findElement(options.container);
|
|
}
|
|
|
|
// Options: trigger
|
|
bindTriggerEvents();
|
|
|
|
// Options: target
|
|
if(options.target) {
|
|
options.target = angular.isElement(options.target) ? options.target : findElement(options.target);
|
|
}
|
|
|
|
// Options: show
|
|
if(options.show) {
|
|
scope.$$postDigest(function() {
|
|
options.trigger === 'focus' ? element[0].focus() : $tooltip.show();
|
|
});
|
|
}
|
|
|
|
};
|
|
|
|
$tooltip.destroy = function() {
|
|
|
|
// Unbind events
|
|
unbindTriggerEvents();
|
|
|
|
// Remove element
|
|
destroyTipElement();
|
|
|
|
// Destroy scope
|
|
scope.$destroy();
|
|
|
|
};
|
|
|
|
$tooltip.enter = function() {
|
|
|
|
clearTimeout(timeout);
|
|
hoverState = 'in';
|
|
if (!options.delay || !options.delay.show) {
|
|
return $tooltip.show();
|
|
}
|
|
|
|
timeout = setTimeout(function() {
|
|
if (hoverState ==='in') $tooltip.show();
|
|
}, options.delay.show);
|
|
|
|
};
|
|
|
|
$tooltip.show = function() {
|
|
if (!options.bsEnabled || $tooltip.$isShown) return;
|
|
|
|
scope.$emit(options.prefixEvent + '.show.before', $tooltip);
|
|
var parent, after;
|
|
if (options.container) {
|
|
parent = tipContainer;
|
|
if (tipContainer[0].lastChild) {
|
|
after = angular.element(tipContainer[0].lastChild);
|
|
} else {
|
|
after = null;
|
|
}
|
|
} else {
|
|
parent = null;
|
|
after = element;
|
|
}
|
|
|
|
|
|
// Hide any existing tipElement
|
|
if(tipElement) destroyTipElement();
|
|
// Fetch a cloned element linked from template
|
|
tipScope = $tooltip.$scope.$new();
|
|
tipElement = $tooltip.$element = tipLinker(tipScope, function(clonedElement, scope) {});
|
|
|
|
// Set the initial positioning. Make the tooltip invisible
|
|
// so IE doesn't try to focus on it off screen.
|
|
tipElement.css({top: '-9999px', left: '-9999px', display: 'block', visibility: 'hidden'});
|
|
|
|
// Options: animation
|
|
if(options.animation) tipElement.addClass(options.animation);
|
|
// Options: type
|
|
if(options.type) tipElement.addClass(options.prefixClass + '-' + options.type);
|
|
// Options: custom classes
|
|
if(options.customClass) tipElement.addClass(options.customClass);
|
|
|
|
// Append the element, without any animations. If we append
|
|
// using $animate.enter, some of the animations cause the placement
|
|
// to be off due to the transforms.
|
|
after ? after.after(tipElement) : parent.prepend(tipElement);
|
|
|
|
$tooltip.$isShown = scope.$isShown = true;
|
|
safeDigest(scope);
|
|
|
|
// Now, apply placement
|
|
$tooltip.$applyPlacement();
|
|
|
|
// Once placed, animate it.
|
|
// Support v1.3+ $animate
|
|
// https://github.com/angular/angular.js/commit/bf0f5502b1bbfddc5cdd2f138efd9188b8c652a9
|
|
var promise = $animate.enter(tipElement, parent, after, enterAnimateCallback);
|
|
if(promise && promise.then) promise.then(enterAnimateCallback);
|
|
safeDigest(scope);
|
|
|
|
$$rAF(function () {
|
|
// Once the tooltip is placed and the animation starts, make the tooltip visible
|
|
if(tipElement) tipElement.css({visibility: 'visible'});
|
|
});
|
|
|
|
// Bind events
|
|
if(options.keyboard) {
|
|
if(options.trigger !== 'focus') {
|
|
$tooltip.focus();
|
|
}
|
|
bindKeyboardEvents();
|
|
}
|
|
|
|
if(options.autoClose) {
|
|
bindAutoCloseEvents();
|
|
}
|
|
|
|
};
|
|
|
|
function enterAnimateCallback() {
|
|
scope.$emit(options.prefixEvent + '.show', $tooltip);
|
|
}
|
|
|
|
$tooltip.leave = function() {
|
|
|
|
clearTimeout(timeout);
|
|
hoverState = 'out';
|
|
if (!options.delay || !options.delay.hide) {
|
|
return $tooltip.hide();
|
|
}
|
|
timeout = setTimeout(function () {
|
|
if (hoverState === 'out') {
|
|
$tooltip.hide();
|
|
}
|
|
}, options.delay.hide);
|
|
|
|
};
|
|
|
|
var _blur;
|
|
var _tipToHide;
|
|
$tooltip.hide = function(blur) {
|
|
|
|
if(!$tooltip.$isShown) return;
|
|
scope.$emit(options.prefixEvent + '.hide.before', $tooltip);
|
|
|
|
// store blur value for leaveAnimateCallback to use
|
|
_blur = blur;
|
|
|
|
// store current tipElement reference to use
|
|
// in leaveAnimateCallback
|
|
_tipToHide = tipElement;
|
|
|
|
// Support v1.3+ $animate
|
|
// https://github.com/angular/angular.js/commit/bf0f5502b1bbfddc5cdd2f138efd9188b8c652a9
|
|
var promise = $animate.leave(tipElement, leaveAnimateCallback);
|
|
if(promise && promise.then) promise.then(leaveAnimateCallback);
|
|
|
|
$tooltip.$isShown = scope.$isShown = false;
|
|
safeDigest(scope);
|
|
|
|
// Unbind events
|
|
if(options.keyboard && tipElement !== null) {
|
|
unbindKeyboardEvents();
|
|
}
|
|
|
|
if(options.autoClose && tipElement !== null) {
|
|
unbindAutoCloseEvents();
|
|
}
|
|
};
|
|
|
|
function leaveAnimateCallback() {
|
|
scope.$emit(options.prefixEvent + '.hide', $tooltip);
|
|
|
|
// check if current tipElement still references
|
|
// the same element when hide was called
|
|
if (tipElement === _tipToHide) {
|
|
// Allow to blur the input when hidden, like when pressing enter key
|
|
if(_blur && options.trigger === 'focus') {
|
|
return element[0].blur();
|
|
}
|
|
|
|
// clean up child scopes
|
|
destroyTipElement();
|
|
}
|
|
}
|
|
|
|
$tooltip.toggle = function() {
|
|
$tooltip.$isShown ? $tooltip.leave() : $tooltip.enter();
|
|
};
|
|
|
|
$tooltip.focus = function() {
|
|
tipElement[0].focus();
|
|
};
|
|
|
|
$tooltip.setEnabled = function(isEnabled) {
|
|
options.bsEnabled = isEnabled;
|
|
};
|
|
|
|
$tooltip.setViewport = function(viewport) {
|
|
options.viewport = viewport;
|
|
};
|
|
|
|
// Protected methods
|
|
|
|
$tooltip.$applyPlacement = function() {
|
|
if(!tipElement) return;
|
|
|
|
// Determine if we're doing an auto or normal placement
|
|
var placement = options.placement,
|
|
autoToken = /\s?auto?\s?/i,
|
|
autoPlace = autoToken.test(placement);
|
|
|
|
if (autoPlace) {
|
|
placement = placement.replace(autoToken, '') || defaults.placement;
|
|
}
|
|
|
|
// Need to add the position class before we get
|
|
// the offsets
|
|
tipElement.addClass(options.placement);
|
|
|
|
// Get the position of the target element
|
|
// and the height and width of the tooltip so we can center it.
|
|
var elementPosition = getPosition(),
|
|
tipWidth = tipElement.prop('offsetWidth'),
|
|
tipHeight = tipElement.prop('offsetHeight');
|
|
|
|
// If we're auto placing, we need to check the positioning
|
|
if (autoPlace) {
|
|
var originalPlacement = placement;
|
|
var container = options.container ? findElement(options.container) : element.parent();
|
|
var containerPosition = getPosition(container);
|
|
|
|
// Determine if the vertical placement
|
|
if (originalPlacement.indexOf('bottom') >= 0 && elementPosition.bottom + tipHeight > containerPosition.bottom) {
|
|
placement = originalPlacement.replace('bottom', 'top');
|
|
} else if (originalPlacement.indexOf('top') >= 0 && elementPosition.top - tipHeight < containerPosition.top) {
|
|
placement = originalPlacement.replace('top', 'bottom');
|
|
}
|
|
|
|
// Determine the horizontal placement
|
|
// The exotic placements of left and right are opposite of the standard placements. Their arrows are put on the left/right
|
|
// and flow in the opposite direction of their placement.
|
|
if ((originalPlacement === 'right' || originalPlacement === 'bottom-left' || originalPlacement === 'top-left') &&
|
|
elementPosition.right + tipWidth > containerPosition.width) {
|
|
|
|
placement = originalPlacement === 'right' ? 'left' : placement.replace('left', 'right');
|
|
} else if ((originalPlacement === 'left' || originalPlacement === 'bottom-right' || originalPlacement === 'top-right') &&
|
|
elementPosition.left - tipWidth < containerPosition.left) {
|
|
|
|
placement = originalPlacement === 'left' ? 'right' : placement.replace('right', 'left');
|
|
}
|
|
|
|
tipElement.removeClass(originalPlacement).addClass(placement);
|
|
}
|
|
|
|
// Get the tooltip's top and left coordinates to center it with this directive.
|
|
var tipPosition = getCalculatedOffset(placement, elementPosition, tipWidth, tipHeight);
|
|
applyPlacement(tipPosition, placement);
|
|
};
|
|
|
|
$tooltip.$onKeyUp = function(evt) {
|
|
if (evt.which === 27 && $tooltip.$isShown) {
|
|
$tooltip.hide();
|
|
evt.stopPropagation();
|
|
}
|
|
};
|
|
|
|
$tooltip.$onFocusKeyUp = function(evt) {
|
|
if (evt.which === 27) {
|
|
element[0].blur();
|
|
evt.stopPropagation();
|
|
}
|
|
};
|
|
|
|
$tooltip.$onFocusElementMouseDown = function(evt) {
|
|
evt.preventDefault();
|
|
evt.stopPropagation();
|
|
// Some browsers do not auto-focus buttons (eg. Safari)
|
|
$tooltip.$isShown ? element[0].blur() : element[0].focus();
|
|
};
|
|
|
|
// bind/unbind events
|
|
function bindTriggerEvents() {
|
|
var triggers = options.trigger.split(' ');
|
|
angular.forEach(triggers, function(trigger) {
|
|
if(trigger === 'click') {
|
|
element.on('click', $tooltip.toggle);
|
|
} else if(trigger !== 'manual') {
|
|
element.on(trigger === 'hover' ? 'mouseenter' : 'focus', $tooltip.enter);
|
|
element.on(trigger === 'hover' ? 'mouseleave' : 'blur', $tooltip.leave);
|
|
nodeName === 'button' && trigger !== 'hover' && element.on(isTouch ? 'touchstart' : 'mousedown', $tooltip.$onFocusElementMouseDown);
|
|
}
|
|
});
|
|
}
|
|
|
|
function unbindTriggerEvents() {
|
|
var triggers = options.trigger.split(' ');
|
|
for (var i = triggers.length; i--;) {
|
|
var trigger = triggers[i];
|
|
if(trigger === 'click') {
|
|
element.off('click', $tooltip.toggle);
|
|
} else if(trigger !== 'manual') {
|
|
element.off(trigger === 'hover' ? 'mouseenter' : 'focus', $tooltip.enter);
|
|
element.off(trigger === 'hover' ? 'mouseleave' : 'blur', $tooltip.leave);
|
|
nodeName === 'button' && trigger !== 'hover' && element.off(isTouch ? 'touchstart' : 'mousedown', $tooltip.$onFocusElementMouseDown);
|
|
}
|
|
}
|
|
}
|
|
|
|
function bindKeyboardEvents() {
|
|
if(options.trigger !== 'focus') {
|
|
tipElement.on('keyup', $tooltip.$onKeyUp);
|
|
} else {
|
|
element.on('keyup', $tooltip.$onFocusKeyUp);
|
|
}
|
|
}
|
|
|
|
function unbindKeyboardEvents() {
|
|
if(options.trigger !== 'focus') {
|
|
tipElement.off('keyup', $tooltip.$onKeyUp);
|
|
} else {
|
|
element.off('keyup', $tooltip.$onFocusKeyUp);
|
|
}
|
|
}
|
|
|
|
var _autoCloseEventsBinded = false;
|
|
function bindAutoCloseEvents() {
|
|
// use timeout to hookup the events to prevent
|
|
// event bubbling from being processed imediately.
|
|
$timeout(function() {
|
|
// Stop propagation when clicking inside tooltip
|
|
tipElement.on('click', stopEventPropagation);
|
|
|
|
// Hide when clicking outside tooltip
|
|
$body.on('click', $tooltip.hide);
|
|
|
|
_autoCloseEventsBinded = true;
|
|
}, 0, false);
|
|
}
|
|
|
|
function unbindAutoCloseEvents() {
|
|
if (_autoCloseEventsBinded) {
|
|
tipElement.off('click', stopEventPropagation);
|
|
$body.off('click', $tooltip.hide);
|
|
_autoCloseEventsBinded = false;
|
|
}
|
|
}
|
|
|
|
function stopEventPropagation(event) {
|
|
event.stopPropagation();
|
|
}
|
|
|
|
// Private methods
|
|
|
|
function getPosition($element) {
|
|
$element = $element || (options.target || element);
|
|
|
|
var el = $element[0],
|
|
isBody = el.tagName === 'BODY';
|
|
|
|
var elRect = el.getBoundingClientRect();
|
|
var rect = {};
|
|
|
|
// IE8 has issues with angular.extend and using elRect directly.
|
|
// By coping the values of elRect into a new object, we can continue to use extend
|
|
for (var p in elRect) {
|
|
// DO NOT use hasOwnProperty when inspecting the return of getBoundingClientRect.
|
|
rect[p] = elRect[p];
|
|
}
|
|
|
|
if (rect.width === null) {
|
|
// width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093
|
|
rect = angular.extend({}, rect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top });
|
|
}
|
|
var elOffset = isBody ? { top: 0, left: 0 } : dimensions.offset(el),
|
|
scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.prop('scrollTop') || 0 },
|
|
outerDims = isBody ? { width: document.documentElement.clientWidth, height: $window.innerHeight } : null;
|
|
|
|
return angular.extend({}, rect, scroll, outerDims, elOffset);
|
|
}
|
|
|
|
function getCalculatedOffset(placement, position, actualWidth, actualHeight) {
|
|
var offset;
|
|
var split = placement.split('-');
|
|
|
|
switch (split[0]) {
|
|
case 'right':
|
|
offset = {
|
|
top: position.top + position.height / 2 - actualHeight / 2,
|
|
left: position.left + position.width
|
|
};
|
|
break;
|
|
case 'bottom':
|
|
offset = {
|
|
top: position.top + position.height,
|
|
left: position.left + position.width / 2 - actualWidth / 2
|
|
};
|
|
break;
|
|
case 'left':
|
|
offset = {
|
|
top: position.top + position.height / 2 - actualHeight / 2,
|
|
left: position.left - actualWidth
|
|
};
|
|
break;
|
|
default:
|
|
offset = {
|
|
top: position.top - actualHeight,
|
|
left: position.left + position.width / 2 - actualWidth / 2
|
|
};
|
|
break;
|
|
}
|
|
|
|
if(!split[1]) {
|
|
return offset;
|
|
}
|
|
|
|
// Add support for corners @todo css
|
|
if(split[0] === 'top' || split[0] === 'bottom') {
|
|
switch (split[1]) {
|
|
case 'left':
|
|
offset.left = position.left;
|
|
break;
|
|
case 'right':
|
|
offset.left = position.left + position.width - actualWidth;
|
|
}
|
|
} else if(split[0] === 'left' || split[0] === 'right') {
|
|
switch (split[1]) {
|
|
case 'top':
|
|
offset.top = position.top - actualHeight;
|
|
break;
|
|
case 'bottom':
|
|
offset.top = position.top + position.height;
|
|
}
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
function applyPlacement(offset, placement) {
|
|
var tip = tipElement[0],
|
|
width = tip.offsetWidth,
|
|
height = tip.offsetHeight;
|
|
|
|
// manually read margins because getBoundingClientRect includes difference
|
|
var marginTop = parseInt(dimensions.css(tip, 'margin-top'), 10),
|
|
marginLeft = parseInt(dimensions.css(tip, '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;
|
|
|
|
// dimensions setOffset doesn't round pixel values
|
|
// so we use setOffset directly with our own function
|
|
dimensions.setOffset(tip, angular.extend({
|
|
using: function (props) {
|
|
tipElement.css({
|
|
top: Math.round(props.top) + 'px',
|
|
left: Math.round(props.left) + 'px'
|
|
});
|
|
}
|
|
}, offset), 0);
|
|
|
|
// check to see if placing tip in new offset caused the tip to resize itself
|
|
var actualWidth = tip.offsetWidth,
|
|
actualHeight = tip.offsetHeight;
|
|
|
|
if (placement === 'top' && actualHeight !== height) {
|
|
offset.top = offset.top + height - actualHeight;
|
|
}
|
|
|
|
// If it's an exotic placement, exit now instead of
|
|
// applying a delta and changing the arrow
|
|
if (/top-left|top-right|bottom-left|bottom-right/.test(placement)) return;
|
|
|
|
var delta = getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight);
|
|
|
|
if (delta.left) {
|
|
offset.left += delta.left;
|
|
} else {
|
|
offset.top += delta.top;
|
|
}
|
|
|
|
dimensions.setOffset(tip, offset);
|
|
|
|
if (/top|right|bottom|left/.test(placement)) {
|
|
var isVertical = /top|bottom/.test(placement),
|
|
arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight,
|
|
arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight';
|
|
|
|
replaceArrow(arrowDelta, tip[arrowOffsetPosition], isVertical);
|
|
}
|
|
}
|
|
|
|
function getViewportAdjustedDelta(placement, position, actualWidth, actualHeight) {
|
|
var delta = { top: 0, left: 0 },
|
|
$viewport = options.viewport && findElement(options.viewport.selector || options.viewport);
|
|
|
|
if (!$viewport) {
|
|
return delta;
|
|
}
|
|
|
|
var viewportPadding = options.viewport && options.viewport.padding || 0,
|
|
viewportDimensions = getPosition($viewport);
|
|
|
|
if (/right|left/.test(placement)) {
|
|
var topEdgeOffset = position.top - viewportPadding - viewportDimensions.scroll,
|
|
bottomEdgeOffset = position.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 = position.left - viewportPadding,
|
|
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;
|
|
}
|
|
}
|
|
|
|
return delta;
|
|
}
|
|
|
|
function replaceArrow(delta, dimension, isHorizontal) {
|
|
var $arrow = findElement('.tooltip-arrow, .arrow', tipElement[0]);
|
|
|
|
$arrow.css(isHorizontal ? 'left' : 'top', 50 * (1 - delta / dimension) + '%')
|
|
.css(isHorizontal ? 'top' : 'left', '');
|
|
}
|
|
|
|
function destroyTipElement() {
|
|
// Cancel pending callbacks
|
|
clearTimeout(timeout);
|
|
|
|
if($tooltip.$isShown && tipElement !== null) {
|
|
if(options.autoClose) {
|
|
unbindAutoCloseEvents();
|
|
}
|
|
|
|
if(options.keyboard) {
|
|
unbindKeyboardEvents();
|
|
}
|
|
}
|
|
|
|
if(tipScope) {
|
|
tipScope.$destroy();
|
|
tipScope = null;
|
|
}
|
|
|
|
if(tipElement) {
|
|
tipElement.remove();
|
|
tipElement = $tooltip.$element = null;
|
|
}
|
|
}
|
|
|
|
return $tooltip;
|
|
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
function safeDigest(scope) {
|
|
scope.$$phase || (scope.$root && scope.$root.$$phase) || scope.$digest();
|
|
}
|
|
|
|
function findElement(query, element) {
|
|
return angular.element((element || document).querySelectorAll(query));
|
|
}
|
|
|
|
var fetchPromises = {};
|
|
function fetchTemplate(template) {
|
|
if(fetchPromises[template]) return fetchPromises[template];
|
|
return (fetchPromises[template] = $http.get(template, {cache: $templateCache}).then(function(res) {
|
|
return res.data;
|
|
}));
|
|
}
|
|
|
|
return TooltipFactory;
|
|
|
|
}];
|
|
|
|
})
|
|
|
|
.directive('bsTooltip', ["$window", "$location", "$sce", "$tooltip", "$$rAF", function($window, $location, $sce, $tooltip, $$rAF) {
|
|
|
|
return {
|
|
restrict: 'EAC',
|
|
scope: true,
|
|
link: function postLink(scope, element, attr, transclusion) {
|
|
|
|
// Directive options
|
|
var options = {scope: scope};
|
|
angular.forEach(['template', 'contentTemplate', 'placement', 'container', 'delay', 'trigger', 'keyboard', 'html', 'animation', 'backdropAnimation', 'type', 'customClass', 'id'], function(key) {
|
|
if(angular.isDefined(attr[key])) options[key] = attr[key];
|
|
});
|
|
|
|
// should not parse target attribute, only data-target
|
|
if(element.attr('data-target')) {
|
|
options.target = element.attr('data-target');
|
|
}
|
|
|
|
// overwrite inherited title value when no value specified
|
|
// fix for angular 1.3.1 531a8de72c439d8ddd064874bf364c00cedabb11
|
|
if (!scope.hasOwnProperty('title')){
|
|
scope.title = '';
|
|
}
|
|
|
|
// Observe scope attributes for change
|
|
attr.$observe('title', function(newValue) {
|
|
if (angular.isDefined(newValue) || !scope.hasOwnProperty('title')) {
|
|
var oldValue = scope.title;
|
|
scope.title = $sce.trustAsHtml(newValue);
|
|
angular.isDefined(oldValue) && $$rAF(function() {
|
|
tooltip && tooltip.$applyPlacement();
|
|
});
|
|
}
|
|
});
|
|
|
|
// Support scope as an object
|
|
attr.bsTooltip && scope.$watch(attr.bsTooltip, function(newValue, oldValue) {
|
|
if(angular.isObject(newValue)) {
|
|
angular.extend(scope, newValue);
|
|
} else {
|
|
scope.title = newValue;
|
|
}
|
|
angular.isDefined(oldValue) && $$rAF(function() {
|
|
tooltip && tooltip.$applyPlacement();
|
|
});
|
|
}, true);
|
|
|
|
// Visibility binding support
|
|
attr.bsShow && scope.$watch(attr.bsShow, function(newValue, oldValue) {
|
|
if(!tooltip || !angular.isDefined(newValue)) return;
|
|
if(angular.isString(newValue)) newValue = !!newValue.match(/true|,?(tooltip),?/i);
|
|
newValue === true ? tooltip.show() : tooltip.hide();
|
|
});
|
|
|
|
// Enabled binding support
|
|
attr.bsEnabled && scope.$watch(attr.bsEnabled, function(newValue, oldValue) {
|
|
// console.warn('scope.$watch(%s)', attr.bsEnabled, newValue, oldValue);
|
|
if(!tooltip || !angular.isDefined(newValue)) return;
|
|
if(angular.isString(newValue)) newValue = !!newValue.match(/true|1|,?(tooltip),?/i);
|
|
newValue === false ? tooltip.setEnabled(false) : tooltip.setEnabled(true);
|
|
});
|
|
|
|
// Viewport support
|
|
attr.viewport && scope.$watch(attr.viewport, function (newValue) {
|
|
if(!tooltip || !angular.isDefined(newValue)) return;
|
|
tooltip.setViewport(newValue);
|
|
});
|
|
|
|
// Initialize popover
|
|
var tooltip = $tooltip(element, options);
|
|
|
|
// Garbage collection
|
|
scope.$on('$destroy', function() {
|
|
if(tooltip) tooltip.destroy();
|
|
options = null;
|
|
tooltip = null;
|
|
});
|
|
|
|
}
|
|
};
|
|
|
|
}]);
|