UNPKG

angular-cached-resource

Version:

An AngularJS module to interact with RESTful resources, even when browser is offline

441 lines (357 loc) 14 kB
'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, placement: 'top', template: 'tooltip/tooltip.tpl.html', contentTemplate: false, trigger: 'hover focus', keyboard: false, html: false, show: false, title: '', type: '', delay: 0 }; this.$get = function($window, $rootScope, $compile, $q, $templateCache, $http, $animate, $timeout, dimensions, $$rAF) { var trim = String.prototype.trim; var isTouch = 'createTouch' in $window.document; var htmlReplaceRegExp = /ng-bind="/ig; 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) { 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); 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() { 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: '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() { scope.$emit(options.prefixEvent + '.show', $tooltip); }); $tooltip.$isShown = scope.$isShown = true; scope.$$phase || 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.$$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(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', 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', '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; }); } }; });