diff --git a/src/tooltip/test/tooltip.spec.js b/src/tooltip/test/tooltip.spec.js index 01f6423fe7..90be4f6907 100644 --- a/src/tooltip/test/tooltip.spec.js +++ b/src/tooltip/test/tooltip.spec.js @@ -98,7 +98,7 @@ describe('tooltip', function() { scope.alt = "Alt Message"; elmBody = $compile( angular.element( - '
Selector Text
' + '
Selector Text
' ) )( scope ); $compile( elmBody )( scope ); @@ -114,6 +114,13 @@ describe('tooltip', function() { expect( ttScope.content ).toBe( scope.tooltipMsg ); elm.trigger( 'mouseleave' ); + + //Isolate scope contents should be the same after hiding and showing again (issue 1191) + elm.trigger( 'mouseenter' ); + + ttScope = angular.element( elmBody.children()[1] ).scope(); + expect( ttScope.placement ).toBe( 'top' ); + expect( ttScope.content ).toBe( scope.tooltipMsg ); })); it('should not show tooltips if there is nothing to show - issue #129', inject(function ($compile) { @@ -136,6 +143,24 @@ describe('tooltip', function() { expect( elmBody.children().length ).toBe( 0 ); })); + it('issue 1191 - isolate scope on the popup should always be child of correct element scope', inject( function ( $compile ) { + var ttScope; + elm.trigger( 'mouseenter' ); + + ttScope = angular.element( elmBody.children()[1] ).scope(); + expect( ttScope.$parent ).toBe( elmScope ); + + elm.trigger( 'mouseleave' ); + + // After leaving and coming back, the scope's parent should be the same + elm.trigger( 'mouseenter' ); + + ttScope = angular.element( elmBody.children()[1] ).scope(); + expect( ttScope.$parent ).toBe( elmScope ); + + elm.trigger( 'mouseleave' ); + })); + describe('with specified enable expression', function() { beforeEach(inject(function ($compile) { @@ -323,18 +348,12 @@ describe('tooltip', function() { elm = elmBody.find('input'); elmScope = elm.scope(); + elm.trigger('fooTrigger'); tooltipScope = elmScope.$$childTail; })); - it( 'should not contain a cached reference', function() { - expect( inCache() ).toBeTruthy(); - elmScope.$destroy(); - expect( inCache() ).toBeFalsy(); - }); - it( 'should not contain a cached reference when visible', inject( function( $timeout ) { expect( inCache() ).toBeTruthy(); - elm.trigger('fooTrigger'); elmScope.$destroy(); expect( inCache() ).toBeFalsy(); })); diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index f6d116647f..c6a610f8f3 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -108,224 +108,245 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap return { restrict: 'EA', scope: true, - link: function link ( scope, element, attrs ) { - var tooltip = $compile( template )( scope ); - var transitionTimeout; - var popupTimeout; - var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; - var triggers = getTriggers( undefined ); - var hasRegisteredTriggers = false; - var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); - - var positionTooltip = function (){ - var position, - ttWidth, - ttHeight, - ttPosition; - // Get the position of the directive element. - position = 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 '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; + compile: function (tElem, tAttrs) { + var tooltipLinker = $compile( template ); + + return function link ( scope, element, attrs ) { + var tooltip; + var transitionTimeout; + var popupTimeout; + var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; + var triggers = getTriggers( undefined ); + var hasRegisteredTriggers = false; + var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); + + var positionTooltip = function (){ + var position, + ttWidth, + ttHeight, + ttPosition; + // Get the position of the directive element. + position = 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 '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 ); + + }; + + // 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(); + } } - ttPosition.top += 'px'; - ttPosition.left += 'px'; + // Show the tooltip with delay if specified, otherwise show it immediately + function showTooltipBind() { + if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { + return; + } + if ( scope.tt_popupDelay ) { + popupTimeout = $timeout( show, scope.tt_popupDelay, false ); + popupTimeout.then(function(reposition){reposition();}); + } else { + show()(); + } + } - // Now set the calculated positioning. - tooltip.css( ttPosition ); + function hideTooltipBind () { + scope.$apply(function () { + hide(); + }); + } - }; + // Show the tooltip popup element. + function show() { - // 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(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { - return; - } - if ( scope.tt_popupDelay ) { - popupTimeout = $timeout( show, scope.tt_popupDelay ); - popupTimeout.then(function(reposition){reposition();}); - } else { - scope.$apply( show )(); - } - } + // Don't show empty tooltips. + if ( ! scope.tt_content ) { + return angular.noop; + } - function hideTooltipBind () { - scope.$apply(function () { - hide(); - }); - } - - // Show the tooltip popup element. - function show() { + createTooltip(); + // If there is a pending remove transition, we must cancel it, lest the + // tooltip be mysteriously removed. + if ( transitionTimeout ) { + $timeout.cancel( transitionTimeout ); + } - // Don't show empty tooltips. - if ( ! scope.tt_content ) { - return angular.noop; - } + // Set the initial positioning. + tooltip.css({ top: 0, left: 0, display: 'block' }); - // 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 ( appendToBody ) { - $document.find( 'body' ).append( tooltip ); - } else { - element.after( tooltip ); + // Now we add it to the DOM because need some info about it. But it's not + // visible yet anyway. + if ( appendToBody ) { + $document.find( 'body' ).append( tooltip ); + } else { + element.after( tooltip ); + } + + positionTooltip(); + + // And show the tooltip. + scope.tt_isOpen = true; + scope.$digest(); // digest required as $apply is not called + + // Return positioning function as promise callback for correct + // positioning after draw. + return positionTooltip; } - positionTooltip(); + // 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 ( scope.tt_animation ) { + transitionTimeout = $timeout(removeTooltip, 500); + } else { + removeTooltip(); + } + } - // And show the tooltip. - scope.tt_isOpen = true; + function createTooltip() { + // There can only be one tooltip element per directive shown at once. + if (tooltip) { + removeTooltip(); + } + tooltip = tooltipLinker(scope, function () {}); - // Return positioning function as promise callback for correct - // positioning after draw. - return positionTooltip; - } - - // Hide the tooltip popup element. - function hide() { - // First things first: we don't show it anymore. - scope.tt_isOpen = false; + // Get contents rendered into the tooltip + scope.$digest(); + } - //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 ( scope.tt_animation ) { - transitionTimeout = $timeout(function () { + function removeTooltip() { + if (tooltip) { tooltip.remove(); - }, 500); - } else { - tooltip.remove(); + tooltip = null; + } } - } - /** - * Observe the relevant attributes. - */ - attrs.$observe( type, function ( val ) { - scope.tt_content = val; + /** + * Observe the relevant attributes. + */ + attrs.$observe( type, function ( val ) { + scope.tt_content = val; - if (!val && scope.tt_isOpen ) { - hide(); - } - }); + if (!val && scope.tt_isOpen ) { + hide(); + } + }); - attrs.$observe( prefix+'Title', function ( val ) { - scope.tt_title = 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+'Placement', function ( val ) { + scope.tt_placement = angular.isDefined( val ) ? val : options.placement; + }); - attrs.$observe( prefix+'PopupDelay', function ( val ) { - var delay = parseInt( val, 10 ); - scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay; - }); + attrs.$observe( prefix+'PopupDelay', function ( val ) { + var delay = parseInt( val, 10 ); + scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay; + }); - var unregisterTriggers = function() { - if (hasRegisteredTriggers) { - element.unbind( triggers.show, showTooltipBind ); - element.unbind( triggers.hide, hideTooltipBind ); - } - }; + var unregisterTriggers = function() { + if (hasRegisteredTriggers) { + element.unbind( triggers.show, showTooltipBind ); + element.unbind( triggers.hide, hideTooltipBind ); + } + }; - attrs.$observe( prefix+'Trigger', function ( val ) { - unregisterTriggers(); + attrs.$observe( prefix+'Trigger', function ( val ) { + unregisterTriggers(); - triggers = getTriggers( val ); + triggers = getTriggers( val ); - if ( triggers.show === triggers.hide ) { - element.bind( triggers.show, toggleTooltipBind ); - } else { - element.bind( triggers.show, showTooltipBind ); - element.bind( triggers.hide, hideTooltipBind ); - } + if ( triggers.show === triggers.hide ) { + element.bind( triggers.show, toggleTooltipBind ); + } else { + element.bind( triggers.show, showTooltipBind ); + element.bind( triggers.hide, hideTooltipBind ); + } - hasRegisteredTriggers = true; - }); + hasRegisteredTriggers = true; + }); - var animation = scope.$eval(attrs[prefix + 'Animation']); - scope.tt_animation = angular.isDefined(animation) ? !!animation : options.animation; + var animation = scope.$eval(attrs[prefix + 'Animation']); + scope.tt_animation = angular.isDefined(animation) ? !!animation : options.animation; - attrs.$observe( prefix+'AppendToBody', function ( val ) { - appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : appendToBody; - }); + attrs.$observe( prefix+'AppendToBody', function ( val ) { + appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : appendToBody; + }); - // if a tooltip is attached to 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(); + // if a tooltip is attached to 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() { - $timeout.cancel( transitionTimeout ); - $timeout.cancel( popupTimeout ); - unregisterTriggers(); - tooltip.remove(); - tooltip.unbind(); - tooltip = null; - }); + + // Make sure tooltip is destroyed and removed. + scope.$on('$destroy', function onDestroyTooltip() { + $timeout.cancel( transitionTimeout ); + $timeout.cancel( popupTimeout ); + unregisterTriggers(); + removeTooltip(); + }); + }; } }; };