gecko-dev/browser/base/content/inspector.js

952 lines
26 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)
*
* 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
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.
* @param aColor
* A string containing an RGB color for the panel background.
* @param aBorderSize
* A number representing the border thickness of the panel.
* @param anOpacity
* A number representing the alpha value of the panel background.
*/
function PanelHighlighter(aBrowser, aColor, aBorderSize, anOpacity)
{
this.panel = document.getElementById("highlighter-panel");
this.panel.hidden = false;
this.browser = aBrowser;
this.win = this.browser.contentWindow;
this.backgroundColor = aColor;
this.border = aBorderSize;
this.opacity = anOpacity;
this.updatePanelStyles();
}
PanelHighlighter.prototype = {
/**
* Update the panel's style object with current settings.
* TODO see bugXXXXXX, https://wiki.mozilla.org/Firefox/Projects/Inspector#0.7
* and, https://wiki.mozilla.org/Firefox/Projects/Inspector#1.0.
*/
updatePanelStyles: function PanelHighlighter_updatePanelStyles()
{
let style = this.panel.style;
style.backgroundColor = this.backgroundColor;
style.border = "solid blue " + this.border + "px";
style.MozBorderRadius = "4px";
style.opacity = this.opacity;
},
/**
* 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 this.win.document.elementFromPoint(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 = this.win.document.elementFromPoint(aEvent.clientX -
browserRect.left, aEvent.clientY - browserRect.top);
if (element && element != this.node) {
InspectorUI.inspectNode(element);
}
},
};
///////////////////////////////////////////////////////////////////////////
//// InspectorTreeView
/**
* TreeView object to manage the view of the DOM tree. Wraps and provides an
* interface to an inIDOMView object
*
* @param aWindow
* a top-level window object
*/
function InspectorTreeView(aWindow)
{
this.tree = document.getElementById("inspector-tree");
this.treeBody = document.getElementById("inspector-tree-body");
this.view = Cc["@mozilla.org/inspector/dom-view;1"]
.createInstance(Ci.inIDOMView);
this.view.showSubDocuments = true;
this.view.whatToShow = NodeFilter.SHOW_ALL;
this.tree.view = this.view;
this.contentWindow = aWindow;
this.view.rootNode = aWindow.document;
this.view.rebuild();
}
InspectorTreeView.prototype = {
get editable() { return false; },
get selection() { return this.view.selection; },
/**
* Destroy the view.
*/
destroy: function ITV_destroy()
{
this.tree.view = null;
this.view = null;
this.tree = null;
},
/**
* Get the cell text at a given row and column.
*
* @param aRow
* The row index of the desired cell.
* @param aCol
* The column index of the desired cell.
* @returns string
*/
getCellText: function ITV_getCellText(aRow, aCol)
{
return this.view.getCellText(aRow, aCol);
},
/**
* Get the index of the selected row.
*
* @returns number
*/
get selectionIndex()
{
return this.selection.currentIndex;
},
/**
* Get the corresponding node for the currently-selected row in the tree.
*
* @returns DOMNode
*/
get selectedNode()
{
let rowIndex = this.selectionIndex;
return this.view.getNodeFromRowIndex(rowIndex);
},
/**
* Set the selected row in the table to the specified index.
*
* @param anIndex
* The index to set the selection to.
*/
set selectedRow(anIndex)
{
this.view.selection.select(anIndex);
this.tree.treeBoxObject.ensureRowIsVisible(anIndex);
},
/**
* Set the selected node to the specified document node.
*
* @param aNode
* The document node to select in the tree.
*/
set selectedNode(aNode)
{
let rowIndex = this.view.getRowIndexFromNode(aNode);
if (rowIndex > -1) {
this.selectedRow = rowIndex;
} else {
this.selectElementInTree(aNode);
}
},
/**
* Select the given node in the tree, searching for and expanding rows
* as-needed.
*
* @param aNode
* The document node to select in the three.
* @returns boolean
* Whether a node was selected or not if not found.
*/
selectElementInTree: function ITV_selectElementInTree(aNode)
{
if (!aNode) {
this.view.selection.select(null);
return false;
}
// Keep searching until a pre-created ancestor is found, then
// open each ancestor until the found element is created.
let domUtils = Cc["@mozilla.org/inspector/dom-utils;1"].
getService(Ci.inIDOMUtils);
let line = [];
let parent = aNode;
let index = null;
while (parent) {
index = this.view.getRowIndexFromNode(parent);
line.push(parent);
if (index < 0) {
// Row for this node hasn't been created yet.
parent = domUtils.getParentForNode(parent,
this.view.showAnonymousContent);
} else {
break;
}
}
// We have all the ancestors, now open them one-by-one from the top
// to bottom.
let lastIndex;
let view = this.tree.treeBoxObject.view;
for (let i = line.length - 1; i >= 0; --i) {
index = this.view.getRowIndexFromNode(line[i]);
if (index < 0) {
// Can't find the row, so stop trying to descend.
break;
}
if (i > 0 && !view.isContainerOpen(index)) {
view.toggleOpenState(index);
}
lastIndex = index;
}
if (lastIndex >= 0) {
this.selectedRow = lastIndex;
return true;
}
return false;
},
};
///////////////////////////////////////////////////////////////////////////
//// InspectorUI
/**
* Main controller class for the Inspector.
*/
var InspectorUI = {
browser: null,
_showTreePanel: true,
_showStylePanel: true,
_showDOMPanel: false,
highlightColor: "#EEEE66",
highlightThickness: 4,
highlightOpacity: 0.4,
selectEventsSuppressed: false,
inspecting: false,
/**
* 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.isPanelOpen) {
this.closeInspectorUI();
} 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._showStylePanel) {
this.stylePanel.hidePopup();
} else {
this.openStylePanel();
if (this.treeView.selectedNode) {
this.updateStylePanel(this.treeView.selectedNode);
}
}
this._showStylePanel = !this._showStylePanel;
},
/**
* Is the tree panel open?
*
* @returns boolean
*/
get isPanelOpen()
{
return this.treePanel && this.treePanel.state == "open";
},
/**
* Is the style panel open?
*
* @returns boolean
*/
get isStylePanelOpen()
{
return this.stylePanel && this.stylePanel.state == "open";
},
/**
* Open the inspector's tree panel and initialize it.
*/
openTreePanel: function IUI_openTreePanel()
{
if (!this.treePanel) {
this.treePanel = document.getElementById("inspector-panel");
this.treePanel.hidden = false;
}
if (!this.isPanelOpen) {
const panelWidthRatio = 7 / 8;
const panelHeightRatio = 1 / 5;
let bar = document.getElementById("status-bar");
this.treePanel.openPopup(bar, "overlap", 120, -120, false, false);
this.treePanel.sizeTo(this.win.outerWidth * panelWidthRatio,
this.win.outerHeight * panelHeightRatio);
this.tree = document.getElementById("inspector-tree");
this.createDocumentModel();
}
},
/**
* Open the style panel if not already onscreen.
*/
openStylePanel: function IUI_openStylePanel()
{
if (!this.stylePanel) {
this.stylePanel = document.getElementById("inspector-style-panel");
this.stylePanel.hidden = false;
}
if (!this.isStylePanelOpen) {
// 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);
}
},
/**
* 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");
}
},
openDOMPanel: function IUI_openDOMPanel()
{
// # todo bug 561782
},
/**
* Open inspector UI. tree, style and DOM panels if enabled. Add listeners for
* document scrolling, resize and tabContainer.TabSelect.
*/
openInspectorUI: function IUI_openInspectorUI()
{
// initialization
this.browser = gBrowser.selectedBrowser;
this.win = this.browser.contentWindow;
if (!this.style) {
Cu.import("resource:///modules/stylePanel.jsm", this);
this.style.initialize();
}
// open inspector UI
if (this._showTreePanel) {
this.openTreePanel();
}
if (this._showStylePanel) {
this.styleBox = document.getElementById("inspector-style-listbox");
this.clearStylePanel();
this.openStylePanel();
}
if (this._showDOMPanel) {
this.openDOMPanel();
}
this.inspectorBundle = Services.strings.createBundle("chrome://browser/locale/inspector.properties");
this.initializeHighlighter();
this.startInspecting();
this.win.document.addEventListener("scroll", this, false);
this.win.addEventListener("resize", this, false);
gBrowser.tabContainer.addEventListener("TabSelect", this, false);
this.inspectCmd.setAttribute("checked", true);
},
/**
* Initialize highlighter.
*/
initializeHighlighter: function IUI_initializeHighlighter()
{
this.highlighter = new PanelHighlighter(this.browser, this.highlightColor,
this.highlightThickness, this.highlightOpacity);
},
/**
* Close inspector UI and associated panels. Unhighlight and stop inspecting.
* Remove event listeners for document scrolling, resize and
* tabContainer.TabSelect.
*/
closeInspectorUI: function IUI_closeInspectorUI()
{
this.win.document.removeEventListener("scroll", this, false);
this.win.removeEventListener("resize", this, false);
gBrowser.tabContainer.removeEventListener("TabSelect", this, false);
this.stopInspecting();
if (this.highlighter && this.highlighter.isHighlighting) {
this.highlighter.unhighlight();
}
if (this.isPanelOpen) {
this.treePanel.hidePopup();
this.treeView.destroy();
}
if (this.isStylePanelOpen) {
this.stylePanel.hidePopup();
}
this.inspectCmd.setAttribute("checked", false);
this.browser = this.win = null; // null out references to browser and window
},
/**
* Begin inspecting webpage, attach page event listeners, activate
* highlighter event listeners.
*/
startInspecting: function IUI_startInspecting()
{
this.attachPageListeners();
this.inspecting = true;
this.toggleDimForPanel(this.stylePanel);
},
/**
* 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);
if (this.treeView.selection) {
this.updateStylePanel(this.treeView.selectedNode);
}
},
/////////////////////////////////////////////////////////////////////////
//// Model Creation Methods
/**
* Create treeView object from content window.
*/
createDocumentModel: function IUI_createDocumentModel()
{
this.treeView = new InspectorTreeView(this.win);
},
/**
* add a new item to the 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.inspectorBundle.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.inspectorBundle.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.inspectorBundle.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);
},
/**
* 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);
},
/////////////////////////////////////////////////////////////////////////
//// Event Handling
/**
* Main callback handler for events.
*
* @param event
* The event to be handled.
*/
handleEvent: function IUI_handleEvent(event)
{
switch (event.type) {
case "TabSelect":
this.closeInspectorUI();
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.win.document.elementFromPoint(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;
}
},
/**
* Event fired when a tree row is selected in the tree panel.
*/
onTreeSelected: function IUI_onTreeSelected()
{
if (this.selectEventsSuppressed) {
return false;
}
let treeView = this.treeView;
let node = treeView.selectedNode;
this.highlighter.highlightNode(node);
this.stopInspecting();
this.updateStylePanel(node);
return true;
},
/**
* 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.treeView.selectedNode = aNode;
this.selectEventsSuppressed = false;
this.updateStylePanel(aNode);
},
///////////////////////////////////////////////////////////////////////////
//// Utility functions
/**
* debug logging facility
* @param msg
* text message to send to the log
*/
_log: function LOG(msg)
{
Services.console.logStringMessage(msg);
},
}
XPCOMUtils.defineLazyGetter(InspectorUI, "inspectCmd", function () {
return document.getElementById("Tools:Inspect");
});