mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
910 lines
28 KiB
JavaScript
910 lines
28 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/. */
|
|
|
|
const {Cc, Ci, Cu, Cr} = require("chrome");
|
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
|
|
let promise = require("devtools/toolkit/deprecated-sync-thenables");
|
|
let EventEmitter = require("devtools/toolkit/event-emitter");
|
|
let {CssLogic} = require("devtools/styleinspector/css-logic");
|
|
let clipboard = require("sdk/clipboard");
|
|
|
|
loader.lazyGetter(this, "MarkupView", () => require("devtools/markupview/markup-view").MarkupView);
|
|
loader.lazyGetter(this, "HTMLBreadcrumbs", () => require("devtools/inspector/breadcrumbs").HTMLBreadcrumbs);
|
|
loader.lazyGetter(this, "ToolSidebar", () => require("devtools/framework/sidebar").ToolSidebar);
|
|
loader.lazyGetter(this, "SelectorSearch", () => require("devtools/inspector/selector-search").SelectorSearch);
|
|
|
|
const LAYOUT_CHANGE_TIMER = 250;
|
|
|
|
/**
|
|
* 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
|
|
* - layout-change
|
|
* Fired when the layout of the inspector changes
|
|
* - 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
|
|
* - rule-view-refreshed
|
|
* Fired when the rule view updates to a new node
|
|
*/
|
|
function InspectorPanel(iframeWindow, toolbox) {
|
|
this._toolbox = toolbox;
|
|
this._target = toolbox._target;
|
|
this.panelDoc = iframeWindow.document;
|
|
this.panelWin = iframeWindow;
|
|
this.panelWin.inspector = this;
|
|
this._inspector = null;
|
|
|
|
this._onBeforeNavigate = this._onBeforeNavigate.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 InspectorPanel_open() {
|
|
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;
|
|
},
|
|
|
|
_deferredOpen: function(defaultSelection) {
|
|
let deferred = promise.defer();
|
|
|
|
this.onNewRoot = this.onNewRoot.bind(this);
|
|
this.walker.on("new-root", this.onNewRoot);
|
|
|
|
this.nodemenu = this.panelDoc.getElementById("inspector-node-popup");
|
|
this.lastNodemenuItem = this.nodemenu.lastChild;
|
|
this._setupNodeMenu = this._setupNodeMenu.bind(this);
|
|
this._resetNodeMenu = this._resetNodeMenu.bind(this);
|
|
this.nodemenu.addEventListener("popupshowing", this._setupNodeMenu, true);
|
|
this.nodemenu.addEventListener("popuphiding", this._resetNodeMenu, true);
|
|
|
|
this.onNewSelection = this.onNewSelection.bind(this);
|
|
this.selection.on("new-node-front", this.onNewSelection);
|
|
this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this);
|
|
this.selection.on("before-new-node-front", this.onBeforeNewSelection);
|
|
this.onDetached = this.onDetached.bind(this);
|
|
this.selection.on("detached-front", this.onDetached);
|
|
|
|
this.breadcrumbs = new HTMLBreadcrumbs(this);
|
|
|
|
if (this.target.isLocalTab) {
|
|
this.browser = this.target.tab.linkedBrowser;
|
|
this.scheduleLayoutChange = this.scheduleLayoutChange.bind(this);
|
|
this.browser.addEventListener("resize", this.scheduleLayoutChange, true);
|
|
|
|
// 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.target.isThreadPaused) {
|
|
let message = this.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.target.isThreadPaused) {
|
|
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;
|
|
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(this.walker.rootNode);
|
|
}).then(node => {
|
|
if (walker !== this.walker) {
|
|
promise.reject(null);
|
|
}
|
|
this._defaultNode = node;
|
|
return node;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Target getter.
|
|
*/
|
|
get target() {
|
|
return this._target;
|
|
},
|
|
|
|
/**
|
|
* Target setter.
|
|
*/
|
|
set target(value) {
|
|
this._target = value;
|
|
},
|
|
|
|
/**
|
|
* Expose gViewSourceUtils so that other tools can make use of them.
|
|
*/
|
|
get viewSourceUtils() {
|
|
return this.panelWin.gViewSourceUtils;
|
|
},
|
|
|
|
/**
|
|
* 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 InspectorPanel_markDirty() {
|
|
this.isDirty = true;
|
|
},
|
|
|
|
/**
|
|
* Hooks the searchbar to show result and auto completion suggestions.
|
|
*/
|
|
setupSearchBox: function InspectorPanel_setupSearchBox() {
|
|
// Initiate the selectors search object.
|
|
if (this.searchSuggestions) {
|
|
this.searchSuggestions.destroy();
|
|
this.searchSuggestions = null;
|
|
}
|
|
this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
|
|
this.searchSuggestions = new SelectorSearch(this, this.searchBox);
|
|
},
|
|
|
|
/**
|
|
* Build the sidebar.
|
|
*/
|
|
setupSidebar: function InspectorPanel_setupSidebar() {
|
|
let tabbox = this.panelDoc.querySelector("#inspector-sidebar");
|
|
this.sidebar = new ToolSidebar(tabbox, this, "inspector");
|
|
|
|
let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");
|
|
|
|
this._setDefaultSidebar = (event, toolId) => {
|
|
Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
|
|
};
|
|
|
|
this.sidebar.on("select", this._setDefaultSidebar);
|
|
|
|
this.sidebar.addTab("ruleview",
|
|
"chrome://browser/content/devtools/cssruleview.xhtml",
|
|
"ruleview" == defaultTab);
|
|
|
|
this.sidebar.addTab("computedview",
|
|
"chrome://browser/content/devtools/computedview.xhtml",
|
|
"computedview" == defaultTab);
|
|
|
|
if (Services.prefs.getBoolPref("devtools.fontinspector.enabled") && !this.target.isRemote) {
|
|
this.sidebar.addTab("fontinspector",
|
|
"chrome://browser/content/devtools/fontinspector/font-inspector.xhtml",
|
|
"fontinspector" == defaultTab);
|
|
}
|
|
|
|
this.sidebar.addTab("layoutview",
|
|
"chrome://browser/content/devtools/layoutview/view.xhtml",
|
|
"layoutview" == defaultTab);
|
|
|
|
let ruleViewTab = this.sidebar.getTab("ruleview");
|
|
|
|
this.sidebar.show();
|
|
},
|
|
|
|
/**
|
|
* Reset the inspector on new root mutation.
|
|
*/
|
|
onNewRoot: function InspectorPanel_onNewRoot() {
|
|
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.setupSearchBox();
|
|
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) {
|
|
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;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* When a new node is selected.
|
|
*/
|
|
onNewSelection: function InspectorPanel_onNewSelection(event, value, reason) {
|
|
if (reason === "selection-destroy") {
|
|
return;
|
|
}
|
|
|
|
this.cancelLayoutChange();
|
|
|
|
// Wait for all the known tools to finish updating and then let the
|
|
// client know.
|
|
let selection = this.selection.nodeFront;
|
|
|
|
// 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 (reason !== "navigateaway" &&
|
|
this.selection.node &&
|
|
this.selection.isElementNode()) {
|
|
this.selectionCssSelector = CssLogic.findCssSelector(this.selection.node);
|
|
}
|
|
|
|
let selfUpdate = this.updating("inspector-panel");
|
|
Services.tm.mainThread.dispatch(() => {
|
|
try {
|
|
selfUpdate(selection);
|
|
} catch(ex) {
|
|
console.error(ex);
|
|
}
|
|
}, Ci.nsIThread.DISPATCH_NORMAL);
|
|
},
|
|
|
|
/**
|
|
* 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.
|
|
var self = this;
|
|
this._updateProgress = {
|
|
node: this.selection.nodeFront,
|
|
outstanding: new Set(),
|
|
checkDone: function() {
|
|
if (this !== self._updateProgress) {
|
|
return;
|
|
}
|
|
if (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 InspectorPanel_onBeforeNewSelection(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 InspectorPanel_onDetached(event, parentNode) {
|
|
this.cancelLayoutChange();
|
|
this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode));
|
|
this.selection.setNodeFront(parentNode ? parentNode : this._defaultNode, "detached");
|
|
},
|
|
|
|
/**
|
|
* Destroy the inspector.
|
|
*/
|
|
destroy: function InspectorPanel__destroy() {
|
|
if (this._panelDestroyer) {
|
|
return this._panelDestroyer;
|
|
}
|
|
|
|
if (this.walker) {
|
|
this.walker.off("new-root", this.onNewRoot);
|
|
this.pageStyle = null;
|
|
}
|
|
|
|
this.cancelUpdate();
|
|
this.cancelLayoutChange();
|
|
|
|
if (this.browser) {
|
|
this.browser.removeEventListener("resize", this.scheduleLayoutChange, true);
|
|
this.browser = null;
|
|
}
|
|
|
|
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);
|
|
|
|
this.sidebar.off("select", this._setDefaultSidebar);
|
|
this.sidebar.destroy();
|
|
this.sidebar = null;
|
|
|
|
this.nodemenu.removeEventListener("popupshowing", this._setupNodeMenu, true);
|
|
this.nodemenu.removeEventListener("popuphiding", this._resetNodeMenu, true);
|
|
this.breadcrumbs.destroy();
|
|
this.searchSuggestions.destroy();
|
|
this.searchBox = 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);
|
|
this._panelDestroyer = this._destroyMarkup();
|
|
this.panelWin.inspector = null;
|
|
this.target = null;
|
|
this.panelDoc = null;
|
|
this.panelWin = null;
|
|
this.breadcrumbs = null;
|
|
this.searchSuggestions = null;
|
|
this.lastNodemenuItem = null;
|
|
this.nodemenu = null;
|
|
this._toolbox = null;
|
|
|
|
return this._panelDestroyer;
|
|
},
|
|
|
|
/**
|
|
* Show the node menu.
|
|
*/
|
|
showNodeMenu: function InspectorPanel_showNodeMenu(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 InspectorPanel_hideNodeMenu() {
|
|
this.nodemenu.hidePopup();
|
|
},
|
|
|
|
/**
|
|
* Returns the clipboard content if it is appropriate for pasting
|
|
* into the current node's outer HTML, otherwise returns null.
|
|
*/
|
|
_getClipboardContentForOuterHTML: function Inspector_getClipboardContentForOuterHTML() {
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* Disable the delete item if needed. Update the pseudo classes.
|
|
*/
|
|
_setupNodeMenu: function InspectorPanel_setupNodeMenu() {
|
|
let isSelectionElement = this.selection.isElementNode();
|
|
|
|
// 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 (this.selection.isRoot() || this.selection.isDocumentTypeNode()) {
|
|
deleteNode.setAttribute("disabled", "true");
|
|
} else {
|
|
deleteNode.removeAttribute("disabled");
|
|
}
|
|
|
|
// Disable / enable "Copy Unique Selector", "Copy inner HTML" &
|
|
// "Copy outer HTML" as appropriate
|
|
let unique = this.panelDoc.getElementById("node-menu-copyuniqueselector");
|
|
let copyInnerHTML = this.panelDoc.getElementById("node-menu-copyinner");
|
|
let copyOuterHTML = this.panelDoc.getElementById("node-menu-copyouter");
|
|
if (isSelectionElement) {
|
|
unique.removeAttribute("disabled");
|
|
copyInnerHTML.removeAttribute("disabled");
|
|
copyOuterHTML.removeAttribute("disabled");
|
|
} else {
|
|
unique.setAttribute("disabled", "true");
|
|
copyInnerHTML.setAttribute("disabled", "true");
|
|
copyOuterHTML.setAttribute("disabled", "true");
|
|
}
|
|
|
|
// 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 (this.isOuterHTMLEditable && isSelectionElement) {
|
|
editHTML.removeAttribute("disabled");
|
|
} else {
|
|
editHTML.setAttribute("disabled", "true");
|
|
}
|
|
|
|
// Enable the "paste outer HTML" item if the selection is an element and
|
|
// the root actor has the appropriate trait (isOuterHTMLEditable) and if
|
|
// the clipbard content is appropriate.
|
|
let pasteOuterHTML = this.panelDoc.getElementById("node-menu-pasteouterhtml");
|
|
if (this.isOuterHTMLEditable && isSelectionElement &&
|
|
this._getClipboardContentForOuterHTML()) {
|
|
pasteOuterHTML.removeAttribute("disabled");
|
|
} else {
|
|
pasteOuterHTML.setAttribute("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");
|
|
let markupContainer = this.markup.getContainer(this.selection.nodeFront);
|
|
if (markupContainer && markupContainer.isPreviewable()) {
|
|
copyImageData.removeAttribute("disabled");
|
|
} else {
|
|
copyImageData.setAttribute("disabled", "true");
|
|
}
|
|
},
|
|
|
|
_resetNodeMenu: function InspectorPanel_resetNodeMenu() {
|
|
// Remove any extra items
|
|
while (this.lastNodemenuItem.nextSibling) {
|
|
let toDelete = this.lastNodemenuItem.nextSibling;
|
|
toDelete.parentNode.removeChild(toDelete);
|
|
}
|
|
},
|
|
|
|
_initMarkup: function InspectorPanel_initMarkup() {
|
|
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._boundMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
|
|
this._markupFrame.addEventListener("load", this._boundMarkupFrameLoad, true);
|
|
|
|
this._markupBox.setAttribute("collapsed", true);
|
|
this._markupBox.appendChild(this._markupFrame);
|
|
this._markupFrame.setAttribute("src", "chrome://browser/content/devtools/markup-view.xhtml");
|
|
this._markupFrame.setAttribute("aria-label", this.strings.GetStringFromName("inspector.panelLabel.markupView"))
|
|
},
|
|
|
|
_onMarkupFrameLoad: function InspectorPanel__onMarkupFrameLoad() {
|
|
this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true);
|
|
delete this._boundMarkupFrameLoad;
|
|
|
|
this._markupFrame.contentWindow.focus();
|
|
|
|
this._markupBox.removeAttribute("collapsed");
|
|
|
|
let controllerWindow = this._toolbox.doc.defaultView;
|
|
this.markup = new MarkupView(this, this._markupFrame, controllerWindow);
|
|
|
|
this.emit("markuploaded");
|
|
},
|
|
|
|
_destroyMarkup: function InspectorPanel__destroyMarkup() {
|
|
let destroyPromise;
|
|
|
|
if (this._boundMarkupFrameLoad) {
|
|
this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true);
|
|
this._boundMarkupFrameLoad = null;
|
|
}
|
|
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* Toggle a pseudo class.
|
|
*/
|
|
togglePseudoClass: function InspectorPanel_togglePseudoClass(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});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Clear any pseudo-class locks applied to the current hierarchy.
|
|
*/
|
|
clearPseudoClasses: function InspectorPanel_clearPseudoClasses() {
|
|
if (!this.walker) {
|
|
return;
|
|
}
|
|
return this.walker.clearPseudoClassLocks().then(null, console.error);
|
|
},
|
|
|
|
/**
|
|
* Edit the outerHTML of the selected Node.
|
|
*/
|
|
editHTML: function InspectorPanel_editHTML()
|
|
{
|
|
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 InspectorPanel_pasteOuterHTML()
|
|
{
|
|
let content = this._getClipboardContentForOuterHTML();
|
|
if (content) {
|
|
let node = this.selection.nodeFront;
|
|
this.markup.getNodeOuterHTML(node).then((oldContent) => {
|
|
this.markup.updateNodeOuterHTML(node, content, oldContent);
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Copy the innerHTML of the selected Node to the clipboard.
|
|
*/
|
|
copyInnerHTML: function InspectorPanel_copyInnerHTML()
|
|
{
|
|
if (!this.selection.isNode()) {
|
|
return;
|
|
}
|
|
this._copyLongStr(this.walker.innerHTML(this.selection.nodeFront));
|
|
},
|
|
|
|
/**
|
|
* Copy the outerHTML of the selected Node to the clipboard.
|
|
*/
|
|
copyOuterHTML: function InspectorPanel_copyOuterHTML()
|
|
{
|
|
if (!this.selection.isNode()) {
|
|
return;
|
|
}
|
|
|
|
this._copyLongStr(this.walker.outerHTML(this.selection.nodeFront));
|
|
},
|
|
|
|
/**
|
|
* Copy the data-uri for the currently selected image in the clipboard.
|
|
*/
|
|
copyImageDataUri: function InspectorPanel_copyImageDataUri()
|
|
{
|
|
let container = this.markup.getContainer(this.selection.nodeFront);
|
|
if (container && container.isPreviewable()) {
|
|
container.copyImageDataUri();
|
|
}
|
|
},
|
|
|
|
_copyLongStr: function InspectorPanel_copyLongStr(promise)
|
|
{
|
|
return promise.then(longstr => {
|
|
return longstr.string().then(toCopy => {
|
|
longstr.release().then(null, console.error);
|
|
clipboardHelper.copyString(toCopy);
|
|
});
|
|
}).then(null, console.error);
|
|
},
|
|
|
|
/**
|
|
* Copy a unique selector of the selected Node to the clipboard.
|
|
*/
|
|
copyUniqueSelector: function InspectorPanel_copyUniqueSelector()
|
|
{
|
|
if (!this.selection.isNode()) {
|
|
return;
|
|
}
|
|
|
|
let toCopy = CssLogic.findCssSelector(this.selection.node);
|
|
if (toCopy) {
|
|
clipboardHelper.copyString(toCopy);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Delete the selected node.
|
|
*/
|
|
deleteNode: function IUI_deleteNode() {
|
|
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);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Trigger a high-priority layout change for things that need to be
|
|
* updated immediately
|
|
*/
|
|
immediateLayoutChange: function Inspector_immediateLayoutChange()
|
|
{
|
|
this.emit("layout-change");
|
|
},
|
|
|
|
/**
|
|
* Schedule a low-priority change event for things like paint
|
|
* and resize.
|
|
*/
|
|
scheduleLayoutChange: function Inspector_scheduleLayoutChange(event)
|
|
{
|
|
// Filter out non browser window resize events (i.e. triggered by iframes)
|
|
if (this.browser.contentWindow === event.target) {
|
|
if (this._timer) {
|
|
return null;
|
|
}
|
|
this._timer = this.panelWin.setTimeout(() => {
|
|
this.emit("layout-change");
|
|
this._timer = null;
|
|
}, LAYOUT_CHANGE_TIMER);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Cancel a pending low-priority change event if any is
|
|
* scheduled.
|
|
*/
|
|
cancelLayoutChange: function Inspector_cancelLayoutChange()
|
|
{
|
|
if (this._timer) {
|
|
this.panelWin.clearTimeout(this._timer);
|
|
delete this._timer;
|
|
}
|
|
}
|
|
};
|
|
|
|
/////////////////////////////////////////////////////////////////////////
|
|
//// Initializers
|
|
|
|
loader.lazyGetter(InspectorPanel.prototype, "strings",
|
|
function () {
|
|
return Services.strings.createBundle(
|
|
"chrome://browser/locale/devtools/inspector.properties");
|
|
});
|
|
|
|
loader.lazyGetter(this, "clipboardHelper", function() {
|
|
return Cc["@mozilla.org/widget/clipboardhelper;1"].
|
|
getService(Ci.nsIClipboardHelper);
|
|
});
|
|
|
|
|
|
loader.lazyGetter(this, "DOMUtils", function () {
|
|
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
|
|
});
|