Skip to content

Commit

Permalink
fix(tooltip): update placement on content change
Browse files Browse the repository at this point in the history
Since the recompute of position is a heavy calculation and given
that this happens only when tooltip is visible, ie position
is already calculated, we can reuse it to update the position when
widht/height changes.

Closes angular-ui#96
  • Loading branch information
bekos committed Dec 24, 2013
1 parent 161a996 commit 7576bd9
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 57 deletions.
39 changes: 37 additions & 2 deletions src/tooltip/test/tooltip2.spec.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
describe('tooltip directive', function () {

var $rootScope, $compile, $document, $timeout;
var $rootScope, $compile, $document, $timeout, $position;

beforeEach(module('ui.bootstrap.tooltip'));
beforeEach(module('template/tooltip/tooltip-popup.html'));
beforeEach(inject(function (_$rootScope_, _$compile_, _$document_, _$timeout_) {
beforeEach(inject(function (_$rootScope_, _$compile_, _$document_, _$timeout_, _$position_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
$document = _$document_;
$timeout = _$timeout_;
$position = _$position_;
}));

beforeEach(function(){
Expand Down Expand Up @@ -102,4 +103,38 @@ describe('tooltip directive', function () {
});

});

describe('position', function() {

beforeEach(function() {
spyOn($position, 'position').andCallThrough();
});

it('is called once on mouse enter and leave', function () {
var fragment = compileTooltip('<span tooltip="tooltip text">Trigger here</span>');

expect($position.position).not.toHaveBeenCalled();

fragment.find('span').trigger( 'mouseenter' );
expect($position.position.calls.length).toEqual(1);

closeTooltip(fragment.find('span'));
expect($position.position.calls.length).toEqual(1);
});

it('is called once for content updates when is open', function () {
$rootScope.content = 'some text';
var fragment = compileTooltip('<span tooltip="{{ content }}">Trigger here</span>');

expect($position.position).not.toHaveBeenCalled();

fragment.find('span').trigger( 'mouseenter' );
expect($position.position.calls.length).toEqual(1);

$rootScope.content = 'some new text';
$rootScope.$digest();
$timeout.flush();
expect($position.position.calls.length).toEqual(1);
});
});
});
116 changes: 61 additions & 55 deletions src/tooltip/tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,15 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
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 tooltip = $compile( template )( scope ),
transitionTimeout,
popupTimeout,
appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false,
triggers = getTriggers( undefined ),
hasRegisteredTriggers = false,
hasEnableExp = angular.isDefined(attrs[prefix+'Enable']),
position,
positionUpdateTimeout;

// By default, the tooltip is not open.
// TODO add ability to start tooltip opened
Expand Down Expand Up @@ -147,77 +149,74 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
});
}

// Show the tooltip popup element.
function show() {
var position,
ttWidth,
ttHeight,
// Calculate tootip's position
function updatePosition() {
var ttWidth = tooltip.prop( 'offsetWidth' ),
ttHeight = tooltip.prop( 'offsetHeight' ),
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 ( appendToBody ) {
$document.find( 'body' ).append( tooltip );
} else {
element.after( tooltip );
}

// 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
top: position.top + position.height / 2 - ttHeight / 2 + 'px',
left: position.left + position.width + 'px'
};
break;
case 'bottom':
ttPosition = {
top: position.top + position.height,
left: position.left + position.width / 2 - ttWidth / 2
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,
left: position.left - ttWidth
top: position.top + position.height / 2 - ttHeight / 2 + 'px',
left: position.left - ttWidth + 'px'
};
break;
default:
ttPosition = {
top: position.top - ttHeight,
left: position.left + position.width / 2 - ttWidth / 2
top: position.top - ttHeight + 'px',
left: position.left + position.width / 2 - ttWidth / 2 + 'px'
};
break;
}

ttPosition.top += 'px';
ttPosition.left += 'px';

// Now set the calculated positioning.
tooltip.css( ttPosition );

}

// Show the tooltip popup element.
function show() {
// 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 );
}

tooltip.css({ 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 );
}

// Get the position of the directive element.
position = appendToBody ? $position.offset( element ) : $position.position( element );

// Place tooltip
updatePosition();

// And show the tooltip.
scope.tt_isOpen = true;
}
Expand Down Expand Up @@ -248,8 +247,14 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
attrs.$observe( type, function ( val ) {
scope.tt_content = val;

if (!val && scope.tt_isOpen ) {
hide();
if (scope.tt_isOpen) {
if (!val) {
hide();
} else {
// Wait for content to be set on the tooltip element and adjust position
// based on the new width/height and last known position of the directive element.
positionUpdateTimeout = $timeout(updatePosition, 0, false);
}
}
});

Expand Down Expand Up @@ -311,6 +316,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
scope.$on('$destroy', function onDestroyTooltip() {
$timeout.cancel( transitionTimeout );
$timeout.cancel( popupTimeout );
$timeout.cancel( positionUpdateTimeout );
unregisterTriggers();
tooltip.remove();
tooltip.unbind();
Expand Down

0 comments on commit 7576bd9

Please sign in to comment.