Skip to content

Compensate for body tag with position absolute, relative or static #150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
4 changes: 3 additions & 1 deletion src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// useful private variables
var $document = $(document),
$window = $(window),
$html = $(document.documentElement),
$body = $('body');

// constants
Expand Down Expand Up @@ -60,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
109 changes: 109 additions & 0 deletions src/csscoordinates.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,34 @@
function CSSCoordinates() {
var me = this;

/**
* Return value the compensated value allowing for the special value of 'auto'.
* @private
* @param {number} value The value to be compensated.
* @param {number} comp The amount by which the value should be adjusted.
* @returns {number} The value less comp unless 'auto'
*/
function compensated(value, comp) {
return value === 'auto' ? value : value - comp;
}

/**
* Return positioned element's origin with respect to the viewport home
* @private
* @param {object} el The positioned element to measure
* @returns {object} The top and left coordinates of the element relative to the viewport.
*/
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 +60,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);
}
});
}
8 changes: 4 additions & 4 deletions src/tooltipcontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ function TooltipController(options) {
// moving the tooltip to the last cursor location after it is hidden
coords.set('top', session.currentY + options.offset);
coords.set('left', session.currentX + options.offset);
tipElement.css(coords);
tipElement.css(coords.getCompensated());

// trigger powerTipClose event
element.trigger('powerTipClose');
Expand Down Expand Up @@ -260,7 +260,7 @@ function TooltipController(options) {
}

// 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
78 changes: 74 additions & 4 deletions src/utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ function getViewportDimensions() {
session.scrollTop = $window.scrollTop();
session.windowWidth = $window.width();
session.windowHeight = $window.height();
session.positionCompensation = computePositionCompensation(session.windowWidth, session.windowHeight);
}

/**
Expand All @@ -69,6 +70,7 @@ function getViewportDimensions() {
function trackResize() {
session.windowWidth = $window.width();
session.windowHeight = $window.height();
session.positionCompensation = computePositionCompensation(session.windowWidth, session.windowHeight);
}

/**
Expand Down Expand Up @@ -166,22 +168,25 @@ function getTooltipContent(element) {
* @return {number} Value with the collision flags.
*/
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,
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 All @@ -201,3 +206,68 @@ function countFlags(value) {
}
return count;
}

/**
* Check whether element has CSS position attribute other than static
* @private
* @param {jQuery} element Element to check
* @return {boolean} indicating whether position attribute is non-static.
*/
function isPositionNotStatic(element) {
return element.css('position') !== 'static';
}

/**
* Get element offsets
* @private
* @param {jQuery} el Element to check
* @return {Object} The top, left, right, bottom offset in pixels
*/
function getElementOffsets(el) {
// jquery offset returns top and left relative to document in pixels.
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;
}

/**
* Compute compensating position offsets if body or html element has non-static position attribute.
* @private
* @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 computePositionCompensation(windowWidth, windowHeight) {
// Check if the element is "positioned". A "positioned" element has a CSS
// position value other than static. Whether the element is positioned is
// relevant because absolutely positioned elements are positioned relative
// to the first positioned ancestor rather than relative to the doc origin.

var offsets;

if (isPositionNotStatic($body)) {
offsets = getElementOffsets($body, windowWidth, windowHeight);
} else if (isPositionNotStatic($html)) {
offsets = getElementOffsets($html, windowWidth, windowHeight);
} else {
// even though body may have offset, no compensation is required
offsets = { top: 0, bottom: 0, left: 0, right: 0 };
}

return offsets;
}
55 changes: 54 additions & 1 deletion test/edge-cases.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,54 @@
$('#powertip-css').attr('href', '../css/jquery.powertip' + theme + '.css');
}

// css position switcher allows testing html and body CSS position values that
// changes the origin from which the tooltip is positioned.
function setCssPosition() {
var attributeSets = {
// default positioning is static
static: { position: '', left: '', right: '', top: '', bottom: '' },
absolute: { position: 'absolute', left: '50px', right: '100px', top: '25px', bottom: '75px' },
relative: { position: 'relative', left: '50px', right: '100px', top: '25px', bottom: '75px' },
fixed: { position: 'fixed', left: '50px', right: '100px', top: '25px', bottom: '75px' },
};
var posType = $('#position-switcher').val();
var $html = $(document.documentElement);
var $body = $(document.body);
$html.css(attributeSets['static']);
$body.css(attributeSets['static']);
switch(posType) {
case 'html-relative':
$html.css(attributeSets['relative']);
break;
case 'html-fixed':
$html.css(attributeSets['fixed']);
break;
case 'html-absolute':
$html.css(attributeSets['absolute']);
break;
case 'body-relative':
$body.css(attributeSets['relative']);
break;
case 'body-fixed':
$body.css(attributeSets['fixed']);
break;
case 'body-absolute':
$body.css(attributeSets['absolute']);
break;
default:
// attributes clear above
break;
}
// Trigger resize to force recalculation of position compensation
window.dispatchEvent(new Event('resize'));
}

// run theme switcher on page load
setTheme();

// hook theme switcher to select change
// hook theme and position switcher to select change
$('#theme-switcher').on('change', setTheme);
$('#position-switcher').on('change', setCssPosition);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also call this function on load (like setTheme() on line 75)? It's nice to be able to reload the page and not have to scroll to the top to reset the position.

Just add setCssPosition() somewhere.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would think it will need more than the function call. The dropdown value would presumably reset on reload unless the value is saved somewhere. I could use a url parameter to preserve the position setting so it can be restored on reload. Would that be helpful?


// session debug info box
var debugOutput = $('#session pre');
Expand Down Expand Up @@ -82,6 +125,16 @@ <h1>PowerTip Edge Case Tests</h1>
<option value="red">Red</option>
<option value="yellow">Yellow</option>
</select>
Tooltip Ancestor CSS Position:
<select id="position-switcher">
<option value="">Default</option>
<option value="html-relative">HTML relative</option>
<option value="html-fixed">HTML fixed</option>
<option value="html-absolute">HTML absolute</option>
<option value="body-relative">BODY relative</option>
<option value="body-fixed">BODY fixed</option>
<option value="body-absolute">BODY absolute</option>
</select>
</p>
</header>
<section id="open-on-load">
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