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