gecko-dev/devtools/client/inspector/inspector-panel.js

1444 lines
47 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Cc, Ci, Cu} = require("chrome");
var Services = require("Services");
var promise = require("promise");
var EventEmitter = require("devtools/shared/event-emitter");
var clipboard = require("sdk/clipboard");
var {HostType} = require("devtools/client/framework/toolbox").Toolbox;
const {executeSoon} = require("devtools/shared/DevToolsUtils");
loader.lazyRequireGetter(this, "CSS", "CSS");
loader.lazyGetter(this, "MarkupView", () => require("devtools/client/inspector/markup/markup").MarkupView);
loader.lazyGetter(this, "HTMLBreadcrumbs", () => require("devtools/client/inspector/breadcrumbs").HTMLBreadcrumbs);
loader.lazyGetter(this, "ToolSidebar", () => require("devtools/client/framework/sidebar").ToolSidebar);
loader.lazyGetter(this, "InspectorSearch", () => require("devtools/client/inspector/inspector-search").InspectorSearch);
loader.lazyGetter(this, "RuleViewTool", () => require("devtools/client/inspector/rules/rules").RuleViewTool);
loader.lazyGetter(this, "ComputedViewTool", () => require("devtools/client/inspector/computed/computed").ComputedViewTool);
loader.lazyGetter(this, "FontInspector", () => require("devtools/client/inspector/fonts/fonts").FontInspector);
loader.lazyGetter(this, "LayoutView", () => require("devtools/client/inspector/layout/layout").LayoutView);
loader.lazyGetter(this, "strings", () => {
return Services.strings.createBundle("chrome://devtools/locale/inspector.properties");
});
loader.lazyGetter(this, "toolboxStrings", () => {
return Services.strings.createBundle("chrome://devtools/locale/toolbox.properties");
});
loader.lazyGetter(this, "clipboardHelper", () => {
return Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
});
loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
/**
* Represents an open instance of the Inspector for a tab.
* The inspector controls the breadcrumbs, the markup view, and the sidebar
* (computed view, rule view, font view and layout view).
*
* Events:
* - ready
* Fired when the inspector panel is opened for the first time and ready to
* use
* - new-root
* Fired after a new root (navigation to a new page) event was fired by
* the walker, and taken into account by the inspector (after the markup
* view has been reloaded)
* - markuploaded
* Fired when the markup-view frame has loaded
* - breadcrumbs-updated
* Fired when the breadcrumb widget updates to a new node
* - layoutview-updated
* Fired when the layoutview (box model) updates to a new node
* - markupmutation
* Fired after markup mutations have been processed by the markup-view
* - computed-view-refreshed
* Fired when the computed rules view updates to a new node
* - computed-view-property-expanded
* Fired when a property is expanded in the computed rules view
* - computed-view-property-collapsed
* Fired when a property is collapsed in the computed rules view
* - computed-view-sourcelinks-updated
* Fired when the stylesheet source links have been updated (when switching
* to source-mapped files)
* - rule-view-refreshed
* Fired when the rule view updates to a new node
* - rule-view-sourcelinks-updated
* Fired when the stylesheet source links have been updated (when switching
* to source-mapped files)
*/
function InspectorPanel(iframeWindow, toolbox) {
this._toolbox = toolbox;
this._target = toolbox._target;
this.panelDoc = iframeWindow.document;
this.panelWin = iframeWindow;
this.panelWin.inspector = this;
this.nodeMenuTriggerInfo = null;
this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
this.onNewRoot = this.onNewRoot.bind(this);
this._setupNodeMenu = this._setupNodeMenu.bind(this);
this._resetNodeMenu = this._resetNodeMenu.bind(this);
this._updateSearchResultsLabel = this._updateSearchResultsLabel.bind(this);
this.onNewSelection = this.onNewSelection.bind(this);
this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this);
this.onDetached = this.onDetached.bind(this);
this.onPaneToggleButtonClicked = this.onPaneToggleButtonClicked.bind(this);
this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
this._target.on("will-navigate", this._onBeforeNavigate);
EventEmitter.decorate(this);
}
exports.InspectorPanel = InspectorPanel;
InspectorPanel.prototype = {
/**
* open is effectively an asynchronous constructor
*/
open: function() {
return this.target.makeRemote().then(() => {
return this._getPageStyle();
}).then(() => {
return this._getDefaultNodeForSelection();
}).then(defaultSelection => {
return this._deferredOpen(defaultSelection);
}).then(null, console.error);
},
get toolbox() {
return this._toolbox;
},
get inspector() {
return this._toolbox.inspector;
},
get walker() {
return this._toolbox.walker;
},
get selection() {
return this._toolbox.selection;
},
get isOuterHTMLEditable() {
return this._target.client.traits.editOuterHTML;
},
get hasUrlToImageDataResolver() {
return this._target.client.traits.urlToImageDataResolver;
},
get canGetUniqueSelector() {
return this._target.client.traits.getUniqueSelector;
},
get canGetUsedFontFaces() {
return this._target.client.traits.getUsedFontFaces;
},
get canPasteInnerOrAdjacentHTML() {
return this._target.client.traits.pasteHTML;
},
_deferredOpen: function(defaultSelection) {
let deferred = promise.defer();
this.walker.on("new-root", this.onNewRoot);
this.nodemenu = this.panelDoc.getElementById("inspector-node-popup");
this.lastNodemenuItem = this.nodemenu.lastChild;
this.nodemenu.addEventListener("popupshowing", this._setupNodeMenu, true);
this.nodemenu.addEventListener("popuphiding", this._resetNodeMenu, true);
this.selection.on("new-node-front", this.onNewSelection);
this.selection.on("before-new-node-front", this.onBeforeNewSelection);
this.selection.on("detached-front", this.onDetached);
this.breadcrumbs = new HTMLBreadcrumbs(this);
if (this.target.isLocalTab) {
// Show a warning when the debugger is paused.
// We show the warning only when the inspector
// is selected.
this.updateDebuggerPausedWarning = () => {
let notificationBox = this._toolbox.getNotificationBox();
let notification = notificationBox.getNotificationWithValue("inspector-script-paused");
if (!notification && this._toolbox.currentToolId == "inspector" &&
this._toolbox.threadClient.paused) {
let message = strings.GetStringFromName("debuggerPausedWarning.message");
notificationBox.appendNotification(message,
"inspector-script-paused", "", notificationBox.PRIORITY_WARNING_HIGH);
}
if (notification && this._toolbox.currentToolId != "inspector") {
notificationBox.removeNotification(notification);
}
if (notification && !this._toolbox.threadClient.paused) {
notificationBox.removeNotification(notification);
}
};
this.target.on("thread-paused", this.updateDebuggerPausedWarning);
this.target.on("thread-resumed", this.updateDebuggerPausedWarning);
this._toolbox.on("select", this.updateDebuggerPausedWarning);
this.updateDebuggerPausedWarning();
}
this._initMarkup();
this.isReady = false;
this.once("markuploaded", () => {
this.isReady = true;
// All the components are initialized. Let's select a node.
this.selection.setNodeFront(defaultSelection, "inspector-open");
this.markup.expandNode(this.selection.nodeFront);
this.emit("ready");
deferred.resolve(this);
});
this.setupSearchBox();
this.setupSidebar();
return deferred.promise;
},
_onBeforeNavigate: function() {
this._defaultNode = null;
this.selection.setNodeFront(null);
this._destroyMarkup();
this.isDirty = false;
this._pendingSelection = null;
},
_getPageStyle: function() {
return this._toolbox.inspector.getPageStyle().then(pageStyle => {
this.pageStyle = pageStyle;
});
},
/**
* Return a promise that will resolve to the default node for selection.
*/
_getDefaultNodeForSelection: function() {
if (this._defaultNode) {
return this._defaultNode;
}
let walker = this.walker;
let rootNode = null;
let pendingSelection = this._pendingSelection;
// A helper to tell if the target has or is about to navigate.
// this._pendingSelection changes on "will-navigate" and "new-root" events.
let hasNavigated = () => pendingSelection !== this._pendingSelection;
// If available, set either the previously selected node or the body
// as default selected, else set documentElement
return walker.getRootNode().then(aRootNode => {
if (hasNavigated()) {
return promise.reject("navigated; resolution of _defaultNode aborted");
}
rootNode = aRootNode;
if (this.selectionCssSelector) {
return walker.querySelector(rootNode, this.selectionCssSelector);
}
}).then(front => {
if (hasNavigated()) {
return promise.reject("navigated; resolution of _defaultNode aborted");
}
if (front) {
return front;
}
return walker.querySelector(rootNode, "body");
}).then(front => {
if (hasNavigated()) {
return promise.reject("navigated; resolution of _defaultNode aborted");
}
if (front) {
return front;
}
return this.walker.documentElement();
}).then(node => {
if (hasNavigated()) {
return promise.reject("navigated; resolution of _defaultNode aborted");
}
this._defaultNode = node;
return node;
});
},
/**
* Target getter.
*/
get target() {
return this._target;
},
/**
* Target setter.
*/
set target(value) {
this._target = value;
},
/**
* Indicate that a tool has modified the state of the page. Used to
* decide whether to show the "are you sure you want to navigate"
* notification.
*/
markDirty: function() {
this.isDirty = true;
},
/**
* Hooks the searchbar to show result and auto completion suggestions.
*/
setupSearchBox: function() {
this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
this.searchResultsLabel = this.panelDoc.getElementById("inspector-searchlabel");
this.search = new InspectorSearch(this, this.searchBox);
this.search.on("search-cleared", this._updateSearchResultsLabel);
this.search.on("search-result", this._updateSearchResultsLabel);
},
get searchSuggestions() {
return this.search.autocompleter;
},
_updateSearchResultsLabel: function(event, result) {
let str = "";
if (event !== "search-cleared") {
if (result) {
str = strings.formatStringFromName(
"inspector.searchResultsCount2",
[result.resultsIndex + 1, result.resultsLength], 2);
} else {
str = strings.GetStringFromName("inspector.searchResultsNone");
}
}
this.searchResultsLabel.textContent = str;
},
/**
* Build the sidebar.
*/
setupSidebar: function() {
let tabbox = this.panelDoc.querySelector("#inspector-sidebar");
this.sidebar = new ToolSidebar(tabbox, this, "inspector", {
showAllTabsMenu: true
});
let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");
if (!Services.prefs.getBoolPref("devtools.fontinspector.enabled") &&
defaultTab == "fontinspector") {
defaultTab = "ruleview";
}
this._setDefaultSidebar = (event, toolId) => {
Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
};
this.sidebar.on("select", this._setDefaultSidebar);
this.ruleview = new RuleViewTool(this, this.panelWin);
this.computedview = new ComputedViewTool(this, this.panelWin);
if (Services.prefs.getBoolPref("devtools.fontinspector.enabled") &&
this.canGetUsedFontFaces) {
this.fontInspector = new FontInspector(this, this.panelWin);
this.panelDoc.getElementById("sidebar-tab-fontinspector").hidden = false;
}
this.layoutview = new LayoutView(this, this.panelWin);
if (this.target.form.animationsActor) {
this.sidebar.addTab("animationinspector",
"chrome://devtools/content/animationinspector/animation-inspector.xhtml",
"animationinspector" == defaultTab);
}
this.sidebar.show(defaultTab);
this.setupSidebarToggle();
},
/**
* Add the expand/collapse behavior for the sidebar panel.
*/
setupSidebarToggle: function() {
this._paneToggleButton = this.panelDoc.getElementById("inspector-pane-toggle");
this._paneToggleButton.addEventListener("mousedown",
this.onPaneToggleButtonClicked);
},
/**
* Reset the inspector on new root mutation.
*/
onNewRoot: function() {
this._defaultNode = null;
this.selection.setNodeFront(null);
this._destroyMarkup();
this.isDirty = false;
let onNodeSelected = defaultNode => {
// Cancel this promise resolution as a new one had
// been queued up.
if (this._pendingSelection != onNodeSelected) {
return;
}
this._pendingSelection = null;
this.selection.setNodeFront(defaultNode, "navigateaway");
this._initMarkup();
this.once("markuploaded", () => {
if (!this.markup) {
return;
}
this.markup.expandNode(this.selection.nodeFront);
this.emit("new-root");
});
};
this._pendingSelection = onNodeSelected;
this._getDefaultNodeForSelection().then(onNodeSelected, console.error);
},
_selectionCssSelector: null,
/**
* Set the currently selected node unique css selector.
* Will store the current target url along with it to allow pre-selection at
* reload
*/
set selectionCssSelector(cssSelector = null) {
if (this._panelDestroyer) {
return;
}
this._selectionCssSelector = {
selector: cssSelector,
url: this._target.url
};
},
/**
* Get the current selection unique css selector if any, that is, if a node
* is actually selected and that node has been selected while on the same url
*/
get selectionCssSelector() {
if (this._selectionCssSelector &&
this._selectionCssSelector.url === this._target.url) {
return this._selectionCssSelector.selector;
} else {
return null;
}
},
/**
* Can a new HTML element be inserted into the currently selected element?
* @return {Boolean}
*/
canAddHTMLChild: function() {
let selection = this.selection;
// Don't allow to insert an element into these elements. This should only
// contain elements where walker.insertAdjacentHTML has no effect.
let invalidTagNames = ["html", "iframe"];
return selection.isHTMLNode() &&
selection.isElementNode() &&
!selection.isPseudoElementNode() &&
!selection.isAnonymousNode() &&
invalidTagNames.indexOf(
selection.nodeFront.nodeName.toLowerCase()) === -1;
},
/**
* When a new node is selected.
*/
onNewSelection: function(event, value, reason) {
if (reason === "selection-destroy") {
return;
}
// Wait for all the known tools to finish updating and then let the
// client know.
let selection = this.selection.nodeFront;
// Update the state of the add button in the toolbar depending on the
// current selection.
let btn = this.panelDoc.querySelector("#inspector-element-add-button");
if (this.canAddHTMLChild()) {
btn.removeAttribute("disabled");
} else {
btn.setAttribute("disabled", "true");
}
// On any new selection made by the user, store the unique css selector
// of the selected node so it can be restored after reload of the same page
if (this.canGetUniqueSelector &&
this.selection.isElementNode()) {
selection.getUniqueSelector().then(selector => {
this.selectionCssSelector = selector;
}).then(null, e => {
// Only log this as an error if the panel hasn't been destroyed in the
// meantime.
if (!this._panelDestroyer) {
console.error(e);
} else {
console.warn("Could not set the unique selector for the newly "+
"selected node, the inspector was destroyed.");
}
});
}
let selfUpdate = this.updating("inspector-panel");
executeSoon(() => {
try {
selfUpdate(selection);
} catch (ex) {
console.error(ex);
}
});
},
/**
* Delay the "inspector-updated" notification while a tool
* is updating itself. Returns a function that must be
* invoked when the tool is done updating with the node
* that the tool is viewing.
*/
updating: function(name) {
if (this._updateProgress && this._updateProgress.node != this.selection.nodeFront) {
this.cancelUpdate();
}
if (!this._updateProgress) {
// Start an update in progress.
let self = this;
this._updateProgress = {
node: this.selection.nodeFront,
outstanding: new Set(),
checkDone: function() {
if (this !== self._updateProgress) {
return;
}
// Cancel update if there is no `selection` anymore.
// It can happen if the inspector panel is already destroyed.
if (!self.selection || (this.node !== self.selection.nodeFront)) {
self.cancelUpdate();
return;
}
if (this.outstanding.size !== 0) {
return;
}
self._updateProgress = null;
self.emit("inspector-updated", name);
},
};
}
let progress = this._updateProgress;
let done = function() {
progress.outstanding.delete(done);
progress.checkDone();
};
progress.outstanding.add(done);
return done;
},
/**
* Cancel notification of inspector updates.
*/
cancelUpdate: function() {
this._updateProgress = null;
},
/**
* When a new node is selected, before the selection has changed.
*/
onBeforeNewSelection: function(event, node) {
if (this.breadcrumbs.indexOf(node) == -1) {
// only clear locks if we'd have to update breadcrumbs
this.clearPseudoClasses();
}
},
/**
* When a node is deleted, select its parent node or the defaultNode if no
* parent is found (may happen when deleting an iframe inside which the
* node was selected).
*/
onDetached: function(event, parentNode) {
this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode));
this.selection.setNodeFront(parentNode ? parentNode : this._defaultNode, "detached");
},
/**
* Destroy the inspector.
*/
destroy: function() {
if (this._panelDestroyer) {
return this._panelDestroyer;
}
if (this.walker) {
this.walker.off("new-root", this.onNewRoot);
this.pageStyle = null;
}
this.cancelUpdate();
this.target.off("will-navigate", this._onBeforeNavigate);
this.target.off("thread-paused", this.updateDebuggerPausedWarning);
this.target.off("thread-resumed", this.updateDebuggerPausedWarning);
this._toolbox.off("select", this.updateDebuggerPausedWarning);
if (this.ruleview) {
this.ruleview.destroy();
}
if (this.computedview) {
this.computedview.destroy();
}
if (this.fontInspector) {
this.fontInspector.destroy();
}
if (this.layoutview) {
this.layoutview.destroy();
}
this.sidebar.off("select", this._setDefaultSidebar);
let sidebarDestroyer = this.sidebar.destroy();
this.sidebar = null;
this.nodemenu.removeEventListener("popupshowing", this._setupNodeMenu, true);
this.nodemenu.removeEventListener("popuphiding", this._resetNodeMenu, true);
this.breadcrumbs.destroy();
this._paneToggleButton.removeEventListener("mousedown",
this.onPaneToggleButtonClicked);
this._paneToggleButton = null;
this.selection.off("new-node-front", this.onNewSelection);
this.selection.off("before-new-node", this.onBeforeNewSelection);
this.selection.off("before-new-node-front", this.onBeforeNewSelection);
this.selection.off("detached-front", this.onDetached);
let markupDestroyer = this._destroyMarkup();
this.panelWin.inspector = null;
this.target = null;
this.panelDoc = null;
this.panelWin = null;
this.breadcrumbs = null;
this.lastNodemenuItem = null;
this.nodemenu = null;
this._toolbox = null;
this.search.destroy();
this.search = null;
this.searchBox = null;
this._panelDestroyer = promise.all([
sidebarDestroyer,
markupDestroyer
]);
return this._panelDestroyer;
},
/**
* Show the node menu.
*/
showNodeMenu: function(aButton, aPosition, aExtraItems) {
if (aExtraItems) {
for (let item of aExtraItems) {
this.nodemenu.appendChild(item);
}
}
this.nodemenu.openPopup(aButton, aPosition, 0, 0, true, false);
},
hideNodeMenu: function() {
this.nodemenu.hidePopup();
},
/**
* Returns the clipboard content if it is appropriate for pasting
* into the current node's outer HTML, otherwise returns null.
*/
_getClipboardContentForPaste: function() {
let flavors = clipboard.currentFlavors;
if (flavors.indexOf("text") != -1 ||
(flavors.indexOf("html") != -1 && flavors.indexOf("image") == -1)) {
let content = clipboard.get();
if (content && content.trim().length > 0) {
return content;
}
}
return null;
},
/**
* Update, enable, disable, hide, show any menu item depending on the current
* element.
*/
_setupNodeMenu: function(event) {
let markupContainer = this.markup.getContainer(this.selection.nodeFront);
this.nodeMenuTriggerInfo =
markupContainer.editor.getInfoAtNode(event.target.triggerNode);
let isSelectionElement = this.selection.isElementNode() &&
!this.selection.isPseudoElementNode();
let isEditableElement = isSelectionElement &&
!this.selection.isAnonymousNode();
let isDuplicatableElement = isSelectionElement &&
!this.selection.isAnonymousNode() &&
!this.selection.isRoot();
let isScreenshotable = isSelectionElement &&
this.canGetUniqueSelector &&
this.selection.nodeFront.isTreeDisplayed;
// Set the pseudo classes
for (let name of ["hover", "active", "focus"]) {
let menu = this.panelDoc.getElementById("node-menu-pseudo-" + name);
if (isSelectionElement) {
let checked = this.selection.nodeFront.hasPseudoClassLock(":" + name);
menu.setAttribute("checked", checked);
menu.removeAttribute("disabled");
} else {
menu.setAttribute("disabled", "true");
}
}
// Disable delete item if needed
let deleteNode = this.panelDoc.getElementById("node-menu-delete");
if (isEditableElement) {
deleteNode.removeAttribute("disabled");
} else {
deleteNode.setAttribute("disabled", "true");
}
// Disable add item if needed
let addNode = this.panelDoc.getElementById("node-menu-add");
if (this.canAddHTMLChild()) {
addNode.removeAttribute("disabled");
} else {
addNode.setAttribute("disabled", "true");
}
// Disable / enable "Copy Unique Selector", "Copy inner HTML",
// "Copy outer HTML", "Scroll Into View" & "Screenshot Node" as appropriate
let unique = this.panelDoc.getElementById("node-menu-copyuniqueselector");
let screenshot = this.panelDoc.getElementById("node-menu-screenshotnode");
let duplicateNode = this.panelDoc.getElementById("node-menu-duplicatenode");
let copyInnerHTML = this.panelDoc.getElementById("node-menu-copyinner");
let copyOuterHTML = this.panelDoc.getElementById("node-menu-copyouter");
let scrollIntoView = this.panelDoc.getElementById("node-menu-scrollnodeintoview");
let expandAll = this.panelDoc.getElementById("node-menu-expand");
let collapse = this.panelDoc.getElementById("node-menu-collapse");
expandAll.setAttribute("disabled", "true");
collapse.setAttribute("disabled", "true");
if (this.selection.isNode() && markupContainer.hasChildren) {
if (markupContainer.expanded) {
collapse.removeAttribute("disabled");
}
expandAll.removeAttribute("disabled");
}
this._target.actorHasMethod("domwalker", "duplicateNode").then(value => {
duplicateNode.hidden = !value;
});
this._target.actorHasMethod("domnode", "scrollIntoView").then(value => {
scrollIntoView.hidden = !value;
});
if (isDuplicatableElement) {
duplicateNode.removeAttribute("disabled");
}
else {
duplicateNode.setAttribute("disabled", "true");
}
if (isSelectionElement) {
unique.removeAttribute("disabled");
copyInnerHTML.removeAttribute("disabled");
copyOuterHTML.removeAttribute("disabled");
scrollIntoView.removeAttribute("disabled");
} else {
unique.setAttribute("disabled", "true");
copyInnerHTML.setAttribute("disabled", "true");
copyOuterHTML.setAttribute("disabled", "true");
scrollIntoView.setAttribute("disabled", "true");
}
if (!this.canGetUniqueSelector) {
unique.hidden = true;
}
if (isScreenshotable) {
screenshot.removeAttribute("disabled");
} else {
screenshot.setAttribute("disabled", "true");
}
// Enable/Disable the link open/copy items.
this._setupNodeLinkMenu();
// Enable the "edit HTML" item if the selection is an element and the root
// actor has the appropriate trait (isOuterHTMLEditable)
let editHTML = this.panelDoc.getElementById("node-menu-edithtml");
if (isEditableElement && this.isOuterHTMLEditable) {
editHTML.removeAttribute("disabled");
} else {
editHTML.setAttribute("disabled", "true");
}
let pasteOuterHTML = this.panelDoc.getElementById("node-menu-pasteouterhtml");
let pasteInnerHTML = this.panelDoc.getElementById("node-menu-pasteinnerhtml");
let pasteBefore = this.panelDoc.getElementById("node-menu-pastebefore");
let pasteAfter = this.panelDoc.getElementById("node-menu-pasteafter");
let pasteFirstChild = this.panelDoc.getElementById("node-menu-pastefirstchild");
let pasteLastChild = this.panelDoc.getElementById("node-menu-pastelastchild");
// Is the clipboard content appropriate? Is the element editable?
if (isEditableElement && this._getClipboardContentForPaste()) {
pasteInnerHTML.disabled = !this.canPasteInnerOrAdjacentHTML;
// Enable the "paste outer HTML" item if the selection is an element and
// the root actor has the appropriate trait (isOuterHTMLEditable).
pasteOuterHTML.disabled = !this.isOuterHTMLEditable;
// Don't paste before / after a root or a BODY or a HEAD element.
pasteBefore.disabled = pasteAfter.disabled =
!this.canPasteInnerOrAdjacentHTML || this.selection.isRoot() ||
this.selection.isBodyNode() || this.selection.isHeadNode();
// Don't paste as a first / last child of a HTML document element.
pasteFirstChild.disabled = pasteLastChild.disabled =
!this.canPasteInnerOrAdjacentHTML || (this.selection.isHTMLNode() &&
this.selection.isRoot());
} else {
pasteOuterHTML.disabled = true;
pasteInnerHTML.disabled = true;
pasteBefore.disabled = true;
pasteAfter.disabled = true;
pasteFirstChild.disabled = true;
pasteLastChild.disabled = true;
}
// Enable the "copy image data-uri" item if the selection is previewable
// which essentially checks if it's an image or canvas tag
let copyImageData = this.panelDoc.getElementById("node-menu-copyimagedatauri");
if (isSelectionElement && markupContainer && markupContainer.isPreviewable()) {
copyImageData.removeAttribute("disabled");
} else {
copyImageData.setAttribute("disabled", "true");
}
// Enable / disable "Add Attribute", "Edit Attribute"
// and "Remove Attribute" items
this._setupAttributeMenu(isEditableElement);
},
_setupAttributeMenu: function(isEditableElement) {
let addAttribute = this.panelDoc.getElementById("node-menu-add-attribute");
let editAttribute = this.panelDoc.getElementById("node-menu-edit-attribute");
let removeAttribute = this.panelDoc.getElementById("node-menu-remove-attribute");
let nodeInfo = this.nodeMenuTriggerInfo;
// Enable "Add Attribute" for all editable elements
if (isEditableElement) {
addAttribute.removeAttribute("disabled");
} else {
addAttribute.setAttribute("disabled", "true");
}
// Enable "Edit Attribute" and "Remove Attribute" only on attribute click
if (isEditableElement && nodeInfo && nodeInfo.type === "attribute") {
editAttribute.removeAttribute("disabled");
editAttribute.setAttribute("label",
strings.formatStringFromName(
"inspector.menu.editAttribute.label", [`"${nodeInfo.name}"`], 1));
removeAttribute.removeAttribute("disabled");
removeAttribute.setAttribute("label",
strings.formatStringFromName(
"inspector.menu.removeAttribute.label", [`"${nodeInfo.name}"`], 1));
} else {
editAttribute.setAttribute("disabled", "true");
editAttribute.setAttribute("label",
strings.formatStringFromName(
"inspector.menu.editAttribute.label", [''], 1));
removeAttribute.setAttribute("disabled", "true");
removeAttribute.setAttribute("label",
strings.formatStringFromName(
"inspector.menu.removeAttribute.label", [''], 1));
}
},
_resetNodeMenu: function() {
// Remove any extra items
while (this.lastNodemenuItem.nextSibling) {
let toDelete = this.lastNodemenuItem.nextSibling;
toDelete.parentNode.removeChild(toDelete);
}
},
/**
* Link menu items can be shown or hidden depending on the context and
* selected node, and their labels can vary.
*/
_setupNodeLinkMenu: function() {
let linkSeparator = this.panelDoc.getElementById("node-menu-link-separator");
let linkFollow = this.panelDoc.getElementById("node-menu-link-follow");
let linkCopy = this.panelDoc.getElementById("node-menu-link-copy");
// Hide all by default.
linkSeparator.setAttribute("hidden", "true");
linkFollow.setAttribute("hidden", "true");
linkCopy.setAttribute("hidden", "true");
// Get information about the right-clicked node.
let popupNode = this.panelDoc.popupNode;
if (!popupNode || !popupNode.classList.contains("link")) {
return;
}
let type = popupNode.dataset.type;
if (type === "uri" || type === "cssresource" || type === "jsresource") {
// First make sure the target can resolve relative URLs.
this.target.actorHasMethod("inspector", "resolveRelativeURL").then(canResolve => {
if (!canResolve) {
return;
}
linkSeparator.removeAttribute("hidden");
// Links can't be opened in new tabs in the browser toolbox.
if (type === "uri" && !this.target.chrome) {
linkFollow.removeAttribute("hidden");
linkFollow.setAttribute("label", strings.GetStringFromName(
"inspector.menu.openUrlInNewTab.label"));
} else if (type === "cssresource") {
linkFollow.removeAttribute("hidden");
linkFollow.setAttribute("label", toolboxStrings.GetStringFromName(
"toolbox.viewCssSourceInStyleEditor.label"));
} else if (type === "jsresource") {
linkFollow.removeAttribute("hidden");
linkFollow.setAttribute("label", toolboxStrings.GetStringFromName(
"toolbox.viewJsSourceInDebugger.label"));
}
linkCopy.removeAttribute("hidden");
linkCopy.setAttribute("label", strings.GetStringFromName(
"inspector.menu.copyUrlToClipboard.label"));
}, console.error);
} else if (type === "idref") {
linkSeparator.removeAttribute("hidden");
linkFollow.removeAttribute("hidden");
linkFollow.setAttribute("label", strings.formatStringFromName(
"inspector.menu.selectElement.label", [popupNode.dataset.link], 1));
}
},
_initMarkup: function() {
let doc = this.panelDoc;
this._markupBox = doc.getElementById("markup-box");
// create tool iframe
this._markupFrame = doc.createElement("iframe");
this._markupFrame.setAttribute("flex", "1");
this._markupFrame.setAttribute("tooltip", "aHTMLTooltip");
this._markupFrame.setAttribute("context", "inspector-node-popup");
// This is needed to enable tooltips inside the iframe document.
this._markupFrame.addEventListener("load", this._onMarkupFrameLoad, true);
this._markupBox.setAttribute("collapsed", true);
this._markupBox.appendChild(this._markupFrame);
this._markupFrame.setAttribute("src", "chrome://devtools/content/inspector/markup/markup.xhtml");
this._markupFrame.setAttribute("aria-label", strings.GetStringFromName("inspector.panelLabel.markupView"));
},
_onMarkupFrameLoad: function() {
this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
this._markupFrame.contentWindow.focus();
this._markupBox.removeAttribute("collapsed");
this.markup = new MarkupView(this, this._markupFrame, this._toolbox.win);
this.emit("markuploaded");
},
_destroyMarkup: function() {
let destroyPromise;
if (this._markupFrame) {
this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
}
if (this.markup) {
destroyPromise = this.markup.destroy();
this.markup = null;
} else {
destroyPromise = promise.resolve();
}
if (this._markupFrame) {
this._markupFrame.parentNode.removeChild(this._markupFrame);
this._markupFrame = null;
}
this._markupBox = null;
return destroyPromise;
},
/**
* When the pane toggle button is clicked, toggle the pane, change the button
* state and tooltip.
*/
onPaneToggleButtonClicked: function(e) {
let sidePane = this.panelDoc.querySelector("#inspector-sidebar");
let button = this._paneToggleButton;
let isVisible = !button.hasAttribute("pane-collapsed");
// Make sure the sidebar has width and height attributes before collapsing
// because ViewHelpers needs it.
if (isVisible) {
let rect = sidePane.getBoundingClientRect();
if (!sidePane.hasAttribute("width")) {
sidePane.setAttribute("width", rect.width);
}
// always refresh the height attribute before collapsing, it could have
// been modified by resizing the container.
sidePane.setAttribute("height", rect.height);
}
ViewHelpers.togglePane({
visible: !isVisible,
animated: true,
delayed: true
}, sidePane);
if (isVisible) {
button.setAttribute("pane-collapsed", "");
button.setAttribute("tooltiptext", strings.GetStringFromName("inspector.expandPane"));
} else {
button.removeAttribute("pane-collapsed");
button.setAttribute("tooltiptext", strings.GetStringFromName("inspector.collapsePane"));
}
},
/**
* Create a new node as the last child of the current selection, expand the
* parent and select the new node.
*/
addNode: Task.async(function* () {
if (!this.canAddHTMLChild()) {
return;
}
let html = "<div></div>";
// Insert the html and expect a childList markup mutation.
let onMutations = this.once("markupmutation");
let {nodes} = yield this.walker.insertAdjacentHTML(this.selection.nodeFront,
"beforeEnd", html);
yield onMutations;
// Select the new node (this will auto-expand its parent).
this.selection.setNodeFront(nodes[0], "node-inserted");
}),
/**
* Toggle a pseudo class.
*/
togglePseudoClass: function(aPseudo) {
if (this.selection.isElementNode()) {
let node = this.selection.nodeFront;
if (node.hasPseudoClassLock(aPseudo)) {
return this.walker.removePseudoClassLock(node, aPseudo, {parents: true});
}
let hierarchical = aPseudo == ":hover" || aPseudo == ":active";
return this.walker.addPseudoClassLock(node, aPseudo, {parents: hierarchical});
}
},
/**
* Show DOM properties
*/
showDOMProperties: function() {
this._toolbox.openSplitConsole().then(() => {
let panel = this._toolbox.getPanel("webconsole");
let jsterm = panel.hud.jsterm;
jsterm.execute("inspect($0)");
jsterm.focus();
});
},
/**
* Use in Console.
*
* Takes the currently selected node in the inspector and assigns it to a
* temp variable on the content window. Also opens the split console and
* autofills it with the temp variable.
*/
useInConsole: function() {
this._toolbox.openSplitConsole().then(() => {
let panel = this._toolbox.getPanel("webconsole");
let jsterm = panel.hud.jsterm;
let evalString = `{ let i = 0;
while (window.hasOwnProperty("temp" + i) && i < 1000) {
i++;
}
window["temp" + i] = $0;
"temp" + i;
}`;
let options = {
selectedNodeActor: this.selection.nodeFront.actorID,
};
jsterm.requestEvaluation(evalString, options).then((res) => {
jsterm.setInputValue(res.result);
this.emit("console-var-ready");
});
});
},
/**
* Clear any pseudo-class locks applied to the current hierarchy.
*/
clearPseudoClasses: function() {
if (!this.walker) {
return;
}
return this.walker.clearPseudoClassLocks().then(null, console.error);
},
/**
* Edit the outerHTML of the selected Node.
*/
editHTML: function() {
if (!this.selection.isNode()) {
return;
}
if (this.markup) {
this.markup.beginEditingOuterHTML(this.selection.nodeFront);
}
},
/**
* Paste the contents of the clipboard into the selected Node's outer HTML.
*/
pasteOuterHTML: function() {
let content = this._getClipboardContentForPaste();
if (!content)
return promise.reject("No clipboard content for paste");
let node = this.selection.nodeFront;
return this.markup.getNodeOuterHTML(node).then(oldContent => {
this.markup.updateNodeOuterHTML(node, content, oldContent);
});
},
/**
* Paste the contents of the clipboard into the selected Node's inner HTML.
*/
pasteInnerHTML: function() {
let content = this._getClipboardContentForPaste();
if (!content)
return promise.reject("No clipboard content for paste");
let node = this.selection.nodeFront;
return this.markup.getNodeInnerHTML(node).then(oldContent => {
this.markup.updateNodeInnerHTML(node, content, oldContent);
});
},
/**
* Paste the contents of the clipboard as adjacent HTML to the selected Node.
* @param position The position as specified for Element.insertAdjacentHTML
* (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
*/
pasteAdjacentHTML: function(position) {
let content = this._getClipboardContentForPaste();
if (!content)
return promise.reject("No clipboard content for paste");
let node = this.selection.nodeFront;
return this.markup.insertAdjacentHTMLToNode(node, position, content);
},
/**
* Copy the innerHTML of the selected Node to the clipboard.
*/
copyInnerHTML: function() {
if (!this.selection.isNode()) {
return;
}
this._copyLongString(this.walker.innerHTML(this.selection.nodeFront));
},
/**
* Copy the outerHTML of the selected Node to the clipboard.
*/
copyOuterHTML: function() {
if (!this.selection.isNode()) {
return;
}
let node = this.selection.nodeFront;
switch (node.nodeType) {
case Ci.nsIDOMNode.ELEMENT_NODE :
this._copyLongString(this.walker.outerHTML(node));
break;
case Ci.nsIDOMNode.COMMENT_NODE :
this._getLongString(node.getNodeValue()).then(comment => {
clipboardHelper.copyString("<!--" + comment + "-->");
});
break;
case Ci.nsIDOMNode.DOCUMENT_TYPE_NODE :
clipboardHelper.copyString(node.doctypeString);
break;
}
},
/**
* Copy the data-uri for the currently selected image in the clipboard.
*/
copyImageDataUri: function() {
let container = this.markup.getContainer(this.selection.nodeFront);
if (container && container.isPreviewable()) {
container.copyImageDataUri();
}
},
/**
* Copy the content of a longString (via a promise resolving a LongStringActor) to the clipboard
* @param {Promise} longStringActorPromise promise expected to resolve a LongStringActor instance
* @return {Promise} promise resolving (with no argument) when the string is sent to the clipboard
*/
_copyLongString: function(longStringActorPromise) {
return this._getLongString(longStringActorPromise).then(string => {
clipboardHelper.copyString(string);
}).catch(e => console.error(e));
},
/**
* Retrieve the content of a longString (via a promise resolving a LongStringActor)
* @param {Promise} longStringActorPromise promise expected to resolve a LongStringActor instance
* @return {Promise} promise resolving with the retrieved string as argument
*/
_getLongString: function(longStringActorPromise) {
return longStringActorPromise.then(longStringActor => {
return longStringActor.string().then(string => {
longStringActor.release().catch(e => console.error(e));
return string;
});
}).catch(e => console.error(e));
},
/**
* Copy a unique selector of the selected Node to the clipboard.
*/
copyUniqueSelector: function() {
if (!this.selection.isNode()) {
return;
}
this.selection.nodeFront.getUniqueSelector().then((selector) => {
clipboardHelper.copyString(selector);
}).then(null, console.error);
},
/**
* Initiate gcli screenshot command on selected node
*/
screenshotNode: function() {
CommandUtils.createRequisition(this._target, {
environment: CommandUtils.createEnvironment(this, '_target')
}).then(requisition => {
// Bug 1180314 - CssSelector might contain white space so need to make sure it is
// passed to screenshot as a single parameter. More work *might* be needed if
// CssSelector could contain escaped single- or double-quotes, backslashes, etc.
requisition.updateExec("screenshot --selector '" + this.selectionCssSelector + "'");
});
},
/**
* Scroll the node into view.
*/
scrollNodeIntoView: function() {
if (!this.selection.isNode()) {
return;
}
this.selection.nodeFront.scrollIntoView();
},
/**
* Duplicate the selected node
*/
duplicateNode: function() {
let selection = this.selection;
if (!selection.isElementNode() ||
selection.isRoot() ||
selection.isAnonymousNode() ||
selection.isPseudoElementNode()) {
return;
}
this.walker.duplicateNode(selection.nodeFront).catch(e => console.error(e));
},
/**
* Delete the selected node.
*/
deleteNode: function() {
if (!this.selection.isNode() ||
this.selection.isRoot()) {
return;
}
// If the markup panel is active, use the markup panel to delete
// the node, making this an undoable action.
if (this.markup) {
this.markup.deleteNode(this.selection.nodeFront);
} else {
// remove the node from content
this.walker.removeNode(this.selection.nodeFront);
}
},
/**
* Add attribute to node.
* Used for node context menu and shouldn't be called directly.
*/
onAddAttribute: function() {
let container = this.markup.getContainer(this.selection.nodeFront);
container.addAttribute();
},
/**
* Edit attribute for node.
* Used for node context menu and shouldn't be called directly.
*/
onEditAttribute: function() {
let container = this.markup.getContainer(this.selection.nodeFront);
container.editAttribute(this.nodeMenuTriggerInfo.name);
},
/**
* Remove attribute from node.
* Used for node context menu and shouldn't be called directly.
*/
onRemoveAttribute: function() {
let container = this.markup.getContainer(this.selection.nodeFront);
container.removeAttribute(this.nodeMenuTriggerInfo.name);
},
expandNode: function() {
this.markup.expandAll(this.selection.nodeFront);
},
collapseNode: function() {
this.markup.collapseNode(this.selection.nodeFront);
},
/**
* This method is here for the benefit of the node-menu-link-follow menu item
* in the inspector contextual-menu.
*/
onFollowLink: function() {
let type = this.panelDoc.popupNode.dataset.type;
let link = this.panelDoc.popupNode.dataset.link;
this.followAttributeLink(type, link);
},
/**
* Given a type and link found in a node's attribute in the markup-view,
* attempt to follow that link (which may result in opening a new tab, the
* style editor or debugger).
*/
followAttributeLink: function(type, link) {
if (!type || !link) {
return;
}
if (type === "uri" || type === "cssresource" || type === "jsresource") {
// Open link in a new tab.
// When the inspector menu was setup on click (see _setupNodeLinkMenu), we
// already checked that resolveRelativeURL existed.
this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
if (type === "uri") {
let browserWin = this.target.tab.ownerDocument.defaultView;
browserWin.openUILinkIn(url, "tab");
} else if (type === "cssresource") {
return this.toolbox.viewSourceInStyleEditor(url);
} else if (type === "jsresource") {
return this.toolbox.viewSourceInDebugger(url);
}
}).catch(e => console.error(e));
} else if (type == "idref") {
// Select the node in the same document.
this.walker.document(this.selection.nodeFront).then(doc => {
return this.walker.querySelector(doc, "#" + CSS.escape(link)).then(node => {
if (!node) {
this.emit("idref-attribute-link-failed");
return;
}
this.selection.setNodeFront(node);
});
}).catch(e => console.error(e));
}
},
/**
* This method is here for the benefit of the node-menu-link-copy menu item
* in the inspector contextual-menu.
*/
onCopyLink: function() {
let link = this.panelDoc.popupNode.dataset.link;
this.copyAttributeLink(link);
},
/**
* This method is here for the benefit of copying links.
*/
copyAttributeLink: function(link) {
// When the inspector menu was setup on click (see _setupNodeLinkMenu), we
// already checked that resolveRelativeURL existed.
this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
clipboardHelper.copyString(url);
}, console.error);
}
};