blob: cbc7e97e233e97bd2d2e6ad89b89a4171b8bbd5b [file] [log] [blame]
/**
* The following features are still outstanding: animation as a
* function, placement as a function, inside, support for more triggers than
* just mouse enter/leave, html tooltips, and selector delegation.
*/
angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.stackedMap'])
/**
* The $tooltip service creates tooltip- and popover-like directives as well as
* houses global options for them.
*/
.provider('$uibTooltip', function() {
// The default options tooltip and popover.
var defaultOptions = {
placement: 'top',
animation: true,
popupDelay: 0,
popupCloseDelay: 0,
useContentExp: false
};
// Default hide triggers for each show trigger
var triggerMap = {
'mouseenter': 'mouseleave',
'click': 'click',
'focus': 'blur',
'none': ''
};
// The options specified to the provider globally.
var globalOptions = {};
/**
* `options({})` allows global configuration of all tooltips in the
* application.
*
* var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) {
* // place tooltips left instead of top by default
* $tooltipProvider.options( { placement: 'left' } );
* });
*/
this.options = function(value) {
angular.extend(globalOptions, value);
};
/**
* This allows you to extend the set of trigger mappings available. E.g.:
*
* $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' );
*/
this.setTriggers = function setTriggers(triggers) {
angular.extend(triggerMap, triggers);
};
/**
* This is a helper function for translating camel-case to snake-case.
*/
function snake_case(name) {
var regexp = /[A-Z]/g;
var separator = '-';
return name.replace(regexp, function(letter, pos) {
return (pos ? separator : '') + letter.toLowerCase();
});
}
/**
* Returns the actual instance of the $tooltip service.
* TODO support multiple triggers
*/
this.$get = ['$window', '$compile', '$timeout', '$document', '$uibPosition', '$interpolate', '$rootScope', '$parse', '$$stackedMap', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse, $$stackedMap) {
var openedTooltips = $$stackedMap.createNew();
$document.on('keypress', function(e) {
if (e.which === 27) {
var last = openedTooltips.top();
if (last) {
last.value.close();
openedTooltips.removeTop();
last = null;
}
}
});
return function $tooltip(ttType, prefix, defaultTriggerShow, options) {
options = angular.extend({}, defaultOptions, globalOptions, options);
/**
* Returns an object of show and hide triggers.
*
* If a trigger is supplied,
* it is used to show the tooltip; otherwise, it will use the `trigger`
* option passed to the `$tooltipProvider.options` method; else it will
* default to the trigger supplied to this directive factory.
*
* The hide trigger is based on the show trigger. If the `trigger` option
* was passed to the `$tooltipProvider.options` method, it will use the
* mapped trigger from `triggerMap` or the passed trigger if the map is
* undefined; otherwise, it uses the `triggerMap` value of the show
* trigger; else it will just use the show trigger.
*/
function getTriggers(trigger) {
var show = (trigger || options.trigger || defaultTriggerShow).split(' ');
var hide = show.map(function(trigger) {
return triggerMap[trigger] || trigger;
});
return {
show: show,
hide: hide
};
}
var directiveName = snake_case(ttType);
var startSym = $interpolate.startSymbol();
var endSym = $interpolate.endSymbol();
var template =
'<div '+ directiveName + '-popup '+
'title="' + startSym + 'title' + endSym + '" '+
(options.useContentExp ?
'content-exp="contentExp()" ' :
'content="' + startSym + 'content' + endSym + '" ') +
'placement="' + startSym + 'placement' + endSym + '" '+
'popup-class="' + startSym + 'popupClass' + endSym + '" '+
'animation="animation" ' +
'is-open="isOpen"' +
'origin-scope="origScope" ' +
'style="visibility: hidden; display: block; top: -9999px; left: -9999px;"' +
'>' +
'</div>';
return {
compile: function(tElem, tAttrs) {
var tooltipLinker = $compile(template);
return function link(scope, element, attrs, tooltipCtrl) {
var tooltip;
var tooltipLinkedScope;
var transitionTimeout;
var showTimeout;
var hideTimeout;
var positionTimeout;
var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false;
var triggers = getTriggers(undefined);
var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']);
var ttScope = scope.$new(true);
var repositionScheduled = false;
var isOpenParse = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false;
var contentParse = options.useContentExp ? $parse(attrs[ttType]) : false;
var observers = [];
var positionTooltip = function() {
// check if tooltip exists and is not empty
if (!tooltip || !tooltip.html()) { return; }
if (!positionTimeout) {
positionTimeout = $timeout(function() {
// Reset the positioning.
tooltip.css({ top: 0, left: 0 });
// Now set the calculated positioning.
var ttCss = $position.positionElements(element, tooltip, ttScope.placement, appendToBody);
ttCss.top += 'px';
ttCss.left += 'px';
ttCss.visibility = 'visible';
tooltip.css(ttCss);
positionTimeout = null;
}, 0, false);
}
};
// Set up the correct scope to allow transclusion later
ttScope.origScope = scope;
// By default, the tooltip is not open.
// TODO add ability to start tooltip opened
ttScope.isOpen = false;
openedTooltips.add(ttScope, {
close: hide
});
function toggleTooltipBind() {
if (!ttScope.isOpen) {
showTooltipBind();
} else {
hideTooltipBind();
}
}
// Show the tooltip with delay if specified, otherwise show it immediately
function showTooltipBind() {
if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) {
return;
}
cancelHide();
prepareTooltip();
if (ttScope.popupDelay) {
// Do nothing if the tooltip was already scheduled to pop-up.
// This happens if show is triggered multiple times before any hide is triggered.
if (!showTimeout) {
showTimeout = $timeout(show, ttScope.popupDelay, false);
}
} else {
show();
}
}
function hideTooltipBind() {
cancelShow();
if (ttScope.popupCloseDelay) {
if (!hideTimeout) {
hideTimeout = $timeout(hide, ttScope.popupCloseDelay, false);
}
} else {
hide();
}
}
// Show the tooltip popup element.
function show() {
cancelShow();
cancelHide();
// Don't show empty tooltips.
if (!ttScope.content) {
return angular.noop;
}
createTooltip();
// And show the tooltip.
ttScope.$evalAsync(function() {
ttScope.isOpen = true;
assignIsOpen(true);
positionTooltip();
});
}
function cancelShow() {
if (showTimeout) {
$timeout.cancel(showTimeout);
showTimeout = null;
}
if (positionTimeout) {
$timeout.cancel(positionTimeout);
positionTimeout = null;
}
}
// Hide the tooltip popup element.
function hide() {
cancelShow();
cancelHide();
if (!ttScope) {
return;
}
// First things first: we don't show it anymore.
ttScope.$evalAsync(function() {
ttScope.isOpen = false;
assignIsOpen(false);
// And now we remove it from the DOM. However, if we have animation, we
// need to wait for it to expire beforehand.
// FIXME: this is a placeholder for a port of the transitions library.
// The fade transition in TWBS is 150ms.
if (ttScope.animation) {
if (!transitionTimeout) {
transitionTimeout = $timeout(removeTooltip, 150, false);
}
} else {
removeTooltip();
}
});
}
function cancelHide() {
if (hideTimeout) {
$timeout.cancel(hideTimeout);
hideTimeout = null;
}
if (transitionTimeout) {
$timeout.cancel(transitionTimeout);
transitionTimeout = null;
}
}
function createTooltip() {
// There can only be one tooltip element per directive shown at once.
if (tooltip) {
return;
}
tooltipLinkedScope = ttScope.$new();
tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) {
if (appendToBody) {
$document.find('body').append(tooltip);
} else {
element.after(tooltip);
}
});
prepObservers();
}
function removeTooltip() {
unregisterObservers();
transitionTimeout = null;
if (tooltip) {
tooltip.remove();
tooltip = null;
}
if (tooltipLinkedScope) {
tooltipLinkedScope.$destroy();
tooltipLinkedScope = null;
}
}
/**
* Set the inital scope values. Once
* the tooltip is created, the observers
* will be added to keep things in synch.
*/
function prepareTooltip() {
ttScope.title = attrs[prefix + 'Title'];
if (contentParse) {
ttScope.content = contentParse(scope);
} else {
ttScope.content = attrs[ttType];
}
ttScope.popupClass = attrs[prefix + 'Class'];
ttScope.placement = angular.isDefined(attrs[prefix + 'Placement']) ? attrs[prefix + 'Placement'] : options.placement;
var delay = parseInt(attrs[prefix + 'PopupDelay'], 10);
var closeDelay = parseInt(attrs[prefix + 'PopupCloseDelay'], 10);
ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay;
ttScope.popupCloseDelay = !isNaN(closeDelay) ? closeDelay : options.popupCloseDelay;
}
function assignIsOpen(isOpen) {
if (isOpenParse && angular.isFunction(isOpenParse.assign)) {
isOpenParse.assign(scope, isOpen);
}
}
ttScope.contentExp = function() {
return ttScope.content;
};
/**
* Observe the relevant attributes.
*/
attrs.$observe('disabled', function(val) {
if (val) {
cancelShow();
}
if (val && ttScope.isOpen) {
hide();
}
});
if (isOpenParse) {
scope.$watch(isOpenParse, function(val) {
/*jshint -W018 */
if (ttScope && !val === ttScope.isOpen) {
toggleTooltipBind();
}
/*jshint +W018 */
});
}
function prepObservers() {
observers.length = 0;
if (contentParse) {
observers.push(
scope.$watch(contentParse, function(val) {
ttScope.content = val;
if (!val && ttScope.isOpen) {
hide();
}
})
);
observers.push(
tooltipLinkedScope.$watch(function() {
if (!repositionScheduled) {
repositionScheduled = true;
tooltipLinkedScope.$$postDigest(function() {
repositionScheduled = false;
if (ttScope && ttScope.isOpen) {
positionTooltip();
}
});
}
})
);
} else {
observers.push(
attrs.$observe(ttType, function(val) {
ttScope.content = val;
if (!val && ttScope.isOpen) {
hide();
} else {
positionTooltip();
}
})
);
}
observers.push(
attrs.$observe(prefix + 'Title', function(val) {
ttScope.title = val;
if (ttScope.isOpen) {
positionTooltip();
}
})
);
observers.push(
attrs.$observe(prefix + 'Placement', function(val) {
ttScope.placement = val ? val : options.placement;
if (ttScope.isOpen) {
positionTooltip();
}
})
);
}
function unregisterObservers() {
if (observers.length) {
angular.forEach(observers, function(observer) {
observer();
});
observers.length = 0;
}
}
var unregisterTriggers = function() {
triggers.show.forEach(function(trigger) {
element.unbind(trigger, showTooltipBind);
});
triggers.hide.forEach(function(trigger) {
trigger.split(' ').forEach(function(hideTrigger) {
element[0].removeEventListener(hideTrigger, hideTooltipBind);
});
});
};
function prepTriggers() {
var val = attrs[prefix + 'Trigger'];
unregisterTriggers();
triggers = getTriggers(val);
if (triggers.show !== 'none') {
triggers.show.forEach(function(trigger, idx) {
// Using raw addEventListener due to jqLite/jQuery bug - #4060
if (trigger === triggers.hide[idx]) {
element[0].addEventListener(trigger, toggleTooltipBind);
} else if (trigger) {
element[0].addEventListener(trigger, showTooltipBind);
triggers.hide[idx].split(' ').forEach(function(trigger) {
element[0].addEventListener(trigger, hideTooltipBind);
});
}
element.on('keypress', function(e) {
if (e.which === 27) {
hideTooltipBind();
}
});
});
}
}
prepTriggers();
var animation = scope.$eval(attrs[prefix + 'Animation']);
ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation;
var appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']);
appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody;
// if a tooltip is attached to <body> we need to remove it on
// location change as its parent scope will probably not be destroyed
// by the change.
if (appendToBody) {
scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess() {
if (ttScope.isOpen) {
hide();
}
});
}
// Make sure tooltip is destroyed and removed.
scope.$on('$destroy', function onDestroyTooltip() {
cancelShow();
cancelHide();
unregisterTriggers();
removeTooltip();
openedTooltips.remove(ttScope);
ttScope = null;
});
};
}
};
};
}];
})
// This is mostly ngInclude code but with a custom scope
.directive('uibTooltipTemplateTransclude', [
'$animate', '$sce', '$compile', '$templateRequest',
function ($animate , $sce , $compile , $templateRequest) {
return {
link: function(scope, elem, attrs) {
var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope);
var changeCounter = 0,
currentScope,
previousElement,
currentElement;
var cleanupLastIncludeContent = function() {
if (previousElement) {
previousElement.remove();
previousElement = null;
}
if (currentScope) {
currentScope.$destroy();
currentScope = null;
}
if (currentElement) {
$animate.leave(currentElement).then(function() {
previousElement = null;
});
previousElement = currentElement;
currentElement = null;
}
};
scope.$watch($sce.parseAsResourceUrl(attrs.uibTooltipTemplateTransclude), function(src) {
var thisChangeId = ++changeCounter;
if (src) {
//set the 2nd param to true to ignore the template request error so that the inner
//contents and scope can be cleaned up.
$templateRequest(src, true).then(function(response) {
if (thisChangeId !== changeCounter) { return; }
var newScope = origScope.$new();
var template = response;
var clone = $compile(template)(newScope, function(clone) {
cleanupLastIncludeContent();
$animate.enter(clone, elem);
});
currentScope = newScope;
currentElement = clone;
currentScope.$emit('$includeContentLoaded', src);
}, function() {
if (thisChangeId === changeCounter) {
cleanupLastIncludeContent();
scope.$emit('$includeContentError', src);
}
});
scope.$emit('$includeContentRequested', src);
} else {
cleanupLastIncludeContent();
}
});
scope.$on('$destroy', cleanupLastIncludeContent);
}
};
}])
/**
* Note that it's intentional that these classes are *not* applied through $animate.
* They must not be animated as they're expected to be present on the tooltip on
* initialization.
*/
.directive('uibTooltipClasses', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
if (scope.placement) {
element.addClass(scope.placement);
}
if (scope.popupClass) {
element.addClass(scope.popupClass);
}
if (scope.animation()) {
element.addClass(attrs.tooltipAnimationClass);
}
}
};
})
.directive('uibTooltipPopup', function() {
return {
replace: true,
scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
templateUrl: 'template/tooltip/tooltip-popup.html',
link: function(scope, element) {
element.addClass('tooltip');
}
};
})
.directive('uibTooltip', [ '$uibTooltip', function($uibTooltip) {
return $uibTooltip('uibTooltip', 'tooltip', 'mouseenter');
}])
.directive('uibTooltipTemplatePopup', function() {
return {
replace: true,
scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
originScope: '&' },
templateUrl: 'template/tooltip/tooltip-template-popup.html',
link: function(scope, element) {
element.addClass('tooltip');
}
};
})
.directive('uibTooltipTemplate', ['$uibTooltip', function($uibTooltip) {
return $uibTooltip('uibTooltipTemplate', 'tooltip', 'mouseenter', {
useContentExp: true
});
}])
.directive('uibTooltipHtmlPopup', function() {
return {
replace: true,
scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
templateUrl: 'template/tooltip/tooltip-html-popup.html',
link: function(scope, element) {
element.addClass('tooltip');
}
};
})
.directive('uibTooltipHtml', ['$uibTooltip', function($uibTooltip) {
return $uibTooltip('uibTooltipHtml', 'tooltip', 'mouseenter', {
useContentExp: true
});
}]);
/* Deprecated tooltip below */
angular.module('ui.bootstrap.tooltip')
.value('$tooltipSuppressWarning', false)
.provider('$tooltip', ['$uibTooltipProvider', function($uibTooltipProvider) {
angular.extend(this, $uibTooltipProvider);
this.$get = ['$log', '$tooltipSuppressWarning', '$injector', function($log, $tooltipSuppressWarning, $injector) {
if (!$tooltipSuppressWarning) {
$log.warn('$tooltip is now deprecated. Use $uibTooltip instead.');
}
return $injector.invoke($uibTooltipProvider.$get);
}];
}])
// This is mostly ngInclude code but with a custom scope
.directive('tooltipTemplateTransclude', [
'$animate', '$sce', '$compile', '$templateRequest', '$log', '$tooltipSuppressWarning',
function ($animate , $sce , $compile , $templateRequest, $log, $tooltipSuppressWarning) {
return {
link: function(scope, elem, attrs) {
if (!$tooltipSuppressWarning) {
$log.warn('tooltip-template-transclude is now deprecated. Use uib-tooltip-template-transclude instead.');
}
var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope);
var changeCounter = 0,
currentScope,
previousElement,
currentElement;
var cleanupLastIncludeContent = function() {
if (previousElement) {
previousElement.remove();
previousElement = null;
}
if (currentScope) {
currentScope.$destroy();
currentScope = null;
}
if (currentElement) {
$animate.leave(currentElement).then(function() {
previousElement = null;
});
previousElement = currentElement;
currentElement = null;
}
};
scope.$watch($sce.parseAsResourceUrl(attrs.tooltipTemplateTransclude), function(src) {
var thisChangeId = ++changeCounter;
if (src) {
//set the 2nd param to true to ignore the template request error so that the inner
//contents and scope can be cleaned up.
$templateRequest(src, true).then(function(response) {
if (thisChangeId !== changeCounter) { return; }
var newScope = origScope.$new();
var template = response;
var clone = $compile(template)(newScope, function(clone) {
cleanupLastIncludeContent();
$animate.enter(clone, elem);
});
currentScope = newScope;
currentElement = clone;
currentScope.$emit('$includeContentLoaded', src);
}, function() {
if (thisChangeId === changeCounter) {
cleanupLastIncludeContent();
scope.$emit('$includeContentError', src);
}
});
scope.$emit('$includeContentRequested', src);
} else {
cleanupLastIncludeContent();
}
});
scope.$on('$destroy', cleanupLastIncludeContent);
}
};
}])
.directive('tooltipClasses', ['$log', '$tooltipSuppressWarning', function($log, $tooltipSuppressWarning) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
if (!$tooltipSuppressWarning) {
$log.warn('tooltip-classes is now deprecated. Use uib-tooltip-classes instead.');
}
if (scope.placement) {
element.addClass(scope.placement);
}
if (scope.popupClass) {
element.addClass(scope.popupClass);
}
if (scope.animation()) {
element.addClass(attrs.tooltipAnimationClass);
}
}
};
}])
.directive('tooltipPopup', ['$log', '$tooltipSuppressWarning', function($log, $tooltipSuppressWarning) {
return {
replace: true,
scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
templateUrl: 'template/tooltip/tooltip-popup.html',
link: function(scope, element) {
if (!$tooltipSuppressWarning) {
$log.warn('tooltip-popup is now deprecated. Use uib-tooltip-popup instead.');
}
element.addClass('tooltip');
}
};
}])
.directive('tooltip', ['$tooltip', function($tooltip) {
return $tooltip('tooltip', 'tooltip', 'mouseenter');
}])
.directive('tooltipTemplatePopup', ['$log', '$tooltipSuppressWarning', function($log, $tooltipSuppressWarning) {
return {
replace: true,
scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
originScope: '&' },
templateUrl: 'template/tooltip/tooltip-template-popup.html',
link: function(scope, element) {
if (!$tooltipSuppressWarning) {
$log.warn('tooltip-template-popup is now deprecated. Use uib-tooltip-template-popup instead.');
}
element.addClass('tooltip');
}
};
}])
.directive('tooltipTemplate', ['$tooltip', function($tooltip) {
return $tooltip('tooltipTemplate', 'tooltip', 'mouseenter', {
useContentExp: true
});
}])
.directive('tooltipHtmlPopup', ['$log', '$tooltipSuppressWarning', function($log, $tooltipSuppressWarning) {
return {
replace: true,
scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
templateUrl: 'template/tooltip/tooltip-html-popup.html',
link: function(scope, element) {
if (!$tooltipSuppressWarning) {
$log.warn('tooltip-html-popup is now deprecated. Use uib-tooltip-html-popup instead.');
}
element.addClass('tooltip');
}
};
}])
.directive('tooltipHtml', ['$tooltip', function($tooltip) {
return $tooltip('tooltipHtml', 'tooltip', 'mouseenter', {
useContentExp: true
});
}]);