From e22f28bfe162206ac2e96744e0949c86e3bfc65b Mon Sep 17 00:00:00 2001 From: Josh David Miller Date: Sun, 10 Mar 2013 22:31:42 -0700 Subject: [PATCH] feat(tooltip,popover): logic moved to $tooltip svc Popover and tooltip directive creation is now performed through the help of the `$tooltip` service that manages all aspects of directive creation and management based on only the name of the component and their default triggers. The tooltip and popover sub-directives and their templates were refactored to use common scope variables that the new service can provide, yielding even greater flexibility. Animation is now enabled by default. Lastly, the `$tooltipProvider` has been established to allow setting default global options for the tooltip and popover, but these features are not yet public as the API is sure to change drastically as we flesh out which global options to allow. But the framework is in place as this was a logical time to incorporate it. --- src/popover/docs/readme.md | 3 + src/popover/popover.js | 144 +------------ src/popover/test/popoverSpec.js | 12 +- src/tooltip/docs/demo.html | 2 +- src/tooltip/test/tooltip.spec.js | 4 +- src/tooltip/tooltip.js | 321 ++++++++++++++++------------ template/popover/popover.html | 4 +- template/tooltip/tooltip-popup.html | 2 +- 8 files changed, 209 insertions(+), 283 deletions(-) diff --git a/src/popover/docs/readme.md b/src/popover/docs/readme.md index eb5f53428a..87316ddeee 100644 --- a/src/popover/docs/readme.md +++ b/src/popover/docs/readme.md @@ -1,2 +1,5 @@ A lightweight, extensible directive for fancy popover creation. The popover directive supports multiple placements, optional transition animation, and more. + +Like the Twitter Bootstrap jQuery plugin, the popover **requires** the tooltip +module. diff --git a/src/popover/popover.js b/src/popover/popover.js index d64e729eff..1cee58f851 100644 --- a/src/popover/popover.js +++ b/src/popover/popover.js @@ -3,152 +3,16 @@ * 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', [] ) +angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) .directive( 'popoverPopup', function () { return { restrict: 'EA', replace: true, - scope: { popoverTitle: '@', popoverContent: '@', placement: '@', animation: '&', isOpen: '&' }, + scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' }, templateUrl: 'template/popover/popover.html' }; }) -.directive( 'popover', [ '$compile', '$timeout', '$parse', '$window', function ( $compile, $timeout, $parse, $window ) { - - var template = - ''+ - ''; - - return { - scope: true, - link: function ( scope, element, attr ) { - var popover = $compile( template )( scope ), - transitionTimeout; - - attr.$observe( 'popover', function ( val ) { - scope.tt_popover = val; - }); - - attr.$observe( 'popoverTitle', function ( val ) { - scope.tt_title = val; - }); - - attr.$observe( 'popoverPlacement', function ( val ) { - // If no placement was provided, default to 'top'. - scope.tt_placement = val || 'top'; - }); - - attr.$observe( 'popoverAnimation', function ( val ) { - scope.tt_animation = $parse( val ); - }); - - // By default, the popover is not open. - scope.tt_isOpen = false; - - // Calculate the current position and size of the directive element. - function getPosition() { - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: element.prop( 'offsetWidth' ), - height: element.prop( 'offsetHeight' ), - top: boundingClientRect.top + $window.pageYOffset, - left: boundingClientRect.left + $window.pageXOffset - }; - } - - function show() { - var position, - ttWidth, - ttHeight, - ttPosition; - - // If there is a pending remove transition, we must cancel it, lest the - // toolip be mysteriously removed. - if ( transitionTimeout ) { - $timeout.cancel( transitionTimeout ); - } - - // Set the initial positioning. - popover.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. - element.after( popover ); - - // Get the position of the directive element. - position = getPosition(); - - // Get the height and width of the popover so we can center it. - ttWidth = popover.prop( 'offsetWidth' ); - ttHeight = popover.prop( 'offsetHeight' ); - - // Calculate the popover's top and left coordinates to center it with - // this directive. - switch ( scope.tt_placement ) { - case 'right': - ttPosition = { - top: (position.top + position.height / 2 - ttHeight / 2) + 'px', - left: (position.left + position.width) + 'px' - }; - break; - case 'bottom': - ttPosition = { - top: (position.top + position.height) + 'px', - left: (position.left + position.width / 2 - ttWidth / 2) + 'px' - }; - break; - case 'left': - ttPosition = { - top: (position.top + position.height / 2 - ttHeight / 2) + 'px', - left: (position.left - ttWidth) + 'px' - }; - break; - default: - ttPosition = { - top: (position.top - ttHeight) + 'px', - left: (position.left + position.width / 2 - ttWidth / 2) + 'px' - }; - break; - } - - // Now set the calculated positioning. - popover.css( ttPosition ); - - // And show the popover. - scope.tt_isOpen = true; - } - - // Hide the popover popup element. - function hide() { - // First things first: we don't show it anymore. - //popover.removeClass( 'in' ); - scope.tt_isOpen = 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. - if ( angular.isDefined( scope.tt_animation ) && scope.tt_animation() ) { - transitionTimeout = $timeout( function () { popover.remove(); }, 500 ); - } else { - popover.remove(); - } - } - - // Register the event listeners. - element.bind( 'click', function() { - if(scope.tt_isOpen){ - scope.$apply( hide ); - } else { - scope.$apply( show ); - } - - }); - } - }; +.directive( 'popover', [ '$compile', '$timeout', '$parse', '$window', '$tooltip', function ( $compile, $timeout, $parse, $window, $tooltip ) { + return $tooltip( 'popover', 'click' ); }]); diff --git a/src/popover/test/popoverSpec.js b/src/popover/test/popoverSpec.js index f98d292b21..633aeb8b2c 100644 --- a/src/popover/test/popoverSpec.js +++ b/src/popover/test/popoverSpec.js @@ -81,7 +81,7 @@ describe('popover', function() { tt.trigger( 'click' ); expect( tt.text() ).toBe( scope.items[0].name ); - expect( tt.scope().tt_popover ).toBe( scope.items[0].popover ); + expect( tt.scope().tt_content ).toBe( scope.items[0].popover ); tt.trigger( 'click' ); })); @@ -89,12 +89,12 @@ describe('popover', function() { it('should only have an isolate scope on the popup', inject( function ( $compile ) { var ttScope; - scope.popoverMsg = "popover Text"; - scope.popoverTitle = "popover Text"; + scope.popoverContent = "Popover Content"; + scope.popoverTitle = "Popover Title"; scope.alt = "Alt Message"; elmBody = $compile( angular.element( - '
Selector Text
' + '
Selector Text
' ) )( scope ); $compile( elmBody )( scope ); @@ -107,8 +107,8 @@ describe('popover', function() { ttScope = angular.element( elmBody.children()[1] ).scope(); expect( ttScope.placement ).toBe( 'top' ); - expect( ttScope.popoverTitle ).toBe( scope.popoverTitle ); - expect( ttScope.popoverContent ).toBe( scope.popoverMsg ); + expect( ttScope.title ).toBe( scope.popoverTitle ); + expect( ttScope.content ).toBe( scope.popoverContent ); elm.trigger( 'click' ); })); diff --git a/src/tooltip/docs/demo.html b/src/tooltip/docs/demo.html index 815c540e2e..954c70e57e 100644 --- a/src/tooltip/docs/demo.html +++ b/src/tooltip/docs/demo.html @@ -12,7 +12,7 @@ nunc sed velit dignissim sodales ut eu sem integer vitae. Turpis egestas bottom pharetra convallis posuere morbi leo urna, - fading + fading at elementum eu, facilisis sed odio morbi quis commodo odio. In cursus turpis massa tincidunt dui ut.

diff --git a/src/tooltip/test/tooltip.spec.js b/src/tooltip/test/tooltip.spec.js index 1eb537a7e8..b9849df338 100644 --- a/src/tooltip/test/tooltip.spec.js +++ b/src/tooltip/test/tooltip.spec.js @@ -81,7 +81,7 @@ describe('tooltip', function() { tt.trigger( 'mouseenter' ); expect( tt.text() ).toBe( scope.items[0].name ); - expect( tt.scope().tt_tooltip ).toBe( scope.items[0].tooltip ); + expect( tt.scope().tt_content ).toBe( scope.items[0].tooltip ); tt.trigger( 'mouseleave' ); })); @@ -106,7 +106,7 @@ describe('tooltip', function() { ttScope = angular.element( elmBody.children()[1] ).scope(); expect( ttScope.placement ).toBe( 'top' ); - expect( ttScope.tooltipTitle ).toBe( scope.tooltipMsg ); + expect( ttScope.content ).toBe( scope.tooltipMsg ); elm.trigger( 'mouseleave' ); })); diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index 7af22a45b3..15617274a8 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -1,52 +1,57 @@ /** * 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 tooltips, and selector delegatation. + * just mouse enter/leave, html tooltips, and selector delegation. */ angular.module( 'ui.bootstrap.tooltip', [] ) -.directive( 'tooltipPopup', function () { - return { - restrict: 'EA', - replace: true, - scope: { tooltipTitle: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-popup.html' + +/** + * 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 }; -}) -.directive( 'tooltip', [ '$compile', '$timeout', '$parse', '$window', function ( $compile, $timeout, $parse, $window) { - - var template = - ''+ - ''; + + // The options specified to the provider globally. + var globalOptions = {}; - return { - scope: true, - link: function ( scope, element, attr ) { - var tooltip = $compile( template )( scope ), - transitionTimeout; - - attr.$observe( 'tooltip', function ( val ) { - scope.tt_tooltip = val; - }); - - attr.$observe( 'tooltipPlacement', function ( val ) { - // If no placement was provided, default to 'top'. - scope.tt_placement = val || 'top'; - }); - - attr.$observe( 'tooltipAnimation', function ( val ) { - scope.tt_animation = $parse( val ); - }); - - // By default, the tooltip is not open. - scope.tt_isOpen = false; - + /** + * The `options({})` allows global configuration of all dialogs 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 ); + }; + + /** + * Returns the actual instance of the $tooltip service. + * TODO support multiple triggers + */ + this.$get = [ '$window', '$compile', '$timeout', '$parse', function ( $window, $compile, $timeout, $parse ) { + return function $tooltip ( type, defaultTriggerShow, defaultTriggerHide ) { + var options = angular.extend( {}, defaultOptions, globalOptions ); + + var template = + '<'+ type +'-popup '+ + 'title="{{tt_title}}" '+ + 'content="{{tt_content}}" '+ + 'placement="{{tt_placement}}" '+ + 'animation="tt_animation()" '+ + 'is-open="tt_isOpen"'+ + '>'+ + ''; + // Calculate the current position and size of the directive element. - function getPosition() { + function getPosition( element ) { var boundingClientRect = element[0].getBoundingClientRect(); return { width: element.prop( 'offsetWidth' ), @@ -55,99 +60,153 @@ angular.module( 'ui.bootstrap.tooltip', [] ) left: boundingClientRect.left + $window.pageXOffset }; } - - // Show the tooltip popup element. - function show() { - var position, - ttWidth, - ttHeight, - ttPosition; - - //don't show empty tooltips - if (!scope.tt_tooltip) { - return; - } + + return { + restrict: 'EA', + scope: true, + link: function link ( scope, element, attrs ) { + var tooltip = $compile( template )( scope ), + transitionTimeout; - // If there is a pending remove transition, we must cancel it, lest the - // toolip 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. - element.after( tooltip ); - - // Get the position of the directive element. - position = getPosition(); - - // 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 'right': - ttPosition = { - top: (position.top + position.height / 2 - ttHeight / 2) + 'px', - left: (position.left + position.width) + 'px' - }; - break; - case 'bottom': - ttPosition = { - top: (position.top + position.height) + 'px', - left: (position.left + position.width / 2 - ttWidth / 2) + 'px' - }; - break; - case 'left': - ttPosition = { - top: (position.top + position.height / 2 - ttHeight / 2) + 'px', - left: (position.left - ttWidth) + 'px' - }; - break; - default: - ttPosition = { - top: (position.top - ttHeight) + 'px', - left: (position.left + position.width / 2 - ttWidth / 2) + 'px' - }; - break; - } - - // Now set the calculated positioning. - tooltip.css( ttPosition ); + attrs.$observe( type, function ( val ) { + scope.tt_content = val; + }); + + attrs.$observe( type+'Title', function ( val ) { + scope.tt_title = val; + }); + + attrs.$observe( type+'Placement', function ( val ) { + scope.tt_placement = angular.isDefined( val ) ? val : options.placement; + }); + + attrs.$observe( type+'Animation', function ( val ) { + scope.tt_animation = angular.isDefined( val ) ? $parse( val ) : function(){ return options.animation; }; + }); + + // By default, the tooltip is not open. + // TODO add ability to start tooltip opened + scope.tt_isOpen = false; - // And show the tooltip. - scope.tt_isOpen = true; - } - - // Hide the tooltip popup element. - function hide() { - // First things first: we don't show it anymore. - //tooltip.removeClass( 'in' ); - scope.tt_isOpen = 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. - if ( angular.isDefined( scope.tt_animation ) && scope.tt_animation() ) { - transitionTimeout = $timeout( function () { tooltip.remove(); }, 500 ); - } else { - tooltip.remove(); + // 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 + // toolip 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. + element.after( tooltip ); + + // Get the position of the directive element. + position = getPosition( 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 'right': + ttPosition = { + top: (position.top + position.height / 2 - ttHeight / 2) + 'px', + left: (position.left + position.width) + 'px' + }; + break; + case 'bottom': + ttPosition = { + top: (position.top + position.height) + 'px', + left: (position.left + position.width / 2 - ttWidth / 2) + 'px' + }; + break; + case 'left': + ttPosition = { + top: (position.top + position.height / 2 - ttHeight / 2) + 'px', + left: (position.left - ttWidth) + 'px' + }; + break; + default: + ttPosition = { + top: (position.top - ttHeight) + 'px', + left: (position.left + position.width / 2 - ttWidth / 2) + 'px' + }; + break; + } + + // 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. + //tooltip.removeClass( 'in' ); + scope.tt_isOpen = 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. + if ( angular.isDefined( scope.tt_animation ) && scope.tt_animation() ) { + transitionTimeout = $timeout( function () { tooltip.remove(); }, 500 ); + } else { + tooltip.remove(); + } + } + + // Register the event listeners. If only one event listener was + // supplied, we use the same event listener for showing and hiding. + // TODO add ability to customize event triggers + if ( ! angular.isDefined( defaultTriggerHide ) ) { + element.bind( defaultTriggerShow, function toggleTooltipBind () { + if ( ! scope.tt_isOpen ) { + scope.$apply( show ); + } else { + scope.$apply( hide ); + } + }); + } else { + element.bind( defaultTriggerShow, function showTooltipBind() { + scope.$apply( show ); + }); + element.bind( defaultTriggerHide, function hideTooltipBind() { + scope.$apply( hide ); + }); + } } - } - - // Register the event listeners. - element.bind( 'mouseenter', function() { - scope.$apply( show ); - }); - element.bind( 'mouseleave', function() { - scope.$apply( 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', 'mouseenter', 'mouseleave' ); }]); diff --git a/template/popover/popover.html b/template/popover/popover.html index 1d1104dd2b..5929ee6e6a 100644 --- a/template/popover/popover.html +++ b/template/popover/popover.html @@ -2,7 +2,7 @@
-

-
+

+
diff --git a/template/tooltip/tooltip-popup.html b/template/tooltip/tooltip-popup.html index 77d35de8e2..fd51120774 100644 --- a/template/tooltip/tooltip-popup.html +++ b/template/tooltip/tooltip-popup.html @@ -1,4 +1,4 @@
-
+