toolkit/devtools/LayoutHelpers.jsm

Wed, 31 Dec 2014 13:27:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 13:27:57 +0100
branch
TOR_BUG_3246
changeset 6
8bccb770b82d
permissions
-rw-r--r--

Ignore runtime configuration files generated during quality assurance.

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

mercurial