From 876991af42539f20863d9816b9a6c7ccba421469 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Fri, 17 Jan 2014 01:56:01 -0800 Subject: [PATCH] fix(tooltip): remove child scope requirement tt_ scope variables are now in the ttScope which is specific to each tooltip directive (multiple tooltip directives can now run on the same element). This allows tooltips to be used alongside any directive that may or may not require different scope types. Closes #1269 Fixes #2320 Fixes #2203 --- src/popover/test/popover.spec.js | 10 ++- src/tooltip/test/tooltip.spec.js | 122 ++++++++++++++++++------------- src/tooltip/tooltip.js | 49 +++++++------ 3 files changed, 102 insertions(+), 79 deletions(-) diff --git a/src/popover/test/popover.spec.js b/src/popover/test/popover.spec.js index 7e6f2b3970..7a23c75c87 100644 --- a/src/popover/test/popover.spec.js +++ b/src/popover/test/popover.spec.js @@ -2,7 +2,8 @@ describe('popover', function() { var elm, elmBody, scope, - elmScope; + elmScope, + tooltipScope; // load the popover code beforeEach(module('ui.bootstrap.popover')); @@ -20,10 +21,11 @@ describe('popover', function() { scope.$digest(); elm = elmBody.find('span'); elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; })); it('should not be open initially', inject(function() { - expect( elmScope.tt_isOpen ).toBe( false ); + expect( tooltipScope.isOpen ).toBe( false ); // We can only test *that* the popover-popup element wasn't created as the // implementation is templated and replaced. @@ -32,7 +34,7 @@ describe('popover', function() { it('should open on click', inject(function() { elm.trigger( 'click' ); - expect( elmScope.tt_isOpen ).toBe( true ); + expect( tooltipScope.isOpen ).toBe( true ); // We can only test *that* the popover-popup element was created as the // implementation is templated and replaced. @@ -42,7 +44,7 @@ describe('popover', function() { it('should close on second click', inject(function() { elm.trigger( 'click' ); elm.trigger( 'click' ); - expect( elmScope.tt_isOpen ).toBe( false ); + expect( tooltipScope.isOpen ).toBe( false ); })); it('should not unbind event handlers created by other directives - issue 456', inject( function( $compile ) { diff --git a/src/tooltip/test/tooltip.spec.js b/src/tooltip/test/tooltip.spec.js index 73d2fcba82..b7f42eb08b 100644 --- a/src/tooltip/test/tooltip.spec.js +++ b/src/tooltip/test/tooltip.spec.js @@ -2,7 +2,8 @@ describe('tooltip', function() { var elm, elmBody, scope, - elmScope; + elmScope, + tooltipScope; // load the tooltip code beforeEach(module('ui.bootstrap.tooltip')); @@ -20,10 +21,11 @@ describe('tooltip', function() { scope.$digest(); elm = elmBody.find('span'); elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; })); it('should not be open initially', inject(function() { - expect( elmScope.tt_isOpen ).toBe( false ); + expect( tooltipScope.isOpen ).toBe( false ); // We can only test *that* the tooltip-popup element wasn't created as the // implementation is templated and replaced. @@ -32,7 +34,7 @@ describe('tooltip', function() { it('should open on mouseenter', inject(function() { elm.trigger( 'mouseenter' ); - expect( elmScope.tt_isOpen ).toBe( true ); + expect( tooltipScope.isOpen ).toBe( true ); // We can only test *that* the tooltip-popup element was created as the // implementation is templated and replaced. @@ -42,16 +44,16 @@ describe('tooltip', function() { it('should close on mouseleave', inject(function() { elm.trigger( 'mouseenter' ); elm.trigger( 'mouseleave' ); - expect( elmScope.tt_isOpen ).toBe( false ); + expect( tooltipScope.isOpen ).toBe( false ); })); it('should not animate on animation set to false', inject(function() { - expect( elmScope.tt_animation ).toBe( false ); + expect( tooltipScope.animation ).toBe( false ); })); it('should have default placement of "top"', inject(function() { elm.trigger( 'mouseenter' ); - expect( elmScope.tt_placement ).toBe( 'top' ); + expect( tooltipScope.placement ).toBe( 'top' ); })); it('should allow specification of placement', inject( function( $compile ) { @@ -60,9 +62,10 @@ describe('tooltip', function() { ) )( scope ); scope.$apply(); elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; elm.trigger( 'mouseenter' ); - expect( elmScope.tt_placement ).toBe( 'bottom' ); + expect( tooltipScope.placement ).toBe( 'bottom' ); })); it('should work inside an ngRepeat', inject( function( $compile ) { @@ -86,7 +89,9 @@ describe('tooltip', function() { tt.trigger( 'mouseenter' ); expect( tt.text() ).toBe( scope.items[0].name ); - expect( tt.scope().tt_content ).toBe( scope.items[0].tooltip ); + + tooltipScope = tt.scope().$$childTail; + expect( tooltipScope.content ).toBe( scope.items[0].tooltip ); tt.trigger( 'mouseleave' ); })); @@ -136,7 +141,7 @@ describe('tooltip', function() { it( 'should close the tooltip when its trigger element is destroyed', inject( function() { elm.trigger( 'mouseenter' ); - expect( elmScope.tt_isOpen ).toBe( true ); + expect( tooltipScope.isOpen ).toBe( true ); elm.remove(); elmScope.$destroy(); @@ -148,7 +153,7 @@ describe('tooltip', function() { elm.trigger( 'mouseenter' ); ttScope = angular.element( elmBody.children()[1] ).isolateScope(); - expect( ttScope.$parent ).toBe( elmScope ); + expect( ttScope.$parent ).toBe( tooltipScope ); elm.trigger( 'mouseleave' ); @@ -156,7 +161,7 @@ describe('tooltip', function() { elm.trigger( 'mouseenter' ); ttScope = angular.element( elmBody.children()[1] ).isolateScope(); - expect( ttScope.$parent ).toBe( elmScope ); + expect( ttScope.$parent ).toBe( tooltipScope ); elm.trigger( 'mouseleave' ); }); @@ -171,13 +176,14 @@ describe('tooltip', function() { scope.$digest(); elm = elmBody.find('span'); elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; })); it('should not open ', inject(function () { elm.trigger('mouseenter'); - expect(elmScope.tt_isOpen).toBeFalsy(); + expect(tooltipScope.isOpen).toBeFalsy(); expect(elmBody.children().length).toBe(1); })); @@ -187,7 +193,7 @@ describe('tooltip', function() { scope.enable = true; scope.$digest(); elm.trigger('mouseenter'); - expect(elmScope.tt_isOpen).toBeTruthy(); + expect(tooltipScope.isOpen).toBeTruthy(); expect(elmBody.children().length).toBe(2); })); @@ -201,33 +207,34 @@ describe('tooltip', function() { 'Selector Text' ))(scope); elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; scope.$digest(); })); it('should open after timeout', inject(function ($timeout) { elm.trigger('mouseenter'); - expect(elmScope.tt_isOpen).toBe(false); + expect(tooltipScope.isOpen).toBe(false); $timeout.flush(); - expect(elmScope.tt_isOpen).toBe(true); + expect(tooltipScope.isOpen).toBe(true); })); it('should not open if mouseleave before timeout', inject(function ($timeout) { elm.trigger('mouseenter'); - expect(elmScope.tt_isOpen).toBe(false); + expect(tooltipScope.isOpen).toBe(false); elm.trigger('mouseleave'); $timeout.flush(); - expect(elmScope.tt_isOpen).toBe(false); + expect(tooltipScope.isOpen).toBe(false); })); it('should use default popup delay if specified delay is not a number', function(){ scope.delay='text1000'; scope.$digest(); elm.trigger('mouseenter'); - expect(elmScope.tt_isOpen).toBe(true); + expect(tooltipScope.isOpen).toBe(true); }); }); @@ -247,12 +254,13 @@ describe('tooltip', function() { scope.$apply(); elm = elmBody.find('input'); elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; - expect( elmScope.tt_isOpen ).toBeFalsy(); + expect( tooltipScope.isOpen ).toBeFalsy(); elm.trigger('focus'); - expect( elmScope.tt_isOpen ).toBeTruthy(); + expect( tooltipScope.isOpen ).toBeTruthy(); elm.trigger('blur'); - expect( elmScope.tt_isOpen ).toBeFalsy(); + expect( tooltipScope.isOpen ).toBeFalsy(); })); it( 'should use it as both the show and hide triggers for unmapped triggers', inject( function( $compile ) { @@ -263,12 +271,13 @@ describe('tooltip', function() { scope.$apply(); elm = elmBody.find('input'); elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; - expect( elmScope.tt_isOpen ).toBeFalsy(); + expect( tooltipScope.isOpen ).toBeFalsy(); elm.trigger('fakeTriggerAttr'); - expect( elmScope.tt_isOpen ).toBeTruthy(); + expect( tooltipScope.isOpen ).toBeTruthy(); elm.trigger('fakeTriggerAttr'); - expect( elmScope.tt_isOpen ).toBeFalsy(); + expect( tooltipScope.isOpen ).toBeFalsy(); })); it('should only set up triggers once', inject( function ($compile) { @@ -287,16 +296,17 @@ describe('tooltip', function() { var elm2 = elmBody.find('input').eq(1); var elmScope1 = elm1.scope(); var elmScope2 = elm2.scope(); + var tooltipScope2 = elmScope2.$$childTail; scope.$apply('test = false'); // click trigger isn't set elm2.click(); - expect( elmScope2.tt_isOpen ).toBeFalsy(); + expect( tooltipScope2.isOpen ).toBeFalsy(); // mouseenter trigger is still set elm2.trigger('mouseenter'); - expect( elmScope2.tt_isOpen ).toBeTruthy(); + expect( tooltipScope2.isOpen ).toBeTruthy(); })); }); @@ -321,11 +331,12 @@ describe('tooltip', function() { scope.$digest(); elm = elmBody.find('span'); elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; var bodyLength = $body.children().length; elm.trigger( 'mouseenter' ); - expect( elmScope.tt_isOpen ).toBe( true ); + expect( tooltipScope.isOpen ).toBe( true ); expect( elmBody.children().length ).toBe( 1 ); expect( $body.children().length ).toEqual( bodyLength + 1 ); })); @@ -355,7 +366,7 @@ describe('tooltip', function() { elm = elmBody.find('input'); elmScope = elm.scope(); elm.trigger('fooTrigger'); - tooltipScope = elmScope.$$childTail; + tooltipScope = elmScope.$$childTail.$$childTail; })); it( 'should not contain a cached reference when visible', inject( function( $timeout ) { @@ -398,7 +409,7 @@ describe('tooltipWithDifferentSymbols', function() { }); describe( 'tooltipHtmlUnsafe', function() { - var elm, elmBody, elmScope, scope; + var elm, elmBody, elmScope, tooltipScope, scope; // load the tooltip code beforeEach(module('ui.bootstrap.tooltip', function ( $tooltipProvider ) { @@ -418,6 +429,7 @@ describe( 'tooltipHtmlUnsafe', function() { scope.$digest(); elm = elmBody.find('span'); elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; })); it( 'should render html properly', inject( function () { @@ -426,16 +438,16 @@ describe( 'tooltipHtmlUnsafe', function() { })); it( 'should show on mouseenter and hide on mouseleave', inject( function () { - expect( elmScope.tt_isOpen ).toBe( false ); + expect( tooltipScope.isOpen ).toBe( false ); elm.trigger( 'mouseenter' ); - expect( elmScope.tt_isOpen ).toBe( true ); + expect( tooltipScope.isOpen ).toBe( true ); expect( elmBody.children().length ).toBe( 2 ); - expect( elmScope.tt_content ).toEqual( scope.html ); + expect( tooltipScope.content ).toEqual( scope.html ); elm.trigger( 'mouseleave' ); - expect( elmScope.tt_isOpen ).toBe( false ); + expect( tooltipScope.isOpen ).toBe( false ); expect( elmBody.children().length ).toBe( 1 ); })); }); @@ -444,7 +456,8 @@ describe( '$tooltipProvider', function() { var elm, elmBody, scope, - elmScope; + elmScope, + tooltipScope; describe( 'popupDelay', function() { beforeEach(module('ui.bootstrap.tooltip', function($tooltipProvider){ @@ -464,15 +477,16 @@ describe( '$tooltipProvider', function() { scope.$digest(); elm = elmBody.find('span'); elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; })); it('should open after timeout', inject(function($timeout) { elm.trigger( 'mouseenter' ); - expect( elmScope.tt_isOpen ).toBe( false ); + expect( tooltipScope.isOpen ).toBe( false ); $timeout.flush(); - expect( elmScope.tt_isOpen ).toBe( true ); + expect( tooltipScope.isOpen ).toBe( true ); })); @@ -503,11 +517,12 @@ describe( '$tooltipProvider', function() { scope.$digest(); elm = elmBody.find('span'); elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; var bodyLength = $body.children().length; elm.trigger( 'mouseenter' ); - expect( elmScope.tt_isOpen ).toBe( true ); + expect( tooltipScope.isOpen ).toBe( true ); expect( elmBody.children().length ).toBe( 1 ); expect( $body.children().length ).toEqual( bodyLength + 1 ); })); @@ -523,13 +538,14 @@ describe( '$tooltipProvider', function() { scope.$digest(); elm = elmBody.find('span'); elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; elm.trigger( 'mouseenter' ); - expect( elmScope.tt_isOpen ).toBe( true ); + expect( tooltipScope.isOpen ).toBe( true ); scope.$broadcast('$locationChangeSuccess'); scope.$digest(); - expect( elmScope.tt_isOpen ).toBe( false ); + expect( tooltipScope.isOpen ).toBe( false ); })); }); @@ -552,12 +568,13 @@ describe( '$tooltipProvider', function() { scope.$digest(); elm = elmBody.find('input'); elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; - expect( elmScope.tt_isOpen ).toBeFalsy(); + expect( tooltipScope.isOpen ).toBeFalsy(); elm.trigger('focus'); - expect( elmScope.tt_isOpen ).toBeTruthy(); + expect( tooltipScope.isOpen ).toBeTruthy(); elm.trigger('blur'); - expect( elmScope.tt_isOpen ).toBeFalsy(); + expect( tooltipScope.isOpen ).toBeFalsy(); })); it( 'should override the show and hide triggers if there is an attribute', inject( function ( $rootScope, $compile ) { @@ -570,12 +587,13 @@ describe( '$tooltipProvider', function() { scope.$digest(); elm = elmBody.find('input'); elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; - expect( elmScope.tt_isOpen ).toBeFalsy(); + expect( tooltipScope.isOpen ).toBeFalsy(); elm.trigger('mouseenter'); - expect( elmScope.tt_isOpen ).toBeTruthy(); + expect( tooltipScope.isOpen ).toBeTruthy(); elm.trigger('mouseleave'); - expect( elmScope.tt_isOpen ).toBeFalsy(); + expect( tooltipScope.isOpen ).toBeFalsy(); })); }); @@ -598,12 +616,13 @@ describe( '$tooltipProvider', function() { scope.$digest(); elm = elmBody.find('input'); elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; - expect( elmScope.tt_isOpen ).toBeFalsy(); + expect( tooltipScope.isOpen ).toBeFalsy(); elm.trigger('customOpenTrigger'); - expect( elmScope.tt_isOpen ).toBeTruthy(); + expect( tooltipScope.isOpen ).toBeTruthy(); elm.trigger('customCloseTrigger'); - expect( elmScope.tt_isOpen ).toBeFalsy(); + expect( tooltipScope.isOpen ).toBeFalsy(); })); }); @@ -625,12 +644,13 @@ describe( '$tooltipProvider', function() { scope.$digest(); elm = elmBody.find('span'); elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; - expect( elmScope.tt_isOpen ).toBeFalsy(); + expect( tooltipScope.isOpen ).toBeFalsy(); elm.trigger('fakeTrigger'); - expect( elmScope.tt_isOpen ).toBeTruthy(); + expect( tooltipScope.isOpen ).toBeTruthy(); elm.trigger('fakeTrigger'); - expect( elmScope.tt_isOpen ).toBeFalsy(); + expect( tooltipScope.isOpen ).toBeFalsy(); })); }); }); diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index 3bbf9be9cc..9980d48c77 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -97,17 +97,16 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap var endSym = $interpolate.endSymbol(); var template = '
'+ '
'; return { restrict: 'EA', - scope: true, compile: function (tElem, tAttrs) { var tooltipLinker = $compile( template ); @@ -118,10 +117,11 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; var triggers = getTriggers( undefined ); var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); + var ttScope = scope.$new(true); var positionTooltip = function () { - var ttPosition = $position.positionElements(element, tooltip, scope.tt_placement, appendToBody); + var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); ttPosition.top += 'px'; ttPosition.left += 'px'; @@ -131,10 +131,10 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap // By default, the tooltip is not open. // TODO add ability to start tooltip opened - scope.tt_isOpen = false; + ttScope.isOpen = false; function toggleTooltipBind () { - if ( ! scope.tt_isOpen ) { + if ( ! ttScope.isOpen ) { showTooltipBind(); } else { hideTooltipBind(); @@ -149,11 +149,11 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap prepareTooltip(); - if ( scope.tt_popupDelay ) { + if ( ttScope.popupDelay ) { // Do nothing if the tooltip was already scheduled to pop-up. // This happens if show is triggered multiple times before any hide is triggered. if (!popupTimeout) { - popupTimeout = $timeout( show, scope.tt_popupDelay, false ); + popupTimeout = $timeout( show, ttScope.popupDelay, false ); popupTimeout.then(function(reposition){reposition();}); } } else { @@ -180,7 +180,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap } // Don't show empty tooltips. - if ( ! scope.tt_content ) { + if ( ! ttScope.content ) { return angular.noop; } @@ -200,8 +200,8 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap positionTooltip(); // And show the tooltip. - scope.tt_isOpen = true; - scope.$digest(); // digest required as $apply is not called + ttScope.isOpen = true; + ttScope.$digest(); // digest required as $apply is not called // Return positioning function as promise callback for correct // positioning after draw. @@ -211,7 +211,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap // Hide the tooltip popup element. function hide() { // First things first: we don't show it anymore. - scope.tt_isOpen = false; + ttScope.isOpen = false; //if tooltip is going to be shown after delay, we must cancel this $timeout.cancel( popupTimeout ); @@ -220,7 +220,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap // 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 ( scope.tt_animation ) { + if ( ttScope.animation ) { if (!transitionTimeout) { transitionTimeout = $timeout(removeTooltip, 500); } @@ -234,7 +234,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap if (tooltip) { removeTooltip(); } - tooltip = tooltipLinker(scope); + tooltip = tooltipLinker(ttScope); } function removeTooltip() { @@ -254,26 +254,26 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap * Observe the relevant attributes. */ attrs.$observe( type, function ( val ) { - scope.tt_content = val; + ttScope.content = val; - if (!val && scope.tt_isOpen ) { + if (!val && ttScope.isOpen ) { hide(); } }); attrs.$observe( prefix+'Title', function ( val ) { - scope.tt_title = val; + ttScope.title = val; }); function prepPlacement() { var val = attrs[ prefix + 'Placement' ]; - scope.tt_placement = angular.isDefined( val ) ? val : options.placement; + ttScope.placement = angular.isDefined( val ) ? val : options.placement; } function prepPopupDelay() { var val = attrs[ prefix + 'PopupDelay' ]; var delay = parseInt( val, 10 ); - scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay; + ttScope.popupDelay = ! isNaN(delay) ? delay : options.popupDelay; } var unregisterTriggers = function () { @@ -297,7 +297,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap prepTriggers(); var animation = scope.$eval(attrs[prefix + 'Animation']); - scope.tt_animation = angular.isDefined(animation) ? !!animation : options.animation; + ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation; var appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']); appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody; @@ -307,7 +307,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap // by the change. if ( appendToBody ) { scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { - if ( scope.tt_isOpen ) { + if ( ttScope.isOpen ) { hide(); } }); @@ -319,6 +319,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap $timeout.cancel( popupTimeout ); unregisterTriggers(); removeTooltip(); + ttScope = null; }); }; }