summaryrefslogtreecommitdiff
path: root/toolkit/devtools/inspector/inspector-panel.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/devtools/inspector/inspector-panel.js')
-rw-r--r--toolkit/devtools/inspector/inspector-panel.js1012
1 files changed, 1012 insertions, 0 deletions
diff --git a/toolkit/devtools/inspector/inspector-panel.js b/toolkit/devtools/inspector/inspector-panel.js
new file mode 100644
index 000000000..c396042a3
--- /dev/null
+++ b/toolkit/devtools/inspector/inspector-panel.js
@@ -0,0 +1,1012 @@
+/* -*- 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("resource://gre/modules/Promise.jsm").Promise;
+let EventEmitter = require("devtools/toolkit/event-emitter");
+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
+ * - 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._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;
+ },
+
+ 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.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;
+ 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(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", {
+ showAllTabsMenu: true
+ });
+
+ 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.canGetUsedFontFaces) {
+ 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);
+
+ if (this.target.form.animationsActor) {
+ this.sidebar.addTab("animationinspector",
+ "chrome://browser/content/devtools/animationinspector/animation-inspector.xhtml",
+ "animationinspector" == defaultTab);
+ }
+
+ 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.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");
+ 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);
+ 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.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);
+ let markupDestroyer = 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;
+
+ this._panelDestroyer = promise.all([
+ sidebarDestroyer,
+ markupDestroyer
+ ]);
+
+ 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.
+ */
+ _getClipboardContentForPaste: function Inspector_getClipboardContentForPaste() {
+ 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() &&
+ !this.selection.isPseudoElementNode();
+ let isEditableElement = isSelectionElement &&
+ !this.selection.isAnonymousNode();
+
+ // 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 / 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");
+ }
+ if (!this.canGetUniqueSelector) {
+ unique.hidden = 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 (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");
+ let markupContainer = this.markup.getContainer(this.selection.nodeFront);
+ if (isSelectionElement && 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});
+ }
+ },
+
+ /**
+ * Show DOM properties
+ */
+ showDOMProperties: function InspectorPanel_showDOMProperties() {
+ this._toolbox.openSplitConsole().then(() => {
+ let panel = this._toolbox.getPanel("webconsole");
+ let jsterm = panel.hud.jsterm;
+
+ jsterm.execute("inspect($0)");
+ jsterm.inputNode.focus();
+ });
+ },
+
+ /**
+ * 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._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 InspectorPanel_pasteInnerHTML() {
+ 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 InspectorPanel_pasteAdjacent(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 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;
+ }
+
+ this.selection.nodeFront.getUniqueSelector().then((selector) => {
+ clipboardHelper.copyString(selector);
+ }).then(null, console.error);
+ },
+
+ /**
+ * 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);
+});