gecko/browser/base/content/inspector.js

1445 lines
40 KiB
JavaScript

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
#ifdef 0
/* ***** 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 Inspector 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 <rcampbell@mozilla.com> (original author)
* Mihai Șucan <mihai.sucan@gmail.com>
* Julian Viereck <jviereck@mozilla.com>
*
* 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 ***** */
#endif
#include insideOutBox.js
const INSPECTOR_INVISIBLE_ELEMENTS = {
"head": true,
"base": true,
"basefont": true,
"isindex": true,
"link": true,
"meta": true,
"script": true,
"style": true,
"title": true,
};
///////////////////////////////////////////////////////////////////////////
//// PanelHighlighter
/**
* A highlighter mechanism using xul panels.
*
* @param aBrowser
* The XUL browser object for the content window being highlighted.
*/
function PanelHighlighter(aBrowser)
{
this.panel = document.getElementById("highlighter-panel");
this.panel.hidden = false;
this.browser = aBrowser;
this.win = this.browser.contentWindow;
}
PanelHighlighter.prototype = {
/**
* Highlight this.node, unhilighting first if necessary.
*
* @param scroll
* Boolean determining whether to scroll or not.
*/
highlight: function PanelHighlighter_highlight(scroll)
{
// node is not set or node is not highlightable, bail
if (!this.isNodeHighlightable()) {
return;
}
this.unhighlight();
let rect = this.node.getBoundingClientRect();
if (scroll) {
this.node.scrollIntoView();
}
if (this.viewContainsRect(rect)) {
// TODO check for offscreen boundaries, bug565301
this.panel.openPopup(this.node, "overlap", 0, 0, false, false);
this.panel.sizeTo(rect.width, rect.height);
} else {
this.highlightVisibleRegion(rect);
}
},
/**
* Highlight the given node.
*
* @param aNode
* a DOM element to be highlighted
* @param aParams
* extra parameters object
*/
highlightNode: function PanelHighlighter_highlightNode(aNode, aParams)
{
this.node = aNode;
this.highlight(aParams && aParams.scroll);
},
/**
* Highlight the visible region of the region described by aRect, if any.
*
* @param aRect
* @returns boolean
* was a region highlighted?
*/
highlightVisibleRegion: function PanelHighlighter_highlightVisibleRegion(aRect)
{
let offsetX = 0;
let offsetY = 0;
let width = 0;
let height = 0;
let visibleWidth = this.win.innerWidth;
let visibleHeight = this.win.innerHeight;
// If any of these edges are out-of-bounds, the node's rectangle is
// completely out-of-view and we can return.
if (aRect.top > visibleHeight || aRect.left > visibleWidth ||
aRect.bottom < 0 || aRect.right < 0) {
return false;
}
// Calculate node offsets, if values are negative, then start the offsets
// at their absolute values from node origin. The delta should be the edge
// of view.
offsetX = aRect.left < 0 ? Math.abs(aRect.left) : 0;
offsetY = aRect.top < 0 ? Math.abs(aRect.top) : 0;
// Calculate actual node width, taking into account the available visible
// width and then subtracting the offset for the final dimension.
width = aRect.right > visibleWidth ? visibleWidth - aRect.left :
aRect.width;
width -= offsetX;
// Calculate actual node height using the same formula as above for width.
height = aRect.bottom > visibleHeight ? visibleHeight - aRect.top :
aRect.height;
height -= offsetY;
// If width and height are non-negative, open the highlighter popup over the
// node and sizeTo width and height.
if (width > 0 && height > 0) {
this.panel.openPopup(this.node, "overlap", offsetX, offsetY, false,
false);
this.panel.sizeTo(width, height);
return true;
}
return false;
},
/**
* Close the highlighter panel.
*/
unhighlight: function PanelHighlighter_unhighlight()
{
if (this.isHighlighting) {
this.panel.hidePopup();
}
},
/**
* Is the highlighter panel open?
*
* @returns boolean
*/
get isHighlighting()
{
return this.panel.state == "open";
},
/**
* Return the midpoint of a line from pointA to pointB.
*
* @param aPointA
* An object with x and y properties.
* @param aPointB
* An object with x and y properties.
* @returns aPoint
* An object with x and y properties.
*/
midPoint: function PanelHighlighter_midPoint(aPointA, aPointB)
{
let pointC = { };
pointC.x = (aPointB.x - aPointA.x) / 2 + aPointA.x;
pointC.y = (aPointB.y - aPointA.y) / 2 + aPointA.y;
return pointC;
},
/**
* Return the node under the highlighter rectangle. Useful for testing.
* Calculation based on midpoint of diagonal from top left to bottom right
* of panel.
*
* @returns a DOM node or null if none
*/
get highlitNode()
{
// No highlighter panel? Bail.
if (!this.isHighlighting) {
return null;
}
let browserRect = this.browser.getBoundingClientRect();
let clientRect = this.panel.getBoundingClientRect();
// Calculate top left point offset minus browser chrome.
let a = {
x: clientRect.left - browserRect.left,
y: clientRect.top - browserRect.top
};
// Calculate bottom right point minus browser chrome.
let b = {
x: clientRect.right - browserRect.left,
y: clientRect.bottom - browserRect.top
};
// Get midpoint of diagonal line.
let midpoint = this.midPoint(a, b);
return InspectorUI.elementFromPoint(this.win.document, midpoint.x,
midpoint.y);
},
/**
* Is this.node highlightable?
*
* @returns boolean
*/
isNodeHighlightable: function PanelHighlighter_isNodeHighlightable()
{
if (!this.node) {
return false;
}
let nodeName = this.node.nodeName.toLowerCase();
if (nodeName[0] == '#') {
return false;
}
return !INSPECTOR_INVISIBLE_ELEMENTS[nodeName];
},
/**
* Returns true if the given viewport-relative rect is within the visible area
* of the window.
*
* @param aRect
* a CSS rectangle object
* @returns boolean
*/
viewContainsRect: function PanelHighlighter_viewContainsRect(aRect)
{
let visibleWidth = this.win.innerWidth;
let visibleHeight = this.win.innerHeight;
return ((0 <= aRect.left) && (aRect.right <= visibleWidth) &&
(0 <= aRect.top) && (aRect.bottom <= visibleHeight))
},
/////////////////////////////////////////////////////////////////////////
//// Event Handling
/**
* Handle mousemoves in panel when InspectorUI.inspecting is true.
*
* @param aEvent
* The MouseEvent triggering the method.
*/
handleMouseMove: function PanelHighlighter_handleMouseMove(aEvent)
{
if (!InspectorUI.inspecting) {
return;
}
let browserRect = this.browser.getBoundingClientRect();
let element = InspectorUI.elementFromPoint(this.win.document,
aEvent.clientX - browserRect.left, aEvent.clientY - browserRect.top);
if (element && element != this.node) {
InspectorUI.inspectNode(element);
}
},
/**
* Handle MozMousePixelScroll in panel when InspectorUI.inspecting is true.
*
* @param aEvent
* The onMozMousePixelScrollEvent triggering the method.
* @returns void
*/
handlePixelScroll: function PanelHighlighter_handlePixelScroll(aEvent) {
if (!InspectorUI.inspecting) {
return;
}
let browserRect = this.browser.getBoundingClientRect();
let element = InspectorUI.elementFromPoint(this.win.document,
aEvent.clientX - browserRect.left, aEvent.clientY - browserRect.top);
let win = element.ownerDocument.defaultView;
if (aEvent.axis == aEvent.HORIZONTAL_AXIS) {
win.scrollBy(aEvent.detail, 0);
} else {
win.scrollBy(0, aEvent.detail);
}
}
};
///////////////////////////////////////////////////////////////////////////
//// InspectorUI
/**
* Main controller class for the Inspector.
*/
var InspectorUI = {
browser: null,
selectEventsSuppressed: false,
showTextNodesWithWhitespace: false,
inspecting: false,
treeLoaded: false,
prefEnabledName: "devtools.inspector.enabled",
/**
* Toggle the inspector interface elements on or off.
*
* @param aEvent
* The event that requested the UI change. Toolbar button or menu.
*/
toggleInspectorUI: function IUI_toggleInspectorUI(aEvent)
{
if (this.isTreePanelOpen) {
this.closeInspectorUI(true);
} else {
this.openInspectorUI();
}
},
/**
* Toggle the status of the inspector, starting or stopping it. Invoked
* from the toolbar's Inspect button.
*/
toggleInspection: function IUI_toggleInspection()
{
if (this.inspecting) {
this.stopInspecting();
} else {
this.startInspecting();
}
},
/**
* Toggle the style panel. Invoked from the toolbar's Style button.
*/
toggleStylePanel: function IUI_toggleStylePanel()
{
if (this.isStylePanelOpen) {
this.stylePanel.hidePopup();
} else {
this.openStylePanel();
if (this.selection) {
this.updateStylePanel(this.selection);
}
}
},
/**
* Toggle the DOM panel. Invoked from the toolbar's DOM button.
*/
toggleDOMPanel: function IUI_toggleDOMPanel()
{
if (this.isDOMPanelOpen) {
this.domPanel.hidePopup();
} else {
this.clearDOMPanel();
this.openDOMPanel();
if (this.selection) {
this.updateDOMPanel(this.selection);
}
}
},
/**
* Is the tree panel open?
*
* @returns boolean
*/
get isTreePanelOpen()
{
return this.treePanel && this.treePanel.state == "open";
},
/**
* Is the style panel open?
*
* @returns boolean
*/
get isStylePanelOpen()
{
return this.stylePanel && this.stylePanel.state == "open";
},
/**
* Is the DOM panel open?
*
* @returns boolean
*/
get isDOMPanelOpen()
{
return this.domPanel && this.domPanel.state == "open";
},
/**
* Return the default selection element for the inspected document.
*/
get defaultSelection()
{
let doc = this.win.document;
return doc.documentElement.lastElementChild;
},
initializeTreePanel: function IUI_initializeTreePanel()
{
this.treeBrowserDocument = this.treeIFrame.contentDocument;
this.treePanelDiv = this.treeBrowserDocument.createElement("div");
this.treeBrowserDocument.body.appendChild(this.treePanelDiv);
this.treePanelDiv.ownerPanel = this;
this.ioBox = new InsideOutBox(this, this.treePanelDiv);
this.ioBox.createObjectBox(this.win.document.documentElement);
this.treeLoaded = true;
if (this.isTreePanelOpen && this.isStylePanelOpen &&
this.isDOMPanelOpen && this.treeLoaded) {
this.notifyReady();
}
},
/**
* Open the inspector's tree panel and initialize it.
*/
openTreePanel: function IUI_openTreePanel()
{
if (!this.treePanel) {
this.treePanel = document.getElementById("inspector-tree-panel");
this.treePanel.hidden = false;
}
this.treeIFrame = document.getElementById("inspector-tree-iframe");
if (!this.treeIFrame) {
let resizerBox = document.getElementById("tree-panel-resizer-box");
this.treeIFrame = document.createElement("iframe");
this.treeIFrame.setAttribute("id", "inspector-tree-iframe");
this.treeIFrame.setAttribute("flex", "1");
this.treeIFrame.setAttribute("type", "content");
this.treeIFrame.setAttribute("onclick", "InspectorUI.onTreeClick(event)");
this.treeIFrame = this.treePanel.insertBefore(this.treeIFrame, resizerBox);
}
const panelWidthRatio = 7 / 8;
const panelHeightRatio = 1 / 5;
this.treePanel.openPopup(this.browser, "overlap", 80, this.win.innerHeight,
false, false);
this.treePanel.sizeTo(this.win.outerWidth * panelWidthRatio,
this.win.outerHeight * panelHeightRatio);
let src = this.treeIFrame.getAttribute("src");
if (src != "chrome://browser/content/inspector.html") {
let self = this;
this.treeIFrame.addEventListener("DOMContentLoaded", function() {
self.treeIFrame.removeEventListener("DOMContentLoaded", arguments.callee, true);
self.initializeTreePanel();
}, true);
this.treeIFrame.setAttribute("src", "chrome://browser/content/inspector.html");
} else {
this.initializeTreePanel();
}
},
createObjectBox: function IUI_createObjectBox(object, isRoot)
{
let tag = this.domplateUtils.getNodeTag(object);
if (tag)
return tag.replace({object: object}, this.treeBrowserDocument);
},
getParentObject: function IUI_getParentObject(node)
{
let parentNode = node ? node.parentNode : null;
if (!parentNode) {
// Documents have no parentNode; Attr, Document, DocumentFragment, Entity,
// and Notation. top level windows have no parentNode
if (node && node == Node.DOCUMENT_NODE) {
// document type
if (node.defaultView) {
let embeddingFrame = node.defaultView.frameElement;
if (embeddingFrame)
return embeddingFrame.parentNode;
}
}
// a Document object without a parentNode or window
return null; // top level has no parent
}
if (parentNode.nodeType == Node.DOCUMENT_NODE) {
if (parentNode.defaultView) {
return parentNode.defaultView.frameElement;
}
if (this.embeddedBrowserParents) {
let skipParent = this.embeddedBrowserParents[node];
// HTML element? could be iframe?
if (skipParent)
return skipParent;
} else // parent is document element, but no window at defaultView.
return null;
} else if (!parentNode.localName) {
return null;
}
return parentNode;
},
getChildObject: function IUI_getChildObject(node, index, previousSibling)
{
if (!node)
return null;
if (node.contentDocument) {
// then the node is a frame
if (index == 0) {
if (!this.embeddedBrowserParents)
this.embeddedBrowserParents = {};
let skipChild = node.contentDocument.documentElement;
this.embeddedBrowserParents[skipChild] = node;
return skipChild; // the node's HTMLElement
}
return null;
}
if (node instanceof GetSVGDocument) {
// then the node is a frame
if (index == 0) {
if (!this.embeddedBrowserParents)
this.embeddedBrowserParents = {};
let skipChild = node.getSVGDocument().documentElement;
this.embeddedBrowserParents[skipChild] = node;
return skipChild; // the node's SVGElement
}
return null;
}
let child = null;
if (previousSibling) // then we are walking
child = this.getNextSibling(previousSibling);
else
child = this.getFirstChild(node);
if (this.showTextNodesWithWhitespace)
return child;
for (; child; child = this.getNextSibling(child)) {
if (!this.domplateUtils.isWhitespaceText(child))
return child;
}
return null; // we have no children worth showing.
},
getFirstChild: function IUI_getFirstChild(node)
{
this.treeWalker = node.ownerDocument.createTreeWalker(node,
NodeFilter.SHOW_ALL, null, false);
return this.treeWalker.firstChild();
},
getNextSibling: function IUI_getNextSibling(node)
{
let next = this.treeWalker.nextSibling();
if (!next)
delete this.treeWalker;
return next;
},
/**
* Open the style panel if not already onscreen.
*/
openStylePanel: function IUI_openStylePanel()
{
if (!this.stylePanel)
this.stylePanel = document.getElementById("inspector-style-panel");
if (!this.isStylePanelOpen) {
this.stylePanel.hidden = false;
// open at top right of browser panel, offset by 20px from top.
this.stylePanel.openPopup(this.browser, "end_before", 0, 20, false, false);
// size panel to 200px wide by half browser height - 60.
this.stylePanel.sizeTo(200, this.win.outerHeight / 2 - 60);
}
},
/**
* Open the DOM panel if not already onscreen.
*/
openDOMPanel: function IUI_openDOMPanel()
{
if (!this.isDOMPanelOpen) {
this.domPanel.hidden = false;
// open at middle right of browser panel, offset by 20px from middle.
this.domPanel.openPopup(this.browser, "end_before", 0,
this.win.outerHeight / 2 - 20, false, false);
// size panel to 200px wide by half browser height - 60.
this.domPanel.sizeTo(200, this.win.outerHeight / 2 - 60);
}
},
/**
* Toggle the dimmed (semi-transparent) state for a panel by setting or
* removing a dimmed attribute.
*
* @param aDim
* The panel to be dimmed.
*/
toggleDimForPanel: function IUI_toggleDimForPanel(aDim)
{
if (aDim.hasAttribute("dimmed")) {
aDim.removeAttribute("dimmed");
} else {
aDim.setAttribute("dimmed", "true");
}
},
/**
* Open inspector UI. tree, style and DOM panels if enabled. Add listeners for
* document scrolling, resize, tabContainer.TabSelect and others.
*/
openInspectorUI: function IUI_openInspectorUI()
{
// initialization
this.browser = gBrowser.selectedBrowser;
this.win = this.browser.contentWindow;
this.winID = this.getWindowID(this.win);
if (!this.domplate) {
Cu.import("resource:///modules/domplate.jsm", this);
this.domplateUtils.setDOM(window);
}
// DOM panel initialization and loading (via PropertyPanel.jsm)
let objectPanelTitle = this.strings.
GetStringFromName("object.objectPanelTitle");
let parent = document.getElementById("inspector-style-panel").parentNode;
this.propertyPanel = new (this.PropertyPanel)(parent, document,
objectPanelTitle, {});
// additional DOM panel setup needed for unittest identification and use
this.domPanel = this.propertyPanel.panel;
this.domPanel.setAttribute("id", "inspector-dom-panel");
this.domBox = this.propertyPanel.tree;
this.domTreeView = this.propertyPanel.treeView;
// open inspector UI
this.openTreePanel();
// style panel setup and activation
this.styleBox = document.getElementById("inspector-style-listbox");
this.clearStylePanel();
this.openStylePanel();
// DOM panel setup and activation
this.clearDOMPanel();
this.openDOMPanel();
// setup highlighter and start inspecting
this.initializeHighlighter();
// Setup the InspectorStore or restore state
this.initializeStore();
if (InspectorStore.getValue(this.winID, "inspecting"))
this.startInspecting();
this.win.document.addEventListener("scroll", this, false);
this.win.addEventListener("resize", this, false);
this.inspectCmd.setAttribute("checked", true);
document.addEventListener("popupshown", this, false);
},
/**
* Initialize highlighter.
*/
initializeHighlighter: function IUI_initializeHighlighter()
{
this.highlighter = new PanelHighlighter(this.browser);
},
/**
* Initialize the InspectorStore.
*/
initializeStore: function IUI_initializeStore()
{
// First time opened, add the TabSelect listener
if (InspectorStore.isEmpty())
gBrowser.tabContainer.addEventListener("TabSelect", this, false);
// Has this windowID been inspected before?
if (InspectorStore.hasID(this.winID)) {
let selectedNode = InspectorStore.getValue(this.winID, "selectedNode");
if (selectedNode) {
this.inspectNode(selectedNode);
}
} else {
// First time inspecting, set state to no selection + live inspection.
InspectorStore.addStore(this.winID);
InspectorStore.setValue(this.winID, "selectedNode", null);
InspectorStore.setValue(this.winID, "inspecting", true);
this.win.addEventListener("pagehide", this, true);
}
},
/**
* Close inspector UI and associated panels. Unhighlight and stop inspecting.
* Remove event listeners for document scrolling, resize,
* tabContainer.TabSelect and others.
*
* @param boolean aClearStore tells if you want the store associated to the
* current tab/window to be cleared or not.
*/
closeInspectorUI: function IUI_closeInspectorUI(aClearStore)
{
if (this.closing || !this.win || !this.browser) {
return;
}
this.closing = true;
if (aClearStore) {
InspectorStore.deleteStore(this.winID);
this.win.removeEventListener("pagehide", this, true);
} else {
// Update the store before closing.
if (this.selection) {
InspectorStore.setValue(this.winID, "selectedNode",
this.selection);
}
InspectorStore.setValue(this.winID, "inspecting", this.inspecting);
}
if (InspectorStore.isEmpty()) {
gBrowser.tabContainer.removeEventListener("TabSelect", this, false);
}
this.win.document.removeEventListener("scroll", this, false);
this.win.removeEventListener("resize", this, false);
this.stopInspecting();
if (this.highlighter && this.highlighter.isHighlighting) {
this.highlighter.unhighlight();
}
if (this.isTreePanelOpen)
this.treePanel.hidePopup();
if (this.treePanelDiv) {
this.treePanelDiv.ownerPanel = null;
let parent = this.treePanelDiv.parentNode;
parent.removeChild(this.treePanelDiv);
delete this.treePanelDiv;
delete this.treeBrowserDocument;
}
if (this.treeIFrame)
delete this.treeIFrame;
delete this.ioBox;
if (this.domplate) {
this.domplateUtils.setDOM(null);
delete this.domplate;
delete this.HTMLTemplates;
delete this.domplateUtils;
}
if (this.isStylePanelOpen) {
this.stylePanel.hidePopup();
}
if (this.domPanel) {
this.domPanel.hidePopup();
this.domBox = null;
this.domTreeView = null;
this.propertyPanel.destroy();
}
this.inspectCmd.setAttribute("checked", false);
this.browser = this.win = null; // null out references to browser and window
this.winID = null;
this.selection = null;
this.treeLoaded = false;
this.closing = false;
Services.obs.notifyObservers(null, "inspector-closed", null);
},
/**
* Begin inspecting webpage, attach page event listeners, activate
* highlighter event listeners.
*/
startInspecting: function IUI_startInspecting()
{
this.attachPageListeners();
this.inspecting = true;
this.toggleDimForPanel(this.stylePanel);
this.toggleDimForPanel(this.domPanel);
},
/**
* Stop inspecting webpage, detach page listeners, disable highlighter
* event listeners.
*/
stopInspecting: function IUI_stopInspecting()
{
if (!this.inspecting)
return;
this.detachPageListeners();
this.inspecting = false;
this.toggleDimForPanel(this.stylePanel);
this.toggleDimForPanel(this.domPanel);
if (this.highlighter.node) {
this.select(this.highlighter.node, true, true);
}
},
/**
* Select an object in the tree view.
* @param aNode
* node to inspect
* @param forceUpdate
* force an update?
* @param aScroll
* force scroll?
*/
select: function IUI_select(aNode, forceUpdate, aScroll)
{
if (!aNode)
aNode = this.defaultSelection;
if (forceUpdate || aNode != this.selection) {
this.selection = aNode;
let box = this.ioBox.createObjectBox(this.selection);
if (!this.inspecting) {
this.highlighter.highlightNode(this.selection);
this.updateStylePanel(this.selection);
this.updateDOMPanel(this.selection);
}
this.ioBox.select(aNode, true, true, aScroll);
}
},
/////////////////////////////////////////////////////////////////////////
//// Model Creation Methods
/**
* Add a new item to the style panel listbox.
*
* @param aLabel
* A bit of text to put in the listitem's label attribute.
* @param aType
* The type of item.
* @param content
* Text content or value of the listitem.
*/
addStyleItem: function IUI_addStyleItem(aLabel, aType, aContent)
{
let itemLabelString = this.strings.GetStringFromName("style.styleItemLabel");
let item = document.createElement("listitem");
// Do not localize these strings
let label = aLabel;
item.className = "style-" + aType;
if (aContent) {
label = itemLabelString.replace("#1", aLabel);
label = label.replace("#2", aContent);
}
item.setAttribute("label", label);
this.styleBox.appendChild(item);
},
/**
* Create items for each rule included in the given array.
*
* @param aRules
* an array of rule objects
*/
createStyleRuleItems: function IUI_createStyleRuleItems(aRules)
{
let selectorLabel = this.strings.GetStringFromName("style.selectorLabel");
aRules.forEach(function(rule) {
this.addStyleItem(selectorLabel, "selector", rule.id);
rule.properties.forEach(function(property) {
if (property.overridden)
return; // property marked overridden elsewhere
// Do not localize the strings below this line
let important = "";
if (property.important)
important += " !important";
this.addStyleItem(property.name, "property", property.value + important);
}, this);
}, this);
},
/**
* Create rule items for each section as well as the element's style rules,
* if any.
*
* @param aRules
* Array of rules corresponding to the element's style object.
* @param aSections
* Array of sections encapsulating the inherited rules for selectors
* and elements.
*/
createStyleItems: function IUI_createStyleItems(aRules, aSections)
{
this.createStyleRuleItems(aRules);
let inheritedString =
this.strings.GetStringFromName("style.inheritedFrom");
aSections.forEach(function(section) {
let sectionTitle = section.element.tagName;
if (section.element.id)
sectionTitle += "#" + section.element.id;
let replacedString = inheritedString.replace("#1", sectionTitle);
this.addStyleItem(replacedString, "section");
this.createStyleRuleItems(section.rules);
}, this);
},
/**
* Remove all items from the Style Panel's listbox.
*/
clearStylePanel: function IUI_clearStylePanel()
{
for (let i = this.styleBox.childElementCount; i >= 0; --i)
this.styleBox.removeItemAt(i);
},
/**
* Remove all items from the DOM Panel's listbox.
*/
clearDOMPanel: function IUI_clearStylePanel()
{
this.domTreeView.data = {};
},
/**
* Update the contents of the style panel with styles for the currently
* inspected node.
*
* @param aNode
* The highlighted node to get styles for.
*/
updateStylePanel: function IUI_updateStylePanel(aNode)
{
if (this.inspecting || !this.isStylePanelOpen) {
return;
}
let rules = [], styleSections = [], usedProperties = {};
this.style.getInheritedRules(aNode, styleSections, usedProperties);
this.style.getElementRules(aNode, rules, usedProperties);
this.clearStylePanel();
this.createStyleItems(rules, styleSections);
},
/**
* Update the contents of the DOM panel with name/value pairs for the
* currently-inspected node.
*/
updateDOMPanel: function IUI_updateDOMPanel(aNode)
{
if (this.inspecting || !this.isDOMPanelOpen) {
return;
}
this.domTreeView.data = aNode;
},
/////////////////////////////////////////////////////////////////////////
//// Event Handling
notifyReady: function IUI_notifyReady()
{
document.removeEventListener("popupshowing", this, false);
Services.obs.notifyObservers(null, "inspector-opened", null);
},
/**
* Main callback handler for events.
*
* @param event
* The event to be handled.
*/
handleEvent: function IUI_handleEvent(event)
{
let winID = null;
let win = null;
let inspectorClosed = false;
switch (event.type) {
case "popupshown":
if (event.target.id == "inspector-tree-panel" ||
event.target.id == "inspector-style-panel" ||
event.target.id == "inspector-dom-panel")
if (this.isTreePanelOpen && this.isStylePanelOpen &&
this.isDOMPanelOpen && this.treeLoaded) {
this.notifyReady();
}
break;
case "TabSelect":
winID = this.getWindowID(gBrowser.selectedBrowser.contentWindow);
if (this.isTreePanelOpen && winID != this.winID) {
this.closeInspectorUI(false);
inspectorClosed = true;
}
if (winID && InspectorStore.hasID(winID)) {
if (inspectorClosed && this.closing) {
Services.obs.addObserver(function () {
InspectorUI.openInspectorUI();
}, "inspector-closed", false);
} else {
this.openInspectorUI();
}
} else if (InspectorStore.isEmpty()) {
gBrowser.tabContainer.removeEventListener("TabSelect", this, false);
}
break;
case "pagehide":
win = event.originalTarget.defaultView;
// Skip iframes/frames.
if (!win || win.frameElement || win.top != win) {
break;
}
win.removeEventListener(event.type, this, true);
winID = this.getWindowID(win);
if (winID && winID != this.winID) {
InspectorStore.deleteStore(winID);
}
if (InspectorStore.isEmpty()) {
gBrowser.tabContainer.removeEventListener("TabSelect", this, false);
}
break;
case "keypress":
switch (event.keyCode) {
case KeyEvent.DOM_VK_RETURN:
case KeyEvent.DOM_VK_ESCAPE:
this.stopInspecting();
break;
}
break;
case "mousemove":
let element = this.elementFromPoint(event.target.ownerDocument,
event.clientX, event.clientY);
if (element && element != this.node) {
this.inspectNode(element);
}
break;
case "click":
this.stopInspecting();
break;
case "scroll":
case "resize":
this.highlighter.highlight();
break;
}
},
/**
* Handle click events in the html tree panel.
* @param aEvent
* The mouse event.
*/
onTreeClick: function IUI_onTreeClick(aEvent)
{
let node;
let target = aEvent.target;
let hitTwisty = false;
if (this.hasClass(target, "twisty")) {
node = this.getRepObject(aEvent.target.nextSibling);
hitTwisty = true;
} else {
node = this.getRepObject(aEvent.target);
}
if (node) {
if (hitTwisty)
this.ioBox.toggleObject(node);
this.select(node, false, false);
}
},
/**
* Attach event listeners to content window and child windows to enable
* highlighting and click to stop inspection.
*/
attachPageListeners: function IUI_attachPageListeners()
{
this.win.addEventListener("keypress", this, true);
this.browser.addEventListener("mousemove", this, true);
this.browser.addEventListener("click", this, true);
},
/**
* Detach event listeners from content window and child windows
* to disable highlighting.
*/
detachPageListeners: function IUI_detachPageListeners()
{
this.win.removeEventListener("keypress", this, true);
this.browser.removeEventListener("mousemove", this, true);
this.browser.removeEventListener("click", this, true);
},
/////////////////////////////////////////////////////////////////////////
//// Utility Methods
/**
* inspect the given node, highlighting it on the page and selecting the
* correct row in the tree panel
*
* @param aNode
* the element in the document to inspect
*/
inspectNode: function IUI_inspectNode(aNode)
{
this.highlighter.highlightNode(aNode);
this.selectEventsSuppressed = true;
this.select(aNode, true, true);
this.selectEventsSuppressed = false;
this.updateStylePanel(aNode);
this.updateDOMPanel(aNode);
},
/**
* 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) {
switch (node.nodeName.toLowerCase()) {
case "iframe":
let rect = node.getBoundingClientRect();
aX -= rect.left;
aY -= rect.top;
case "frame":
let subnode = this.elementFromPoint(node.contentDocument, aX, aY);
if (subnode) {
node = subnode;
}
}
}
return node;
},
///////////////////////////////////////////////////////////////////////////
//// Utility functions
/**
* Does the given object have a class attribute?
* @param aNode
* the DOM node.
* @param aClass
* The class string.
* @returns boolean
*/
hasClass: function IUI_hasClass(aNode, aClass)
{
if (!(aNode instanceof Element))
return false;
return aNode.classList.contains(aClass);
},
/**
* Add the class name to the given object.
* @param aNode
* the DOM node.
* @param aClass
* The class string.
*/
addClass: function IUI_addClass(aNode, aClass)
{
if (aNode instanceof Element)
aNode.classList.add(aClass);
},
/**
* Remove the class name from the given object
* @param aNode
* the DOM node.
* @param aClass
* The class string.
*/
removeClass: function IUI_removeClass(aNode, aClass)
{
if (aNode instanceof Element)
aNode.classList.remove(aClass);
},
/**
* Retrieve the unique ID of a window object.
*
* @param nsIDOMWindow aWindow
* @returns integer ID
*/
getWindowID: function IUI_getWindowID(aWindow)
{
if (!aWindow) {
return null;
}
let util = {};
try {
util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIDOMWindowUtils);
} catch (ex) { }
return util.currentInnerWindowID;
},
/**
* Get the "repObject" from the HTML panel's domplate-constructed DOM node.
* In this system, a "repObject" is the Object being Represented by the box
* object. It is the "real" object that we're building our facade around.
*
* @param element
* The element in the HTML panel the user clicked.
* @returns either a real node or null
*/
getRepObject: function IUI_getRepObject(element)
{
let target = null;
for (let child = element; child; child = child.parentNode) {
if (this.hasClass(child, "repTarget"))
target = child;
if (child.repObject) {
if (!target && this.hasClass(child.repObject, "repIgnore"))
break;
else
return child.repObject;
}
}
return null;
},
/**
* @param msg
* text message to send to the log
*/
_log: function LOG(msg)
{
Services.console.logStringMessage(msg);
},
}
/**
* The Inspector store is used for storing data specific to each tab window.
*/
var InspectorStore = {
store: {},
length: 0,
/**
* Check if there is any data recorded for any tab/window.
*
* @returns boolean True if there are no stores for any window/tab, or false
* otherwise.
*/
isEmpty: function IS_isEmpty()
{
return this.length == 0 ? true : false;
},
/**
* Add a new store.
*
* @param string aID The Store ID you want created.
* @returns boolean True if the store was added successfully, or false
* otherwise.
*/
addStore: function IS_addStore(aID)
{
let result = false;
if (!(aID in this.store)) {
this.store[aID] = {};
this.length++;
result = true;
}
return result;
},
/**
* Delete a store by ID.
*
* @param string aID The store ID you want deleted.
* @returns boolean True if the store was removed successfully, or false
* otherwise.
*/
deleteStore: function IS_deleteStore(aID)
{
let result = false;
if (aID in this.store) {
delete this.store[aID];
this.length--;
result = true;
}
return result;
},
/**
* Check store existence.
*
* @param string aID The store ID you want to check.
* @returns boolean True if the store ID is registered, or false otherwise.
*/
hasID: function IS_hasID(aID)
{
return (aID in this.store);
},
/**
* Retrieve a value from a store for a given key.
*
* @param string aID The store ID you want to read the value from.
* @param string aKey The key name of the value you want.
* @returns mixed the value associated to your store and key.
*/
getValue: function IS_getValue(aID, aKey)
{
if (!this.hasID(aID))
return null;
if (aKey in this.store[aID])
return this.store[aID][aKey];
return null;
},
/**
* Set a value for a given key and store.
*
* @param string aID The store ID where you want to store the value into.
* @param string aKey The key name for which you want to save the value.
* @param mixed aValue The value you want stored.
* @returns boolean True if the value was stored successfully, or false
* otherwise.
*/
setValue: function IS_setValue(aID, aKey, aValue)
{
let result = false;
if (aID in this.store) {
this.store[aID][aKey] = aValue;
result = true;
}
return result;
},
/**
* Delete a value for a given key and store.
*
* @param string aID The store ID where you want to store the value into.
* @param string aKey The key name for which you want to save the value.
* @returns boolean True if the value was removed successfully, or false
* otherwise.
*/
deleteValue: function IS_deleteValue(aID, aKey)
{
let result = false;
if (aID in this.store && aKey in this.store[aID]) {
delete this.store[aID][aKey];
result = true;
}
return result;
}
};
/////////////////////////////////////////////////////////////////////////
//// Initializors
XPCOMUtils.defineLazyGetter(InspectorUI, "inspectCmd", function () {
return document.getElementById("Tools:Inspect");
});
XPCOMUtils.defineLazyGetter(InspectorUI, "strings", function () {
return Services.strings.createBundle("chrome://browser/locale/inspector.properties");
});
XPCOMUtils.defineLazyGetter(InspectorUI, "PropertyTreeView", function () {
var obj = {};
Cu.import("resource:///modules/PropertyPanel.jsm", obj);
return obj.PropertyTreeView;
});
XPCOMUtils.defineLazyGetter(InspectorUI, "PropertyPanel", function () {
var obj = {};
Cu.import("resource:///modules/PropertyPanel.jsm", obj);
return obj.PropertyPanel;
});
XPCOMUtils.defineLazyGetter(InspectorUI, "style", function () {
var obj = {};
Cu.import("resource:///modules/stylePanel.jsm", obj);
obj.style.initialize();
return obj.style;
});