From 9f9b2fbcd87163e898a4d35ff277a966921cf8ab Mon Sep 17 00:00:00 2001 From: Joel Steres Date: Wed, 24 Aug 2016 20:39:38 -0700 Subject: [PATCH 1/9] Compensate for body tag with position absolute, relative or static Also opts for making all placement based on top rather than bottom. --- src/placementcalculator.js | 48 ++++++++++---------- src/tooltipcontroller.js | 16 +++---- src/utility.js | 46 ++++++++++++++++++- test/bodyoffset-abs.html | 78 ++++++++++++++++++++++++++++++++ test/bodyoffset-rel.html | 78 ++++++++++++++++++++++++++++++++ test/tests-bodyoffset.js | 69 ++++++++++++++++++++++++++++ test/unit/placementcalculator.js | 14 +----- 7 files changed, 303 insertions(+), 46 deletions(-) create mode 100644 test/bodyoffset-abs.html create mode 100644 test/bodyoffset-rel.html create mode 100644 test/tests-bodyoffset.js diff --git a/src/placementcalculator.js b/src/placementcalculator.js index 1ec10ed6..567ce9fd 100644 --- a/src/placementcalculator.js +++ b/src/placementcalculator.js @@ -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)); - coords.set('bottom', session.windowHeight - position.top + offset); + coords.set('left', position.left - (tipWidth / 2) - session.positionCompensation.left); + coords.set('top', position.top - tipHeight - offset - session.positionCompensation.top); break; case 'e': - coords.set('left', position.left + offset); - coords.set('top', position.top - (tipHeight / 2)); + coords.set('left', position.left + offset - session.positionCompensation.left); + coords.set('top', position.top - (tipHeight / 2) - session.positionCompensation.top); break; case 's': - coords.set('left', position.left - (tipWidth / 2)); - coords.set('top', position.top + offset); + coords.set('left', position.left - (tipWidth / 2) - session.positionCompensation.left); + coords.set('top', position.top + offset - session.positionCompensation.top); break; case 'w': - coords.set('top', position.top - (tipHeight / 2)); - coords.set('right', session.windowWidth - position.left + offset); + coords.set('top', position.top - (tipHeight / 2) - session.positionCompensation.top); + coords.set('right', session.windowWidth - position.left + offset - session.positionCompensation.right); break; case 'nw': - coords.set('bottom', session.windowHeight - position.top + offset); - coords.set('right', session.windowWidth - position.left - 20); + coords.set('top', position.top - tipHeight - offset - session.positionCompensation.top); + coords.set('right', session.windowWidth - position.left - session.positionCompensation.right - 20); break; case 'nw-alt': - coords.set('left', position.left); - coords.set('bottom', session.windowHeight - position.top + offset); + coords.set('left', position.left - session.positionCompensation.left); + coords.set('top', position.top - tipHeight - offset - session.positionCompensation.top); break; case 'ne': - coords.set('left', position.left - 20); - coords.set('bottom', session.windowHeight - position.top + offset); + coords.set('left', position.left - session.positionCompensation.left - 20); + coords.set('top', position.top - tipHeight - offset - session.positionCompensation.top); break; case 'ne-alt': - coords.set('bottom', session.windowHeight - position.top + offset); - coords.set('right', session.windowWidth - position.left); + coords.set('top', position.top - tipHeight - offset - session.positionCompensation.top); + coords.set('right', session.windowWidth - position.left - session.positionCompensation.right); break; case 'sw': - coords.set('top', position.top + offset); - coords.set('right', session.windowWidth - position.left - 20); + coords.set('top', position.top + offset - session.positionCompensation.top); + coords.set('right', session.windowWidth - position.left - session.positionCompensation.right - 20); break; case 'sw-alt': - coords.set('left', position.left); - coords.set('top', position.top + offset); + coords.set('left', position.left - session.positionCompensation.left); + coords.set('top', position.top + offset - session.positionCompensation.top); break; case 'se': - coords.set('left', position.left - 20); - coords.set('top', position.top + offset); + coords.set('left', position.left - session.positionCompensation.left - 20); + coords.set('top', position.top + offset - session.positionCompensation.top); break; case 'se-alt': - coords.set('top', position.top + offset); - coords.set('right', session.windowWidth - position.left); + coords.set('top', position.top + offset - session.positionCompensation.top); + coords.set('right', session.windowWidth - position.left - session.positionCompensation.right); break; } diff --git a/src/tooltipcontroller.js b/src/tooltipcontroller.js index f5d258f7..81d6ec86 100644 --- a/src/tooltipcontroller.js +++ b/src/tooltipcontroller.js @@ -202,8 +202,8 @@ 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); - coords.set('left', session.currentX + options.offset); + coords.set('top', session.currentY + options.offset - session.positionCompensation.top); + coords.set('left', session.currentX + options.offset - session.positionCompensation.left); tipElement.css(coords); // trigger powerTipClose event @@ -231,8 +231,8 @@ function TooltipController(options) { collisionCount; // grab collisions - coords.set('top', session.currentY + options.offset); - coords.set('left', session.currentX + options.offset); + coords.set('top', session.currentY + options.offset - session.positionCompensation.top); + coords.set('left', session.currentX + options.offset - session.positionCompensation.left); collisions = getViewportCollisions( coords, tipWidth, @@ -246,16 +246,16 @@ 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); + coords.set('left', session.windowWidth - tipWidth - session.positionCompensation.left); } else if (collisions === Collision.bottom) { - coords.set('top', session.scrollTop + session.windowHeight - tipHeight); + coords.set('top', session.scrollTop + session.windowHeight - tipHeight - session.positionCompensation.top); } } 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); - coords.set('top', session.currentY - tipHeight - options.offset); + coords.set('left', session.currentX - tipWidth - options.offset - session.positionCompensation.left); + coords.set('top', session.currentY - tipHeight - options.offset - session.positionCompensation.top); } } diff --git a/src/utility.js b/src/utility.js index b1b5455f..be68c88e 100644 --- a/src/utility.js +++ b/src/utility.js @@ -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); } /** @@ -69,6 +70,7 @@ function getViewportDimensions() { function trackResize() { session.windowWidth = $window.width(); session.windowHeight = $window.height(); + session.positionCompensation = computePositionCompensation(session.windowWidth, session.windowHeight); } /** @@ -166,8 +168,10 @@ function getTooltipContent(element) { * @return {number} Value with the collision flags. */ function getViewportCollisions(coords, elementWidth, elementHeight) { - var viewportTop = session.scrollTop, - viewportLeft = session.scrollLeft, + // 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, viewportBottom = viewportTop + session.windowHeight, viewportRight = viewportLeft + session.windowWidth, collisions = Collision.none; @@ -201,3 +205,41 @@ function countFlags(value) { } return count; } + +/** + * Compute compensating position offsets if body element has non-standard position attribute. + * @private + * @param {number} windowWidth Window width in pixels. + * @param {number} windowHeight Window height in pixels. + * @return {Offsets} The top, left, right, bottom offset in pixels + */ +function computePositionCompensation(windowWidth, windowHeight) { + var bodyWidthWithMargin, + bodyHeightWithMargin, + offsets, + bodyPositionPx; + + switch ($body.css('position')) { + case 'absolute': + case 'fixed': + case 'relative': + // jquery offset and position functions return top and left + // offset function computes position + margin + offsets = $body.offset(); + bodyPositionPx = $body.position(); + // because element might be positioned compute right margin using the different between + // outerWidth computations and add position offset + bodyWidthWithMargin = $body.outerWidth(true); + bodyHeightWithMargin = $body.outerHeight(true); + // right offset = right margin + body right position + offsets.right = (bodyWidthWithMargin - $body.outerWidth() - (offsets.left - bodyPositionPx.left)) + (windowWidth - bodyWidthWithMargin - bodyPositionPx.left); + // bottom offset = bottom margin + body bottom position + offsets.bottom = (bodyHeightWithMargin - $body.outerHeight() - offsets.top) + (windowHeight - bodyHeightWithMargin - bodyPositionPx.top); + break; + default: + // even though body may have offset, no compensation is required + offsets = { top: 0, bottom: 0, left: 0, right: 0 }; + } + + return offsets; +} diff --git a/test/bodyoffset-abs.html b/test/bodyoffset-abs.html new file mode 100644 index 00000000..62775272 --- /dev/null +++ b/test/bodyoffset-abs.html @@ -0,0 +1,78 @@ + + + + + PowerTip Test Suite + + + + + + + + + + + + + + + + + + + + +
+

Huge Text

+

The tooltips for the buttons below have a lot of text. The tooltip div is completely elastic for this demo. The tooltips should be properly placed when they render.

+
+ + + + +
+ +
+ + + + + +
+
+
+

Huge Text with Smart Placement

+

The tooltips for the buttons below have a lot of text. The tooltip div is completely elastic for this demo. The tooltips should be properly placed when they render.

+
+ + + + +
+ +
+ + + + + +
+
+
+

Trapped mouse following tooltip

+

This box has a mouse following tooltip.

+

Trap it in the bottom right corner of the viewport. It should flip out of the way. It should not flip if it only hits one edge.

+
+
+ + diff --git a/test/bodyoffset-rel.html b/test/bodyoffset-rel.html new file mode 100644 index 00000000..47c58e94 --- /dev/null +++ b/test/bodyoffset-rel.html @@ -0,0 +1,78 @@ + + + + + PowerTip Test Suite + + + + + + + + + + + + + + + + + + + + +
+

Huge Text

+

The tooltips for the buttons below have a lot of text. The tooltip div is completely elastic for this demo. The tooltips should be properly placed when they render.

+
+ + + + +
+ +
+ + + + + +
+
+
+

Huge Text with Smart Placement

+

The tooltips for the buttons below have a lot of text. The tooltip div is completely elastic for this demo. The tooltips should be properly placed when they render.

+
+ + + + +
+ +
+ + + + + +
+
+
+

Trapped mouse following tooltip

+

This box has a mouse following tooltip.

+

Trap it in the bottom right corner of the viewport. It should flip out of the way. It should not flip if it only hits one edge.

+
+
+ + diff --git a/test/tests-bodyoffset.js b/test/tests-bodyoffset.js new file mode 100644 index 00000000..34603564 --- /dev/null +++ b/test/tests-bodyoffset.js @@ -0,0 +1,69 @@ +$(function() { + 'use strict'; + + // setup huge text tooltips + var hugeText = [ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent sed', + 'volutpat tellus. Fusce mollis iaculis est at sodales. Proin aliquam', + 'bibendum neque, nec blandit orci porttitor non. Cras lacinia varius', + 'felis vel ultricies. Nulla eu sapien arcu, dapibus tempor eros.', + 'Praesent aliquet hendrerit commodo. Pellentesque habitant morbi', + 'tristique senectus et netus et malesuada fames ac turpis egestas.', + 'Proin gravida justo faucibus urna dictum id egestas velit hendrerit.', + 'Praesent dapibus rutrum tempor. Sed ultrices varius purus, eu rhoncus', + 'tortor scelerisque sit amet. Sed vitae molestie diam. Pellentesque', + 'posuere euismod venenatis. Proin ut ligula vel urna lacinia accumsan.', + 'Quisque commodo ultrices orci ut cursus. Aliquam in dolor orci. Nunc', + 'pretium euismod odio.' + ].join(' '); + $.each( + [ + 'north', + 'east', + 'south', + 'west', + 'north-west', + 'north-east', + 'south-west', + 'south-east', + 'north-west-alt', + 'north-east-alt', + 'south-west-alt', + 'south-east-alt' + ], + function(i, val) { + $('.' + val).data('powertip', hugeText); + } + ); + + // Huge text + $('#huge-text .north').powerTip({ placement: 'n' }); + $('#huge-text .east').powerTip({ placement: 'e' }); + $('#huge-text .south').powerTip({ placement: 's' }); + $('#huge-text .west').powerTip({ placement: 'w' }); + $('#huge-text .north-west').powerTip({ placement: 'nw' }); + $('#huge-text .north-east').powerTip({ placement: 'ne' }); + $('#huge-text .south-west').powerTip({ placement: 'sw' }); + $('#huge-text .south-east').powerTip({ placement: 'se' }); + $('#huge-text .north-west-alt').powerTip({ placement: 'nw-alt' }); + $('#huge-text .north-east-alt').powerTip({ placement: 'ne-alt' }); + $('#huge-text .south-west-alt').powerTip({ placement: 'sw-alt' }); + $('#huge-text .south-east-alt').powerTip({ placement: 'se-alt' }); + + // Huge text with smart placement + $('#huge-text-smart .north').powerTip({ placement: 'n', smartPlacement: true }); + $('#huge-text-smart .east').powerTip({ placement: 'e', smartPlacement: true }); + $('#huge-text-smart .south').powerTip({ placement: 's', smartPlacement: true }); + $('#huge-text-smart .west').powerTip({ placement: 'w', smartPlacement: true }); + $('#huge-text-smart .north-west').powerTip({ placement: 'nw', smartPlacement: true }); + $('#huge-text-smart .north-east').powerTip({ placement: 'ne', smartPlacement: true }); + $('#huge-text-smart .south-west').powerTip({ placement: 'sw', smartPlacement: true }); + $('#huge-text-smart .south-east').powerTip({ placement: 'se', smartPlacement: true }); + $('#huge-text-smart .north-west-alt').powerTip({ placement: 'nw-alt', smartPlacement: true }); + $('#huge-text-smart .north-east-alt').powerTip({ placement: 'ne-alt', smartPlacement: true }); + $('#huge-text-smart .south-west-alt').powerTip({ placement: 'sw-alt', smartPlacement: true }); + $('#huge-text-smart .south-east-alt').powerTip({ placement: 'se-alt', smartPlacement: true }); + + // Trapped mouse following tooltip + $('#trapped-mousefollow').powerTip({ followMouse: true }); +}); diff --git a/test/unit/placementcalculator.js b/test/unit/placementcalculator.js index b18c57aa..dd09873c 100644 --- a/test/unit/placementcalculator.js +++ b/test/unit/placementcalculator.js @@ -40,11 +40,6 @@ $(function() { case 'n': case 'ne': case 'nw-alt': - assert.strictEqual(coords.top, 'auto', key + ': top property is set to auto'); - assert.strictEqual($.isNumeric(coords.left), true, key + ': left property is set to a number'); - assert.strictEqual(coords.right, 'auto', key + ': right property is set to auto'); - assert.strictEqual($.isNumeric(coords.bottom), true, key + ': bottom property is set to a number'); - break; case 'e': case 's': case 'se': @@ -57,17 +52,12 @@ $(function() { case 'w': case 'sw': case 'se-alt': - assert.strictEqual($.isNumeric(coords.top), true, key + ': top property is set to a number'); - assert.strictEqual(coords.left, 'auto', key + ': left property is set to auto'); - assert.strictEqual($.isNumeric(coords.right), true, key + ': right property is set to a number'); - assert.strictEqual(coords.bottom, 'auto', key + ': bottom property is set to auto'); - break; case 'nw': case 'ne-alt': - assert.strictEqual(coords.top, 'auto', key + ': top property is set to auto'); + assert.strictEqual($.isNumeric(coords.top), true, key + ': top property is set to a number'); assert.strictEqual(coords.left, 'auto', key + ': left property is set to auto'); assert.strictEqual($.isNumeric(coords.right), true, key + ': right property is set to a number'); - assert.strictEqual($.isNumeric(coords.bottom), true, key + ': bottom property is set to a number'); + assert.strictEqual(coords.bottom, 'auto', key + ': bottom property is set to auto'); break; } }); From 66baec7b5d2ae34d2bffb197c99b59e02d380e23 Mon Sep 17 00:00:00 2001 From: Joel Steres Date: Mon, 17 Oct 2016 14:02:47 -0700 Subject: [PATCH 2/9] Position north side placements using bottom css As requested in #151 referencing issue #31 for rationale --- src/placementcalculator.js | 10 +++++----- test/unit/placementcalculator.js | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/placementcalculator.js b/src/placementcalculator.js index 567ce9fd..e476d77f 100644 --- a/src/placementcalculator.js +++ b/src/placementcalculator.js @@ -39,7 +39,7 @@ function PlacementCalculator() { switch (placement) { case 'n': coords.set('left', position.left - (tipWidth / 2) - session.positionCompensation.left); - coords.set('top', position.top - tipHeight - offset - session.positionCompensation.top); + coords.set('bottom', session.windowHeight - position.top + offset - session.positionCompensation.bottom); break; case 'e': coords.set('left', position.left + offset - session.positionCompensation.left); @@ -54,19 +54,19 @@ function PlacementCalculator() { coords.set('right', session.windowWidth - position.left + offset - session.positionCompensation.right); break; case 'nw': - coords.set('top', position.top - tipHeight - offset - session.positionCompensation.top); + coords.set('bottom', session.windowHeight - position.top + offset - session.positionCompensation.bottom); coords.set('right', session.windowWidth - position.left - session.positionCompensation.right - 20); break; case 'nw-alt': coords.set('left', position.left - session.positionCompensation.left); - coords.set('top', position.top - tipHeight - offset - session.positionCompensation.top); + coords.set('bottom', session.windowHeight - position.top + offset - session.positionCompensation.bottom); break; case 'ne': coords.set('left', position.left - session.positionCompensation.left - 20); - coords.set('top', position.top - tipHeight - offset - session.positionCompensation.top); + coords.set('bottom', session.windowHeight - position.top + offset - session.positionCompensation.bottom); break; case 'ne-alt': - coords.set('top', position.top - tipHeight - offset - session.positionCompensation.top); + coords.set('bottom', session.windowHeight - position.top + offset - session.positionCompensation.bottom); coords.set('right', session.windowWidth - position.left - session.positionCompensation.right); break; case 'sw': diff --git a/test/unit/placementcalculator.js b/test/unit/placementcalculator.js index dd09873c..b18c57aa 100644 --- a/test/unit/placementcalculator.js +++ b/test/unit/placementcalculator.js @@ -40,6 +40,11 @@ $(function() { case 'n': case 'ne': case 'nw-alt': + assert.strictEqual(coords.top, 'auto', key + ': top property is set to auto'); + assert.strictEqual($.isNumeric(coords.left), true, key + ': left property is set to a number'); + assert.strictEqual(coords.right, 'auto', key + ': right property is set to auto'); + assert.strictEqual($.isNumeric(coords.bottom), true, key + ': bottom property is set to a number'); + break; case 'e': case 's': case 'se': @@ -52,13 +57,18 @@ $(function() { case 'w': case 'sw': case 'se-alt': - case 'nw': - case 'ne-alt': assert.strictEqual($.isNumeric(coords.top), true, key + ': top property is set to a number'); assert.strictEqual(coords.left, 'auto', key + ': left property is set to auto'); assert.strictEqual($.isNumeric(coords.right), true, key + ': right property is set to a number'); assert.strictEqual(coords.bottom, 'auto', key + ': bottom property is set to auto'); break; + case 'nw': + case 'ne-alt': + assert.strictEqual(coords.top, 'auto', key + ': top property is set to auto'); + assert.strictEqual(coords.left, 'auto', key + ': left property is set to auto'); + assert.strictEqual($.isNumeric(coords.right), true, key + ': right property is set to a number'); + assert.strictEqual($.isNumeric(coords.bottom), true, key + ': bottom property is set to a number'); + break; } }); }); From 5bf6a63637857c9c49c7d194039381f7ec6d898a Mon Sep 17 00:00:00 2001 From: Joel Steres Date: Fri, 9 Dec 2016 09:05:17 -0800 Subject: [PATCH 3/9] Compensate for positioned HTML element --- src/core.js | 1 + src/utility.js | 65 +++++++++++++++++++-------------- test/htmloffset-rel.html | 78 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 26 deletions(-) create mode 100644 test/htmloffset-rel.html diff --git a/src/core.js b/src/core.js index cc11d8c5..27bd7c78 100644 --- a/src/core.js +++ b/src/core.js @@ -10,6 +10,7 @@ // useful private variables var $document = $(document), $window = $(window), + $html = $(document.documentElement), $body = $('body'); // constants diff --git a/src/utility.js b/src/utility.js index be68c88e..d8a94998 100644 --- a/src/utility.js +++ b/src/utility.js @@ -207,38 +207,51 @@ function countFlags(value) { } /** - * Compute compensating position offsets if body element has non-standard position attribute. + * 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 {Offsets} The top, left, right, bottom offset in pixels */ function computePositionCompensation(windowWidth, windowHeight) { - var bodyWidthWithMargin, - bodyHeightWithMargin, - offsets, - bodyPositionPx; - - switch ($body.css('position')) { - case 'absolute': - case 'fixed': - case 'relative': - // jquery offset and position functions return top and left - // offset function computes position + margin - offsets = $body.offset(); - bodyPositionPx = $body.position(); - // because element might be positioned compute right margin using the different between - // outerWidth computations and add position offset - bodyWidthWithMargin = $body.outerWidth(true); - bodyHeightWithMargin = $body.outerHeight(true); - // right offset = right margin + body right position - offsets.right = (bodyWidthWithMargin - $body.outerWidth() - (offsets.left - bodyPositionPx.left)) + (windowWidth - bodyWidthWithMargin - bodyPositionPx.left); - // bottom offset = bottom margin + body bottom position - offsets.bottom = (bodyHeightWithMargin - $body.outerHeight() - offsets.top) + (windowHeight - bodyHeightWithMargin - bodyPositionPx.top); - break; - default: - // even though body may have offset, no compensation is required - offsets = { top: 0, bottom: 0, left: 0, right: 0 }; + // 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 isPositioned = function(el) { + return el.css('position') !== 'static'; + }; + + var getElementOffsets = function(el) { + var elWidthWithMargin, + elHeightWithMargin, + elPositionPx, + offsets; + // jquery offset and position functions return top and left + // offset function computes position + margin + offsets = el.offset(); + elPositionPx = el.position(); + + // Compute the far margins based off the outerWidth values. + elWidthWithMargin = el.outerWidth(true); + elHeightWithMargin = el.outerHeight(true); + + // right offset = right margin + body right position + offsets.right = (elWidthWithMargin - el.outerWidth() - (offsets.left - elPositionPx.left)) + (windowWidth - elWidthWithMargin - elPositionPx.left); + // bottom offset = bottom margin + body bottom position + offsets.bottom = (elHeightWithMargin - el.outerHeight() - offsets.top) + (windowHeight - elHeightWithMargin - elPositionPx.top); + return offsets; + }; + + var offsets; + + if (isPositioned($body)) { + offsets = getElementOffsets($body); + } else if (isPositioned($html)) { + offsets = getElementOffsets($html); + } else { + // even though body may have offset, no compensation is required + offsets = { top: 0, bottom: 0, left: 0, right: 0 }; } return offsets; diff --git a/test/htmloffset-rel.html b/test/htmloffset-rel.html new file mode 100644 index 00000000..42fa900b --- /dev/null +++ b/test/htmloffset-rel.html @@ -0,0 +1,78 @@ + + + + + PowerTip Test Suite + + + + + + + + + + + + + + + + + + + + +
+

Huge Text

+

The tooltips for the buttons below have a lot of text. The tooltip div is completely elastic for this demo. The tooltips should be properly placed when they render.

+
+ + + + +
+ +
+ + + + + +
+
+
+

Huge Text with Smart Placement

+

The tooltips for the buttons below have a lot of text. The tooltip div is completely elastic for this demo. The tooltips should be properly placed when they render.

+
+ + + + +
+ +
+ + + + + +
+
+
+

Trapped mouse following tooltip

+

This box has a mouse following tooltip.

+

Trap it in the bottom right corner of the viewport. It should flip out of the way. It should not flip if it only hits one edge.

+
+
+ + From 2d41b1e557c74726dfd9c5eb621742cf3e3ad166 Mon Sep 17 00:00:00 2001 From: Joel Steres Date: Mon, 30 Jan 2017 15:44:32 -0800 Subject: [PATCH 4/9] Refactor inlined functions to top level per feedback. Fix bottom calc. Also simplified both bottom and right offset calculations by removing canceled terms. --- src/utility.js | 61 ++++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/src/utility.js b/src/utility.js index d8a94998..27310078 100644 --- a/src/utility.js +++ b/src/utility.js @@ -206,49 +206,52 @@ 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 + * @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) { + // 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; + 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 {Offsets} The top, left, right, bottom offset 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 isPositioned = function(el) { - return el.css('position') !== 'static'; - }; - - var getElementOffsets = function(el) { - var elWidthWithMargin, - elHeightWithMargin, - elPositionPx, - offsets; - // jquery offset and position functions return top and left - // offset function computes position + margin - offsets = el.offset(); - elPositionPx = el.position(); - - // Compute the far margins based off the outerWidth values. - elWidthWithMargin = el.outerWidth(true); - elHeightWithMargin = el.outerHeight(true); - - // right offset = right margin + body right position - offsets.right = (elWidthWithMargin - el.outerWidth() - (offsets.left - elPositionPx.left)) + (windowWidth - elWidthWithMargin - elPositionPx.left); - // bottom offset = bottom margin + body bottom position - offsets.bottom = (elHeightWithMargin - el.outerHeight() - offsets.top) + (windowHeight - elHeightWithMargin - elPositionPx.top); - return offsets; - }; var offsets; - if (isPositioned($body)) { - offsets = getElementOffsets($body); - } else if (isPositioned($html)) { - offsets = getElementOffsets($html); + 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 }; From 700edb760ac7d029ed494feb666b5b0fc0031186 Mon Sep 17 00:00:00 2001 From: Joel Steres Date: Tue, 31 Jan 2017 04:23:01 -0800 Subject: [PATCH 5/9] Add position test cases to edge-cases --- test/edge-cases.html | 55 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/test/edge-cases.html b/test/edge-cases.html index 0da70549..b41267d7 100644 --- a/test/edge-cases.html +++ b/test/edge-cases.html @@ -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); // session debug info box var debugOutput = $('#session pre'); @@ -82,6 +125,16 @@

PowerTip Edge Case Tests

+ Tooltip Ancestor CSS Position: +

From fcedf6975288137465a118d7bd2027a1cfdf287e Mon Sep 17 00:00:00 2001 From: Joel Steres Date: Fri, 16 Jun 2017 03:13:48 -0700 Subject: [PATCH 6/9] Remove obsolete test files. Tests integrated in edge-cases.html. --- test/bodyoffset-abs.html | 78 ---------------------------------------- test/bodyoffset-rel.html | 78 ---------------------------------------- test/htmloffset-rel.html | 78 ---------------------------------------- test/tests-bodyoffset.js | 69 ----------------------------------- 4 files changed, 303 deletions(-) delete mode 100644 test/bodyoffset-abs.html delete mode 100644 test/bodyoffset-rel.html delete mode 100644 test/htmloffset-rel.html delete mode 100644 test/tests-bodyoffset.js diff --git a/test/bodyoffset-abs.html b/test/bodyoffset-abs.html deleted file mode 100644 index 62775272..00000000 --- a/test/bodyoffset-abs.html +++ /dev/null @@ -1,78 +0,0 @@ - - - - - PowerTip Test Suite - - - - - - - - - - - - - - - - - - - - -
-

Huge Text

-

The tooltips for the buttons below have a lot of text. The tooltip div is completely elastic for this demo. The tooltips should be properly placed when they render.

-
- - - - -
- -
- - - - - -
-
-
-

Huge Text with Smart Placement

-

The tooltips for the buttons below have a lot of text. The tooltip div is completely elastic for this demo. The tooltips should be properly placed when they render.

-
- - - - -
- -
- - - - - -
-
-
-

Trapped mouse following tooltip

-

This box has a mouse following tooltip.

-

Trap it in the bottom right corner of the viewport. It should flip out of the way. It should not flip if it only hits one edge.

-
-
- - diff --git a/test/bodyoffset-rel.html b/test/bodyoffset-rel.html deleted file mode 100644 index 47c58e94..00000000 --- a/test/bodyoffset-rel.html +++ /dev/null @@ -1,78 +0,0 @@ - - - - - PowerTip Test Suite - - - - - - - - - - - - - - - - - - - - -
-

Huge Text

-

The tooltips for the buttons below have a lot of text. The tooltip div is completely elastic for this demo. The tooltips should be properly placed when they render.

-
- - - - -
- -
- - - - - -
-
-
-

Huge Text with Smart Placement

-

The tooltips for the buttons below have a lot of text. The tooltip div is completely elastic for this demo. The tooltips should be properly placed when they render.

-
- - - - -
- -
- - - - - -
-
-
-

Trapped mouse following tooltip

-

This box has a mouse following tooltip.

-

Trap it in the bottom right corner of the viewport. It should flip out of the way. It should not flip if it only hits one edge.

-
-
- - diff --git a/test/htmloffset-rel.html b/test/htmloffset-rel.html deleted file mode 100644 index 42fa900b..00000000 --- a/test/htmloffset-rel.html +++ /dev/null @@ -1,78 +0,0 @@ - - - - - PowerTip Test Suite - - - - - - - - - - - - - - - - - - - - -
-

Huge Text

-

The tooltips for the buttons below have a lot of text. The tooltip div is completely elastic for this demo. The tooltips should be properly placed when they render.

-
- - - - -
- -
- - - - - -
-
-
-

Huge Text with Smart Placement

-

The tooltips for the buttons below have a lot of text. The tooltip div is completely elastic for this demo. The tooltips should be properly placed when they render.

-
- - - - -
- -
- - - - - -
-
-
-

Trapped mouse following tooltip

-

This box has a mouse following tooltip.

-

Trap it in the bottom right corner of the viewport. It should flip out of the way. It should not flip if it only hits one edge.

-
-
- - diff --git a/test/tests-bodyoffset.js b/test/tests-bodyoffset.js deleted file mode 100644 index 34603564..00000000 --- a/test/tests-bodyoffset.js +++ /dev/null @@ -1,69 +0,0 @@ -$(function() { - 'use strict'; - - // setup huge text tooltips - var hugeText = [ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent sed', - 'volutpat tellus. Fusce mollis iaculis est at sodales. Proin aliquam', - 'bibendum neque, nec blandit orci porttitor non. Cras lacinia varius', - 'felis vel ultricies. Nulla eu sapien arcu, dapibus tempor eros.', - 'Praesent aliquet hendrerit commodo. Pellentesque habitant morbi', - 'tristique senectus et netus et malesuada fames ac turpis egestas.', - 'Proin gravida justo faucibus urna dictum id egestas velit hendrerit.', - 'Praesent dapibus rutrum tempor. Sed ultrices varius purus, eu rhoncus', - 'tortor scelerisque sit amet. Sed vitae molestie diam. Pellentesque', - 'posuere euismod venenatis. Proin ut ligula vel urna lacinia accumsan.', - 'Quisque commodo ultrices orci ut cursus. Aliquam in dolor orci. Nunc', - 'pretium euismod odio.' - ].join(' '); - $.each( - [ - 'north', - 'east', - 'south', - 'west', - 'north-west', - 'north-east', - 'south-west', - 'south-east', - 'north-west-alt', - 'north-east-alt', - 'south-west-alt', - 'south-east-alt' - ], - function(i, val) { - $('.' + val).data('powertip', hugeText); - } - ); - - // Huge text - $('#huge-text .north').powerTip({ placement: 'n' }); - $('#huge-text .east').powerTip({ placement: 'e' }); - $('#huge-text .south').powerTip({ placement: 's' }); - $('#huge-text .west').powerTip({ placement: 'w' }); - $('#huge-text .north-west').powerTip({ placement: 'nw' }); - $('#huge-text .north-east').powerTip({ placement: 'ne' }); - $('#huge-text .south-west').powerTip({ placement: 'sw' }); - $('#huge-text .south-east').powerTip({ placement: 'se' }); - $('#huge-text .north-west-alt').powerTip({ placement: 'nw-alt' }); - $('#huge-text .north-east-alt').powerTip({ placement: 'ne-alt' }); - $('#huge-text .south-west-alt').powerTip({ placement: 'sw-alt' }); - $('#huge-text .south-east-alt').powerTip({ placement: 'se-alt' }); - - // Huge text with smart placement - $('#huge-text-smart .north').powerTip({ placement: 'n', smartPlacement: true }); - $('#huge-text-smart .east').powerTip({ placement: 'e', smartPlacement: true }); - $('#huge-text-smart .south').powerTip({ placement: 's', smartPlacement: true }); - $('#huge-text-smart .west').powerTip({ placement: 'w', smartPlacement: true }); - $('#huge-text-smart .north-west').powerTip({ placement: 'nw', smartPlacement: true }); - $('#huge-text-smart .north-east').powerTip({ placement: 'ne', smartPlacement: true }); - $('#huge-text-smart .south-west').powerTip({ placement: 'sw', smartPlacement: true }); - $('#huge-text-smart .south-east').powerTip({ placement: 'se', smartPlacement: true }); - $('#huge-text-smart .north-west-alt').powerTip({ placement: 'nw-alt', smartPlacement: true }); - $('#huge-text-smart .north-east-alt').powerTip({ placement: 'ne-alt', smartPlacement: true }); - $('#huge-text-smart .south-west-alt').powerTip({ placement: 'sw-alt', smartPlacement: true }); - $('#huge-text-smart .south-east-alt').powerTip({ placement: 'se-alt', smartPlacement: true }); - - // Trapped mouse following tooltip - $('#trapped-mousefollow').powerTip({ followMouse: true }); -}); From b9713e40a7377d8e201d11bac0641e701378a4e1 Mon Sep 17 00:00:00 2001 From: Joel Steres Date: Fri, 16 Jun 2017 03:15:01 -0700 Subject: [PATCH 7/9] Reworked positioning and collision computations. Measurement compensation moved to CSSCoordinates. Calculations redesigned based on study of measurement origin changes based on CSS position, margin and border widths. --- src/core.js | 3 +- src/csscoordinates.js | 101 ++++++++++++++++++++++++++++++++++++ src/placementcalculator.js | 48 ++++++++--------- src/tooltipcontroller.js | 24 ++++----- src/utility.js | 38 +++++++++----- test/unit/csscoordinates.js | 1 + test/unit/utility.js | 47 ++++++++++++----- 7 files changed, 198 insertions(+), 64 deletions(-) diff --git a/src/core.js b/src/core.js index 27bd7c78..e849efb3 100644 --- a/src/core.js +++ b/src/core.js @@ -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 } }; /** diff --git a/src/csscoordinates.js b/src/csscoordinates.js index 45b9ac2e..e2e33376 100644 --- a/src/csscoordinates.js +++ b/src/csscoordinates.js @@ -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'; @@ -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); + } + }); } diff --git a/src/placementcalculator.js b/src/placementcalculator.js index e476d77f..1ec10ed6 100644 --- a/src/placementcalculator.js +++ b/src/placementcalculator.js @@ -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; } diff --git a/src/tooltipcontroller.js b/src/tooltipcontroller.js index 81d6ec86..dc4a6d4d 100644 --- a/src/tooltipcontroller.js +++ b/src/tooltipcontroller.js @@ -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'); @@ -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, @@ -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()); } } @@ -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 @@ -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 && diff --git a/src/utility.js b/src/utility.js index 27310078..6378b880 100644 --- a/src/utility.js +++ b/src/utility.js @@ -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; } @@ -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; } diff --git a/test/unit/csscoordinates.js b/test/unit/csscoordinates.js index 21c77139..c0d0b99b 100644 --- a/test/unit/csscoordinates.js +++ b/test/unit/csscoordinates.js @@ -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) { diff --git a/test/unit/utility.js b/test/unit/utility.js index df18e732..fc207bec 100644 --- a/test/unit/utility.js +++ b/test/unit/utility.js @@ -143,29 +143,48 @@ $(function() { assert.strictEqual(countFlags(topLeft), 2, 'exactly two collisions detected for top-left test'); } + function makeCoords(val) { + var coords = new CSSCoordinates(); + assert.ok(coords); + assert.ok(coords.getCompensated); + if ('top' in val) { + coords.set('top', val.top); + } + if ('left' in val) { + coords.set('left', val.left); + } + if ('right' in val) { + coords.set('right', val.right); + } + if ('bottom' in val) { + coords.set('bottom', val.bottom); + } + return coords; + } + // need to make sure initTracking() has been invoked to populate the // viewport dimensions cache initTracking(); // top/left placement - none = getViewportCollisions({ top: 0, left: 0 }, 200, 100); - right = getViewportCollisions({ top: 0, left: windowWidth - 199 }, 200, 100); - bottom = getViewportCollisions({ top: windowHeight - 99, left: 0 }, 200, 100); - bottomRight = getViewportCollisions({ top: windowHeight - 99, left: windowWidth - 199 }, 200, 100); - top = getViewportCollisions({ top: -1, left: 0 }, 200, 100); - left = getViewportCollisions({ top: 0, left: -1 }, 200, 100); - topLeft = getViewportCollisions({ top: -1, left: -1 }, 200, 100); + none = getViewportCollisions(makeCoords({ top: 0, left: 0 }), 200, 100); + right = getViewportCollisions(makeCoords({ top: 0, left: windowWidth - 199 }), 200, 100); + bottom = getViewportCollisions(makeCoords({ top: windowHeight - 99, left: 0 }), 200, 100); + bottomRight = getViewportCollisions(makeCoords({ top: windowHeight - 99, left: windowWidth - 199 }), 200, 100); + top = getViewportCollisions(makeCoords({ top: -1, left: 0 }), 200, 100); + left = getViewportCollisions(makeCoords({ top: 0, left: -1 }), 200, 100); + topLeft = getViewportCollisions(makeCoords({ top: -1, left: -1 }), 200, 100); doTests(); // bottom/right placement - none = getViewportCollisions({ bottom: 0, right: 0 }, 200, 100); - right = getViewportCollisions({ bottom: 0, right: -1 }, 200, 100); - bottom = getViewportCollisions({ bottom: -1, right: 0 }, 200, 100); - bottomRight = getViewportCollisions({ bottom: -1, right: -1 }, 200, 100); - top = getViewportCollisions({ bottom: windowHeight - 99, right: 0 }, 200, 100); - left = getViewportCollisions({ bottom: 0, right: windowWidth - 199 }, 200, 100); - topLeft = getViewportCollisions({ bottom: windowHeight - 99, right: windowWidth - 199 }, 200, 100); + none = getViewportCollisions(makeCoords({ bottom: 0, right: 0 }), 200, 100); + right = getViewportCollisions(makeCoords({ bottom: 0, right: -1 }), 200, 100); + bottom = getViewportCollisions(makeCoords({ bottom: -1, right: 0 }), 200, 100); + bottomRight = getViewportCollisions(makeCoords({ bottom: -1, right: -1 }), 200, 100); + top = getViewportCollisions(makeCoords({ bottom: windowHeight - 99, right: 0 }), 200, 100); + left = getViewportCollisions(makeCoords({ bottom: 0, right: windowWidth - 199 }), 200, 100); + topLeft = getViewportCollisions(makeCoords({ bottom: windowHeight - 99, right: windowWidth - 199 }), 200, 100); doTests(); }); From ac7faa4c0dabe097b0206d4486a61369b435c7f7 Mon Sep 17 00:00:00 2001 From: Joel Steres Date: Wed, 10 Jan 2018 05:01:07 -0800 Subject: [PATCH 8/9] Add docstring to compensation function. --- src/csscoordinates.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/csscoordinates.js b/src/csscoordinates.js index e2e33376..4aeff590 100644 --- a/src/csscoordinates.js +++ b/src/csscoordinates.js @@ -15,8 +15,15 @@ function CSSCoordinates() { var me = this; - function compensated(val, comp) { - return val === 'auto' ? val : val - comp; + /** + * 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; } /** From 9ff077b411b74330ee0be3dd1b23e6347bceeb36 Mon Sep 17 00:00:00 2001 From: Joel Steres Date: Wed, 10 Jan 2018 05:05:27 -0800 Subject: [PATCH 9/9] Add docstring returns description to positionedParentViewportHomeOffset --- src/csscoordinates.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/csscoordinates.js b/src/csscoordinates.js index 4aeff590..345d7611 100644 --- a/src/csscoordinates.js +++ b/src/csscoordinates.js @@ -30,6 +30,7 @@ function CSSCoordinates() { * 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,