Wed, 31 Dec 2014 13:27:57 +0100
Ignore runtime configuration files generated during quality assurance.
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/. */
7 const Cu = Components.utils;
8 const Ci = Components.interfaces;
9 const Cr = Components.results;
11 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
13 XPCOMUtils.defineLazyModuleGetter(this, "Services",
14 "resource://gre/modules/Services.jsm");
16 this.EXPORTED_SYMBOLS = ["LayoutHelpers"];
18 this.LayoutHelpers = LayoutHelpers = function(aTopLevelWindow) {
19 this._topDocShell = aTopLevelWindow.QueryInterface(Ci.nsIInterfaceRequestor)
20 .getInterface(Ci.nsIWebNavigation)
21 .QueryInterface(Ci.nsIDocShell);
22 };
24 LayoutHelpers.prototype = {
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 }
40 let [quads] = node.getBoxQuads({
41 box: region
42 });
44 if (!quads) {
45 return;
46 }
48 let [xOffset, yOffset] = this._getNodeOffsets(node);
49 let scale = this.calculateScale(node);
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 },
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 },
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();
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};
116 // We iterate through all the parent windows.
117 while (true) {
119 // Are we in the top-level window?
120 if (this.isTopLevelWindow(frameWin)) {
121 break;
122 }
124 let frameElement = this.getFrameElement(frameWin);
125 if (!frameElement) {
126 break;
127 }
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();
134 let [offsetTop, offsetLeft] =
135 this.getIframeContentOffset(frameElement);
137 rect.top += frameRect.top + offsetTop;
138 rect.left += frameRect.left + offsetLeft;
140 frameWin = this.getParentWindow(frameWin);
141 }
143 return rect;
144 },
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);
163 // In some cases, the computed style is null
164 if (!style) {
165 return [0, 0];
166 }
168 let paddingTop = parseInt(style.getPropertyValue("padding-top"));
169 let paddingLeft = parseInt(style.getPropertyValue("padding-left"));
171 let borderTop = parseInt(style.getPropertyValue("border-top-width"));
172 let borderLeft = parseInt(style.getPropertyValue("border-left-width"));
174 return [borderTop + paddingTop, borderLeft + paddingLeft];
175 },
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();
192 // Gap between the iframe and its content window.
193 let [offsetTop, offsetLeft] = this.getIframeContentOffset(node);
195 aX -= rect.left + offsetLeft;
196 aY -= rect.top + offsetTop;
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 },
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;
227 let win = elem.ownerDocument.defaultView;
228 let clientRect = elem.getBoundingClientRect();
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.
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.
242 // Whatever `centered` is, the behavior is the same if the box is
243 // (even partially) visible.
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 }
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 }
267 // If we want it centered, and the box is completely hidden,
268 // then we center it explicitly.
270 if (centered) {
272 if (yAllowed && (topToBottom <= 0 || bottomToTop >= 0)) {
273 win.scroll(win.scrollX,
274 win.scrollY + clientRect.top
275 - (win.innerHeight - elem.offsetHeight) / 2);
276 }
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 }
285 if (!this.isTopLevelWindow(win)) {
286 // We are inside an iframe.
287 let frameElement = this.getFrameElement(win);
288 this.scrollIntoViewIfNeeded(frameElement, centered);
289 }
290 },
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 },
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);
319 return docShell === this._topDocShell;
320 },
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 }
330 let parent = this.getParentWindow(win);
331 if (!parent || parent === win) {
332 return false;
333 }
335 return this.isIncludedInTopLevelWindow(parent);
336 },
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 }
346 let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor)
347 .getInterface(Ci.nsIWebNavigation)
348 .QueryInterface(Ci.nsIDocShell);
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 },
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 }
369 let winUtils = win.
370 QueryInterface(Components.interfaces.nsIInterfaceRequestor).
371 getInterface(Components.interfaces.nsIDOMWindowUtils);
373 return winUtils.containerElement;
374 },
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);
388 while (true) {
389 // Are we in the top-level window?
390 if (this.isTopLevelWindow(frameWin)) {
391 break;
392 }
394 let frameElement = this.getFrameElement(frameWin);
395 if (!frameElement) {
396 break;
397 }
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();
404 let [offsetTop, offsetLeft] =
405 this.getIframeContentOffset(frameElement);
407 xOffset += frameRect.left + offsetLeft;
408 yOffset += frameRect.top + offsetTop;
410 frameWin = this.getParentWindow(frameWin);
411 }
413 return [xOffset * scale, yOffset * scale];
414 },
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);
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 };
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 };
455 return out;
456 },
458 _parseNb: function(distance) {
459 let nb = parseFloat(distance, 10);
460 return isNaN(nb) ? 0 : nb;
461 },
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();
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 }
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: {}};
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 }
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 }
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 },
506 /********************************************************************
507 * GetBoxQuads POLYFILL END
508 ********************************************************************/
509 };