diff --git a/src/popover/docs/demo.html b/src/popover/docs/demo.html index d8bbf5b10c..2a9bdfb1b4 100644 --- a/src/popover/docs/demo.html +++ b/src/popover/docs/demo.html @@ -14,6 +14,13 @@

Positional

+
+

Triggers

+ + +

Other

diff --git a/src/popover/docs/readme.md b/src/popover/docs/readme.md index 8b43c66c62..e94651ccb3 100644 --- a/src/popover/docs/readme.md +++ b/src/popover/docs/readme.md @@ -13,6 +13,8 @@ will display: - `popover-animation`: Should it fade in and out? Defaults to "true". - `popover-popup-delay`: For how long should the user have to have the mouse over the element before the popover shows (in milliseconds)? Defaults to 0. +- `popover-trigger`: What should trigger the show of the popover? See the + `tooltip` directive for supported values. The popover directives require the `$position` service. diff --git a/src/popover/test/popoverSpec.js b/src/popover/test/popoverSpec.js index 81e37d4734..b7c5b34ad7 100644 --- a/src/popover/test/popoverSpec.js +++ b/src/popover/test/popoverSpec.js @@ -44,92 +44,6 @@ describe('popover', function() { elm.trigger( 'click' ); expect( elmScope.tt_isOpen ).toBe( false ); })); - - it('should have default placement of "top"', inject(function() { - elm.trigger( 'click' ); - expect( elmScope.tt_placement ).toBe( "top" ); - })); - - it('should allow specification of placement', inject( function( $compile ) { - elm = $compile( angular.element( - 'Selector Text' - ) )( scope ); - elmScope = elm.scope(); - - elm.trigger( 'click' ); - expect( elmScope.tt_placement ).toBe( "bottom" ); - })); - - it('should work inside an ngRepeat', inject( function( $compile ) { - - elm = $compile( angular.element( - '' - ) )( scope ); - - scope.items = [ - { name: "One", popover: "First popover" } - ]; - - scope.$digest(); - - var tt = angular.element( elm.find("li > span")[0] ); - - tt.trigger( 'click' ); - - expect( tt.text() ).toBe( scope.items[0].name ); - expect( tt.scope().tt_content ).toBe( scope.items[0].popover ); - - tt.trigger( 'click' ); - })); - - it('should only have an isolate scope on the popup', inject( function ( $compile ) { - var ttScope; - - scope.popoverContent = "Popover Content"; - scope.popoverTitle = "Popover Title"; - scope.alt = "Alt Message"; - - elmBody = $compile( angular.element( - '
Selector Text
' - ) )( scope ); - - $compile( elmBody )( scope ); - scope.$digest(); - elm = elmBody.find( 'span' ); - elmScope = elm.scope(); - - elm.trigger( 'click' ); - expect( elm.attr( 'alt' ) ).toBe( scope.alt ); - - ttScope = angular.element( elmBody.children()[1] ).scope(); - expect( ttScope.placement ).toBe( 'top' ); - expect( ttScope.title ).toBe( scope.popoverTitle ); - expect( ttScope.content ).toBe( scope.popoverContent ); - - elm.trigger( 'click' ); - })); - - - it( 'should allow specification of delay', inject( function ($timeout, $compile) { - - elm = $compile( angular.element( - 'Selector Text' - ) )( scope ); - elmScope = elm.scope(); - scope.$digest(); - - elm.trigger( 'click' ); - expect( elmScope.tt_isOpen ).toBe( false ); - - $timeout.flush(); - expect( elmScope.tt_isOpen ).toBe( true ); - - } ) ); - }); diff --git a/src/tooltip/docs/demo.html b/src/tooltip/docs/demo.html index 48047c559c..40c8a3c110 100644 --- a/src/tooltip/docs/demo.html +++ b/src/tooltip/docs/demo.html @@ -20,5 +20,12 @@

I can even contain HTML. Check me out!

+

+ Or use custom triggers, like focus: + +

diff --git a/src/tooltip/docs/readme.md b/src/tooltip/docs/readme.md index ca8f1b4a73..5ecb79563d 100644 --- a/src/tooltip/docs/readme.md +++ b/src/tooltip/docs/readme.md @@ -15,6 +15,19 @@ will display: - `tooltip-animation`: Should it fade in and out? Defaults to "true". - `tooltip-popup-delay`: For how long should the user have to have the mouse over the element before the tooltip shows (in milliseconds)? Defaults to 0. +- `tooltip-trigger`: What should trigger a show of the tooltip? The tooltip directives require the `$position` service. +**Triggers** + +The following show triggers are supported out of the box, along with their +provided hide triggers: + +- `mouseenter`: `mouseleave` +- `click`: `click` +- `focus`: `blur` + +For any non-supported value, the trigger will be used to both show and hide the +tooltip. + diff --git a/src/tooltip/test/tooltip.spec.js b/src/tooltip/test/tooltip.spec.js index fcda8b6997..1bc4e2e69b 100644 --- a/src/tooltip/test/tooltip.spec.js +++ b/src/tooltip/test/tooltip.spec.js @@ -54,6 +54,7 @@ describe('tooltip', function() { elm = $compile( angular.element( 'Selector Text' ) )( scope ); + scope.$apply(); elmScope = elm.scope(); elm.trigger( 'mouseenter' ); @@ -161,6 +162,46 @@ describe('tooltip', function() { }); + describe( 'with a trigger attribute', function() { + var scope, elmBody, elm, elmScope; + + beforeEach( inject( function( $rootScope ) { + scope = $rootScope; + })); + + it( 'should use it to show but set the hide trigger based on the map for mapped triggers', inject( function( $compile ) { + elmBody = angular.element( + '
' + ); + $compile(elmBody)(scope); + scope.$apply(); + elm = elmBody.find('input'); + elmScope = elm.scope(); + + expect( elmScope.tt_isOpen ).toBeFalsy(); + elm.trigger('focus'); + expect( elmScope.tt_isOpen ).toBeTruthy(); + elm.trigger('blur'); + expect( elmScope.tt_isOpen ).toBeFalsy(); + })); + + it( 'should use it as both the show and hide triggers for unmapped triggers', inject( function( $compile ) { + elmBody = angular.element( + '
' + ); + $compile(elmBody)(scope); + scope.$apply(); + elm = elmBody.find('input'); + elmScope = elm.scope(); + + expect( elmScope.tt_isOpen ).toBeFalsy(); + elm.trigger('fakeTriggerAttr'); + expect( elmScope.tt_isOpen ).toBeTruthy(); + elm.trigger('fakeTriggerAttr'); + expect( elmScope.tt_isOpen ).toBeFalsy(); + })); + }); + }); describe( 'tooltipHtmlUnsafe', function() { @@ -202,13 +243,13 @@ describe( 'tooltipHtmlUnsafe', function() { }); describe( '$tooltipProvider', function() { - - describe( 'popupDelay', function() { - var elm, + var elm, elmBody, - scope, - elmScope; + scope, + elmScope, + body; + describe( 'popupDelay', function() { beforeEach(module('ui.bootstrap.tooltip', function($tooltipProvider){ $tooltipProvider.options({popupDelay: 1000}); })); @@ -241,12 +282,6 @@ describe( '$tooltipProvider', function() { }); describe('appendToBody', function() { - var elm, - elmBody, - scope, - elmScope, - body; - // load the tooltip code beforeEach(module('ui.bootstrap.tooltip', function ( $tooltipProvider ) { $tooltipProvider.options({ appendToBody: true }); @@ -275,5 +310,61 @@ describe( '$tooltipProvider', function() { expect( $body.children().length ).toEqual( bodyLength + 1 ); })); }); + + describe( 'triggers', function() { + describe( 'triggers with a mapped value', function() { + beforeEach(module('ui.bootstrap.tooltip', function($tooltipProvider){ + $tooltipProvider.options({trigger: 'focus'}); + })); + + // load the template + beforeEach(module('template/tooltip/tooltip-popup.html')); + + it( 'should use the show trigger and the mapped value for the hide trigger', inject( function ( $rootScope, $compile ) { + elmBody = angular.element( + '
' + ); + + scope = $rootScope; + $compile(elmBody)(scope); + scope.$digest(); + elm = elmBody.find('input'); + elmScope = elm.scope(); + + expect( elmScope.tt_isOpen ).toBeFalsy(); + elm.trigger('focus'); + expect( elmScope.tt_isOpen ).toBeTruthy(); + elm.trigger('blur'); + expect( elmScope.tt_isOpen ).toBeFalsy(); + })); + }); + + describe( 'triggers without a mapped value', function() { + beforeEach(module('ui.bootstrap.tooltip', function($tooltipProvider){ + $tooltipProvider.options({trigger: 'fakeTrigger'}); + })); + + // load the template + beforeEach(module('template/tooltip/tooltip-popup.html')); + + it( 'should use the show trigger to hide', inject( function ( $rootScope, $compile ) { + elmBody = angular.element( + '
Selector Text
' + ); + + scope = $rootScope; + $compile(elmBody)(scope); + scope.$digest(); + elm = elmBody.find('span'); + elmScope = elm.scope(); + + expect( elmScope.tt_isOpen ).toBeFalsy(); + elm.trigger('fakeTrigger'); + expect( elmScope.tt_isOpen ).toBeTruthy(); + elm.trigger('fakeTrigger'); + expect( elmScope.tt_isOpen ).toBeFalsy(); + })); + }); + }); }); diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index c72484ce71..281d532a4a 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -17,6 +17,13 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) 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 = {}; @@ -49,9 +56,41 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) * TODO support multiple triggers */ this.$get = [ '$window', '$compile', '$timeout', '$parse', '$document', '$position', function ( $window, $compile, $timeout, $parse, $document, $position ) { - return function $tooltip ( type, prefix, defaultTriggerShow, defaultTriggerHide ) { + 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 template = '<'+ directiveName +'-popup '+ @@ -72,39 +111,32 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) var popupTimeout; var $body; - 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; - }); - // By default, the tooltip is not open. // TODO add ability to start tooltip opened scope.tt_isOpen = false; - //show the tooltip with delay if specified, otherwise show it immediately - function showWithDelay() { - if( scope.tt_popupDelay ){ + 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 { + } else { scope.$apply( show ); } } + + function hideTooltipBind () { + scope.$apply(function () { + hide(); + }); + } // Show the tooltip popup element. function show() { @@ -182,7 +214,6 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) // Hide the tooltip popup element. function hide() { // First things first: we don't show it anymore. - //tooltip.removeClass( 'in' ); scope.tt_isOpen = false; //if tooltip is going to be shown after delay, we must cancel this @@ -198,25 +229,43 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) } } - // 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 ) { - showWithDelay(); - } else { - scope.$apply( hide ); - } - }); - } else { - element.bind( defaultTriggerShow, function showTooltipBind() { - showWithDelay(); - }); - element.bind( defaultTriggerHide, function hideTooltipBind() { - scope.$apply( 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 ); + } + }); } }; }; @@ -233,7 +282,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) }) .directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'tooltip', 'tooltip', 'mouseenter', 'mouseleave' ); + return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); }]) .directive( 'tooltipHtmlUnsafePopup', function () { @@ -246,7 +295,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) }) .directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter', 'mouseleave' ); + return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); }]) ;