diff --git a/browser/devtools/highlighter/TreePanel.jsm b/browser/devtools/highlighter/TreePanel.jsm index 4f26ea26bb2..e18196b0c8e 100644 --- a/browser/devtools/highlighter/TreePanel.jsm +++ b/browser/devtools/highlighter/TreePanel.jsm @@ -361,7 +361,7 @@ TreePanel.prototype = { this.IUI.stopInspecting(true); } else { this.IUI.select(node, true, false); - this.IUI.highlighter.highlightNode(node); + this.IUI.highlighter.highlight(node); } } } diff --git a/browser/devtools/highlighter/highlighter.jsm b/browser/devtools/highlighter/highlighter.jsm index 23e137e102a..5b0ce895dda 100644 --- a/browser/devtools/highlighter/highlighter.jsm +++ b/browser/devtools/highlighter/highlighter.jsm @@ -1,29 +1,144 @@ -//// Highlighter +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is the Mozilla Highlighter Module. + * + * The Initial Developer of the Original Code is + * The Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Rob Campbell (original author) + * Mihai Șucan + * Julian Viereck + * Paul Rouget + * Kyle Simpson + * Johan Charlez + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const Cu = Components.utils; +Cu.import("resource:///modules/devtools/LayoutHelpers.jsm"); + +var EXPORTED_SYMBOLS = ["Highlighter"]; + +const INSPECTOR_INVISIBLE_ELEMENTS = { + "head": true, + "base": true, + "basefont": true, + "isindex": true, + "link": true, + "meta": true, + "script": true, + "style": true, + "title": true, +}; /** * A highlighter mechanism. * - * The highlighter is built dynamically once the Inspector is invoked: - * - * ... - * + * ... + * = 0 && aRectScaled.top >= 0 && aRectScaled.width > 0 && aRectScaled.height > 0) { @@ -384,7 +513,7 @@ Highlighter.prototype = { this._contentRect = aRect; // save orig (non-scaled) rect this._highlightRect = aRectScaled; // and save the scaled rect. - return this._highlighting; + return; }, /** @@ -396,8 +525,6 @@ Highlighter.prototype = { this.veilMiddleBox.style.height = 0; this.veilTransparentBox.style.width = 0; this.veilTransparentBox.style.visibility = "hidden"; - Services.obs.notifyObservers(null, - INSPECTOR_NOTIFICATIONS.UNHIGHLIGHTING, null); }, /** @@ -543,7 +670,7 @@ Highlighter.prototype = { // Get midpoint of diagonal line. let midpoint = this.midPoint(a, b); - return this.IUI.elementFromPoint(this.win.document, midpoint.x, + return LayoutHelpers.getElementFromPoint(this.win.document, midpoint.x, midpoint.y); }, @@ -557,10 +684,50 @@ Highlighter.prototype = { .screenPixelsPerCSSPixel; }, + ///////////////////////////////////////////////////////////////////////// + //// Event Emitter Mechanism + + addListener: function Highlighter_addListener(aEvent, aListener) + { + if (!(aEvent in this.events)) + this.events[aEvent] = []; + this.events[aEvent].push(aListener); + }, + + removeListener: function Highlighter_removeListener(aEvent, aListener) + { + if (!(aEvent in this.events)) + return; + let idx = this.events[aEvent].indexOf(aListener); + if (idx > -1) + this.events[aEvent].splice(idx, 1); + }, + + emitEvent: function Highlighter_emitEvent(aEvent, aArgv) + { + if (!(aEvent in this.events)) + return; + + let listeners = this.events[aEvent]; + let highlighter = this; + listeners.forEach(function(aListener) { + try { + aListener.apply(highlighter, aArgv); + } catch(e) {} + }); + }, + + removeAllListeners: function Highlighter_removeAllIsteners() + { + for (let event in this.events) { + delete this.events[event]; + } + }, + ///////////////////////////////////////////////////////////////////////// //// Event Handling - attachInspectListeners: function Highlighter_attachInspectListeners() + attachMouseListeners: function Highlighter_attachMouseListeners() { this.browser.addEventListener("mousemove", this, true); this.browser.addEventListener("click", this, true); @@ -569,7 +736,7 @@ Highlighter.prototype = { this.browser.addEventListener("mouseup", this, true); }, - detachInspectListeners: function Highlighter_detachInspectListeners() + detachMouseListeners: function Highlighter_detachMouseListeners() { this.browser.removeEventListener("mousemove", this, true); this.browser.removeEventListener("click", this, true); @@ -578,6 +745,29 @@ Highlighter.prototype = { this.browser.removeEventListener("mouseup", this, true); }, + attachPageListeners: function Highlighter_attachPageListeners() + { + this.browser.addEventListener("resize", this, true); + this.browser.addEventListener("scroll", this, true); + }, + + detachPageListeners: function Highlighter_detachPageListeners() + { + this.browser.removeEventListener("resize", this, true); + this.browser.removeEventListener("scroll", this, true); + }, + + attachKeysListeners: function Highlighter_attachKeysListeners() + { + this.browser.addEventListener("keypress", this, true); + this.highlighterContainer.addEventListener("keypress", this, true); + }, + + detachKeysListeners: function Highlighter_detachKeysListeners() + { + this.browser.removeEventListener("keypress", this, true); + this.highlighterContainer.removeEventListener("keypress", this, true); + }, /** * Generic event handler. @@ -595,9 +785,10 @@ Highlighter.prototype = { this.handleMouseMove(aEvent); break; case "resize": + case "scroll": this.computeZoomFactor(); this.brieflyDisableTransitions(); - this.handleResize(aEvent); + this.invalidateSize(); break; case "dblclick": case "mousedown": @@ -605,10 +796,78 @@ Highlighter.prototype = { aEvent.stopPropagation(); aEvent.preventDefault(); break; - case "scroll": - this.brieflyDisableTransitions(); - this.highlight(); break; + case "keypress": + switch (aEvent.keyCode) { + case this.chromeWin.KeyEvent.DOM_VK_RETURN: + this.locked ? this.unlock() : this.lock(); + aEvent.preventDefault(); + aEvent.stopPropagation(); + break; + case this.chromeWin.KeyEvent.DOM_VK_LEFT: + let node; + if (this.node) { + node = this.node.parentNode; + } else { + node = this.defaultSelection; + } + if (node && this.isNodeHighlightable(node)) { + this.highlight(node); + } + aEvent.preventDefault(); + aEvent.stopPropagation(); + break; + case this.chromeWin.KeyEvent.DOM_VK_RIGHT: + if (this.node) { + // Find the first child that is highlightable. + for (let i = 0; i < this.node.childNodes.length; i++) { + node = this.node.childNodes[i]; + if (node && this.isNodeHighlightable(node)) { + break; + } + } + } else { + node = this.defaultSelection; + } + if (node && this.isNodeHighlightable(node)) { + this.highlight(node, true); + } + aEvent.preventDefault(); + aEvent.stopPropagation(); + break; + case this.chromeWin.KeyEvent.DOM_VK_UP: + if (this.node) { + // Find a previous sibling that is highlightable. + node = this.node.previousSibling; + while (node && !this.isNodeHighlightable(node)) { + node = node.previousSibling; + } + } else { + node = this.defaultSelection; + } + if (node && this.isNodeHighlightable(node)) { + this.highlight(node, true); + } + aEvent.preventDefault(); + aEvent.stopPropagation(); + break; + case this.chromeWin.KeyEvent.DOM_VK_DOWN: + if (this.node) { + // Find a next sibling that is highlightable. + node = this.node.nextSibling; + while (node && !this.isNodeHighlightable(node)) { + node = node.nextSibling; + } + } else { + node = this.defaultSelection; + } + if (node && this.isNodeHighlightable(node)) { + this.highlight(node, true); + } + aEvent.preventDefault(); + aEvent.stopPropagation(); + break; + } } }, @@ -619,13 +878,13 @@ Highlighter.prototype = { brieflyDisableTransitions: function Highlighter_brieflyDisableTransitions() { if (this.transitionDisabler) { - this.IUI.win.clearTimeout(this.transitionDisabler); + this.chromeWin.clearTimeout(this.transitionDisabler); } else { this.veilContainer.setAttribute("disable-transitions", "true"); this.nodeInfo.container.setAttribute("disable-transitions", "true"); } this.transitionDisabler = - this.IUI.win.setTimeout(function() { + this.chromeWin.setTimeout(function() { this.veilContainer.removeAttribute("disable-transitions"); this.nodeInfo.container.removeAttribute("disable-transitions"); this.transitionDisabler = null; @@ -643,7 +902,7 @@ Highlighter.prototype = { // Stop inspection when the user clicks on a node. if (aEvent.button == 0) { let win = aEvent.target.ownerDocument.defaultView; - this.IUI.stopInspecting(); + this.lock(); win.focus(); } aEvent.preventDefault(); @@ -651,27 +910,19 @@ Highlighter.prototype = { }, /** - * Handle mousemoves in panel when InspectorUI.inspecting is true. + * Handle mousemoves in panel. * * @param nsiDOMEvent aEvent * The MouseEvent triggering the method. */ handleMouseMove: function Highlighter_handleMouseMove(aEvent) { - let element = this.IUI.elementFromPoint(aEvent.target.ownerDocument, + let element = LayoutHelpers.getElementFromPoint(aEvent.target.ownerDocument, aEvent.clientX, aEvent.clientY); if (element && element != this.node) { - this.IUI.inspectNode(element); + this.highlight(element); } }, - - /** - * Handle window resize events. - */ - handleResize: function Highlighter_handleResize() - { - this.highlight(); - }, }; /////////////////////////////////////////////////////////////////////////// diff --git a/browser/devtools/highlighter/inspector.jsm b/browser/devtools/highlighter/inspector.jsm index 842e4d28a55..1fe6d54f9c0 100644 --- a/browser/devtools/highlighter/inspector.jsm +++ b/browser/devtools/highlighter/inspector.jsm @@ -52,27 +52,11 @@ Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource:///modules/TreePanel.jsm"); Cu.import("resource:///modules/devtools/CssRuleView.jsm"); - -const INSPECTOR_INVISIBLE_ELEMENTS = { - "head": true, - "base": true, - "basefont": true, - "isindex": true, - "link": true, - "meta": true, - "script": true, - "style": true, - "title": true, -}; +Cu.import("resource:///modules/highlighter.jsm"); +Cu.import("resource:///modules/devtools/LayoutHelpers.jsm"); // Inspector notifications dispatched through the nsIObserverService. const INSPECTOR_NOTIFICATIONS = { - // Fires once the Inspector highlights an element in the page. - HIGHLIGHTING: "inspector-highlighting", - - // Fires once the Inspector stops highlighting any element. - UNHIGHLIGHTING: "inspector-unhighlighting", - // Fires once the Inspector completes the initialization and opens up on // screen. OPENED: "inspector-opened", @@ -298,8 +282,11 @@ InspectorUI.prototype = { this.progressListener = new InspectorProgressListener(this); + this.chromeWin.addEventListener("keypress", this, false); + // initialize the highlighter - this.initializeHighlighter(); + this.highlighter = new Highlighter(this.chromeWin); + this.highlighterReady(); }, /** @@ -335,17 +322,6 @@ InspectorUI.prototype = { // Extras go here. }, - /** - * Initialize highlighter. - */ - initializeHighlighter: function IUI_initializeHighlighter() - { - this.highlighter = new Highlighter(this); - this.browser.addEventListener("keypress", this, true); - this.highlighter.highlighterContainer.addEventListener("keypress", this, true); - this.highlighterReady(); - }, - /** * Initialize the InspectorStore. */ @@ -419,8 +395,9 @@ InspectorUI.prototype = { this.tabbrowser.tabContainer.removeEventListener("TabSelect", this, false); } + this.chromeWin.removeEventListener("keypress", this, false); + this.stopInspecting(); - this.browser.removeEventListener("keypress", this, true); this.saveToolState(this.winID); this.toolsDo(function IUI_toolsHide(aTool) { @@ -431,9 +408,6 @@ InspectorUI.prototype = { this.hideSidebar(); if (this.highlighter) { - this.highlighter.highlighterContainer.removeEventListener("keypress", - this, - true); this.highlighter.destroy(); this.highlighter = null; } @@ -470,12 +444,10 @@ InspectorUI.prototype = { this.treePanel.closeEditor(); this.inspectToolbutton.checked = true; - this.highlighter.attachInspectListeners(); this.inspecting = true; this.toolsDim(true); - this.highlighter.veilContainer.removeAttribute("locked"); - this.highlighter.nodeInfo.container.removeAttribute("locked"); + this.highlighter.unlock(); }, /** @@ -491,21 +463,15 @@ InspectorUI.prototype = { } this.inspectToolbutton.checked = false; - // Detach event listeners from content window and child windows to disable - // highlighting. We still want to be notified if the user presses "ESCAPE" - // to close the inspector, or "RETURN" to unlock the node, so we don't - // remove the "keypress" event until the highlighter is removed. - this.highlighter.detachInspectListeners(); this.inspecting = false; this.toolsDim(false); - if (this.highlighter.node) { - this.select(this.highlighter.node, true, true, !aPreventScroll); + if (this.highlighter.getNode()) { + this.select(this.highlighter.getNode(), true, true, !aPreventScroll); } else { this.select(null, true, true); } - this.highlighter.veilContainer.setAttribute("locked", true); - this.highlighter.nodeInfo.container.setAttribute("locked", true); + this.highlighter.lock(); }, /** @@ -530,7 +496,7 @@ InspectorUI.prototype = { if (forceUpdate || aNode != this.selection) { this.selection = aNode; if (!this.inspecting) { - this.highlighter.highlightNode(this.selection); + this.highlighter.highlight(this.selection); } } @@ -549,7 +515,7 @@ InspectorUI.prototype = { */ nodeChanged: function IUI_nodeChanged(aUpdater) { - this.highlighter.highlight(); + this.highlighter.invalidateSize(); this.toolsOnChanged(aUpdater); }, @@ -561,6 +527,20 @@ InspectorUI.prototype = { // Setup the InspectorStore or restore state this.initializeStore(); + let self = this; + + this.highlighter.addListener("locked", function() { + self.stopInspecting(); + }); + + this.highlighter.addListener("unlocked", function() { + self.startInspecting(); + }); + + this.highlighter.addListener("nodeselected", function() { + self.select(self.highlighter.getNode(), false, false); + }); + if (this.store.getValue(this.winID, "inspecting")) { this.startInspecting(); } @@ -570,6 +550,8 @@ InspectorUI.prototype = { this.win.focus(); Services.obs.notifyObservers({wrappedJSObject: this}, INSPECTOR_NOTIFICATIONS.OPENED, null); + + this.highlighter.highlight(); }, /** @@ -636,74 +618,6 @@ InspectorUI.prototype = { event.preventDefault(); event.stopPropagation(); break; - case this.chromeWin.KeyEvent.DOM_VK_RETURN: - this.toggleInspection(); - event.preventDefault(); - event.stopPropagation(); - break; - case this.chromeWin.KeyEvent.DOM_VK_LEFT: - let node; - if (this.selection) { - node = this.selection.parentNode; - } else { - node = this.defaultSelection; - } - if (node && this.highlighter.isNodeHighlightable(node)) { - this.inspectNode(node, true); - } - event.preventDefault(); - event.stopPropagation(); - break; - case this.chromeWin.KeyEvent.DOM_VK_RIGHT: - if (this.selection) { - // Find the first child that is highlightable. - for (let i = 0; i < this.selection.childNodes.length; i++) { - node = this.selection.childNodes[i]; - if (node && this.highlighter.isNodeHighlightable(node)) { - break; - } - } - } else { - node = this.defaultSelection; - } - if (node && this.highlighter.isNodeHighlightable(node)) { - this.inspectNode(node, true); - } - event.preventDefault(); - event.stopPropagation(); - break; - case this.chromeWin.KeyEvent.DOM_VK_UP: - if (this.selection) { - // Find a previous sibling that is highlightable. - node = this.selection.previousSibling; - while (node && !this.highlighter.isNodeHighlightable(node)) { - node = node.previousSibling; - } - } else { - node = this.defaultSelection; - } - if (node && this.highlighter.isNodeHighlightable(node)) { - this.inspectNode(node, true); - } - event.preventDefault(); - event.stopPropagation(); - break; - case this.chromeWin.KeyEvent.DOM_VK_DOWN: - if (this.selection) { - // Find a next sibling that is highlightable. - node = this.selection.nextSibling; - while (node && !this.highlighter.isNodeHighlightable(node)) { - node = node.nextSibling; - } - } else { - node = this.defaultSelection; - } - if (node && this.highlighter.isNodeHighlightable(node)) { - this.inspectNode(node, true); - } - event.preventDefault(); - event.stopPropagation(); - break; } break; } @@ -826,77 +740,12 @@ InspectorUI.prototype = { inspectNode: function IUI_inspectNode(aNode, aScroll) { this.select(aNode, true, true); - this.highlighter.highlightNode(aNode, { scroll: aScroll }); - }, - - /** - * Find an element from the given coordinates. This method descends through - * frames to find the element the user clicked inside frames. - * - * @param DOMDocument aDocument the document to look into. - * @param integer aX - * @param integer aY - * @returns Node|null the element node found at the given coordinates. - */ - elementFromPoint: function IUI_elementFromPoint(aDocument, aX, aY) - { - let node = aDocument.elementFromPoint(aX, aY); - if (node && node.contentDocument) { - if (node instanceof Ci.nsIDOMHTMLIFrameElement) { - let rect = node.getBoundingClientRect(); - - // Gap between the iframe and its content window. - let [offsetTop, offsetLeft] = this.getIframeContentOffset(node); - - aX -= rect.left + offsetLeft; - aY -= rect.top + offsetTop; - - if (aX < 0 || aY < 0) { - // Didn't reach the content document, still over the iframe. - return node; - } - } - if (node instanceof Ci.nsIDOMHTMLIFrameElement || - node instanceof Ci.nsIDOMHTMLFrameElement) { - let subnode = this.elementFromPoint(node.contentDocument, aX, aY); - if (subnode) { - node = subnode; - } - } - } - return node; + this.highlighter.highlight(aNode, aScroll); }, /////////////////////////////////////////////////////////////////////////// //// Utility functions - /** - * Returns iframe content offset (iframe border + padding). - * Note: this function shouldn't need to exist, had the platform provided a - * suitable API for determining the offset between the iframe's content and - * its bounding client rect. Bug 626359 should provide us with such an API. - * - * @param aIframe - * The iframe. - * @returns array [offsetTop, offsetLeft] - * offsetTop is the distance from the top of the iframe and the - * top of the content document. - * offsetLeft is the distance from the left of the iframe and the - * left of the content document. - */ - getIframeContentOffset: function IUI_getIframeContentOffset(aIframe) - { - let style = aIframe.contentWindow.getComputedStyle(aIframe, null); - - let paddingTop = parseInt(style.getPropertyValue("padding-top")); - let paddingLeft = parseInt(style.getPropertyValue("padding-left")); - - let borderTop = parseInt(style.getPropertyValue("border-top-width")); - let borderLeft = parseInt(style.getPropertyValue("border-left-width")); - - return [borderTop + paddingTop, borderLeft + paddingLeft]; - }, - /** * Retrieve the unique ID of a window object. * @@ -1252,6 +1101,12 @@ InspectorUI.prototype = { this.getToolbarButtonId(tool.id)).removeAttribute("checked"); }.bind(this)); } + if (this.store.getValue(this.winID, "inspecting")) { + this.highlighter.unlock(); + } else { + this.highlighter.lock(); + } + Services.obs.notifyObservers(null, INSPECTOR_NOTIFICATIONS.STATE_RESTORED, null); }, diff --git a/browser/devtools/shared/LayoutHelpers.jsm b/browser/devtools/shared/LayoutHelpers.jsm new file mode 100644 index 00000000000..c5523985e7e --- /dev/null +++ b/browser/devtools/shared/LayoutHelpers.jsm @@ -0,0 +1,204 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is the Mozilla LayoutHelpers Module. + * + * The Initial Developer of the Original Code is + * The Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Rob Campbell (original author) + * Mihai Șucan + * Julian Viereck + * Paul Rouget + * Kyle Simpson + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const Cu = Components.utils; +const Ci = Components.interfaces; +const Cr = Components.results; + +var EXPORTED_SYMBOLS = ["LayoutHelpers"]; + +LayoutHelpers = { + + /** + * Compute the position and the dimensions for the visible portion + * of a node, relativalely to the root window. + * + * @param nsIDOMNode aNode + * a DOM element to be highlighted + */ + getDirtyRect: function LH_getDirectyRect(aNode) { + let frameWin = aNode.ownerDocument.defaultView; + let clientRect = aNode.getBoundingClientRect(); + + // Go up in the tree of frames to determine the correct rectangle. + // clientRect is read-only, we need to be able to change properties. + rect = {top: clientRect.top, + left: clientRect.left, + width: clientRect.width, + height: clientRect.height}; + + // We iterate through all the parent windows. + while (true) { + + // Does the selection overflow on the right of its window? + let diffx = frameWin.innerWidth - (rect.left + rect.width); + if (diffx < 0) { + rect.width += diffx; + } + + // Does the selection overflow on the bottom of its window? + let diffy = frameWin.innerHeight - (rect.top + rect.height); + if (diffy < 0) { + rect.height += diffy; + } + + // Does the selection overflow on the left of its window? + if (rect.left < 0) { + rect.width += rect.left; + rect.left = 0; + } + + // Does the selection overflow on the top of its window? + if (rect.top < 0) { + rect.height += rect.top; + rect.top = 0; + } + + // Selection has been clipped to fit in its own window. + + // Are we in the top-level window? + if (frameWin.parent === frameWin || !frameWin.frameElement) { + break; + } + + // We are in an iframe. + // We take into account the parent iframe position and its + // offset (borders and padding). + let frameRect = frameWin.frameElement.getBoundingClientRect(); + + let [offsetTop, offsetLeft] = + this.getIframeContentOffset(frameWin.frameElement); + + rect.top += frameRect.top + offsetTop; + rect.left += frameRect.left + offsetLeft; + + frameWin = frameWin.parent; + } + + return rect; + }, + + /** + * Returns iframe content offset (iframe border + padding). + * Note: this function shouldn't need to exist, had the platform provided a + * suitable API for determining the offset between the iframe's content and + * its bounding client rect. Bug 626359 should provide us with such an API. + * + * @param aIframe + * The iframe. + * @returns array [offsetTop, offsetLeft] + * offsetTop is the distance from the top of the iframe and the + * top of the content document. + * offsetLeft is the distance from the left of the iframe and the + * left of the content document. + */ + getIframeContentOffset: function LH_getIframeContentOffset(aIframe) { + let style = aIframe.contentWindow.getComputedStyle(aIframe, null); + + let paddingTop = parseInt(style.getPropertyValue("padding-top")); + let paddingLeft = parseInt(style.getPropertyValue("padding-left")); + + let borderTop = parseInt(style.getPropertyValue("border-top-width")); + let borderLeft = parseInt(style.getPropertyValue("border-left-width")); + + return [borderTop + paddingTop, borderLeft + paddingLeft]; + }, + + /** + * Apply the page zoom factor. + */ + getZoomedRect: function LH_getZoomedRect(aWin, aRect) { + // get page zoom factor, if any + let zoom = + aWin.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils) + .screenPixelsPerCSSPixel; + + // adjust rect for zoom scaling + let aRectScaled = {}; + for (let prop in aRect) { + aRectScaled[prop] = aRect[prop] * zoom; + } + + return aRectScaled; + }, + + + /** + * Find an element from the given coordinates. This method descends through + * frames to find the element the user clicked inside frames. + * + * @param DOMDocument aDocument the document to look into. + * @param integer aX + * @param integer aY + * @returns Node|null the element node found at the given coordinates. + */ + getElementFromPoint: function LH_elementFromPoint(aDocument, aX, aY) + { + let node = aDocument.elementFromPoint(aX, aY); + if (node && node.contentDocument) { + if (node instanceof Ci.nsIDOMHTMLIFrameElement) { + let rect = node.getBoundingClientRect(); + + // Gap between the iframe and its content window. + let [offsetTop, offsetLeft] = LayoutHelpers.getIframeContentOffset(node); + + aX -= rect.left + offsetLeft; + aY -= rect.top + offsetTop; + + if (aX < 0 || aY < 0) { + // Didn't reach the content document, still over the iframe. + return node; + } + } + if (node instanceof Ci.nsIDOMHTMLIFrameElement || + node instanceof Ci.nsIDOMHTMLFrameElement) { + let subnode = this.getElementFromPoint(node.contentDocument, aX, aY); + if (subnode) { + node = subnode; + } + } + } + return node; + }, +}; diff --git a/browser/devtools/shared/Makefile.in b/browser/devtools/shared/Makefile.in index 48ecf619fcb..52f4253cb6a 100644 --- a/browser/devtools/shared/Makefile.in +++ b/browser/devtools/shared/Makefile.in @@ -54,3 +54,4 @@ include $(topsrcdir)/config/rules.mk libs:: $(NSINSTALL) $(srcdir)/Templater.jsm $(FINAL_TARGET)/modules/devtools $(NSINSTALL) $(srcdir)/Promise.jsm $(FINAL_TARGET)/modules/devtools + $(NSINSTALL) $(srcdir)/LayoutHelpers.jsm $(FINAL_TARGET)/modules/devtools