diff options
Diffstat (limited to 'devtools/client/inspector/inspector.js')
-rw-r--r-- | devtools/client/inspector/inspector.js | 1936 |
1 files changed, 1936 insertions, 0 deletions
diff --git a/devtools/client/inspector/inspector.js b/devtools/client/inspector/inspector.js new file mode 100644 index 000000000..c056c213f --- /dev/null +++ b/devtools/client/inspector/inspector.js @@ -0,0 +1,1936 @@ +/* -*- 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/. */ + +/* global window */ + +"use strict"; + +var Cu = Components.utils; +var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +var Services = require("Services"); +var promise = require("promise"); +var defer = require("devtools/shared/defer"); +var EventEmitter = require("devtools/shared/event-emitter"); +const {executeSoon} = require("devtools/shared/DevToolsUtils"); +var {KeyShortcuts} = require("devtools/client/shared/key-shortcuts"); +var {Task} = require("devtools/shared/task"); +const {initCssProperties} = require("devtools/shared/fronts/css-properties"); +const nodeConstants = require("devtools/shared/dom-node-constants"); +const Telemetry = require("devtools/client/shared/telemetry"); + +const Menu = require("devtools/client/framework/menu"); +const MenuItem = require("devtools/client/framework/menu-item"); + +const {CommandUtils} = require("devtools/client/shared/developer-toolbar"); +const {ComputedViewTool} = require("devtools/client/inspector/computed/computed"); +const {FontInspector} = require("devtools/client/inspector/fonts/fonts"); +const {HTMLBreadcrumbs} = require("devtools/client/inspector/breadcrumbs"); +const {InspectorSearch} = require("devtools/client/inspector/inspector-search"); +const MarkupView = require("devtools/client/inspector/markup/markup"); +const {RuleViewTool} = require("devtools/client/inspector/rules/rules"); +const {ToolSidebar} = require("devtools/client/inspector/toolsidebar"); +const {ViewHelpers} = require("devtools/client/shared/widgets/view-helpers"); +const clipboardHelper = require("devtools/shared/platform/clipboard"); + +const {LocalizationHelper, localizeMarkup} = require("devtools/shared/l10n"); +const INSPECTOR_L10N = + new LocalizationHelper("devtools/client/locales/inspector.properties"); +const TOOLBOX_L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties"); + +// Sidebar dimensions +const INITIAL_SIDEBAR_SIZE = 350; + +// If the toolbox width is smaller than given amount of pixels, +// the sidebar automatically switches from 'landscape' to 'portrait' mode. +const PORTRAIT_MODE_WIDTH = 700; + +/** + * 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 animation inspector). + * + * 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 + * - boxmodel-view-updated + * Fired when the 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) + * - computed-view-filtered + * Fired when the computed rules view is filtered + * - 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 Inspector(toolbox) { + this._toolbox = toolbox; + this._target = toolbox.target; + this.panelDoc = window.document; + this.panelWin = window; + this.panelWin.inspector = this; + + this.telemetry = new Telemetry(); + + this.nodeMenuTriggerInfo = null; + + this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this); + this._onBeforeNavigate = this._onBeforeNavigate.bind(this); + this.onNewRoot = this.onNewRoot.bind(this); + this._onContextMenu = this._onContextMenu.bind(this); + this.onTextBoxContextMenu = this.onTextBoxContextMenu.bind(this); + this._updateSearchResultsLabel = this._updateSearchResultsLabel.bind(this); + this.onNewSelection = this.onNewSelection.bind(this); + this.onDetached = this.onDetached.bind(this); + this.onPaneToggleButtonClicked = this.onPaneToggleButtonClicked.bind(this); + this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this); + this.onPanelWindowResize = this.onPanelWindowResize.bind(this); + this.onSidebarShown = this.onSidebarShown.bind(this); + this.onSidebarHidden = this.onSidebarHidden.bind(this); + + this._target.on("will-navigate", this._onBeforeNavigate); + this._detectingActorFeatures = this._detectActorFeatures(); + + EventEmitter.decorate(this); +} + +Inspector.prototype = { + /** + * open is effectively an asynchronous constructor + */ + init: Task.async(function* () { + // Localize all the nodes containing a data-localization attribute. + localizeMarkup(this.panelDoc); + + this._cssPropertiesLoaded = initCssProperties(this.toolbox); + yield this._cssPropertiesLoaded; + yield this.target.makeRemote(); + yield this._getPageStyle(); + + // This may throw if the document is still loading and we are + // refering to a dead about:blank document + let defaultSelection = yield this._getDefaultNodeForSelection() + .catch(this._handleRejectionIfNotDestroyed); + + return yield this._deferredOpen(defaultSelection); + }), + + get toolbox() { + return this._toolbox; + }, + + get inspector() { + return this._toolbox.inspector; + }, + + get walker() { + return this._toolbox.walker; + }, + + get selection() { + return this._toolbox.selection; + }, + + get highlighter() { + return this._toolbox.highlighter; + }, + + 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; + }, + + /** + * Handle promise rejections for various asynchronous actions, and only log errors if + * the inspector panel still exists. + * This is useful to silence useless errors that happen when the inspector is closed + * while still initializing (and making protocol requests). + */ + _handleRejectionIfNotDestroyed: function (e) { + if (!this._panelDestroyer) { + console.error(e); + } + }, + + /** + * Figure out what features the backend supports + */ + _detectActorFeatures: function () { + this._supportsDuplicateNode = false; + this._supportsScrollIntoView = false; + this._supportsResolveRelativeURL = false; + + // Use getActorDescription first so that all actorHasMethod calls use + // a cached response from the server. + return this._target.getActorDescription("domwalker").then(desc => { + return promise.all([ + this._target.actorHasMethod("domwalker", "duplicateNode").then(value => { + this._supportsDuplicateNode = value; + }).catch(e => console.error(e)), + this._target.actorHasMethod("domnode", "scrollIntoView").then(value => { + this._supportsScrollIntoView = value; + }).catch(e => console.error(e)), + this._target.actorHasMethod("inspector", "resolveRelativeURL").then(value => { + this._supportsResolveRelativeURL = value; + }).catch(e => console.error(e)), + ]); + }); + }, + + _deferredOpen: function (defaultSelection) { + let deferred = defer(); + + this.breadcrumbs = new HTMLBreadcrumbs(this); + + this.walker.on("new-root", this.onNewRoot); + + this.selection.on("new-node-front", this.onNewSelection); + this.selection.on("detached-front", this.onDetached); + + 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 = INSPECTOR_L10N.getStr("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. + if (defaultSelection) { + this.selection.setNodeFront(defaultSelection, "inspector-open"); + this.markup.expandNode(this.selection.nodeFront); + } + + // And setup the toolbar only now because it may depend on the document. + this.setupToolbar(); + + 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.inspector.getPageStyle().then(pageStyle => { + this.pageStyle = pageStyle; + }, this._handleRejectionIfNotDestroyed); + }, + + /** + * 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(node => { + if (hasNavigated()) { + return promise.reject("navigated; resolution of _defaultNode aborted"); + } + + rootNode = node; + if (this.selectionCssSelector) { + return walker.querySelector(rootNode, this.selectionCssSelector); + } + return null; + }).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.searchClearButton = this.panelDoc.getElementById("inspector-searchinput-clear"); + this.searchResultsLabel = this.panelDoc.getElementById("inspector-searchlabel"); + + this.search = new InspectorSearch(this, this.searchBox, this.searchClearButton); + this.search.on("search-cleared", this._updateSearchResultsLabel); + this.search.on("search-result", this._updateSearchResultsLabel); + + let shortcuts = new KeyShortcuts({ + window: this.panelDoc.defaultView, + }); + let key = INSPECTOR_L10N.getStr("inspector.searchHTML.key"); + shortcuts.on(key, (name, event) => { + // Prevent overriding same shortcut from the computed/rule views + if (event.target.closest("#sidebar-panel-ruleview") || + event.target.closest("#sidebar-panel-computedview")) { + return; + } + event.preventDefault(); + this.searchBox.focus(); + }); + }, + + get searchSuggestions() { + return this.search.autocompleter; + }, + + _updateSearchResultsLabel: function (event, result) { + let str = ""; + if (event !== "search-cleared") { + if (result) { + str = INSPECTOR_L10N.getFormatStr( + "inspector.searchResultsCount2", result.resultsIndex + 1, result.resultsLength); + } else { + str = INSPECTOR_L10N.getStr("inspector.searchResultsNone"); + } + } + + this.searchResultsLabel.textContent = str; + }, + + get React() { + return this._toolbox.React; + }, + + get ReactDOM() { + return this._toolbox.ReactDOM; + }, + + get ReactRedux() { + return this._toolbox.ReactRedux; + }, + + get browserRequire() { + return this._toolbox.browserRequire; + }, + + get InspectorTabPanel() { + if (!this._InspectorTabPanel) { + this._InspectorTabPanel = + this.React.createFactory(this.browserRequire( + "devtools/client/inspector/components/inspector-tab-panel")); + } + return this._InspectorTabPanel; + }, + + /** + * Check if the inspector should use the landscape mode. + * + * @return {Boolean} true if the inspector should be in landscape mode. + */ + useLandscapeMode: function () { + let { clientWidth } = this.panelDoc.getElementById("inspector-splitter-box"); + return clientWidth > PORTRAIT_MODE_WIDTH; + }, + + /** + * Build Splitter located between the main and side area of + * the Inspector panel. + */ + setupSplitter: function () { + let SplitBox = this.React.createFactory(this.browserRequire( + "devtools/client/shared/components/splitter/split-box")); + + let splitter = SplitBox({ + className: "inspector-sidebar-splitter", + initialWidth: INITIAL_SIDEBAR_SIZE, + initialHeight: INITIAL_SIDEBAR_SIZE, + splitterSize: 1, + endPanelControl: true, + startPanel: this.InspectorTabPanel({ + id: "inspector-main-content" + }), + endPanel: this.InspectorTabPanel({ + id: "inspector-sidebar-container" + }), + vert: this.useLandscapeMode(), + }); + + this._splitter = this.ReactDOM.render(splitter, + this.panelDoc.getElementById("inspector-splitter-box")); + + this.panelWin.addEventListener("resize", this.onPanelWindowResize, true); + + // Persist splitter state in preferences. + this.sidebar.on("show", this.onSidebarShown); + this.sidebar.on("hide", this.onSidebarHidden); + this.sidebar.on("destroy", this.onSidebarHidden); + }, + + /** + * Splitter clean up. + */ + teardownSplitter: function () { + this.panelWin.removeEventListener("resize", this.onPanelWindowResize, true); + + this.sidebar.off("show", this.onSidebarShown); + this.sidebar.off("hide", this.onSidebarHidden); + this.sidebar.off("destroy", this.onSidebarHidden); + }, + + /** + * If Toolbox width is less than 600 px, the splitter changes its mode + * to `horizontal` to support portrait view. + */ + onPanelWindowResize: function () { + this._splitter.setState({ + vert: this.useLandscapeMode(), + }); + }, + + onSidebarShown: function () { + let width; + let height; + + // Initialize splitter size from preferences. + try { + width = Services.prefs.getIntPref("devtools.toolsidebar-width.inspector"); + height = Services.prefs.getIntPref("devtools.toolsidebar-height.inspector"); + } catch (e) { + // Set width and height of the splitter. Only one + // value is really useful at a time depending on the current + // orientation (vertical/horizontal). + // Having both is supported by the splitter component. + width = INITIAL_SIDEBAR_SIZE; + height = INITIAL_SIDEBAR_SIZE; + } + + this._splitter.setState({width, height}); + }, + + onSidebarHidden: function () { + // Store the current splitter size to preferences. + let state = this._splitter.state; + Services.prefs.setIntPref("devtools.toolsidebar-width.inspector", state.width); + Services.prefs.setIntPref("devtools.toolsidebar-height.inspector", state.height); + }, + + /** + * 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"); + + this._setDefaultSidebar = (event, toolId) => { + Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId); + }; + + this.sidebar.on("select", this._setDefaultSidebar); + + if (!Services.prefs.getBoolPref("devtools.fontinspector.enabled") && + defaultTab == "fontinspector") { + defaultTab = "ruleview"; + } + + // Append all side panels + this.sidebar.addExistingTab( + "ruleview", + INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"), + defaultTab == "ruleview"); + + this.sidebar.addExistingTab( + "computedview", + INSPECTOR_L10N.getStr("inspector.sidebar.computedViewTitle"), + defaultTab == "computedview"); + + this.ruleview = new RuleViewTool(this, this.panelWin); + this.computedview = new ComputedViewTool(this, this.panelWin); + + if (Services.prefs.getBoolPref("devtools.layoutview.enabled")) { + const {LayoutView} = this.browserRequire("devtools/client/inspector/layout/layout"); + this.layoutview = new LayoutView(this, this.panelWin); + } + + if (this.target.form.animationsActor) { + this.sidebar.addFrameTab( + "animationinspector", + INSPECTOR_L10N.getStr("inspector.sidebar.animationInspectorTitle"), + "chrome://devtools/content/animationinspector/animation-inspector.xhtml", + defaultTab == "animationinspector"); + } + + if (Services.prefs.getBoolPref("devtools.fontinspector.enabled") && + this.canGetUsedFontFaces) { + this.sidebar.addExistingTab( + "fontinspector", + INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle"), + defaultTab == "fontinspector"); + + this.fontInspector = new FontInspector(this, this.panelWin); + this.sidebar.toggleTab(true, "fontinspector"); + } + + // Setup the splitter before the sidebar is displayed so, + // we don't miss any events. + this.setupSplitter(); + + this.sidebar.show(defaultTab); + }, + + /** + * Register a side-panel tab. This API can be used outside of + * DevTools (e.g. from an extension) as well as by DevTools + * code base. + * + * @param {string} tab uniq id + * @param {string} title tab title + * @param {React.Component} panel component. See `InspectorPanelTab` as an example. + * @param {boolean} selected true if the panel should be selected + */ + addSidebarTab: function (id, title, panel, selected) { + this.sidebar.addTab(id, title, panel, selected); + }, + + setupToolbar: function () { + this.teardownToolbar(); + + // Setup the sidebar toggle button. + let SidebarToggle = this.React.createFactory(this.browserRequire( + "devtools/client/shared/components/sidebar-toggle")); + + let sidebarToggle = SidebarToggle({ + onClick: this.onPaneToggleButtonClicked, + collapsed: false, + expandPaneTitle: INSPECTOR_L10N.getStr("inspector.expandPane"), + collapsePaneTitle: INSPECTOR_L10N.getStr("inspector.collapsePane"), + }); + + let parentBox = this.panelDoc.getElementById("inspector-sidebar-toggle-box"); + this._sidebarToggle = this.ReactDOM.render(sidebarToggle, parentBox); + + // Setup the add-node button. + this.addNode = this.addNode.bind(this); + this.addNodeButton = this.panelDoc.getElementById("inspector-element-add-button"); + this.addNodeButton.addEventListener("click", this.addNode); + + // Setup the eye-dropper icon if we're in an HTML document and we have actor support. + if (this.selection.nodeFront && this.selection.nodeFront.isInHTMLDocument) { + this.target.actorHasMethod("inspector", "pickColorFromPage").then(value => { + if (!value) { + return; + } + + this.onEyeDropperDone = this.onEyeDropperDone.bind(this); + this.onEyeDropperButtonClicked = this.onEyeDropperButtonClicked.bind(this); + this.eyeDropperButton = this.panelDoc + .getElementById("inspector-eyedropper-toggle"); + this.eyeDropperButton.disabled = false; + this.eyeDropperButton.title = INSPECTOR_L10N.getStr("inspector.eyedropper.label"); + this.eyeDropperButton.addEventListener("click", this.onEyeDropperButtonClicked); + }, e => console.error(e)); + } else { + let eyeDropperButton = this.panelDoc.getElementById("inspector-eyedropper-toggle"); + eyeDropperButton.disabled = true; + eyeDropperButton.title = INSPECTOR_L10N.getStr("eyedropper.disabled.title"); + } + }, + + teardownToolbar: function () { + this._sidebarToggle = null; + + if (this.addNodeButton) { + this.addNodeButton.removeEventListener("click", this.addNode); + this.addNodeButton = null; + } + + if (this.eyeDropperButton) { + this.eyeDropperButton.removeEventListener("click", this.onEyeDropperButtonClicked); + this.eyeDropperButton = null; + } + }, + + /** + * 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"); + }); + + // Setup the toolbar again, since its content may depend on the current document. + this.setupToolbar(); + }; + this._pendingSelection = onNodeSelected; + this._getDefaultNodeForSelection() + .then(onNodeSelected, this._handleRejectionIfNotDestroyed); + }, + + _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; + } + 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; + }, this._handleRejectionIfNotDestroyed); + } + + 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 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.layoutview) { + this.layoutview.destroy(); + } + + if (this.fontInspector) { + this.fontInspector.destroy(); + } + + let cssPropertiesDestroyer = this._cssPropertiesLoaded.then(({front}) => { + if (front) { + front.destroy(); + } + }); + + this.sidebar.off("select", this._setDefaultSidebar); + let sidebarDestroyer = this.sidebar.destroy(); + + this.teardownSplitter(); + + this.sidebar = null; + + this.teardownToolbar(); + this.breadcrumbs.destroy(); + this.selection.off("new-node-front", this.onNewSelection); + 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._toolbox = null; + this.search.destroy(); + this.search = null; + this.searchBox = null; + + this._panelDestroyer = promise.all([ + sidebarDestroyer, + markupDestroyer, + cssPropertiesDestroyer + ]); + + return this._panelDestroyer; + }, + + /** + * Returns the clipboard content if it is appropriate for pasting + * into the current node's outer HTML, otherwise returns null. + */ + _getClipboardContentForPaste: function () { + let flavors = clipboardHelper.getCurrentFlavors(); + if (flavors.indexOf("text") != -1 || + (flavors.indexOf("html") != -1 && flavors.indexOf("image") == -1)) { + let content = clipboardHelper.getData(); + if (content && content.trim().length > 0) { + return content; + } + } + return null; + }, + + _onContextMenu: function (e) { + e.preventDefault(); + this._openMenu({ + screenX: e.screenX, + screenY: e.screenY, + target: e.target, + }); + }, + + /** + * This is meant to be called by all the search, filter, inplace text boxes in the + * inspector, and just calls through to the toolbox openTextBoxContextMenu helper. + * @param {DOMEvent} e + */ + onTextBoxContextMenu: function (e) { + e.stopPropagation(); + e.preventDefault(); + this.toolbox.openTextBoxContextMenu(e.screenX, e.screenY); + }, + + _openMenu: function ({ target, screenX = 0, screenY = 0 } = { }) { + let markupContainer = this.markup.getContainer(this.selection.nodeFront); + + this.contextMenuTarget = target; + this.nodeMenuTriggerInfo = markupContainer && + markupContainer.editor.getInfoAtNode(target); + + 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; + + let menu = new Menu(); + menu.append(new MenuItem({ + id: "node-menu-edithtml", + label: INSPECTOR_L10N.getStr("inspectorHTMLEdit.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorHTMLEdit.accesskey"), + disabled: !isEditableElement || !this.isOuterHTMLEditable, + click: () => this.editHTML(), + })); + menu.append(new MenuItem({ + id: "node-menu-add", + label: INSPECTOR_L10N.getStr("inspectorAddNode.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorAddNode.accesskey"), + disabled: !this.canAddHTMLChild(), + click: () => this.addNode(), + })); + menu.append(new MenuItem({ + id: "node-menu-duplicatenode", + label: INSPECTOR_L10N.getStr("inspectorDuplicateNode.label"), + hidden: !this._supportsDuplicateNode, + disabled: !isDuplicatableElement, + click: () => this.duplicateNode(), + })); + menu.append(new MenuItem({ + id: "node-menu-delete", + label: INSPECTOR_L10N.getStr("inspectorHTMLDelete.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorHTMLDelete.accesskey"), + disabled: !isEditableElement, + click: () => this.deleteNode(), + })); + + menu.append(new MenuItem({ + label: INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.label"), + accesskey: + INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.accesskey"), + submenu: this._getAttributesSubmenu(isEditableElement), + })); + + menu.append(new MenuItem({ + type: "separator", + })); + + // Set the pseudo classes + for (let name of ["hover", "active", "focus"]) { + let menuitem = new MenuItem({ + id: "node-menu-pseudo-" + name, + label: name, + type: "checkbox", + click: this.togglePseudoClass.bind(this, ":" + name), + }); + + if (isSelectionElement) { + let checked = this.selection.nodeFront.hasPseudoClassLock(":" + name); + menuitem.checked = checked; + } else { + menuitem.disabled = true; + } + + menu.append(menuitem); + } + + menu.append(new MenuItem({ + type: "separator", + })); + + let copySubmenu = new Menu(); + copySubmenu.append(new MenuItem({ + id: "node-menu-copyinner", + label: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.accesskey"), + disabled: !isSelectionElement, + click: () => this.copyInnerHTML(), + })); + copySubmenu.append(new MenuItem({ + id: "node-menu-copyouter", + label: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.accesskey"), + disabled: !isSelectionElement, + click: () => this.copyOuterHTML(), + })); + copySubmenu.append(new MenuItem({ + id: "node-menu-copyuniqueselector", + label: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.label"), + accesskey: + INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.accesskey"), + disabled: !isSelectionElement, + hidden: !this.canGetUniqueSelector, + click: () => this.copyUniqueSelector(), + })); + copySubmenu.append(new MenuItem({ + id: "node-menu-copyimagedatauri", + label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"), + disabled: !isSelectionElement || !markupContainer || + !markupContainer.isPreviewable(), + click: () => this.copyImageDataUri(), + })); + + menu.append(new MenuItem({ + label: INSPECTOR_L10N.getStr("inspectorCopyHTMLSubmenu.label"), + submenu: copySubmenu, + })); + + menu.append(new MenuItem({ + label: INSPECTOR_L10N.getStr("inspectorPasteHTMLSubmenu.label"), + submenu: this._getPasteSubmenu(isEditableElement), + })); + + menu.append(new MenuItem({ + type: "separator", + })); + + let isNodeWithChildren = this.selection.isNode() && + markupContainer.hasChildren; + menu.append(new MenuItem({ + id: "node-menu-expand", + label: INSPECTOR_L10N.getStr("inspectorExpandNode.label"), + disabled: !isNodeWithChildren, + click: () => this.expandNode(), + })); + menu.append(new MenuItem({ + id: "node-menu-collapse", + label: INSPECTOR_L10N.getStr("inspectorCollapseNode.label"), + disabled: !isNodeWithChildren || !markupContainer.expanded, + click: () => this.collapseNode(), + })); + + menu.append(new MenuItem({ + type: "separator", + })); + + menu.append(new MenuItem({ + id: "node-menu-scrollnodeintoview", + label: INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.label"), + accesskey: + INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.accesskey"), + hidden: !this._supportsScrollIntoView, + disabled: !isSelectionElement, + click: () => this.scrollNodeIntoView(), + })); + menu.append(new MenuItem({ + id: "node-menu-screenshotnode", + label: INSPECTOR_L10N.getStr("inspectorScreenshotNode.label"), + disabled: !isScreenshotable, + click: () => this.screenshotNode(), + })); + menu.append(new MenuItem({ + id: "node-menu-useinconsole", + label: INSPECTOR_L10N.getStr("inspectorUseInConsole.label"), + click: () => this.useInConsole(), + })); + menu.append(new MenuItem({ + id: "node-menu-showdomproperties", + label: INSPECTOR_L10N.getStr("inspectorShowDOMProperties.label"), + click: () => this.showDOMProperties(), + })); + + let nodeLinkMenuItems = this._getNodeLinkMenuItems(); + if (nodeLinkMenuItems.filter(item => item.visible).length > 0) { + menu.append(new MenuItem({ + id: "node-menu-link-separator", + type: "separator", + })); + } + + for (let menuitem of nodeLinkMenuItems) { + menu.append(menuitem); + } + + menu.popup(screenX, screenY, this._toolbox); + return menu; + }, + + _getPasteSubmenu: function (isEditableElement) { + let isPasteable = isEditableElement && this._getClipboardContentForPaste(); + let disableAdjacentPaste = !isPasteable || + !this.canPasteInnerOrAdjacentHTML || this.selection.isRoot() || + this.selection.isBodyNode() || this.selection.isHeadNode(); + let disableFirstLastPaste = !isPasteable || + !this.canPasteInnerOrAdjacentHTML || (this.selection.isHTMLNode() && + this.selection.isRoot()); + + let pasteSubmenu = new Menu(); + pasteSubmenu.append(new MenuItem({ + id: "node-menu-pasteinnerhtml", + label: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.accesskey"), + disabled: !isPasteable || !this.canPasteInnerOrAdjacentHTML, + click: () => this.pasteInnerHTML(), + })); + pasteSubmenu.append(new MenuItem({ + id: "node-menu-pasteouterhtml", + label: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.accesskey"), + disabled: !isPasteable || !this.isOuterHTMLEditable, + click: () => this.pasteOuterHTML(), + })); + pasteSubmenu.append(new MenuItem({ + id: "node-menu-pastebefore", + label: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.label"), + accesskey: + INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.accesskey"), + disabled: disableAdjacentPaste, + click: () => this.pasteAdjacentHTML("beforeBegin"), + })); + pasteSubmenu.append(new MenuItem({ + id: "node-menu-pasteafter", + label: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.label"), + accesskey: + INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.accesskey"), + disabled: disableAdjacentPaste, + click: () => this.pasteAdjacentHTML("afterEnd"), + })); + pasteSubmenu.append(new MenuItem({ + id: "node-menu-pastefirstchild", + label: INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.label"), + accesskey: + INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.accesskey"), + disabled: disableFirstLastPaste, + click: () => this.pasteAdjacentHTML("afterBegin"), + })); + pasteSubmenu.append(new MenuItem({ + id: "node-menu-pastelastchild", + label: INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.label"), + accesskey: + INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.accesskey"), + disabled: disableFirstLastPaste, + click: () => this.pasteAdjacentHTML("beforeEnd"), + })); + + return pasteSubmenu; + }, + + _getAttributesSubmenu: function (isEditableElement) { + let attributesSubmenu = new Menu(); + let nodeInfo = this.nodeMenuTriggerInfo; + let isAttributeClicked = isEditableElement && nodeInfo && + nodeInfo.type === "attribute"; + + attributesSubmenu.append(new MenuItem({ + id: "node-menu-add-attribute", + label: INSPECTOR_L10N.getStr("inspectorAddAttribute.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorAddAttribute.accesskey"), + disabled: !isEditableElement, + click: () => this.onAddAttribute(), + })); + attributesSubmenu.append(new MenuItem({ + id: "node-menu-edit-attribute", + label: INSPECTOR_L10N.getFormatStr("inspectorEditAttribute.label", + isAttributeClicked ? `"${nodeInfo.name}"` : ""), + accesskey: INSPECTOR_L10N.getStr("inspectorEditAttribute.accesskey"), + disabled: !isAttributeClicked, + click: () => this.onEditAttribute(), + })); + + attributesSubmenu.append(new MenuItem({ + id: "node-menu-remove-attribute", + label: INSPECTOR_L10N.getFormatStr("inspectorRemoveAttribute.label", + isAttributeClicked ? `"${nodeInfo.name}"` : ""), + accesskey: + INSPECTOR_L10N.getStr("inspectorRemoveAttribute.accesskey"), + disabled: !isAttributeClicked, + click: () => this.onRemoveAttribute(), + })); + + return attributesSubmenu; + }, + + /** + * Link menu items can be shown or hidden depending on the context and + * selected node, and their labels can vary. + * + * @return {Array} list of visible menu items related to links. + */ + _getNodeLinkMenuItems: function () { + let linkFollow = new MenuItem({ + id: "node-menu-link-follow", + visible: false, + click: () => this.onFollowLink(), + }); + let linkCopy = new MenuItem({ + id: "node-menu-link-copy", + visible: false, + click: () => this.onCopyLink(), + }); + + // Get information about the right-clicked node. + let popupNode = this.contextMenuTarget; + if (!popupNode || !popupNode.classList.contains("link")) { + return [linkFollow, linkCopy]; + } + + let type = popupNode.dataset.type; + if (this._supportsResolveRelativeURL && + (type === "uri" || type === "cssresource" || type === "jsresource")) { + // Links can't be opened in new tabs in the browser toolbox. + if (type === "uri" && !this.target.chrome) { + linkFollow.visible = true; + linkFollow.label = INSPECTOR_L10N.getStr( + "inspector.menu.openUrlInNewTab.label"); + } else if (type === "cssresource") { + linkFollow.visible = true; + linkFollow.label = TOOLBOX_L10N.getStr( + "toolbox.viewCssSourceInStyleEditor.label"); + } else if (type === "jsresource") { + linkFollow.visible = true; + linkFollow.label = TOOLBOX_L10N.getStr( + "toolbox.viewJsSourceInDebugger.label"); + } + + linkCopy.visible = true; + linkCopy.label = INSPECTOR_L10N.getStr( + "inspector.menu.copyUrlToClipboard.label"); + } else if (type === "idref") { + linkFollow.visible = true; + linkFollow.label = INSPECTOR_L10N.getFormatStr( + "inspector.menu.selectElement.label", popupNode.dataset.link); + } + + return [linkFollow, linkCopy]; + }, + + _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.addEventListener("contextmenu", this._onContextMenu); + + // 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", + INSPECTOR_L10N.getStr("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); + this._markupFrame.removeEventListener("contextmenu", this._onContextMenu); + } + + 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 or pressed, toggle the pane, change the button + * state and tooltip. + */ + onPaneToggleButtonClicked: function (e) { + let sidePaneContainer = this.panelDoc.querySelector( + "#inspector-splitter-box .controlled"); + let isVisible = !this._sidebarToggle.state.collapsed; + + // Make sure the sidebar has width and height attributes before collapsing + // because ViewHelpers needs it. + if (isVisible) { + let rect = sidePaneContainer.getBoundingClientRect(); + if (!sidePaneContainer.hasAttribute("width")) { + sidePaneContainer.setAttribute("width", rect.width); + } + // always refresh the height attribute before collapsing, it could have + // been modified by resizing the container. + sidePaneContainer.setAttribute("height", rect.height); + } + + let onAnimationDone = () => { + if (isVisible) { + this._sidebarToggle.setState({collapsed: true}); + } else { + this._sidebarToggle.setState({collapsed: false}); + } + }; + + ViewHelpers.togglePane({ + visible: !isVisible, + animated: true, + delayed: true, + callback: onAnimationDone + }, sidePaneContainer); + }, + + onEyeDropperButtonClicked: function () { + this.eyeDropperButton.hasAttribute("checked") + ? this.hideEyeDropper() + : this.showEyeDropper(); + }, + + startEyeDropperListeners: function () { + this.inspector.once("color-pick-canceled", this.onEyeDropperDone); + this.inspector.once("color-picked", this.onEyeDropperDone); + this.walker.once("new-root", this.onEyeDropperDone); + }, + + stopEyeDropperListeners: function () { + this.inspector.off("color-pick-canceled", this.onEyeDropperDone); + this.inspector.off("color-picked", this.onEyeDropperDone); + this.walker.off("new-root", this.onEyeDropperDone); + }, + + onEyeDropperDone: function () { + this.eyeDropperButton.removeAttribute("checked"); + this.stopEyeDropperListeners(); + }, + + /** + * Show the eyedropper on the page. + * @return {Promise} resolves when the eyedropper is visible. + */ + showEyeDropper: function () { + // The eyedropper button doesn't exist, most probably because the actor doesn't + // support the pickColorFromPage, or because the page isn't HTML. + if (!this.eyeDropperButton) { + return null; + } + + this.telemetry.toolOpened("toolbareyedropper"); + this.eyeDropperButton.setAttribute("checked", "true"); + this.startEyeDropperListeners(); + return this.inspector.pickColorFromPage(this.toolbox, {copyOnSelect: true}) + .catch(e => console.error(e)); + }, + + /** + * Hide the eyedropper. + * @return {Promise} resolves when the eyedropper is hidden. + */ + hideEyeDropper: function () { + // The eyedropper button doesn't exist, most probably because the actor doesn't + // support the pickColorFromPage, or because the page isn't HTML. + if (!this.eyeDropperButton) { + return null; + } + + this.eyeDropperButton.removeAttribute("checked"); + this.stopEyeDropperListeners(); + return this.inspector.cancelPickColorFromPage() + .catch(e => console.error(e)); + }, + + /** + * 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 (pseudo) { + if (this.selection.isElementNode()) { + let node = this.selection.nodeFront; + if (node.hasPseudoClassLock(pseudo)) { + return this.walker.removePseudoClassLock(node, pseudo, {parents: true}); + } + + let hierarchical = pseudo == ":hover" || pseudo == ":active"; + return this.walker.addPseudoClassLock(node, pseudo, {parents: hierarchical}); + } + return promise.resolve(); + }, + + /** + * 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"); + }); + }); + }, + + /** + * 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 nodeConstants.ELEMENT_NODE : + this._copyLongString(this.walker.outerHTML(node)); + break; + case nodeConstants.COMMENT_NODE : + this._getLongString(node.getNodeValue()).then(comment => { + clipboardHelper.copyString("<!--" + comment + "-->"); + }); + break; + case nodeConstants.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.contextMenuTarget.dataset.type; + let link = this.contextMenuTarget.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 _getNodeLinkMenuItems), 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); + } + return null; + }).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.contextMenuTarget.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 _getNodeLinkMenuItems), we + // already checked that resolveRelativeURL existed. + this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => { + clipboardHelper.copyString(url); + }, console.error); + } +}; + +// URL constructor doesn't support chrome: scheme +let href = window.location.href.replace(/chrome:/, "http://"); +let url = new window.URL(href); + +// Only use this method to attach the toolbox if some query parameters are given +if (url.search.length > 1) { + const { targetFromURL } = require("devtools/client/framework/target-from-url"); + const { attachThread } = require("devtools/client/framework/attach-thread"); + const { BrowserLoader } = + Cu.import("resource://devtools/client/shared/browser-loader.js", {}); + + const { Selection } = require("devtools/client/framework/selection"); + const { InspectorFront } = require("devtools/shared/fronts/inspector"); + const { getHighlighterUtils } = require("devtools/client/framework/toolbox-highlighter-utils"); + + Task.spawn(function* () { + let target = yield targetFromURL(url); + + let notImplemented = function () { + throw new Error("Not implemented in a tab"); + }; + let fakeToolbox = { + target, + hostType: "bottom", + doc: window.document, + win: window, + on() {}, emit() {}, off() {}, + initInspector() {}, + browserRequire: BrowserLoader({ + window: window, + useOnlyShared: true + }).require, + get React() { + return this.browserRequire("devtools/client/shared/vendor/react"); + }, + get ReactDOM() { + return this.browserRequire("devtools/client/shared/vendor/react-dom"); + }, + isToolRegistered() { + return false; + }, + currentToolId: "inspector", + getCurrentPanel() { + return "inspector"; + }, + get textboxContextMenuPopup() { + notImplemented(); + }, + getPanel: notImplemented, + openSplitConsole: notImplemented, + viewCssSourceInStyleEditor: notImplemented, + viewJsSourceInDebugger: notImplemented, + viewSource: notImplemented, + viewSourceInDebugger: notImplemented, + viewSourceInStyleEditor: notImplemented, + + // For attachThread: + highlightTool() {}, + unhighlightTool() {}, + selectTool() {}, + raise() {}, + getNotificationBox() {} + }; + + // attachThread also expect a toolbox as argument + fakeToolbox.threadClient = yield attachThread(fakeToolbox); + + let inspector = InspectorFront(target.client, target.form); + let showAllAnonymousContent = + Services.prefs.getBoolPref("devtools.inspector.showAllAnonymousContent"); + let walker = yield inspector.getWalker({ showAllAnonymousContent }); + let selection = new Selection(walker); + let highlighter = yield inspector.getHighlighter(false); + + fakeToolbox.inspector = inspector; + fakeToolbox.walker = walker; + fakeToolbox.selection = selection; + fakeToolbox.highlighter = highlighter; + fakeToolbox.highlighterUtils = getHighlighterUtils(fakeToolbox); + + let inspectorUI = new Inspector(fakeToolbox); + inspectorUI.init(); + }).then(null, e => { + window.alert("Unable to start the inspector:" + e.message + "\n" + e.stack); + }); +} |