var app = angular.module('plunker', ["ui.bootstrap.tooltip","ui.bootstrap.popover", "ui.bootstrap.position"]);
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
});
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet">
<link rel="stylesheet" href="style.css" />
<script data-require="angular.js@1.0.x" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js" data-semver="1.0.7"></script>
<script src="position.js"></script>
<script src="popover.js"></script>
<script src="tooltip.js"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<div class="container main">
<div class="row">
<div class="offset4 span4">
<button class='btn' popover="test">show popover </button>
</div>
</div>
</div>
</body>
</html>
/* Put your css in here */
.main{
margin-top: 100px;
}
/**
* 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' ] )
/**
* The $tooltip service creates tooltip- and popover-like directives as well as
* houses global options for them.
*/
.provider( '$tooltip', function () {
// The default options tooltip and popover.
var defaultOptions = {
placement: 'top',
animation: true,
popupDelay: 0
};
// Default hide triggers for each show trigger
var triggerMap = {
'mouseenter': 'mouseleave',
'click': 'click',
'focus': 'blur'
};
// 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', '$parse', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $parse, $document, $position, $interpolate ) {
return function $tooltip ( type, prefix, defaultTriggerShow ) {
var options = angular.extend( {}, defaultOptions, globalOptions );
/**
* 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 setTriggers ( trigger ) {
var show, hide;
show = trigger || options.trigger || defaultTriggerShow;
if ( angular.isDefined ( options.trigger ) ) {
hide = triggerMap[options.trigger] || show;
} else {
hide = triggerMap[show] || show;
}
return {
show: show,
hide: hide
};
}
var directiveName = snake_case( type );
var triggers = setTriggers( undefined );
var startSym = $interpolate.startSymbol();
var endSym = $interpolate.endSymbol();
//add manual-hide attribute for the button in the tooltip to hide the tooltip
var template =
'<'+ directiveName +'-popup '+
'title="'+startSym+'tt_title'+endSym+'" '+
'content="'+startSym+'tt_content'+endSym+'" '+
'placement="'+startSym+'tt_placement'+endSym+'" '+
'animation="tt_animation()" '+
'is-open="tt_isOpen" '+
'manual-hide="Hide()" ' +
'>'+
'</'+ directiveName +'-popup>';
return {
restrict: 'EA',
scope: true,
link: function link ( scope, element, attrs ) {
var tooltip = $compile( template )( scope );
tooltip.show = function (){
this.css({ display: 'block'});
};
tooltip.hide = function (){
this.css({ display: 'none'});
};
var isTooltipExist = false;
var transitionTimeout;
var popupTimeout;
var $body;
var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false;
// By default, the tooltip is not open.
// TODO add ability to start tooltip opened
scope.tt_isOpen = false;
function toggleTooltipBind () {
if ( ! scope.tt_isOpen ) {
showTooltipBind();
} else {
hideTooltipBind();
}
}
// Show the tooltip with delay if specified, otherwise show it immediately
function showTooltipBind() {
if ( scope.tt_popupDelay ) {
popupTimeout = $timeout( show, scope.tt_popupDelay );
} else {
scope.$apply( show );
}
}
function hideTooltipBind () {
scope.$apply(function () {
hide();
});
}
// Show the tooltip popup element.
function show() {
var position,
ttWidth,
ttHeight,
ttPosition;
// Don't show empty tooltips.
if ( ! scope.tt_content ) {
return;
}
// If there is a pending remove transition, we must cancel it, lest the
// tooltip be mysteriously removed.
if ( transitionTimeout ) {
$timeout.cancel( transitionTimeout );
}
// Set the initial positioning.
tooltip.css({ top: 0, left: 0, display: 'block' });
// Now we add it to the DOM because need some info about it. But it's not
// visible yet anyway.
// if tooltip is created, just show the old instance
if (!isTooltipExist) {
if ( appendToBody ) {
$body = $body || $document.find( 'body' );
$body.append( tooltip );
} else {
element.after( tooltip );
}
} else {
tooltip.show();
}
// Get the position of the directive element.
position = options.appendToBody ? $position.offset( element ) : $position.position( element );
// Get the height and width of the tooltip so we can center it.
ttWidth = tooltip.prop( 'offsetWidth' );
ttHeight = tooltip.prop( 'offsetHeight' );
// Calculate the tooltip's top and left coordinates to center it with
// this directive.
switch ( scope.tt_placement ) {
case 'mouse':
var mousePos = $position.mouse();
ttPosition = {
top: mousePos.y,
left: mousePos.x
};
break;
case 'right':
ttPosition = {
top: position.top + position.height / 2 - ttHeight / 2,
left: position.left + position.width
};
break;
case 'bottom':
ttPosition = {
top: position.top + position.height,
left: position.left + position.width / 2 - ttWidth / 2
};
break;
case 'left':
ttPosition = {
top: position.top + position.height / 2 - ttHeight / 2,
left: position.left - ttWidth
};
break;
default:
ttPosition = {
top: position.top - ttHeight,
left: position.left + position.width / 2 - ttWidth / 2
};
break;
}
ttPosition.top += 'px';
ttPosition.left += 'px';
// Now set the calculated positioning.
tooltip.css( ttPosition );
// And show the tooltip.
scope.tt_isOpen = true;
}
// Hide the tooltip popup element.
function hide() {
// First things first: we don't show it anymore.
scope.tt_isOpen = false;
//if tooltip is going to be shown after delay, we must cancel this
$timeout.cancel( popupTimeout );
// 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.
if ( angular.isDefined( scope.tt_animation ) && scope.tt_animation() ) {
transitionTimeout = $timeout( function () { tooltip.hide(); }, 500 );
} else {
//just hide the tooltip instead of remove it, otherwise tooltip need to compile again
tooltip.hide();
}
}
//expose the function to scope
scope.Hide = hide;
/**
* Observe the relevant attributes.
*/
attrs.$observe( type, function ( val ) {
scope.tt_content = val;
});
attrs.$observe( prefix+'Title', function ( val ) {
scope.tt_title = val;
});
attrs.$observe( prefix+'Placement', function ( val ) {
scope.tt_placement = angular.isDefined( val ) ? val : options.placement;
});
attrs.$observe( prefix+'Animation', function ( val ) {
scope.tt_animation = angular.isDefined( val ) ? $parse( val ) : function(){ return options.animation; };
});
attrs.$observe( prefix+'PopupDelay', function ( val ) {
var delay = parseInt( val, 10 );
scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay;
});
attrs.$observe( prefix+'Trigger', function ( val ) {
element.unbind( triggers.show );
element.unbind( triggers.hide );
triggers = setTriggers( val );
if ( triggers.show === triggers.hide ) {
element.bind( triggers.show, toggleTooltipBind );
} else {
element.bind( triggers.show, showTooltipBind );
element.bind( triggers.hide, hideTooltipBind );
}
});
attrs.$observe( prefix+'AppendToBody', function ( val ) {
appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : 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 ( scope.tt_isOpen ) {
hide();
}
});
}
// Make sure tooltip is destroyed and removed.
scope.$on('$destroy', function onDestroyTooltip() {
if ( scope.tt_isOpen ) {
hide();
} else {
// Not sure if we should use hide or remove here. if we use remove, tooltip need to recompile
tooltip.hide();
}
});
}
};
};
}];
})
.directive( 'tooltipPopup', function () {
return {
restrict: 'E',
replace: true,
scope: { content: '@', placement: '@', animation: '&', isOpen: '&' },
templateUrl: 'template/tooltip/tooltip-popup.html'
};
})
.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) {
return $tooltip( 'tooltip', 'tooltip', 'mouseenter' );
}])
.directive( 'tooltipHtmlUnsafePopup', function () {
return {
restrict: 'E',
replace: true,
scope: { content: '@', placement: '@', animation: '&', isOpen: '&' },
templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html'
};
})
.directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) {
return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' );
}]);
angular.module('ui.bootstrap.position', [])
/**
* A set of utility methods that can be use to retrieve position of DOM elements.
* It is meant to be used where we need to absolute-position DOM elements in
* relation to other, existing elements (this is the case for tooltips, popovers,
* typeahead suggestions etc.).
*/
.factory('$position', ['$document', '$window', function ($document, $window) {
var mouseX, mouseY;
$document.bind('mousemove', function mouseMoved(event) {
mouseX = event.pageX;
mouseY = event.pageY;
});
function getStyle(el, cssprop) {
if (el.currentStyle) { //IE
return el.currentStyle[cssprop];
} else if ($window.getComputedStyle) {
return $window.getComputedStyle(el)[cssprop];
}
// finally try and get inline style
return el.style[cssprop];
}
/**
* Checks if a given element is statically positioned
* @param element - raw DOM element
*/
function isStaticPositioned(element) {
return (getStyle(element, "position") || 'static') === 'static';
}
/**
* returns the closest, non-statically positioned parentOffset of a given element
* @param element
*/
var parentOffsetEl = function (element) {
var docDomEl = $document[0];
var offsetParent = element.offsetParent || docDomEl;
while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent)) {
offsetParent = offsetParent.offsetParent;
}
return offsetParent || docDomEl;
};
return {
/**
* Provides read-only equivalent of jQuery's position function:
* http://api.jquery.com/position/
*/
position: function (element) {
var elBCR = this.offset(element);
var offsetParentBCR = { top: 0, left: 0 };
var offsetParentEl = parentOffsetEl(element[0]);
if (offsetParentEl != $document[0]) {
offsetParentBCR = this.offset(angular.element(offsetParentEl));
offsetParentBCR.top += offsetParentEl.clientTop;
offsetParentBCR.left += offsetParentEl.clientLeft;
}
return {
width: element.prop('offsetWidth'),
height: element.prop('offsetHeight'),
top: elBCR.top - offsetParentBCR.top,
left: elBCR.left - offsetParentBCR.left
};
},
/**
* Provides read-only equivalent of jQuery's offset function:
* http://api.jquery.com/offset/
*/
offset: function (element) {
var boundingClientRect = element[0].getBoundingClientRect();
return {
width: element.prop('offsetWidth'),
height: element.prop('offsetHeight'),
top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop),
left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft)
};
},
/**
* Provides the coordinates of the mouse
*/
mouse: function () {
return { x: mouseX, y: mouseY };
}
};
}]);
/**
* The following features are still outstanding: popup delay, animation as a
* function, placement as a function, inside, support for more triggers than
* just mouse enter/leave, html popovers, and selector delegatation.
*/
angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] )
.directive( 'popoverPopup', function () {
return {
restrict: 'EA',
replace: true,
scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&', manualHide: '&' },
templateUrl: 'popover.html'
};
})
.directive( 'popover', [ '$compile', '$timeout', '$parse', '$window', '$tooltip', function ( $compile, $timeout, $parse, $window, $tooltip ) {
return $tooltip( 'popover', 'popover', 'click' );
}]);
<div class="popover {{placement}}" ng-class="{ in: isOpen(), fade: animation() }">
<div class="arrow"></div>
<div class="popover-inner">
<h3 class="popover-title" ng-bind="popoveritle" ng-show="popoverTitle"></h3>
<div class="popover-content" ng-bind="content"></div>
<button class="btn btn-success" ng-click="manualHide()">Close</button>
</div>
</div>