michael@0: /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const {Cc, Ci, Cu, Cr} = require("chrome"); michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: let promise = require("devtools/toolkit/deprecated-sync-thenables"); michael@0: let EventEmitter = require("devtools/toolkit/event-emitter"); michael@0: let {CssLogic} = require("devtools/styleinspector/css-logic"); michael@0: michael@0: loader.lazyGetter(this, "MarkupView", () => require("devtools/markupview/markup-view").MarkupView); michael@0: loader.lazyGetter(this, "HTMLBreadcrumbs", () => require("devtools/inspector/breadcrumbs").HTMLBreadcrumbs); michael@0: loader.lazyGetter(this, "ToolSidebar", () => require("devtools/framework/sidebar").ToolSidebar); michael@0: loader.lazyGetter(this, "SelectorSearch", () => require("devtools/inspector/selector-search").SelectorSearch); michael@0: michael@0: const LAYOUT_CHANGE_TIMER = 250; michael@0: michael@0: /** michael@0: * Represents an open instance of the Inspector for a tab. michael@0: * The inspector controls the breadcrumbs, the markup view, and the sidebar michael@0: * (computed view, rule view, font view and layout view). michael@0: * michael@0: * Events: michael@0: * - ready michael@0: * Fired when the inspector panel is opened for the first time and ready to michael@0: * use michael@0: * - new-root michael@0: * Fired after a new root (navigation to a new page) event was fired by michael@0: * the walker, and taken into account by the inspector (after the markup michael@0: * view has been reloaded) michael@0: * - markuploaded michael@0: * Fired when the markup-view frame has loaded michael@0: * - layout-change michael@0: * Fired when the layout of the inspector changes michael@0: * - breadcrumbs-updated michael@0: * Fired when the breadcrumb widget updates to a new node michael@0: * - layoutview-updated michael@0: * Fired when the layoutview (box model) updates to a new node michael@0: * - markupmutation michael@0: * Fired after markup mutations have been processed by the markup-view michael@0: * - computed-view-refreshed michael@0: * Fired when the computed rules view updates to a new node michael@0: * - computed-view-property-expanded michael@0: * Fired when a property is expanded in the computed rules view michael@0: * - computed-view-property-collapsed michael@0: * Fired when a property is collapsed in the computed rules view michael@0: * - rule-view-refreshed michael@0: * Fired when the rule view updates to a new node michael@0: */ michael@0: function InspectorPanel(iframeWindow, toolbox) { michael@0: this._toolbox = toolbox; michael@0: this._target = toolbox._target; michael@0: this.panelDoc = iframeWindow.document; michael@0: this.panelWin = iframeWindow; michael@0: this.panelWin.inspector = this; michael@0: this._inspector = null; michael@0: michael@0: this._onBeforeNavigate = this._onBeforeNavigate.bind(this); michael@0: this._target.on("will-navigate", this._onBeforeNavigate); michael@0: michael@0: EventEmitter.decorate(this); michael@0: } michael@0: michael@0: exports.InspectorPanel = InspectorPanel; michael@0: michael@0: InspectorPanel.prototype = { michael@0: /** michael@0: * open is effectively an asynchronous constructor michael@0: */ michael@0: open: function InspectorPanel_open() { michael@0: return this.target.makeRemote().then(() => { michael@0: return this._getPageStyle(); michael@0: }).then(() => { michael@0: return this._getDefaultNodeForSelection(); michael@0: }).then(defaultSelection => { michael@0: return this._deferredOpen(defaultSelection); michael@0: }).then(null, console.error); michael@0: }, michael@0: michael@0: get toolbox() { michael@0: return this._toolbox; michael@0: }, michael@0: michael@0: get inspector() { michael@0: return this._toolbox.inspector; michael@0: }, michael@0: michael@0: get walker() { michael@0: return this._toolbox.walker; michael@0: }, michael@0: michael@0: get selection() { michael@0: return this._toolbox.selection; michael@0: }, michael@0: michael@0: get isOuterHTMLEditable() { michael@0: return this._target.client.traits.editOuterHTML; michael@0: }, michael@0: michael@0: get hasUrlToImageDataResolver() { michael@0: return this._target.client.traits.urlToImageDataResolver; michael@0: }, michael@0: michael@0: _deferredOpen: function(defaultSelection) { michael@0: let deferred = promise.defer(); michael@0: michael@0: this.onNewRoot = this.onNewRoot.bind(this); michael@0: this.walker.on("new-root", this.onNewRoot); michael@0: michael@0: this.nodemenu = this.panelDoc.getElementById("inspector-node-popup"); michael@0: this.lastNodemenuItem = this.nodemenu.lastChild; michael@0: this._setupNodeMenu = this._setupNodeMenu.bind(this); michael@0: this._resetNodeMenu = this._resetNodeMenu.bind(this); michael@0: this.nodemenu.addEventListener("popupshowing", this._setupNodeMenu, true); michael@0: this.nodemenu.addEventListener("popuphiding", this._resetNodeMenu, true); michael@0: michael@0: this.onNewSelection = this.onNewSelection.bind(this); michael@0: this.selection.on("new-node-front", this.onNewSelection); michael@0: this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this); michael@0: this.selection.on("before-new-node-front", this.onBeforeNewSelection); michael@0: this.onDetached = this.onDetached.bind(this); michael@0: this.selection.on("detached-front", this.onDetached); michael@0: michael@0: this.breadcrumbs = new HTMLBreadcrumbs(this); michael@0: michael@0: if (this.target.isLocalTab) { michael@0: this.browser = this.target.tab.linkedBrowser; michael@0: this.scheduleLayoutChange = this.scheduleLayoutChange.bind(this); michael@0: this.browser.addEventListener("resize", this.scheduleLayoutChange, true); michael@0: michael@0: // Show a warning when the debugger is paused. michael@0: // We show the warning only when the inspector michael@0: // is selected. michael@0: this.updateDebuggerPausedWarning = function() { michael@0: let notificationBox = this._toolbox.getNotificationBox(); michael@0: let notification = notificationBox.getNotificationWithValue("inspector-script-paused"); michael@0: if (!notification && this._toolbox.currentToolId == "inspector" && michael@0: this.target.isThreadPaused) { michael@0: let message = this.strings.GetStringFromName("debuggerPausedWarning.message"); michael@0: notificationBox.appendNotification(message, michael@0: "inspector-script-paused", "", notificationBox.PRIORITY_WARNING_HIGH); michael@0: } michael@0: michael@0: if (notification && this._toolbox.currentToolId != "inspector") { michael@0: notificationBox.removeNotification(notification); michael@0: } michael@0: michael@0: if (notification && !this.target.isThreadPaused) { michael@0: notificationBox.removeNotification(notification); michael@0: } michael@0: michael@0: }.bind(this); michael@0: this.target.on("thread-paused", this.updateDebuggerPausedWarning); michael@0: this.target.on("thread-resumed", this.updateDebuggerPausedWarning); michael@0: this._toolbox.on("select", this.updateDebuggerPausedWarning); michael@0: this.updateDebuggerPausedWarning(); michael@0: } michael@0: michael@0: this._initMarkup(); michael@0: this.isReady = false; michael@0: michael@0: this.once("markuploaded", function() { michael@0: this.isReady = true; michael@0: michael@0: // All the components are initialized. Let's select a node. michael@0: this.selection.setNodeFront(defaultSelection, "inspector-open"); michael@0: michael@0: this.markup.expandNode(this.selection.nodeFront); michael@0: michael@0: this.emit("ready"); michael@0: deferred.resolve(this); michael@0: }.bind(this)); michael@0: michael@0: this.setupSearchBox(); michael@0: this.setupSidebar(); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: _onBeforeNavigate: function() { michael@0: this._defaultNode = null; michael@0: this.selection.setNodeFront(null); michael@0: this._destroyMarkup(); michael@0: this.isDirty = false; michael@0: }, michael@0: michael@0: _getPageStyle: function() { michael@0: return this._toolbox.inspector.getPageStyle().then(pageStyle => { michael@0: this.pageStyle = pageStyle; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Return a promise that will resolve to the default node for selection. michael@0: */ michael@0: _getDefaultNodeForSelection: function() { michael@0: if (this._defaultNode) { michael@0: return this._defaultNode; michael@0: } michael@0: let walker = this.walker; michael@0: let rootNode = null; michael@0: michael@0: // If available, set either the previously selected node or the body michael@0: // as default selected, else set documentElement michael@0: return walker.getRootNode().then(aRootNode => { michael@0: rootNode = aRootNode; michael@0: return walker.querySelector(rootNode, this.selectionCssSelector); michael@0: }).then(front => { michael@0: if (front) { michael@0: return front; michael@0: } michael@0: return walker.querySelector(rootNode, "body"); michael@0: }).then(front => { michael@0: if (front) { michael@0: return front; michael@0: } michael@0: return this.walker.documentElement(this.walker.rootNode); michael@0: }).then(node => { michael@0: if (walker !== this.walker) { michael@0: promise.reject(null); michael@0: } michael@0: this._defaultNode = node; michael@0: return node; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Target getter. michael@0: */ michael@0: get target() { michael@0: return this._target; michael@0: }, michael@0: michael@0: /** michael@0: * Target setter. michael@0: */ michael@0: set target(value) { michael@0: this._target = value; michael@0: }, michael@0: michael@0: /** michael@0: * Expose gViewSourceUtils so that other tools can make use of them. michael@0: */ michael@0: get viewSourceUtils() { michael@0: return this.panelWin.gViewSourceUtils; michael@0: }, michael@0: michael@0: /** michael@0: * Indicate that a tool has modified the state of the page. Used to michael@0: * decide whether to show the "are you sure you want to navigate" michael@0: * notification. michael@0: */ michael@0: markDirty: function InspectorPanel_markDirty() { michael@0: this.isDirty = true; michael@0: }, michael@0: michael@0: /** michael@0: * Hooks the searchbar to show result and auto completion suggestions. michael@0: */ michael@0: setupSearchBox: function InspectorPanel_setupSearchBox() { michael@0: // Initiate the selectors search object. michael@0: if (this.searchSuggestions) { michael@0: this.searchSuggestions.destroy(); michael@0: this.searchSuggestions = null; michael@0: } michael@0: this.searchBox = this.panelDoc.getElementById("inspector-searchbox"); michael@0: this.searchSuggestions = new SelectorSearch(this, this.searchBox); michael@0: }, michael@0: michael@0: /** michael@0: * Build the sidebar. michael@0: */ michael@0: setupSidebar: function InspectorPanel_setupSidebar() { michael@0: let tabbox = this.panelDoc.querySelector("#inspector-sidebar"); michael@0: this.sidebar = new ToolSidebar(tabbox, this, "inspector"); michael@0: michael@0: let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar"); michael@0: michael@0: this._setDefaultSidebar = function(event, toolId) { michael@0: Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId); michael@0: }.bind(this); michael@0: michael@0: this.sidebar.on("select", this._setDefaultSidebar); michael@0: michael@0: this.sidebar.addTab("ruleview", michael@0: "chrome://browser/content/devtools/cssruleview.xhtml", michael@0: "ruleview" == defaultTab); michael@0: michael@0: this.sidebar.addTab("computedview", michael@0: "chrome://browser/content/devtools/computedview.xhtml", michael@0: "computedview" == defaultTab); michael@0: michael@0: if (Services.prefs.getBoolPref("devtools.fontinspector.enabled") && !this.target.isRemote) { michael@0: this.sidebar.addTab("fontinspector", michael@0: "chrome://browser/content/devtools/fontinspector/font-inspector.xhtml", michael@0: "fontinspector" == defaultTab); michael@0: } michael@0: michael@0: this.sidebar.addTab("layoutview", michael@0: "chrome://browser/content/devtools/layoutview/view.xhtml", michael@0: "layoutview" == defaultTab); michael@0: michael@0: let ruleViewTab = this.sidebar.getTab("ruleview"); michael@0: michael@0: this.sidebar.show(); michael@0: }, michael@0: michael@0: /** michael@0: * Reset the inspector on new root mutation. michael@0: */ michael@0: onNewRoot: function InspectorPanel_onNewRoot() { michael@0: this._defaultNode = null; michael@0: this.selection.setNodeFront(null); michael@0: this._destroyMarkup(); michael@0: this.isDirty = false; michael@0: michael@0: let onNodeSelected = defaultNode => { michael@0: // Cancel this promise resolution as a new one had michael@0: // been queued up. michael@0: if (this._pendingSelection != onNodeSelected) { michael@0: return; michael@0: } michael@0: this._pendingSelection = null; michael@0: this.selection.setNodeFront(defaultNode, "navigateaway"); michael@0: michael@0: this._initMarkup(); michael@0: this.once("markuploaded", () => { michael@0: if (!this.markup) { michael@0: return; michael@0: } michael@0: this.markup.expandNode(this.selection.nodeFront); michael@0: this.setupSearchBox(); michael@0: this.emit("new-root"); michael@0: }); michael@0: }; michael@0: this._pendingSelection = onNodeSelected; michael@0: this._getDefaultNodeForSelection().then(onNodeSelected); michael@0: }, michael@0: michael@0: _selectionCssSelector: null, michael@0: michael@0: /** michael@0: * Set the currently selected node unique css selector. michael@0: * Will store the current target url along with it to allow pre-selection at michael@0: * reload michael@0: */ michael@0: set selectionCssSelector(cssSelector) { michael@0: this._selectionCssSelector = { michael@0: selector: cssSelector, michael@0: url: this._target.url michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Get the current selection unique css selector if any, that is, if a node michael@0: * is actually selected and that node has been selected while on the same url michael@0: */ michael@0: get selectionCssSelector() { michael@0: if (this._selectionCssSelector && michael@0: this._selectionCssSelector.url === this._target.url) { michael@0: return this._selectionCssSelector.selector; michael@0: } else { michael@0: return null; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * When a new node is selected. michael@0: */ michael@0: onNewSelection: function InspectorPanel_onNewSelection(event, value, reason) { michael@0: if (reason === "selection-destroy") { michael@0: return; michael@0: } michael@0: michael@0: this.cancelLayoutChange(); michael@0: michael@0: // Wait for all the known tools to finish updating and then let the michael@0: // client know. michael@0: let selection = this.selection.nodeFront; michael@0: michael@0: // On any new selection made by the user, store the unique css selector michael@0: // of the selected node so it can be restored after reload of the same page michael@0: if (reason !== "navigateaway" && michael@0: this.selection.node && michael@0: this.selection.isElementNode()) { michael@0: this.selectionCssSelector = CssLogic.findCssSelector(this.selection.node); michael@0: } michael@0: michael@0: let selfUpdate = this.updating("inspector-panel"); michael@0: Services.tm.mainThread.dispatch(() => { michael@0: try { michael@0: selfUpdate(selection); michael@0: } catch(ex) { michael@0: console.error(ex); michael@0: } michael@0: }, Ci.nsIThread.DISPATCH_NORMAL); michael@0: }, michael@0: michael@0: /** michael@0: * Delay the "inspector-updated" notification while a tool michael@0: * is updating itself. Returns a function that must be michael@0: * invoked when the tool is done updating with the node michael@0: * that the tool is viewing. michael@0: */ michael@0: updating: function(name) { michael@0: if (this._updateProgress && this._updateProgress.node != this.selection.nodeFront) { michael@0: this.cancelUpdate(); michael@0: } michael@0: michael@0: if (!this._updateProgress) { michael@0: // Start an update in progress. michael@0: var self = this; michael@0: this._updateProgress = { michael@0: node: this.selection.nodeFront, michael@0: outstanding: new Set(), michael@0: checkDone: function() { michael@0: if (this !== self._updateProgress) { michael@0: return; michael@0: } michael@0: if (this.node !== self.selection.nodeFront) { michael@0: self.cancelUpdate(); michael@0: return; michael@0: } michael@0: if (this.outstanding.size !== 0) { michael@0: return; michael@0: } michael@0: michael@0: self._updateProgress = null; michael@0: self.emit("inspector-updated", name); michael@0: }, michael@0: }; michael@0: } michael@0: michael@0: let progress = this._updateProgress; michael@0: let done = function() { michael@0: progress.outstanding.delete(done); michael@0: progress.checkDone(); michael@0: }; michael@0: progress.outstanding.add(done); michael@0: return done; michael@0: }, michael@0: michael@0: /** michael@0: * Cancel notification of inspector updates. michael@0: */ michael@0: cancelUpdate: function() { michael@0: this._updateProgress = null; michael@0: }, michael@0: michael@0: /** michael@0: * When a new node is selected, before the selection has changed. michael@0: */ michael@0: onBeforeNewSelection: function InspectorPanel_onBeforeNewSelection(event, michael@0: node) { michael@0: if (this.breadcrumbs.indexOf(node) == -1) { michael@0: // only clear locks if we'd have to update breadcrumbs michael@0: this.clearPseudoClasses(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * When a node is deleted, select its parent node or the defaultNode if no michael@0: * parent is found (may happen when deleting an iframe inside which the michael@0: * node was selected). michael@0: */ michael@0: onDetached: function InspectorPanel_onDetached(event, parentNode) { michael@0: this.cancelLayoutChange(); michael@0: this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode)); michael@0: this.selection.setNodeFront(parentNode ? parentNode : this._defaultNode, "detached"); michael@0: }, michael@0: michael@0: /** michael@0: * Destroy the inspector. michael@0: */ michael@0: destroy: function InspectorPanel__destroy() { michael@0: if (this._panelDestroyer) { michael@0: return this._panelDestroyer; michael@0: } michael@0: michael@0: if (this.walker) { michael@0: this.walker.off("new-root", this.onNewRoot); michael@0: this.pageStyle = null; michael@0: } michael@0: michael@0: this.cancelUpdate(); michael@0: this.cancelLayoutChange(); michael@0: michael@0: if (this.browser) { michael@0: this.browser.removeEventListener("resize", this.scheduleLayoutChange, true); michael@0: this.browser = null; michael@0: } michael@0: michael@0: this.target.off("will-navigate", this._onBeforeNavigate); michael@0: michael@0: this.target.off("thread-paused", this.updateDebuggerPausedWarning); michael@0: this.target.off("thread-resumed", this.updateDebuggerPausedWarning); michael@0: this._toolbox.off("select", this.updateDebuggerPausedWarning); michael@0: michael@0: this.sidebar.off("select", this._setDefaultSidebar); michael@0: this.sidebar.destroy(); michael@0: this.sidebar = null; michael@0: michael@0: this.nodemenu.removeEventListener("popupshowing", this._setupNodeMenu, true); michael@0: this.nodemenu.removeEventListener("popuphiding", this._resetNodeMenu, true); michael@0: this.breadcrumbs.destroy(); michael@0: this.searchSuggestions.destroy(); michael@0: this.searchBox = null; michael@0: this.selection.off("new-node-front", this.onNewSelection); michael@0: this.selection.off("before-new-node", this.onBeforeNewSelection); michael@0: this.selection.off("before-new-node-front", this.onBeforeNewSelection); michael@0: this.selection.off("detached-front", this.onDetached); michael@0: this._panelDestroyer = this._destroyMarkup(); michael@0: this.panelWin.inspector = null; michael@0: this.target = null; michael@0: this.panelDoc = null; michael@0: this.panelWin = null; michael@0: this.breadcrumbs = null; michael@0: this.searchSuggestions = null; michael@0: this.lastNodemenuItem = null; michael@0: this.nodemenu = null; michael@0: this._toolbox = null; michael@0: michael@0: return this._panelDestroyer; michael@0: }, michael@0: michael@0: /** michael@0: * Show the node menu. michael@0: */ michael@0: showNodeMenu: function InspectorPanel_showNodeMenu(aButton, aPosition, aExtraItems) { michael@0: if (aExtraItems) { michael@0: for (let item of aExtraItems) { michael@0: this.nodemenu.appendChild(item); michael@0: } michael@0: } michael@0: this.nodemenu.openPopup(aButton, aPosition, 0, 0, true, false); michael@0: }, michael@0: michael@0: hideNodeMenu: function InspectorPanel_hideNodeMenu() { michael@0: this.nodemenu.hidePopup(); michael@0: }, michael@0: michael@0: /** michael@0: * Disable the delete item if needed. Update the pseudo classes. michael@0: */ michael@0: _setupNodeMenu: function InspectorPanel_setupNodeMenu() { michael@0: let isSelectionElement = this.selection.isElementNode(); michael@0: michael@0: // Set the pseudo classes michael@0: for (let name of ["hover", "active", "focus"]) { michael@0: let menu = this.panelDoc.getElementById("node-menu-pseudo-" + name); michael@0: michael@0: if (isSelectionElement) { michael@0: let checked = this.selection.nodeFront.hasPseudoClassLock(":" + name); michael@0: menu.setAttribute("checked", checked); michael@0: menu.removeAttribute("disabled"); michael@0: } else { michael@0: menu.setAttribute("disabled", "true"); michael@0: } michael@0: } michael@0: michael@0: // Disable delete item if needed michael@0: let deleteNode = this.panelDoc.getElementById("node-menu-delete"); michael@0: if (this.selection.isRoot() || this.selection.isDocumentTypeNode()) { michael@0: deleteNode.setAttribute("disabled", "true"); michael@0: } else { michael@0: deleteNode.removeAttribute("disabled"); michael@0: } michael@0: michael@0: // Disable / enable "Copy Unique Selector", "Copy inner HTML" & michael@0: // "Copy outer HTML" as appropriate michael@0: let unique = this.panelDoc.getElementById("node-menu-copyuniqueselector"); michael@0: let copyInnerHTML = this.panelDoc.getElementById("node-menu-copyinner"); michael@0: let copyOuterHTML = this.panelDoc.getElementById("node-menu-copyouter"); michael@0: if (isSelectionElement) { michael@0: unique.removeAttribute("disabled"); michael@0: copyInnerHTML.removeAttribute("disabled"); michael@0: copyOuterHTML.removeAttribute("disabled"); michael@0: } else { michael@0: unique.setAttribute("disabled", "true"); michael@0: copyInnerHTML.setAttribute("disabled", "true"); michael@0: copyOuterHTML.setAttribute("disabled", "true"); michael@0: } michael@0: michael@0: // Enable the "edit HTML" item if the selection is an element and the root michael@0: // actor has the appropriate trait (isOuterHTMLEditable) michael@0: let editHTML = this.panelDoc.getElementById("node-menu-edithtml"); michael@0: if (this.isOuterHTMLEditable && isSelectionElement) { michael@0: editHTML.removeAttribute("disabled"); michael@0: } else { michael@0: editHTML.setAttribute("disabled", "true"); michael@0: } michael@0: michael@0: // Enable the "copy image data-uri" item if the selection is previewable michael@0: // which essentially checks if it's an image or canvas tag michael@0: let copyImageData = this.panelDoc.getElementById("node-menu-copyimagedatauri"); michael@0: let markupContainer = this.markup.getContainer(this.selection.nodeFront); michael@0: if (markupContainer && markupContainer.isPreviewable()) { michael@0: copyImageData.removeAttribute("disabled"); michael@0: } else { michael@0: copyImageData.setAttribute("disabled", "true"); michael@0: } michael@0: }, michael@0: michael@0: _resetNodeMenu: function InspectorPanel_resetNodeMenu() { michael@0: // Remove any extra items michael@0: while (this.lastNodemenuItem.nextSibling) { michael@0: let toDelete = this.lastNodemenuItem.nextSibling; michael@0: toDelete.parentNode.removeChild(toDelete); michael@0: } michael@0: }, michael@0: michael@0: _initMarkup: function InspectorPanel_initMarkup() { michael@0: let doc = this.panelDoc; michael@0: michael@0: this._markupBox = doc.getElementById("markup-box"); michael@0: michael@0: // create tool iframe michael@0: this._markupFrame = doc.createElement("iframe"); michael@0: this._markupFrame.setAttribute("flex", "1"); michael@0: this._markupFrame.setAttribute("tooltip", "aHTMLTooltip"); michael@0: this._markupFrame.setAttribute("context", "inspector-node-popup"); michael@0: michael@0: // This is needed to enable tooltips inside the iframe document. michael@0: this._boundMarkupFrameLoad = this._onMarkupFrameLoad.bind(this); michael@0: this._markupFrame.addEventListener("load", this._boundMarkupFrameLoad, true); michael@0: michael@0: this._markupBox.setAttribute("collapsed", true); michael@0: this._markupBox.appendChild(this._markupFrame); michael@0: this._markupFrame.setAttribute("src", "chrome://browser/content/devtools/markup-view.xhtml"); michael@0: }, michael@0: michael@0: _onMarkupFrameLoad: function InspectorPanel__onMarkupFrameLoad() { michael@0: this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true); michael@0: delete this._boundMarkupFrameLoad; michael@0: michael@0: this._markupFrame.contentWindow.focus(); michael@0: michael@0: this._markupBox.removeAttribute("collapsed"); michael@0: michael@0: let controllerWindow = this._toolbox.doc.defaultView; michael@0: this.markup = new MarkupView(this, this._markupFrame, controllerWindow); michael@0: michael@0: this.emit("markuploaded"); michael@0: }, michael@0: michael@0: _destroyMarkup: function InspectorPanel__destroyMarkup() { michael@0: let destroyPromise; michael@0: michael@0: if (this._boundMarkupFrameLoad) { michael@0: this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true); michael@0: this._boundMarkupFrameLoad = null; michael@0: } michael@0: michael@0: if (this.markup) { michael@0: destroyPromise = this.markup.destroy(); michael@0: this.markup = null; michael@0: } else { michael@0: destroyPromise = promise.resolve(); michael@0: } michael@0: michael@0: if (this._markupFrame) { michael@0: this._markupFrame.parentNode.removeChild(this._markupFrame); michael@0: this._markupFrame = null; michael@0: } michael@0: michael@0: this._markupBox = null; michael@0: michael@0: return destroyPromise; michael@0: }, michael@0: michael@0: /** michael@0: * Toggle a pseudo class. michael@0: */ michael@0: togglePseudoClass: function InspectorPanel_togglePseudoClass(aPseudo) { michael@0: if (this.selection.isElementNode()) { michael@0: let node = this.selection.nodeFront; michael@0: if (node.hasPseudoClassLock(aPseudo)) { michael@0: return this.walker.removePseudoClassLock(node, aPseudo, {parents: true}); michael@0: } michael@0: michael@0: let hierarchical = aPseudo == ":hover" || aPseudo == ":active"; michael@0: return this.walker.addPseudoClassLock(node, aPseudo, {parents: hierarchical}); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Clear any pseudo-class locks applied to the current hierarchy. michael@0: */ michael@0: clearPseudoClasses: function InspectorPanel_clearPseudoClasses() { michael@0: if (!this.walker) { michael@0: return; michael@0: } michael@0: return this.walker.clearPseudoClassLocks().then(null, console.error); michael@0: }, michael@0: michael@0: /** michael@0: * Edit the outerHTML of the selected Node. michael@0: */ michael@0: editHTML: function InspectorPanel_editHTML() michael@0: { michael@0: if (!this.selection.isNode()) { michael@0: return; michael@0: } michael@0: if (this.markup) { michael@0: this.markup.beginEditingOuterHTML(this.selection.nodeFront); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Copy the innerHTML of the selected Node to the clipboard. michael@0: */ michael@0: copyInnerHTML: function InspectorPanel_copyInnerHTML() michael@0: { michael@0: if (!this.selection.isNode()) { michael@0: return; michael@0: } michael@0: this._copyLongStr(this.walker.innerHTML(this.selection.nodeFront)); michael@0: }, michael@0: michael@0: /** michael@0: * Copy the outerHTML of the selected Node to the clipboard. michael@0: */ michael@0: copyOuterHTML: function InspectorPanel_copyOuterHTML() michael@0: { michael@0: if (!this.selection.isNode()) { michael@0: return; michael@0: } michael@0: michael@0: this._copyLongStr(this.walker.outerHTML(this.selection.nodeFront)); michael@0: }, michael@0: michael@0: /** michael@0: * Copy the data-uri for the currently selected image in the clipboard. michael@0: */ michael@0: copyImageDataUri: function InspectorPanel_copyImageDataUri() michael@0: { michael@0: let container = this.markup.getContainer(this.selection.nodeFront); michael@0: if (container && container.isPreviewable()) { michael@0: container.copyImageDataUri(); michael@0: } michael@0: }, michael@0: michael@0: _copyLongStr: function InspectorPanel_copyLongStr(promise) michael@0: { michael@0: return promise.then(longstr => { michael@0: return longstr.string().then(toCopy => { michael@0: longstr.release().then(null, console.error); michael@0: clipboardHelper.copyString(toCopy); michael@0: }); michael@0: }).then(null, console.error); michael@0: }, michael@0: michael@0: /** michael@0: * Copy a unique selector of the selected Node to the clipboard. michael@0: */ michael@0: copyUniqueSelector: function InspectorPanel_copyUniqueSelector() michael@0: { michael@0: if (!this.selection.isNode()) { michael@0: return; michael@0: } michael@0: michael@0: let toCopy = CssLogic.findCssSelector(this.selection.node); michael@0: if (toCopy) { michael@0: clipboardHelper.copyString(toCopy); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Delete the selected node. michael@0: */ michael@0: deleteNode: function IUI_deleteNode() { michael@0: if (!this.selection.isNode() || michael@0: this.selection.isRoot()) { michael@0: return; michael@0: } michael@0: michael@0: // If the markup panel is active, use the markup panel to delete michael@0: // the node, making this an undoable action. michael@0: if (this.markup) { michael@0: this.markup.deleteNode(this.selection.nodeFront); michael@0: } else { michael@0: // remove the node from content michael@0: this.walker.removeNode(this.selection.nodeFront); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Trigger a high-priority layout change for things that need to be michael@0: * updated immediately michael@0: */ michael@0: immediateLayoutChange: function Inspector_immediateLayoutChange() michael@0: { michael@0: this.emit("layout-change"); michael@0: }, michael@0: michael@0: /** michael@0: * Schedule a low-priority change event for things like paint michael@0: * and resize. michael@0: */ michael@0: scheduleLayoutChange: function Inspector_scheduleLayoutChange(event) michael@0: { michael@0: // Filter out non browser window resize events (i.e. triggered by iframes) michael@0: if (this.browser.contentWindow === event.target) { michael@0: if (this._timer) { michael@0: return null; michael@0: } michael@0: this._timer = this.panelWin.setTimeout(function() { michael@0: this.emit("layout-change"); michael@0: this._timer = null; michael@0: }.bind(this), LAYOUT_CHANGE_TIMER); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Cancel a pending low-priority change event if any is michael@0: * scheduled. michael@0: */ michael@0: cancelLayoutChange: function Inspector_cancelLayoutChange() michael@0: { michael@0: if (this._timer) { michael@0: this.panelWin.clearTimeout(this._timer); michael@0: delete this._timer; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: ///////////////////////////////////////////////////////////////////////// michael@0: //// Initializers michael@0: michael@0: loader.lazyGetter(InspectorPanel.prototype, "strings", michael@0: function () { michael@0: return Services.strings.createBundle( michael@0: "chrome://browser/locale/devtools/inspector.properties"); michael@0: }); michael@0: michael@0: loader.lazyGetter(this, "clipboardHelper", function() { michael@0: return Cc["@mozilla.org/widget/clipboardhelper;1"]. michael@0: getService(Ci.nsIClipboardHelper); michael@0: }); michael@0: michael@0: michael@0: loader.lazyGetter(this, "DOMUtils", function () { michael@0: return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); michael@0: });