Skip to content

Instantly share code, notes, and snippets.

@tlvince
Created March 25, 2014 00:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tlvince/9752424 to your computer and use it in GitHub Desktop.
Save tlvince/9752424 to your computer and use it in GitHub Desktop.
/**
* angular-strap
* @version v2.0.0-rc.4 - 2014-03-07
* @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', [
'ngAnimate',
'mgcrea.ngStrap.helpers.dimensions'
]).provider('$tooltip', function () {
var defaults = this.defaults = {
animation: 'am-fade',
prefixClass: 'tooltip',
container: false,
placement: 'top',
template: '',
contentTemplate: false,
trigger: 'hover focus',
keyboard: false,
html: false,
show: false,
title: '',
type: '',
delay: 0
};
this.$get = [
'$window',
'$rootScope',
'$compile',
'$q',
'$templateCache',
'$http',
'$animate',
'$timeout',
'dimensions',
'$$animateReflow',
function ($window, $rootScope, $compile, $q, $templateCache, $http, $animate, $timeout, dimensions, $$animateReflow) {
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 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) {
findElement('[ng-bind="content"]', templateEl[0]).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') {
if(!element.on) { return; }
element.on(trigger === 'hover' ? 'mouseenter' : 'focus', $tooltip.enter);
element.on(trigger === 'hover' ? 'mouseleave' : 'blur', $tooltip.leave);
trigger !== 'hover' && element.on(isTouch ? 'touchstart' : 'mousedown', $tooltip.$onFocusElementMouseDown);
}
});
// 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);
trigger !== 'hover' && element.off(isTouch ? 'touchstart' : 'mousedown', $tooltip.$onFocusElementMouseDown);
}
}
// Remove element
if (tipElement) {
tipElement.remove();
tipElement = null;
}
// Destroy scope
scope.$destroy();
};
$tooltip.enter = function () {
console.log("value");
clearTimeout(timeout);
if (!options.delay || !options.delay.show) {
return $tooltip.show();
}
timeout = setTimeout(function () {
if (hoverState === 'in')
$tooltip.show();
}, options.delay.show);
};
$tooltip.show = function () {
var parent = options.container ? tipContainer : null;
var after = options.container ? null : element;
// Remove 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: '0px',
left: '0px',
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 () {
});
$tooltip.$isShown = scope.$isShown = true;
scope.$$phase || scope.$digest();
$$animateReflow($tooltip.$applyPlacement);
// 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;
$animate.leave(tipElement, function () {
tipElement = null;
});
$tooltip.$isShown = scope.$isShown = false;
scope.$$phase || scope.$digest();
// Unbind events
if (options.keyboard) {
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(element[0]);
} else {
return dimensions.position(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',
'$$animateReflow',
function ($window, $location, $sce, $tooltip, $$animateReflow) {
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',
'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) && $$animateReflow(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.content = newValue;
}
angular.isDefined(oldValue) && $$animateReflow(function () {
tooltip && tooltip.$applyPlacement();
});
}, true);
// Initialize popover
var tooltip = $tooltip(element, options);
// Garbage collection
scope.$on('$destroy', function () {
tooltip.destroy();
options = null;
tooltip = null;
});
}
};
}
]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment