/** * angular-strap * @version v2.0.3 - 2014-05-30 * @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', 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 }; this.$get = [ '$window', '$rootScope', '$compile', '$q', '$templateCache', '$http', '$animate', 'dimensions', '$$rAF', function ($window, $rootScope, $compile, $q, $templateCache, $http, $animate, dimensions, $$rAF) { var trim = String.prototype.trim; var isTouch = 'createTouch' in $window.document; var htmlReplaceRegExp = /ng-bind="/gi; 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)) { options.delay = parseFloat(options.delay); } // Support scope as string options if (options.title) { $tooltip.$scope.title = options.title; } // Provide scope helpers scope.$hide = function () { scope.$$postDigest(function () { $tooltip.hide(); }); }; scope.$show = function () { scope.$$postDigest(function () { $tooltip.show(); }); }; scope.$toggle = function () { scope.$$postDigest(function () { $tooltip.toggle(); }); }; $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; $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 (options.container) { tipContainer = findElement(options.container); } // Options: trigger 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); } }); // Options: target if (options.target) { options.target = angular.isElement(options.target) ? options.target : findElement(options.target)[0]; } // Options: show if (options.show) { scope.$$postDigest(function () { options.trigger === 'focus' ? element[0].focus() : $tooltip.show(); }); } }; $tooltip.destroy = function () { // Unbind events 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); } } // Remove element if (tipElement) { tipElement.remove(); tipElement = null; } // Cancel pending callbacks clearTimeout(timeout); // 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 () { scope.$emit(options.prefixEvent + '.show.before', $tooltip); var parent = options.container ? tipContainer : null; var after = options.container ? null : element; // Hide any existing tipElement if (tipElement) tipElement.remove(); // Fetch a cloned element linked from template tipElement = $tooltip.$element = tipLinker(scope, function (clonedElement, scope) { }); // Set the initial positioning. tipElement.css({ top: '-9999px', left: '-9999px', display: 'block' }).addClass(options.placement); // Options: animation if (options.animation) tipElement.addClass(options.animation); // Options: type if (options.type) tipElement.addClass(options.prefixClass + '-' + options.type); $animate.enter(tipElement, parent, after, function () { scope.$emit(options.prefixEvent + '.show', $tooltip); }); $tooltip.$isShown = scope.$isShown = true; scope.$$phase || scope.$root && scope.$root.$$phase || scope.$digest(); $$rAF($tooltip.$applyPlacement); // var a = bodyEl.offsetWidth + 1; ? // Bind events if (options.keyboard) { if (options.trigger !== 'focus') { $tooltip.focus(); tipElement.on('keyup', $tooltip.$onKeyUp); } else { element.on('keyup', $tooltip.$onFocusKeyUp); } } }; $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); }; $tooltip.hide = function (blur) { if (!$tooltip.$isShown) return; scope.$emit(options.prefixEvent + '.hide.before', $tooltip); $animate.leave(tipElement, function () { scope.$emit(options.prefixEvent + '.hide', $tooltip); }); $tooltip.$isShown = scope.$isShown = false; scope.$$phase || scope.$root && scope.$root.$$phase || scope.$digest(); // Unbind events if (options.keyboard && tipElement !== null) { tipElement.off('keyup', $tooltip.$onKeyUp); } // Allow to blur the input when hidden, like when pressing enter key if (blur && options.trigger === 'focus') { return element[0].blur(); } }; $tooltip.toggle = function () { $tooltip.$isShown ? $tooltip.leave() : $tooltip.enter(); }; $tooltip.focus = function () { tipElement[0].focus(); }; // Protected methods $tooltip.$applyPlacement = function () { if (!tipElement) return; // Get the position of the tooltip element. var elementPosition = getPosition(); // Get the height and width of the tooltip so we can center it. var tipWidth = tipElement.prop('offsetWidth'), tipHeight = tipElement.prop('offsetHeight'); // Get the tooltip's top and left coordinates to center it with this directive. var tipPosition = getCalculatedOffset(options.placement, elementPosition, tipWidth, tipHeight); // Now set the calculated positioning. tipPosition.top += 'px'; tipPosition.left += 'px'; tipElement.css(tipPosition); }; $tooltip.$onKeyUp = function (evt) { evt.which === 27 && $tooltip.hide(); }; $tooltip.$onFocusKeyUp = function (evt) { evt.which === 27 && element[0].blur(); }; $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(); }; // Private methods function getPosition() { if (options.container === 'body') { return dimensions.offset(options.target || element[0]); } else { return dimensions.position(options.target || element[0]); } } 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; } return $tooltip; } // Helper functions function findElement(query, element) { return angular.element((element || document).querySelectorAll(query)); } function fetchTemplate(template) { return $q.when($templateCache.get(template) || $http.get(template)).then(function (res) { if (angular.isObject(res)) { $templateCache.put(template, res.data); return res.data; } return res; }); } 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', 'target', 'delay', 'trigger', 'keyboard', 'html', 'animation', 'type' ], function (key) { if (angular.isDefined(attr[key])) options[key] = attr[key]; }); // Observe scope attributes for change angular.forEach(['title'], function (key) { attr[key] && attr.$observe(key, function (newValue, oldValue) { scope[key] = $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); // Initialize popover var tooltip = $tooltip(element, options); // Garbage collection scope.$on('$destroy', function () { tooltip.destroy(); options = null; tooltip = null; }); } }; } ]);