Skip to content

Commit b9713e4

Browse files
author
Joel Steres
committed
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.
1 parent fcedf69 commit b9713e4

File tree

7 files changed

+198
-64
lines changed

7 files changed

+198
-64
lines changed

src/core.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ var session = {
6161
windowWidth: 0,
6262
windowHeight: 0,
6363
scrollTop: 0,
64-
scrollLeft: 0
64+
scrollLeft: 0,
65+
positionCompensation: { top: 0, bottom: 0, left: 0, right: 0 }
6566
};
6667

6768
/**

src/csscoordinates.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,26 @@
1515
function CSSCoordinates() {
1616
var me = this;
1717

18+
function compensated(val, comp) {
19+
return val === 'auto' ? val : val - comp;
20+
}
21+
22+
/**
23+
* Return positioned element's origin with respect to the viewport home
24+
* @private
25+
* @param {object} el The positioned element to measure
26+
*/
27+
function positionedParentViewportHomeOffset(el) {
28+
var originX = el[0].getBoundingClientRect().left,
29+
originY = el[0].getBoundingClientRect().top,
30+
borderTopWidth = parseFloat(el.css('borderTopWidth')),
31+
borderLeftWidth = parseFloat(el.css('borderLeftWidth'));
32+
return {
33+
top: originY + borderTopWidth + $document.scrollTop(),
34+
left: originX + borderLeftWidth + $document.scrollLeft()
35+
};
36+
}
37+
1838
// initialize object properties
1939
me.top = 'auto';
2040
me.left = 'auto';
@@ -32,4 +52,85 @@ function CSSCoordinates() {
3252
me[property] = Math.round(value);
3353
}
3454
};
55+
56+
me.getCompensated = function() {
57+
return {
58+
top: me.topCompensated,
59+
left: me.leftCompensated,
60+
right: me.rightCompensated,
61+
bottom: me.bottomCompensated
62+
};
63+
};
64+
65+
me.fromViewportHome = function() {
66+
// Coordinates with respect to viewport origin when scrolled to (0,0).
67+
var coords = me.getCompensated(),
68+
originOffset;
69+
70+
// For the cases where there is a positioned ancestor, compensate for offset of
71+
// ancestor origin. Note that bounding rect includes border, if any.
72+
if (isPositionNotStatic($body)) {
73+
originOffset = positionedParentViewportHomeOffset($body);
74+
if (coords.top !== 'auto') {
75+
coords.top = coords.top + originOffset.top;
76+
}
77+
if (coords.left !== 'auto') {
78+
coords.left = coords.left + originOffset.left;
79+
}
80+
if (coords.right !== 'auto') {
81+
coords.right = originOffset.left + $body.width() - coords.right;
82+
}
83+
if (coords.bottom !== 'auto') {
84+
coords.bottom = originOffset.top + $body.height() - coords.bottom;
85+
}
86+
} else if (isPositionNotStatic($html)) {
87+
originOffset = positionedParentViewportHomeOffset($html);
88+
if (coords.top !== 'auto') {
89+
coords.top = coords.top + originOffset.top;
90+
}
91+
if (coords.left !== 'auto') {
92+
coords.left = coords.left + originOffset.left;
93+
}
94+
if (coords.right !== 'auto') {
95+
coords.right = originOffset.left + $body.width() - coords.right;
96+
}
97+
if (coords.bottom !== 'auto') {
98+
coords.bottom = originOffset.top + $body.height() - coords.bottom;
99+
}
100+
} else {
101+
// Change origin of right, bottom measurement to viewport (0,0) and invert sign
102+
if (coords.right !== 'auto') {
103+
coords.right = session.windowWidth - coords.right;
104+
}
105+
if (coords.bottom !== 'auto') {
106+
coords.bottom = session.windowHeight - coords.bottom;
107+
}
108+
}
109+
110+
return coords;
111+
};
112+
113+
Object.defineProperty(me, 'topCompensated', {
114+
get: function() {
115+
return compensated(me.top, session.positionCompensation.top);
116+
}
117+
});
118+
119+
Object.defineProperty(me, 'bottomCompensated', {
120+
get: function() {
121+
return compensated(me.bottom, session.positionCompensation.bottom);
122+
}
123+
});
124+
125+
Object.defineProperty(me, 'leftCompensated', {
126+
get: function() {
127+
return compensated(me.left, session.positionCompensation.left);
128+
}
129+
});
130+
131+
Object.defineProperty(me, 'rightCompensated', {
132+
get: function() {
133+
return compensated(me.right, session.positionCompensation.right);
134+
}
135+
});
35136
}

src/placementcalculator.js

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,52 +38,52 @@ function PlacementCalculator() {
3838
// calculate the appropriate x and y position in the document
3939
switch (placement) {
4040
case 'n':
41-
coords.set('left', position.left - (tipWidth / 2) - session.positionCompensation.left);
42-
coords.set('bottom', session.windowHeight - position.top + offset - session.positionCompensation.bottom);
41+
coords.set('left', position.left - (tipWidth / 2));
42+
coords.set('bottom', session.windowHeight - position.top + offset);
4343
break;
4444
case 'e':
45-
coords.set('left', position.left + offset - session.positionCompensation.left);
46-
coords.set('top', position.top - (tipHeight / 2) - session.positionCompensation.top);
45+
coords.set('left', position.left + offset);
46+
coords.set('top', position.top - (tipHeight / 2));
4747
break;
4848
case 's':
49-
coords.set('left', position.left - (tipWidth / 2) - session.positionCompensation.left);
50-
coords.set('top', position.top + offset - session.positionCompensation.top);
49+
coords.set('left', position.left - (tipWidth / 2));
50+
coords.set('top', position.top + offset);
5151
break;
5252
case 'w':
53-
coords.set('top', position.top - (tipHeight / 2) - session.positionCompensation.top);
54-
coords.set('right', session.windowWidth - position.left + offset - session.positionCompensation.right);
53+
coords.set('top', position.top - (tipHeight / 2));
54+
coords.set('right', session.windowWidth - position.left + offset);
5555
break;
5656
case 'nw':
57-
coords.set('bottom', session.windowHeight - position.top + offset - session.positionCompensation.bottom);
58-
coords.set('right', session.windowWidth - position.left - session.positionCompensation.right - 20);
57+
coords.set('bottom', session.windowHeight - position.top + offset);
58+
coords.set('right', session.windowWidth - position.left - 20);
5959
break;
6060
case 'nw-alt':
61-
coords.set('left', position.left - session.positionCompensation.left);
62-
coords.set('bottom', session.windowHeight - position.top + offset - session.positionCompensation.bottom);
61+
coords.set('left', position.left);
62+
coords.set('bottom', session.windowHeight - position.top + offset);
6363
break;
6464
case 'ne':
65-
coords.set('left', position.left - session.positionCompensation.left - 20);
66-
coords.set('bottom', session.windowHeight - position.top + offset - session.positionCompensation.bottom);
65+
coords.set('left', position.left - 20);
66+
coords.set('bottom', session.windowHeight - position.top + offset);
6767
break;
6868
case 'ne-alt':
69-
coords.set('bottom', session.windowHeight - position.top + offset - session.positionCompensation.bottom);
70-
coords.set('right', session.windowWidth - position.left - session.positionCompensation.right);
69+
coords.set('bottom', session.windowHeight - position.top + offset);
70+
coords.set('right', session.windowWidth - position.left);
7171
break;
7272
case 'sw':
73-
coords.set('top', position.top + offset - session.positionCompensation.top);
74-
coords.set('right', session.windowWidth - position.left - session.positionCompensation.right - 20);
73+
coords.set('top', position.top + offset);
74+
coords.set('right', session.windowWidth - position.left - 20);
7575
break;
7676
case 'sw-alt':
77-
coords.set('left', position.left - session.positionCompensation.left);
78-
coords.set('top', position.top + offset - session.positionCompensation.top);
77+
coords.set('left', position.left);
78+
coords.set('top', position.top + offset);
7979
break;
8080
case 'se':
81-
coords.set('left', position.left - session.positionCompensation.left - 20);
82-
coords.set('top', position.top + offset - session.positionCompensation.top);
81+
coords.set('left', position.left - 20);
82+
coords.set('top', position.top + offset);
8383
break;
8484
case 'se-alt':
85-
coords.set('top', position.top + offset - session.positionCompensation.top);
86-
coords.set('right', session.windowWidth - position.left - session.positionCompensation.right);
85+
coords.set('top', position.top + offset);
86+
coords.set('right', session.windowWidth - position.left);
8787
break;
8888
}
8989

src/tooltipcontroller.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,9 @@ function TooltipController(options) {
202202

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

209209
// trigger powerTipClose event
210210
element.trigger('powerTipClose');
@@ -231,8 +231,8 @@ function TooltipController(options) {
231231
collisionCount;
232232

233233
// grab collisions
234-
coords.set('top', session.currentY + options.offset - session.positionCompensation.top);
235-
coords.set('left', session.currentX + options.offset - session.positionCompensation.left);
234+
coords.set('top', session.currentY + options.offset);
235+
coords.set('left', session.currentX + options.offset);
236236
collisions = getViewportCollisions(
237237
coords,
238238
tipWidth,
@@ -246,21 +246,21 @@ function TooltipController(options) {
246246
// if there is only one collision (bottom or right) then
247247
// simply constrain the tooltip to the view port
248248
if (collisions === Collision.right) {
249-
coords.set('left', session.windowWidth - tipWidth - session.positionCompensation.left);
249+
coords.set('left', session.windowWidth - tipWidth);
250250
} else if (collisions === Collision.bottom) {
251-
coords.set('top', session.scrollTop + session.windowHeight - tipHeight - session.positionCompensation.top);
251+
coords.set('top', session.scrollTop + session.windowHeight - tipHeight);
252252
}
253253
} else {
254254
// if the tooltip has more than one collision then it is
255255
// trapped in the corner and should be flipped to get it out
256256
// of the users way
257-
coords.set('left', session.currentX - tipWidth - options.offset - session.positionCompensation.left);
258-
coords.set('top', session.currentY - tipHeight - options.offset - session.positionCompensation.top);
257+
coords.set('left', session.currentX - tipWidth - options.offset);
258+
coords.set('top', session.currentY - tipHeight - options.offset);
259259
}
260260
}
261261

262262
// position the tooltip
263-
tipElement.css(coords);
263+
tipElement.css(coords.getCompensated());
264264
}
265265
}
266266

@@ -325,7 +325,7 @@ function TooltipController(options) {
325325
// set the tip to 0,0 to get the full expanded width
326326
coords.set('top', 0);
327327
coords.set('left', 0);
328-
tipElement.css(coords);
328+
tipElement.css(coords.getCompensated());
329329

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

346346
// place the tooltip
347-
tipElement.css(coords);
347+
tipElement.css(coords.getCompensated());
348348
} while (
349349
// sanity check: limit to 5 iterations, and...
350350
++iterationCount <= 5 &&

src/utility.js

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -170,22 +170,23 @@ function getTooltipContent(element) {
170170
function getViewportCollisions(coords, elementWidth, elementHeight) {
171171
// adjusting viewport even though it might be negative because coords
172172
// comparing with are relative to compensated position
173-
var viewportTop = session.scrollTop - session.positionCompensation.top,
174-
viewportLeft = session.scrollLeft - session.positionCompensation.left,
173+
var viewportTop = session.scrollTop,
174+
viewportLeft = session.scrollLeft,
175175
viewportBottom = viewportTop + session.windowHeight,
176176
viewportRight = viewportLeft + session.windowWidth,
177+
coordsFromViewport = coords.fromViewportHome(),
177178
collisions = Collision.none;
178179

179-
if (coords.top < viewportTop || Math.abs(coords.bottom - session.windowHeight) - elementHeight < viewportTop) {
180+
if (coordsFromViewport.top < viewportTop || coordsFromViewport.bottom - elementHeight < viewportTop) {
180181
collisions |= Collision.top;
181182
}
182-
if (coords.top + elementHeight > viewportBottom || Math.abs(coords.bottom - session.windowHeight) > viewportBottom) {
183+
if (coordsFromViewport.top + elementHeight > viewportBottom || coordsFromViewport.bottom > viewportBottom) {
183184
collisions |= Collision.bottom;
184185
}
185-
if (coords.left < viewportLeft || coords.right + elementWidth > viewportRight) {
186+
if (coordsFromViewport.left < viewportLeft || coordsFromViewport.right - elementWidth < viewportLeft) {
186187
collisions |= Collision.left;
187188
}
188-
if (coords.left + elementWidth > viewportRight || coords.right < viewportLeft) {
189+
if (coordsFromViewport.left + elementWidth > viewportRight || coordsFromViewport.right > viewportRight) {
189190
collisions |= Collision.right;
190191
}
191192

@@ -220,16 +221,27 @@ function isPositionNotStatic(element) {
220221
* Get element offsets
221222
* @private
222223
* @param {jQuery} el Element to check
223-
* @param {number} windowWidth Window width in pixels.
224-
* @param {number} windowHeight Window height in pixels.
225224
* @return {Object} The top, left, right, bottom offset in pixels
226225
*/
227-
function getElementOffsets(el, windowWidth, windowHeight) {
226+
function getElementOffsets(el) {
228227
// jquery offset returns top and left relative to document in pixels.
229-
var offsets = el.offset();
230-
// right and bottom offset relative to window width/height
231-
offsets.right = windowWidth - el.outerWidth() - offsets.left;
232-
offsets.bottom = windowHeight - el.outerHeight() - offsets.top;
228+
var offsets = el.offset(),
229+
borderLeftWidth = parseFloat(el.css('border-left-width')),
230+
borderTopWidth = parseFloat(el.css('border-top-width')),
231+
right,
232+
bottom;
233+
234+
// right and bottom offset were relative to where screen.width,
235+
// screen.height fell in document. Change reference point to inner-bottom,
236+
// inner-right of element. Compensate for border which is outside
237+
// measurement area. Avoid updating any measurement set to 'auto' which will
238+
// result in a computed result of NaN.
239+
right = session.windowWidth - el.innerWidth() - offsets.left - borderLeftWidth;
240+
bottom = session.windowHeight - el.innerHeight() - offsets.top - borderTopWidth;
241+
offsets.top = offsets.top + borderTopWidth;
242+
offsets.left = offsets.left + borderLeftWidth;
243+
offsets.right = right ? right : 0;
244+
offsets.bottom = bottom ? bottom : 0;
233245
return offsets;
234246
}
235247

test/unit/csscoordinates.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ $(function() {
1414
QUnit.test('expose methods', function(assert) {
1515
var coords = new CSSCoordinates();
1616
assert.strictEqual(typeof coords.set, 'function', 'set method is defined');
17+
assert.strictEqual(typeof coords.getCompensated, 'function', 'getCompensated method is defined');
1718
});
1819

1920
QUnit.test('decimal values are rounded', function(assert) {

0 commit comments

Comments
 (0)