|
1 /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 const Cu = Components.utils; |
|
8 const Ci = Components.interfaces; |
|
9 const Cr = Components.results; |
|
10 |
|
11 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
12 |
|
13 XPCOMUtils.defineLazyModuleGetter(this, "Services", |
|
14 "resource://gre/modules/Services.jsm"); |
|
15 |
|
16 this.EXPORTED_SYMBOLS = ["LayoutHelpers"]; |
|
17 |
|
18 this.LayoutHelpers = LayoutHelpers = function(aTopLevelWindow) { |
|
19 this._topDocShell = aTopLevelWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
|
20 .getInterface(Ci.nsIWebNavigation) |
|
21 .QueryInterface(Ci.nsIDocShell); |
|
22 }; |
|
23 |
|
24 LayoutHelpers.prototype = { |
|
25 |
|
26 /** |
|
27 * Get box quads adjusted for iframes and zoom level. |
|
28 * |
|
29 * @param {DOMNode} node |
|
30 * The node for which we are to get the box model region quads |
|
31 * @param {String} region |
|
32 * The box model region to return: |
|
33 * "content", "padding", "border" or "margin" |
|
34 */ |
|
35 getAdjustedQuads: function(node, region) { |
|
36 if (!node) { |
|
37 return; |
|
38 } |
|
39 |
|
40 let [quads] = node.getBoxQuads({ |
|
41 box: region |
|
42 }); |
|
43 |
|
44 if (!quads) { |
|
45 return; |
|
46 } |
|
47 |
|
48 let [xOffset, yOffset] = this._getNodeOffsets(node); |
|
49 let scale = this.calculateScale(node); |
|
50 |
|
51 return { |
|
52 p1: { |
|
53 w: quads.p1.w * scale, |
|
54 x: quads.p1.x * scale + xOffset, |
|
55 y: quads.p1.y * scale + yOffset, |
|
56 z: quads.p1.z * scale |
|
57 }, |
|
58 p2: { |
|
59 w: quads.p2.w * scale, |
|
60 x: quads.p2.x * scale + xOffset, |
|
61 y: quads.p2.y * scale + yOffset, |
|
62 z: quads.p2.z * scale |
|
63 }, |
|
64 p3: { |
|
65 w: quads.p3.w * scale, |
|
66 x: quads.p3.x * scale + xOffset, |
|
67 y: quads.p3.y * scale + yOffset, |
|
68 z: quads.p3.z * scale |
|
69 }, |
|
70 p4: { |
|
71 w: quads.p4.w * scale, |
|
72 x: quads.p4.x * scale + xOffset, |
|
73 y: quads.p4.y * scale + yOffset, |
|
74 z: quads.p4.z * scale |
|
75 }, |
|
76 bounds: { |
|
77 bottom: quads.bounds.bottom * scale + yOffset, |
|
78 height: quads.bounds.height * scale, |
|
79 left: quads.bounds.left * scale + xOffset, |
|
80 right: quads.bounds.right * scale + xOffset, |
|
81 top: quads.bounds.top * scale + yOffset, |
|
82 width: quads.bounds.width * scale, |
|
83 x: quads.bounds.x * scale + xOffset, |
|
84 y: quads.bounds.y * scale + yOffset |
|
85 } |
|
86 }; |
|
87 }, |
|
88 |
|
89 calculateScale: function(node) { |
|
90 let win = node.ownerDocument.defaultView; |
|
91 let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) |
|
92 .getInterface(Ci.nsIDOMWindowUtils); |
|
93 return winUtils.fullZoom; |
|
94 }, |
|
95 |
|
96 /** |
|
97 * Compute the absolute position and the dimensions of a node, relativalely |
|
98 * to the root window. |
|
99 * |
|
100 * @param nsIDOMNode aNode |
|
101 * a DOM element to get the bounds for |
|
102 * @param nsIWindow aContentWindow |
|
103 * the content window holding the node |
|
104 */ |
|
105 getRect: function LH_getRect(aNode, aContentWindow) { |
|
106 let frameWin = aNode.ownerDocument.defaultView; |
|
107 let clientRect = aNode.getBoundingClientRect(); |
|
108 |
|
109 // Go up in the tree of frames to determine the correct rectangle. |
|
110 // clientRect is read-only, we need to be able to change properties. |
|
111 let rect = {top: clientRect.top + aContentWindow.pageYOffset, |
|
112 left: clientRect.left + aContentWindow.pageXOffset, |
|
113 width: clientRect.width, |
|
114 height: clientRect.height}; |
|
115 |
|
116 // We iterate through all the parent windows. |
|
117 while (true) { |
|
118 |
|
119 // Are we in the top-level window? |
|
120 if (this.isTopLevelWindow(frameWin)) { |
|
121 break; |
|
122 } |
|
123 |
|
124 let frameElement = this.getFrameElement(frameWin); |
|
125 if (!frameElement) { |
|
126 break; |
|
127 } |
|
128 |
|
129 // We are in an iframe. |
|
130 // We take into account the parent iframe position and its |
|
131 // offset (borders and padding). |
|
132 let frameRect = frameElement.getBoundingClientRect(); |
|
133 |
|
134 let [offsetTop, offsetLeft] = |
|
135 this.getIframeContentOffset(frameElement); |
|
136 |
|
137 rect.top += frameRect.top + offsetTop; |
|
138 rect.left += frameRect.left + offsetLeft; |
|
139 |
|
140 frameWin = this.getParentWindow(frameWin); |
|
141 } |
|
142 |
|
143 return rect; |
|
144 }, |
|
145 |
|
146 /** |
|
147 * Returns iframe content offset (iframe border + padding). |
|
148 * Note: this function shouldn't need to exist, had the platform provided a |
|
149 * suitable API for determining the offset between the iframe's content and |
|
150 * its bounding client rect. Bug 626359 should provide us with such an API. |
|
151 * |
|
152 * @param aIframe |
|
153 * The iframe. |
|
154 * @returns array [offsetTop, offsetLeft] |
|
155 * offsetTop is the distance from the top of the iframe and the |
|
156 * top of the content document. |
|
157 * offsetLeft is the distance from the left of the iframe and the |
|
158 * left of the content document. |
|
159 */ |
|
160 getIframeContentOffset: function LH_getIframeContentOffset(aIframe) { |
|
161 let style = aIframe.contentWindow.getComputedStyle(aIframe, null); |
|
162 |
|
163 // In some cases, the computed style is null |
|
164 if (!style) { |
|
165 return [0, 0]; |
|
166 } |
|
167 |
|
168 let paddingTop = parseInt(style.getPropertyValue("padding-top")); |
|
169 let paddingLeft = parseInt(style.getPropertyValue("padding-left")); |
|
170 |
|
171 let borderTop = parseInt(style.getPropertyValue("border-top-width")); |
|
172 let borderLeft = parseInt(style.getPropertyValue("border-left-width")); |
|
173 |
|
174 return [borderTop + paddingTop, borderLeft + paddingLeft]; |
|
175 }, |
|
176 |
|
177 /** |
|
178 * Find an element from the given coordinates. This method descends through |
|
179 * frames to find the element the user clicked inside frames. |
|
180 * |
|
181 * @param DOMDocument aDocument the document to look into. |
|
182 * @param integer aX |
|
183 * @param integer aY |
|
184 * @returns Node|null the element node found at the given coordinates. |
|
185 */ |
|
186 getElementFromPoint: function LH_elementFromPoint(aDocument, aX, aY) { |
|
187 let node = aDocument.elementFromPoint(aX, aY); |
|
188 if (node && node.contentDocument) { |
|
189 if (node instanceof Ci.nsIDOMHTMLIFrameElement) { |
|
190 let rect = node.getBoundingClientRect(); |
|
191 |
|
192 // Gap between the iframe and its content window. |
|
193 let [offsetTop, offsetLeft] = this.getIframeContentOffset(node); |
|
194 |
|
195 aX -= rect.left + offsetLeft; |
|
196 aY -= rect.top + offsetTop; |
|
197 |
|
198 if (aX < 0 || aY < 0) { |
|
199 // Didn't reach the content document, still over the iframe. |
|
200 return node; |
|
201 } |
|
202 } |
|
203 if (node instanceof Ci.nsIDOMHTMLIFrameElement || |
|
204 node instanceof Ci.nsIDOMHTMLFrameElement) { |
|
205 let subnode = this.getElementFromPoint(node.contentDocument, aX, aY); |
|
206 if (subnode) { |
|
207 node = subnode; |
|
208 } |
|
209 } |
|
210 } |
|
211 return node; |
|
212 }, |
|
213 |
|
214 /** |
|
215 * Scroll the document so that the element "elem" appears in the viewport. |
|
216 * |
|
217 * @param Element elem the element that needs to appear in the viewport. |
|
218 * @param bool centered true if you want it centered, false if you want it to |
|
219 * appear on the top of the viewport. It is true by default, and that is |
|
220 * usually what you want. |
|
221 */ |
|
222 scrollIntoViewIfNeeded: function(elem, centered) { |
|
223 // We want to default to centering the element in the page, |
|
224 // so as to keep the context of the element. |
|
225 centered = centered === undefined? true: !!centered; |
|
226 |
|
227 let win = elem.ownerDocument.defaultView; |
|
228 let clientRect = elem.getBoundingClientRect(); |
|
229 |
|
230 // The following are always from the {top, bottom, left, right} |
|
231 // of the viewport, to the {top, …} of the box. |
|
232 // Think of them as geometrical vectors, it helps. |
|
233 // The origin is at the top left. |
|
234 |
|
235 let topToBottom = clientRect.bottom; |
|
236 let bottomToTop = clientRect.top - win.innerHeight; |
|
237 let leftToRight = clientRect.right; |
|
238 let rightToLeft = clientRect.left - win.innerWidth; |
|
239 let xAllowed = true; // We allow one translation on the x axis, |
|
240 let yAllowed = true; // and one on the y axis. |
|
241 |
|
242 // Whatever `centered` is, the behavior is the same if the box is |
|
243 // (even partially) visible. |
|
244 |
|
245 if ((topToBottom > 0 || !centered) && topToBottom <= elem.offsetHeight) { |
|
246 win.scrollBy(0, topToBottom - elem.offsetHeight); |
|
247 yAllowed = false; |
|
248 } else |
|
249 if ((bottomToTop < 0 || !centered) && bottomToTop >= -elem.offsetHeight) { |
|
250 win.scrollBy(0, bottomToTop + elem.offsetHeight); |
|
251 yAllowed = false; |
|
252 } |
|
253 |
|
254 if ((leftToRight > 0 || !centered) && leftToRight <= elem.offsetWidth) { |
|
255 if (xAllowed) { |
|
256 win.scrollBy(leftToRight - elem.offsetWidth, 0); |
|
257 xAllowed = false; |
|
258 } |
|
259 } else |
|
260 if ((rightToLeft < 0 || !centered) && rightToLeft >= -elem.offsetWidth) { |
|
261 if (xAllowed) { |
|
262 win.scrollBy(rightToLeft + elem.offsetWidth, 0); |
|
263 xAllowed = false; |
|
264 } |
|
265 } |
|
266 |
|
267 // If we want it centered, and the box is completely hidden, |
|
268 // then we center it explicitly. |
|
269 |
|
270 if (centered) { |
|
271 |
|
272 if (yAllowed && (topToBottom <= 0 || bottomToTop >= 0)) { |
|
273 win.scroll(win.scrollX, |
|
274 win.scrollY + clientRect.top |
|
275 - (win.innerHeight - elem.offsetHeight) / 2); |
|
276 } |
|
277 |
|
278 if (xAllowed && (leftToRight <= 0 || rightToLeft <= 0)) { |
|
279 win.scroll(win.scrollX + clientRect.left |
|
280 - (win.innerWidth - elem.offsetWidth) / 2, |
|
281 win.scrollY); |
|
282 } |
|
283 } |
|
284 |
|
285 if (!this.isTopLevelWindow(win)) { |
|
286 // We are inside an iframe. |
|
287 let frameElement = this.getFrameElement(win); |
|
288 this.scrollIntoViewIfNeeded(frameElement, centered); |
|
289 } |
|
290 }, |
|
291 |
|
292 /** |
|
293 * Check if a node and its document are still alive |
|
294 * and attached to the window. |
|
295 * |
|
296 * @param aNode |
|
297 */ |
|
298 isNodeConnected: function LH_isNodeConnected(aNode) |
|
299 { |
|
300 try { |
|
301 let connected = (aNode.ownerDocument && aNode.ownerDocument.defaultView && |
|
302 !(aNode.compareDocumentPosition(aNode.ownerDocument.documentElement) & |
|
303 aNode.DOCUMENT_POSITION_DISCONNECTED)); |
|
304 return connected; |
|
305 } catch (e) { |
|
306 // "can't access dead object" error |
|
307 return false; |
|
308 } |
|
309 }, |
|
310 |
|
311 /** |
|
312 * like win.parent === win, but goes through mozbrowsers and mozapps iframes. |
|
313 */ |
|
314 isTopLevelWindow: function LH_isTopLevelWindow(win) { |
|
315 let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor) |
|
316 .getInterface(Ci.nsIWebNavigation) |
|
317 .QueryInterface(Ci.nsIDocShell); |
|
318 |
|
319 return docShell === this._topDocShell; |
|
320 }, |
|
321 |
|
322 /** |
|
323 * Check a window is part of the top level window. |
|
324 */ |
|
325 isIncludedInTopLevelWindow: function LH_isIncludedInTopLevelWindow(win) { |
|
326 if (this.isTopLevelWindow(win)) { |
|
327 return true; |
|
328 } |
|
329 |
|
330 let parent = this.getParentWindow(win); |
|
331 if (!parent || parent === win) { |
|
332 return false; |
|
333 } |
|
334 |
|
335 return this.isIncludedInTopLevelWindow(parent); |
|
336 }, |
|
337 |
|
338 /** |
|
339 * like win.parent, but goes through mozbrowsers and mozapps iframes. |
|
340 */ |
|
341 getParentWindow: function LH_getParentWindow(win) { |
|
342 if (this.isTopLevelWindow(win)) { |
|
343 return null; |
|
344 } |
|
345 |
|
346 let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor) |
|
347 .getInterface(Ci.nsIWebNavigation) |
|
348 .QueryInterface(Ci.nsIDocShell); |
|
349 |
|
350 if (docShell.isBrowserOrApp) { |
|
351 let parentDocShell = docShell.getSameTypeParentIgnoreBrowserAndAppBoundaries(); |
|
352 return parentDocShell ? parentDocShell.contentViewer.DOMDocument.defaultView : null; |
|
353 } else { |
|
354 return win.parent; |
|
355 } |
|
356 }, |
|
357 |
|
358 /** |
|
359 * like win.frameElement, but goes through mozbrowsers and mozapps iframes. |
|
360 * |
|
361 * @param DOMWindow win The window to get the frame for |
|
362 * @return DOMElement The element in which the window is embedded. |
|
363 */ |
|
364 getFrameElement: function LH_getFrameElement(win) { |
|
365 if (this.isTopLevelWindow(win)) { |
|
366 return null; |
|
367 } |
|
368 |
|
369 let winUtils = win. |
|
370 QueryInterface(Components.interfaces.nsIInterfaceRequestor). |
|
371 getInterface(Components.interfaces.nsIDOMWindowUtils); |
|
372 |
|
373 return winUtils.containerElement; |
|
374 }, |
|
375 |
|
376 /** |
|
377 * Get the x and y offsets for a node taking iframes into account. |
|
378 * |
|
379 * @param {DOMNode} node |
|
380 * The node for which we are to get the offset |
|
381 */ |
|
382 _getNodeOffsets: function(node) { |
|
383 let xOffset = 0; |
|
384 let yOffset = 0; |
|
385 let frameWin = node.ownerDocument.defaultView; |
|
386 let scale = this.calculateScale(node); |
|
387 |
|
388 while (true) { |
|
389 // Are we in the top-level window? |
|
390 if (this.isTopLevelWindow(frameWin)) { |
|
391 break; |
|
392 } |
|
393 |
|
394 let frameElement = this.getFrameElement(frameWin); |
|
395 if (!frameElement) { |
|
396 break; |
|
397 } |
|
398 |
|
399 // We are in an iframe. |
|
400 // We take into account the parent iframe position and its |
|
401 // offset (borders and padding). |
|
402 let frameRect = frameElement.getBoundingClientRect(); |
|
403 |
|
404 let [offsetTop, offsetLeft] = |
|
405 this.getIframeContentOffset(frameElement); |
|
406 |
|
407 xOffset += frameRect.left + offsetLeft; |
|
408 yOffset += frameRect.top + offsetTop; |
|
409 |
|
410 frameWin = this.getParentWindow(frameWin); |
|
411 } |
|
412 |
|
413 return [xOffset * scale, yOffset * scale]; |
|
414 }, |
|
415 |
|
416 |
|
417 |
|
418 /******************************************************************** |
|
419 * GetBoxQuads POLYFILL START TODO: Remove this when bug 917755 is fixed. |
|
420 ********************************************************************/ |
|
421 _getBoxQuadsFromRect: function(rect, node) { |
|
422 let scale = this.calculateScale(node); |
|
423 let [xOffset, yOffset] = this._getNodeOffsets(node); |
|
424 |
|
425 let out = { |
|
426 p1: { |
|
427 x: rect.left * scale + xOffset, |
|
428 y: rect.top * scale + yOffset |
|
429 }, |
|
430 p2: { |
|
431 x: (rect.left + rect.width) * scale + xOffset, |
|
432 y: rect.top * scale + yOffset |
|
433 }, |
|
434 p3: { |
|
435 x: (rect.left + rect.width) * scale + xOffset, |
|
436 y: (rect.top + rect.height) * scale + yOffset |
|
437 }, |
|
438 p4: { |
|
439 x: rect.left * scale + xOffset, |
|
440 y: (rect.top + rect.height) * scale + yOffset |
|
441 } |
|
442 }; |
|
443 |
|
444 out.bounds = { |
|
445 bottom: out.p4.y, |
|
446 height: out.p4.y - out.p1.y, |
|
447 left: out.p1.x, |
|
448 right: out.p2.x, |
|
449 top: out.p1.y, |
|
450 width: out.p2.x - out.p1.x, |
|
451 x: out.p1.x, |
|
452 y: out.p1.y |
|
453 }; |
|
454 |
|
455 return out; |
|
456 }, |
|
457 |
|
458 _parseNb: function(distance) { |
|
459 let nb = parseFloat(distance, 10); |
|
460 return isNaN(nb) ? 0 : nb; |
|
461 }, |
|
462 |
|
463 getAdjustedQuadsPolyfill: function(node, region) { |
|
464 // Get the border-box rect |
|
465 // Note that this is relative to the node's viewport, so before we can use |
|
466 // it, will need to go back up the frames like getRect |
|
467 let borderRect = node.getBoundingClientRect(); |
|
468 |
|
469 // If the boxType is border, no need to go any further, we're done |
|
470 if (region === "border") { |
|
471 return this._getBoxQuadsFromRect(borderRect, node); |
|
472 } |
|
473 |
|
474 // Else, need to get margin/padding/border distances |
|
475 let style = node.ownerDocument.defaultView.getComputedStyle(node); |
|
476 let camel = s => s.substring(0, 1).toUpperCase() + s.substring(1); |
|
477 let distances = {border:{}, padding:{}, margin: {}}; |
|
478 |
|
479 for (let side of ["top", "right", "bottom", "left"]) { |
|
480 distances.border[side] = this._parseNb(style["border" + camel(side) + "Width"]); |
|
481 distances.padding[side] = this._parseNb(style["padding" + camel(side)]); |
|
482 distances.margin[side] = this._parseNb(style["margin" + camel(side)]); |
|
483 } |
|
484 |
|
485 // From the border-box rect, calculate the content-box, padding-box and |
|
486 // margin-box rects |
|
487 function offsetRect(rect, offsetType, dir=1) { |
|
488 return { |
|
489 top: rect.top + (dir * distances[offsetType].top), |
|
490 left: rect.left + (dir * distances[offsetType].left), |
|
491 width: rect.width - (dir * (distances[offsetType].left + distances[offsetType].right)), |
|
492 height: rect.height - (dir * (distances[offsetType].top + distances[offsetType].bottom)) |
|
493 }; |
|
494 } |
|
495 |
|
496 if (region === "margin") { |
|
497 return this._getBoxQuadsFromRect(offsetRect(borderRect, "margin", -1), node); |
|
498 } else if (region === "padding") { |
|
499 return this._getBoxQuadsFromRect(offsetRect(borderRect, "border"), node); |
|
500 } else if (region === "content") { |
|
501 let paddingRect = offsetRect(borderRect, "border"); |
|
502 return this._getBoxQuadsFromRect(offsetRect(paddingRect, "padding"), node); |
|
503 } |
|
504 }, |
|
505 |
|
506 /******************************************************************** |
|
507 * GetBoxQuads POLYFILL END |
|
508 ********************************************************************/ |
|
509 }; |