Skip to content

Commit

Permalink
Reworked positioning and collision computations.
Browse files Browse the repository at this point in the history
Measurement compensation moved to CSSCoordinates.
Calculations redesigned based on study of measurement origin changes
based on CSS position, margin and border widths.
  • Loading branch information
Joel Steres committed Jun 16, 2017
1 parent fcedf69 commit b9713e4
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 64 deletions.
3 changes: 2 additions & 1 deletion src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ var session = {
windowWidth: 0,
windowHeight: 0,
scrollTop: 0,
scrollLeft: 0
scrollLeft: 0,
positionCompensation: { top: 0, bottom: 0, left: 0, right: 0 }
};

/**
Expand Down
101 changes: 101 additions & 0 deletions src/csscoordinates.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,26 @@
function CSSCoordinates() {
var me = this;

function compensated(val, comp) {
return val === 'auto' ? val : val - comp;
}

/**
* Return positioned element's origin with respect to the viewport home
* @private
* @param {object} el The positioned element to measure
*/
function positionedParentViewportHomeOffset(el) {
var originX = el[0].getBoundingClientRect().left,
originY = el[0].getBoundingClientRect().top,
borderTopWidth = parseFloat(el.css('borderTopWidth')),
borderLeftWidth = parseFloat(el.css('borderLeftWidth'));
return {
top: originY + borderTopWidth + $document.scrollTop(),
left: originX + borderLeftWidth + $document.scrollLeft()
};
}

// initialize object properties
me.top = 'auto';
me.left = 'auto';
Expand All @@ -32,4 +52,85 @@ function CSSCoordinates() {
me[property] = Math.round(value);
}
};

me.getCompensated = function() {
return {
top: me.topCompensated,
left: me.leftCompensated,
right: me.rightCompensated,
bottom: me.bottomCompensated
};
};

me.fromViewportHome = function() {
// Coordinates with respect to viewport origin when scrolled to (0,0).
var coords = me.getCompensated(),
originOffset;

// For the cases where there is a positioned ancestor, compensate for offset of
// ancestor origin. Note that bounding rect includes border, if any.
if (isPositionNotStatic($body)) {
originOffset = positionedParentViewportHomeOffset($body);
if (coords.top !== 'auto') {
coords.top = coords.top + originOffset.top;
}
if (coords.left !== 'auto') {
coords.left = coords.left + originOffset.left;
}
if (coords.right !== 'auto') {
coords.right = originOffset.left + $body.width() - coords.right;
}
if (coords.bottom !== 'auto') {
coords.bottom = originOffset.top + $body.height() - coords.bottom;
}
} else if (isPositionNotStatic($html)) {
originOffset = positionedParentViewportHomeOffset($html);
if (coords.top !== 'auto') {
coords.top = coords.top + originOffset.top;
}
if (coords.left !== 'auto') {
coords.left = coords.left + originOffset.left;
}
if (coords.right !== 'auto') {
coords.right = originOffset.left + $body.width() - coords.right;
}
if (coords.bottom !== 'auto') {
coords.bottom = originOffset.top + $body.height() - coords.bottom;
}
} else {
// Change origin of right, bottom measurement to viewport (0,0) and invert sign
if (coords.right !== 'auto') {
coords.right = session.windowWidth - coords.right;
}
if (coords.bottom !== 'auto') {
coords.bottom = session.windowHeight - coords.bottom;
}
}

return coords;
};

Object.defineProperty(me, 'topCompensated', {
get: function() {
return compensated(me.top, session.positionCompensation.top);
}
});

Object.defineProperty(me, 'bottomCompensated', {
get: function() {
return compensated(me.bottom, session.positionCompensation.bottom);
}
});

Object.defineProperty(me, 'leftCompensated', {
get: function() {
return compensated(me.left, session.positionCompensation.left);
}
});

Object.defineProperty(me, 'rightCompensated', {
get: function() {
return compensated(me.right, session.positionCompensation.right);
}
});
}
48 changes: 24 additions & 24 deletions src/placementcalculator.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,52 +38,52 @@ function PlacementCalculator() {
// calculate the appropriate x and y position in the document
switch (placement) {
case 'n':
coords.set('left', position.left - (tipWidth / 2) - session.positionCompensation.left);
coords.set('bottom', session.windowHeight - position.top + offset - session.positionCompensation.bottom);
coords.set('left', position.left - (tipWidth / 2));
coords.set('bottom', session.windowHeight - position.top + offset);
break;
case 'e':
coords.set('left', position.left + offset - session.positionCompensation.left);
coords.set('top', position.top - (tipHeight / 2) - session.positionCompensation.top);
coords.set('left', position.left + offset);
coords.set('top', position.top - (tipHeight / 2));
break;
case 's':
coords.set('left', position.left - (tipWidth / 2) - session.positionCompensation.left);
coords.set('top', position.top + offset - session.positionCompensation.top);
coords.set('left', position.left - (tipWidth / 2));
coords.set('top', position.top + offset);
break;
case 'w':
coords.set('top', position.top - (tipHeight / 2) - session.positionCompensation.top);
coords.set('right', session.windowWidth - position.left + offset - session.positionCompensation.right);
coords.set('top', position.top - (tipHeight / 2));
coords.set('right', session.windowWidth - position.left + offset);
break;
case 'nw':
coords.set('bottom', session.windowHeight - position.top + offset - session.positionCompensation.bottom);
coords.set('right', session.windowWidth - position.left - session.positionCompensation.right - 20);
coords.set('bottom', session.windowHeight - position.top + offset);
coords.set('right', session.windowWidth - position.left - 20);
break;
case 'nw-alt':
coords.set('left', position.left - session.positionCompensation.left);
coords.set('bottom', session.windowHeight - position.top + offset - session.positionCompensation.bottom);
coords.set('left', position.left);
coords.set('bottom', session.windowHeight - position.top + offset);
break;
case 'ne':
coords.set('left', position.left - session.positionCompensation.left - 20);
coords.set('bottom', session.windowHeight - position.top + offset - session.positionCompensation.bottom);
coords.set('left', position.left - 20);
coords.set('bottom', session.windowHeight - position.top + offset);
break;
case 'ne-alt':
coords.set('bottom', session.windowHeight - position.top + offset - session.positionCompensation.bottom);
coords.set('right', session.windowWidth - position.left - session.positionCompensation.right);
coords.set('bottom', session.windowHeight - position.top + offset);
coords.set('right', session.windowWidth - position.left);
break;
case 'sw':
coords.set('top', position.top + offset - session.positionCompensation.top);
coords.set('right', session.windowWidth - position.left - session.positionCompensation.right - 20);
coords.set('top', position.top + offset);
coords.set('right', session.windowWidth - position.left - 20);
break;
case 'sw-alt':
coords.set('left', position.left - session.positionCompensation.left);
coords.set('top', position.top + offset - session.positionCompensation.top);
coords.set('left', position.left);
coords.set('top', position.top + offset);
break;
case 'se':
coords.set('left', position.left - session.positionCompensation.left - 20);
coords.set('top', position.top + offset - session.positionCompensation.top);
coords.set('left', position.left - 20);
coords.set('top', position.top + offset);
break;
case 'se-alt':
coords.set('top', position.top + offset - session.positionCompensation.top);
coords.set('right', session.windowWidth - position.left - session.positionCompensation.right);
coords.set('top', position.top + offset);
coords.set('right', session.windowWidth - position.left);
break;
}

Expand Down
24 changes: 12 additions & 12 deletions src/tooltipcontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,9 @@ function TooltipController(options) {

// support mouse-follow and fixed position tips at the same time by
// moving the tooltip to the last cursor location after it is hidden
coords.set('top', session.currentY + options.offset - session.positionCompensation.top);
coords.set('left', session.currentX + options.offset - session.positionCompensation.left);
tipElement.css(coords);
coords.set('top', session.currentY + options.offset);
coords.set('left', session.currentX + options.offset);
tipElement.css(coords.getCompensated());

// trigger powerTipClose event
element.trigger('powerTipClose');
Expand All @@ -231,8 +231,8 @@ function TooltipController(options) {
collisionCount;

// grab collisions
coords.set('top', session.currentY + options.offset - session.positionCompensation.top);
coords.set('left', session.currentX + options.offset - session.positionCompensation.left);
coords.set('top', session.currentY + options.offset);
coords.set('left', session.currentX + options.offset);
collisions = getViewportCollisions(
coords,
tipWidth,
Expand All @@ -246,21 +246,21 @@ function TooltipController(options) {
// if there is only one collision (bottom or right) then
// simply constrain the tooltip to the view port
if (collisions === Collision.right) {
coords.set('left', session.windowWidth - tipWidth - session.positionCompensation.left);
coords.set('left', session.windowWidth - tipWidth);
} else if (collisions === Collision.bottom) {
coords.set('top', session.scrollTop + session.windowHeight - tipHeight - session.positionCompensation.top);
coords.set('top', session.scrollTop + session.windowHeight - tipHeight);
}
} else {
// if the tooltip has more than one collision then it is
// trapped in the corner and should be flipped to get it out
// of the users way
coords.set('left', session.currentX - tipWidth - options.offset - session.positionCompensation.left);
coords.set('top', session.currentY - tipHeight - options.offset - session.positionCompensation.top);
coords.set('left', session.currentX - tipWidth - options.offset);
coords.set('top', session.currentY - tipHeight - options.offset);
}
}

// position the tooltip
tipElement.css(coords);
tipElement.css(coords.getCompensated());
}
}

Expand Down Expand Up @@ -325,7 +325,7 @@ function TooltipController(options) {
// set the tip to 0,0 to get the full expanded width
coords.set('top', 0);
coords.set('left', 0);
tipElement.css(coords);
tipElement.css(coords.getCompensated());

// to support elastic tooltips we need to check for a change in the
// rendered dimensions after the tooltip has been positioned
Expand All @@ -344,7 +344,7 @@ function TooltipController(options) {
);

// place the tooltip
tipElement.css(coords);
tipElement.css(coords.getCompensated());
} while (
// sanity check: limit to 5 iterations, and...
++iterationCount <= 5 &&
Expand Down
38 changes: 25 additions & 13 deletions src/utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,22 +170,23 @@ function getTooltipContent(element) {
function getViewportCollisions(coords, elementWidth, elementHeight) {
// adjusting viewport even though it might be negative because coords
// comparing with are relative to compensated position
var viewportTop = session.scrollTop - session.positionCompensation.top,
viewportLeft = session.scrollLeft - session.positionCompensation.left,
var viewportTop = session.scrollTop,
viewportLeft = session.scrollLeft,
viewportBottom = viewportTop + session.windowHeight,
viewportRight = viewportLeft + session.windowWidth,
coordsFromViewport = coords.fromViewportHome(),
collisions = Collision.none;

if (coords.top < viewportTop || Math.abs(coords.bottom - session.windowHeight) - elementHeight < viewportTop) {
if (coordsFromViewport.top < viewportTop || coordsFromViewport.bottom - elementHeight < viewportTop) {
collisions |= Collision.top;
}
if (coords.top + elementHeight > viewportBottom || Math.abs(coords.bottom - session.windowHeight) > viewportBottom) {
if (coordsFromViewport.top + elementHeight > viewportBottom || coordsFromViewport.bottom > viewportBottom) {
collisions |= Collision.bottom;
}
if (coords.left < viewportLeft || coords.right + elementWidth > viewportRight) {
if (coordsFromViewport.left < viewportLeft || coordsFromViewport.right - elementWidth < viewportLeft) {
collisions |= Collision.left;
}
if (coords.left + elementWidth > viewportRight || coords.right < viewportLeft) {
if (coordsFromViewport.left + elementWidth > viewportRight || coordsFromViewport.right > viewportRight) {
collisions |= Collision.right;
}

Expand Down Expand Up @@ -220,16 +221,27 @@ function isPositionNotStatic(element) {
* Get element offsets
* @private
* @param {jQuery} el Element to check
* @param {number} windowWidth Window width in pixels.
* @param {number} windowHeight Window height in pixels.
* @return {Object} The top, left, right, bottom offset in pixels
*/
function getElementOffsets(el, windowWidth, windowHeight) {
function getElementOffsets(el) {
// jquery offset returns top and left relative to document in pixels.
var offsets = el.offset();
// right and bottom offset relative to window width/height
offsets.right = windowWidth - el.outerWidth() - offsets.left;
offsets.bottom = windowHeight - el.outerHeight() - offsets.top;
var offsets = el.offset(),
borderLeftWidth = parseFloat(el.css('border-left-width')),
borderTopWidth = parseFloat(el.css('border-top-width')),
right,
bottom;

// right and bottom offset were relative to where screen.width,
// screen.height fell in document. Change reference point to inner-bottom,
// inner-right of element. Compensate for border which is outside
// measurement area. Avoid updating any measurement set to 'auto' which will
// result in a computed result of NaN.
right = session.windowWidth - el.innerWidth() - offsets.left - borderLeftWidth;
bottom = session.windowHeight - el.innerHeight() - offsets.top - borderTopWidth;
offsets.top = offsets.top + borderTopWidth;
offsets.left = offsets.left + borderLeftWidth;
offsets.right = right ? right : 0;
offsets.bottom = bottom ? bottom : 0;
return offsets;
}

Expand Down
1 change: 1 addition & 0 deletions test/unit/csscoordinates.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ $(function() {
QUnit.test('expose methods', function(assert) {
var coords = new CSSCoordinates();
assert.strictEqual(typeof coords.set, 'function', 'set method is defined');
assert.strictEqual(typeof coords.getCompensated, 'function', 'getCompensated method is defined');
});

QUnit.test('decimal values are rounded', function(assert) {
Expand Down
Loading

0 comments on commit b9713e4

Please sign in to comment.