diff --git a/src/core.js b/src/core.js index cc11d8c5..e849efb3 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 @@ -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 } }; /** diff --git a/src/csscoordinates.js b/src/csscoordinates.js index 45b9ac2e..345d7611 100644 --- a/src/csscoordinates.js +++ b/src/csscoordinates.js @@ -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'; @@ -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); + } + }); } diff --git a/src/tooltipcontroller.js b/src/tooltipcontroller.js index f5d258f7..dc4a6d4d 100644 --- a/src/tooltipcontroller.js +++ b/src/tooltipcontroller.js @@ -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'); @@ -260,7 +260,7 @@ function TooltipController(options) { } // 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 b1b5455f..6378b880 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,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; } @@ -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; +} 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 @@