1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/devtools/LayoutHelpers.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,509 @@ 1.4 +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +const Cu = Components.utils; 1.11 +const Ci = Components.interfaces; 1.12 +const Cr = Components.results; 1.13 + 1.14 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.15 + 1.16 +XPCOMUtils.defineLazyModuleGetter(this, "Services", 1.17 + "resource://gre/modules/Services.jsm"); 1.18 + 1.19 +this.EXPORTED_SYMBOLS = ["LayoutHelpers"]; 1.20 + 1.21 +this.LayoutHelpers = LayoutHelpers = function(aTopLevelWindow) { 1.22 + this._topDocShell = aTopLevelWindow.QueryInterface(Ci.nsIInterfaceRequestor) 1.23 + .getInterface(Ci.nsIWebNavigation) 1.24 + .QueryInterface(Ci.nsIDocShell); 1.25 +}; 1.26 + 1.27 +LayoutHelpers.prototype = { 1.28 + 1.29 + /** 1.30 + * Get box quads adjusted for iframes and zoom level. 1.31 + * 1.32 + * @param {DOMNode} node 1.33 + * The node for which we are to get the box model region quads 1.34 + * @param {String} region 1.35 + * The box model region to return: 1.36 + * "content", "padding", "border" or "margin" 1.37 + */ 1.38 + getAdjustedQuads: function(node, region) { 1.39 + if (!node) { 1.40 + return; 1.41 + } 1.42 + 1.43 + let [quads] = node.getBoxQuads({ 1.44 + box: region 1.45 + }); 1.46 + 1.47 + if (!quads) { 1.48 + return; 1.49 + } 1.50 + 1.51 + let [xOffset, yOffset] = this._getNodeOffsets(node); 1.52 + let scale = this.calculateScale(node); 1.53 + 1.54 + return { 1.55 + p1: { 1.56 + w: quads.p1.w * scale, 1.57 + x: quads.p1.x * scale + xOffset, 1.58 + y: quads.p1.y * scale + yOffset, 1.59 + z: quads.p1.z * scale 1.60 + }, 1.61 + p2: { 1.62 + w: quads.p2.w * scale, 1.63 + x: quads.p2.x * scale + xOffset, 1.64 + y: quads.p2.y * scale + yOffset, 1.65 + z: quads.p2.z * scale 1.66 + }, 1.67 + p3: { 1.68 + w: quads.p3.w * scale, 1.69 + x: quads.p3.x * scale + xOffset, 1.70 + y: quads.p3.y * scale + yOffset, 1.71 + z: quads.p3.z * scale 1.72 + }, 1.73 + p4: { 1.74 + w: quads.p4.w * scale, 1.75 + x: quads.p4.x * scale + xOffset, 1.76 + y: quads.p4.y * scale + yOffset, 1.77 + z: quads.p4.z * scale 1.78 + }, 1.79 + bounds: { 1.80 + bottom: quads.bounds.bottom * scale + yOffset, 1.81 + height: quads.bounds.height * scale, 1.82 + left: quads.bounds.left * scale + xOffset, 1.83 + right: quads.bounds.right * scale + xOffset, 1.84 + top: quads.bounds.top * scale + yOffset, 1.85 + width: quads.bounds.width * scale, 1.86 + x: quads.bounds.x * scale + xOffset, 1.87 + y: quads.bounds.y * scale + yOffset 1.88 + } 1.89 + }; 1.90 + }, 1.91 + 1.92 + calculateScale: function(node) { 1.93 + let win = node.ownerDocument.defaultView; 1.94 + let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) 1.95 + .getInterface(Ci.nsIDOMWindowUtils); 1.96 + return winUtils.fullZoom; 1.97 + }, 1.98 + 1.99 + /** 1.100 + * Compute the absolute position and the dimensions of a node, relativalely 1.101 + * to the root window. 1.102 + * 1.103 + * @param nsIDOMNode aNode 1.104 + * a DOM element to get the bounds for 1.105 + * @param nsIWindow aContentWindow 1.106 + * the content window holding the node 1.107 + */ 1.108 + getRect: function LH_getRect(aNode, aContentWindow) { 1.109 + let frameWin = aNode.ownerDocument.defaultView; 1.110 + let clientRect = aNode.getBoundingClientRect(); 1.111 + 1.112 + // Go up in the tree of frames to determine the correct rectangle. 1.113 + // clientRect is read-only, we need to be able to change properties. 1.114 + let rect = {top: clientRect.top + aContentWindow.pageYOffset, 1.115 + left: clientRect.left + aContentWindow.pageXOffset, 1.116 + width: clientRect.width, 1.117 + height: clientRect.height}; 1.118 + 1.119 + // We iterate through all the parent windows. 1.120 + while (true) { 1.121 + 1.122 + // Are we in the top-level window? 1.123 + if (this.isTopLevelWindow(frameWin)) { 1.124 + break; 1.125 + } 1.126 + 1.127 + let frameElement = this.getFrameElement(frameWin); 1.128 + if (!frameElement) { 1.129 + break; 1.130 + } 1.131 + 1.132 + // We are in an iframe. 1.133 + // We take into account the parent iframe position and its 1.134 + // offset (borders and padding). 1.135 + let frameRect = frameElement.getBoundingClientRect(); 1.136 + 1.137 + let [offsetTop, offsetLeft] = 1.138 + this.getIframeContentOffset(frameElement); 1.139 + 1.140 + rect.top += frameRect.top + offsetTop; 1.141 + rect.left += frameRect.left + offsetLeft; 1.142 + 1.143 + frameWin = this.getParentWindow(frameWin); 1.144 + } 1.145 + 1.146 + return rect; 1.147 + }, 1.148 + 1.149 + /** 1.150 + * Returns iframe content offset (iframe border + padding). 1.151 + * Note: this function shouldn't need to exist, had the platform provided a 1.152 + * suitable API for determining the offset between the iframe's content and 1.153 + * its bounding client rect. Bug 626359 should provide us with such an API. 1.154 + * 1.155 + * @param aIframe 1.156 + * The iframe. 1.157 + * @returns array [offsetTop, offsetLeft] 1.158 + * offsetTop is the distance from the top of the iframe and the 1.159 + * top of the content document. 1.160 + * offsetLeft is the distance from the left of the iframe and the 1.161 + * left of the content document. 1.162 + */ 1.163 + getIframeContentOffset: function LH_getIframeContentOffset(aIframe) { 1.164 + let style = aIframe.contentWindow.getComputedStyle(aIframe, null); 1.165 + 1.166 + // In some cases, the computed style is null 1.167 + if (!style) { 1.168 + return [0, 0]; 1.169 + } 1.170 + 1.171 + let paddingTop = parseInt(style.getPropertyValue("padding-top")); 1.172 + let paddingLeft = parseInt(style.getPropertyValue("padding-left")); 1.173 + 1.174 + let borderTop = parseInt(style.getPropertyValue("border-top-width")); 1.175 + let borderLeft = parseInt(style.getPropertyValue("border-left-width")); 1.176 + 1.177 + return [borderTop + paddingTop, borderLeft + paddingLeft]; 1.178 + }, 1.179 + 1.180 + /** 1.181 + * Find an element from the given coordinates. This method descends through 1.182 + * frames to find the element the user clicked inside frames. 1.183 + * 1.184 + * @param DOMDocument aDocument the document to look into. 1.185 + * @param integer aX 1.186 + * @param integer aY 1.187 + * @returns Node|null the element node found at the given coordinates. 1.188 + */ 1.189 + getElementFromPoint: function LH_elementFromPoint(aDocument, aX, aY) { 1.190 + let node = aDocument.elementFromPoint(aX, aY); 1.191 + if (node && node.contentDocument) { 1.192 + if (node instanceof Ci.nsIDOMHTMLIFrameElement) { 1.193 + let rect = node.getBoundingClientRect(); 1.194 + 1.195 + // Gap between the iframe and its content window. 1.196 + let [offsetTop, offsetLeft] = this.getIframeContentOffset(node); 1.197 + 1.198 + aX -= rect.left + offsetLeft; 1.199 + aY -= rect.top + offsetTop; 1.200 + 1.201 + if (aX < 0 || aY < 0) { 1.202 + // Didn't reach the content document, still over the iframe. 1.203 + return node; 1.204 + } 1.205 + } 1.206 + if (node instanceof Ci.nsIDOMHTMLIFrameElement || 1.207 + node instanceof Ci.nsIDOMHTMLFrameElement) { 1.208 + let subnode = this.getElementFromPoint(node.contentDocument, aX, aY); 1.209 + if (subnode) { 1.210 + node = subnode; 1.211 + } 1.212 + } 1.213 + } 1.214 + return node; 1.215 + }, 1.216 + 1.217 + /** 1.218 + * Scroll the document so that the element "elem" appears in the viewport. 1.219 + * 1.220 + * @param Element elem the element that needs to appear in the viewport. 1.221 + * @param bool centered true if you want it centered, false if you want it to 1.222 + * appear on the top of the viewport. It is true by default, and that is 1.223 + * usually what you want. 1.224 + */ 1.225 + scrollIntoViewIfNeeded: function(elem, centered) { 1.226 + // We want to default to centering the element in the page, 1.227 + // so as to keep the context of the element. 1.228 + centered = centered === undefined? true: !!centered; 1.229 + 1.230 + let win = elem.ownerDocument.defaultView; 1.231 + let clientRect = elem.getBoundingClientRect(); 1.232 + 1.233 + // The following are always from the {top, bottom, left, right} 1.234 + // of the viewport, to the {top, …} of the box. 1.235 + // Think of them as geometrical vectors, it helps. 1.236 + // The origin is at the top left. 1.237 + 1.238 + let topToBottom = clientRect.bottom; 1.239 + let bottomToTop = clientRect.top - win.innerHeight; 1.240 + let leftToRight = clientRect.right; 1.241 + let rightToLeft = clientRect.left - win.innerWidth; 1.242 + let xAllowed = true; // We allow one translation on the x axis, 1.243 + let yAllowed = true; // and one on the y axis. 1.244 + 1.245 + // Whatever `centered` is, the behavior is the same if the box is 1.246 + // (even partially) visible. 1.247 + 1.248 + if ((topToBottom > 0 || !centered) && topToBottom <= elem.offsetHeight) { 1.249 + win.scrollBy(0, topToBottom - elem.offsetHeight); 1.250 + yAllowed = false; 1.251 + } else 1.252 + if ((bottomToTop < 0 || !centered) && bottomToTop >= -elem.offsetHeight) { 1.253 + win.scrollBy(0, bottomToTop + elem.offsetHeight); 1.254 + yAllowed = false; 1.255 + } 1.256 + 1.257 + if ((leftToRight > 0 || !centered) && leftToRight <= elem.offsetWidth) { 1.258 + if (xAllowed) { 1.259 + win.scrollBy(leftToRight - elem.offsetWidth, 0); 1.260 + xAllowed = false; 1.261 + } 1.262 + } else 1.263 + if ((rightToLeft < 0 || !centered) && rightToLeft >= -elem.offsetWidth) { 1.264 + if (xAllowed) { 1.265 + win.scrollBy(rightToLeft + elem.offsetWidth, 0); 1.266 + xAllowed = false; 1.267 + } 1.268 + } 1.269 + 1.270 + // If we want it centered, and the box is completely hidden, 1.271 + // then we center it explicitly. 1.272 + 1.273 + if (centered) { 1.274 + 1.275 + if (yAllowed && (topToBottom <= 0 || bottomToTop >= 0)) { 1.276 + win.scroll(win.scrollX, 1.277 + win.scrollY + clientRect.top 1.278 + - (win.innerHeight - elem.offsetHeight) / 2); 1.279 + } 1.280 + 1.281 + if (xAllowed && (leftToRight <= 0 || rightToLeft <= 0)) { 1.282 + win.scroll(win.scrollX + clientRect.left 1.283 + - (win.innerWidth - elem.offsetWidth) / 2, 1.284 + win.scrollY); 1.285 + } 1.286 + } 1.287 + 1.288 + if (!this.isTopLevelWindow(win)) { 1.289 + // We are inside an iframe. 1.290 + let frameElement = this.getFrameElement(win); 1.291 + this.scrollIntoViewIfNeeded(frameElement, centered); 1.292 + } 1.293 + }, 1.294 + 1.295 + /** 1.296 + * Check if a node and its document are still alive 1.297 + * and attached to the window. 1.298 + * 1.299 + * @param aNode 1.300 + */ 1.301 + isNodeConnected: function LH_isNodeConnected(aNode) 1.302 + { 1.303 + try { 1.304 + let connected = (aNode.ownerDocument && aNode.ownerDocument.defaultView && 1.305 + !(aNode.compareDocumentPosition(aNode.ownerDocument.documentElement) & 1.306 + aNode.DOCUMENT_POSITION_DISCONNECTED)); 1.307 + return connected; 1.308 + } catch (e) { 1.309 + // "can't access dead object" error 1.310 + return false; 1.311 + } 1.312 + }, 1.313 + 1.314 + /** 1.315 + * like win.parent === win, but goes through mozbrowsers and mozapps iframes. 1.316 + */ 1.317 + isTopLevelWindow: function LH_isTopLevelWindow(win) { 1.318 + let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor) 1.319 + .getInterface(Ci.nsIWebNavigation) 1.320 + .QueryInterface(Ci.nsIDocShell); 1.321 + 1.322 + return docShell === this._topDocShell; 1.323 + }, 1.324 + 1.325 + /** 1.326 + * Check a window is part of the top level window. 1.327 + */ 1.328 + isIncludedInTopLevelWindow: function LH_isIncludedInTopLevelWindow(win) { 1.329 + if (this.isTopLevelWindow(win)) { 1.330 + return true; 1.331 + } 1.332 + 1.333 + let parent = this.getParentWindow(win); 1.334 + if (!parent || parent === win) { 1.335 + return false; 1.336 + } 1.337 + 1.338 + return this.isIncludedInTopLevelWindow(parent); 1.339 + }, 1.340 + 1.341 + /** 1.342 + * like win.parent, but goes through mozbrowsers and mozapps iframes. 1.343 + */ 1.344 + getParentWindow: function LH_getParentWindow(win) { 1.345 + if (this.isTopLevelWindow(win)) { 1.346 + return null; 1.347 + } 1.348 + 1.349 + let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor) 1.350 + .getInterface(Ci.nsIWebNavigation) 1.351 + .QueryInterface(Ci.nsIDocShell); 1.352 + 1.353 + if (docShell.isBrowserOrApp) { 1.354 + let parentDocShell = docShell.getSameTypeParentIgnoreBrowserAndAppBoundaries(); 1.355 + return parentDocShell ? parentDocShell.contentViewer.DOMDocument.defaultView : null; 1.356 + } else { 1.357 + return win.parent; 1.358 + } 1.359 + }, 1.360 + 1.361 + /** 1.362 + * like win.frameElement, but goes through mozbrowsers and mozapps iframes. 1.363 + * 1.364 + * @param DOMWindow win The window to get the frame for 1.365 + * @return DOMElement The element in which the window is embedded. 1.366 + */ 1.367 + getFrameElement: function LH_getFrameElement(win) { 1.368 + if (this.isTopLevelWindow(win)) { 1.369 + return null; 1.370 + } 1.371 + 1.372 + let winUtils = win. 1.373 + QueryInterface(Components.interfaces.nsIInterfaceRequestor). 1.374 + getInterface(Components.interfaces.nsIDOMWindowUtils); 1.375 + 1.376 + return winUtils.containerElement; 1.377 + }, 1.378 + 1.379 + /** 1.380 + * Get the x and y offsets for a node taking iframes into account. 1.381 + * 1.382 + * @param {DOMNode} node 1.383 + * The node for which we are to get the offset 1.384 + */ 1.385 + _getNodeOffsets: function(node) { 1.386 + let xOffset = 0; 1.387 + let yOffset = 0; 1.388 + let frameWin = node.ownerDocument.defaultView; 1.389 + let scale = this.calculateScale(node); 1.390 + 1.391 + while (true) { 1.392 + // Are we in the top-level window? 1.393 + if (this.isTopLevelWindow(frameWin)) { 1.394 + break; 1.395 + } 1.396 + 1.397 + let frameElement = this.getFrameElement(frameWin); 1.398 + if (!frameElement) { 1.399 + break; 1.400 + } 1.401 + 1.402 + // We are in an iframe. 1.403 + // We take into account the parent iframe position and its 1.404 + // offset (borders and padding). 1.405 + let frameRect = frameElement.getBoundingClientRect(); 1.406 + 1.407 + let [offsetTop, offsetLeft] = 1.408 + this.getIframeContentOffset(frameElement); 1.409 + 1.410 + xOffset += frameRect.left + offsetLeft; 1.411 + yOffset += frameRect.top + offsetTop; 1.412 + 1.413 + frameWin = this.getParentWindow(frameWin); 1.414 + } 1.415 + 1.416 + return [xOffset * scale, yOffset * scale]; 1.417 + }, 1.418 + 1.419 + 1.420 + 1.421 + /******************************************************************** 1.422 + * GetBoxQuads POLYFILL START TODO: Remove this when bug 917755 is fixed. 1.423 + ********************************************************************/ 1.424 + _getBoxQuadsFromRect: function(rect, node) { 1.425 + let scale = this.calculateScale(node); 1.426 + let [xOffset, yOffset] = this._getNodeOffsets(node); 1.427 + 1.428 + let out = { 1.429 + p1: { 1.430 + x: rect.left * scale + xOffset, 1.431 + y: rect.top * scale + yOffset 1.432 + }, 1.433 + p2: { 1.434 + x: (rect.left + rect.width) * scale + xOffset, 1.435 + y: rect.top * scale + yOffset 1.436 + }, 1.437 + p3: { 1.438 + x: (rect.left + rect.width) * scale + xOffset, 1.439 + y: (rect.top + rect.height) * scale + yOffset 1.440 + }, 1.441 + p4: { 1.442 + x: rect.left * scale + xOffset, 1.443 + y: (rect.top + rect.height) * scale + yOffset 1.444 + } 1.445 + }; 1.446 + 1.447 + out.bounds = { 1.448 + bottom: out.p4.y, 1.449 + height: out.p4.y - out.p1.y, 1.450 + left: out.p1.x, 1.451 + right: out.p2.x, 1.452 + top: out.p1.y, 1.453 + width: out.p2.x - out.p1.x, 1.454 + x: out.p1.x, 1.455 + y: out.p1.y 1.456 + }; 1.457 + 1.458 + return out; 1.459 + }, 1.460 + 1.461 + _parseNb: function(distance) { 1.462 + let nb = parseFloat(distance, 10); 1.463 + return isNaN(nb) ? 0 : nb; 1.464 + }, 1.465 + 1.466 + getAdjustedQuadsPolyfill: function(node, region) { 1.467 + // Get the border-box rect 1.468 + // Note that this is relative to the node's viewport, so before we can use 1.469 + // it, will need to go back up the frames like getRect 1.470 + let borderRect = node.getBoundingClientRect(); 1.471 + 1.472 + // If the boxType is border, no need to go any further, we're done 1.473 + if (region === "border") { 1.474 + return this._getBoxQuadsFromRect(borderRect, node); 1.475 + } 1.476 + 1.477 + // Else, need to get margin/padding/border distances 1.478 + let style = node.ownerDocument.defaultView.getComputedStyle(node); 1.479 + let camel = s => s.substring(0, 1).toUpperCase() + s.substring(1); 1.480 + let distances = {border:{}, padding:{}, margin: {}}; 1.481 + 1.482 + for (let side of ["top", "right", "bottom", "left"]) { 1.483 + distances.border[side] = this._parseNb(style["border" + camel(side) + "Width"]); 1.484 + distances.padding[side] = this._parseNb(style["padding" + camel(side)]); 1.485 + distances.margin[side] = this._parseNb(style["margin" + camel(side)]); 1.486 + } 1.487 + 1.488 + // From the border-box rect, calculate the content-box, padding-box and 1.489 + // margin-box rects 1.490 + function offsetRect(rect, offsetType, dir=1) { 1.491 + return { 1.492 + top: rect.top + (dir * distances[offsetType].top), 1.493 + left: rect.left + (dir * distances[offsetType].left), 1.494 + width: rect.width - (dir * (distances[offsetType].left + distances[offsetType].right)), 1.495 + height: rect.height - (dir * (distances[offsetType].top + distances[offsetType].bottom)) 1.496 + }; 1.497 + } 1.498 + 1.499 + if (region === "margin") { 1.500 + return this._getBoxQuadsFromRect(offsetRect(borderRect, "margin", -1), node); 1.501 + } else if (region === "padding") { 1.502 + return this._getBoxQuadsFromRect(offsetRect(borderRect, "border"), node); 1.503 + } else if (region === "content") { 1.504 + let paddingRect = offsetRect(borderRect, "border"); 1.505 + return this._getBoxQuadsFromRect(offsetRect(paddingRect, "padding"), node); 1.506 + } 1.507 + }, 1.508 + 1.509 + /******************************************************************** 1.510 + * GetBoxQuads POLYFILL END 1.511 + ********************************************************************/ 1.512 +};