1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/inspector/inspector-panel.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,848 @@ 1.4 +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +const {Cc, Ci, Cu, Cr} = require("chrome"); 1.11 + 1.12 +Cu.import("resource://gre/modules/Services.jsm"); 1.13 + 1.14 +let promise = require("devtools/toolkit/deprecated-sync-thenables"); 1.15 +let EventEmitter = require("devtools/toolkit/event-emitter"); 1.16 +let {CssLogic} = require("devtools/styleinspector/css-logic"); 1.17 + 1.18 +loader.lazyGetter(this, "MarkupView", () => require("devtools/markupview/markup-view").MarkupView); 1.19 +loader.lazyGetter(this, "HTMLBreadcrumbs", () => require("devtools/inspector/breadcrumbs").HTMLBreadcrumbs); 1.20 +loader.lazyGetter(this, "ToolSidebar", () => require("devtools/framework/sidebar").ToolSidebar); 1.21 +loader.lazyGetter(this, "SelectorSearch", () => require("devtools/inspector/selector-search").SelectorSearch); 1.22 + 1.23 +const LAYOUT_CHANGE_TIMER = 250; 1.24 + 1.25 +/** 1.26 + * Represents an open instance of the Inspector for a tab. 1.27 + * The inspector controls the breadcrumbs, the markup view, and the sidebar 1.28 + * (computed view, rule view, font view and layout view). 1.29 + * 1.30 + * Events: 1.31 + * - ready 1.32 + * Fired when the inspector panel is opened for the first time and ready to 1.33 + * use 1.34 + * - new-root 1.35 + * Fired after a new root (navigation to a new page) event was fired by 1.36 + * the walker, and taken into account by the inspector (after the markup 1.37 + * view has been reloaded) 1.38 + * - markuploaded 1.39 + * Fired when the markup-view frame has loaded 1.40 + * - layout-change 1.41 + * Fired when the layout of the inspector changes 1.42 + * - breadcrumbs-updated 1.43 + * Fired when the breadcrumb widget updates to a new node 1.44 + * - layoutview-updated 1.45 + * Fired when the layoutview (box model) updates to a new node 1.46 + * - markupmutation 1.47 + * Fired after markup mutations have been processed by the markup-view 1.48 + * - computed-view-refreshed 1.49 + * Fired when the computed rules view updates to a new node 1.50 + * - computed-view-property-expanded 1.51 + * Fired when a property is expanded in the computed rules view 1.52 + * - computed-view-property-collapsed 1.53 + * Fired when a property is collapsed in the computed rules view 1.54 + * - rule-view-refreshed 1.55 + * Fired when the rule view updates to a new node 1.56 + */ 1.57 +function InspectorPanel(iframeWindow, toolbox) { 1.58 + this._toolbox = toolbox; 1.59 + this._target = toolbox._target; 1.60 + this.panelDoc = iframeWindow.document; 1.61 + this.panelWin = iframeWindow; 1.62 + this.panelWin.inspector = this; 1.63 + this._inspector = null; 1.64 + 1.65 + this._onBeforeNavigate = this._onBeforeNavigate.bind(this); 1.66 + this._target.on("will-navigate", this._onBeforeNavigate); 1.67 + 1.68 + EventEmitter.decorate(this); 1.69 +} 1.70 + 1.71 +exports.InspectorPanel = InspectorPanel; 1.72 + 1.73 +InspectorPanel.prototype = { 1.74 + /** 1.75 + * open is effectively an asynchronous constructor 1.76 + */ 1.77 + open: function InspectorPanel_open() { 1.78 + return this.target.makeRemote().then(() => { 1.79 + return this._getPageStyle(); 1.80 + }).then(() => { 1.81 + return this._getDefaultNodeForSelection(); 1.82 + }).then(defaultSelection => { 1.83 + return this._deferredOpen(defaultSelection); 1.84 + }).then(null, console.error); 1.85 + }, 1.86 + 1.87 + get toolbox() { 1.88 + return this._toolbox; 1.89 + }, 1.90 + 1.91 + get inspector() { 1.92 + return this._toolbox.inspector; 1.93 + }, 1.94 + 1.95 + get walker() { 1.96 + return this._toolbox.walker; 1.97 + }, 1.98 + 1.99 + get selection() { 1.100 + return this._toolbox.selection; 1.101 + }, 1.102 + 1.103 + get isOuterHTMLEditable() { 1.104 + return this._target.client.traits.editOuterHTML; 1.105 + }, 1.106 + 1.107 + get hasUrlToImageDataResolver() { 1.108 + return this._target.client.traits.urlToImageDataResolver; 1.109 + }, 1.110 + 1.111 + _deferredOpen: function(defaultSelection) { 1.112 + let deferred = promise.defer(); 1.113 + 1.114 + this.onNewRoot = this.onNewRoot.bind(this); 1.115 + this.walker.on("new-root", this.onNewRoot); 1.116 + 1.117 + this.nodemenu = this.panelDoc.getElementById("inspector-node-popup"); 1.118 + this.lastNodemenuItem = this.nodemenu.lastChild; 1.119 + this._setupNodeMenu = this._setupNodeMenu.bind(this); 1.120 + this._resetNodeMenu = this._resetNodeMenu.bind(this); 1.121 + this.nodemenu.addEventListener("popupshowing", this._setupNodeMenu, true); 1.122 + this.nodemenu.addEventListener("popuphiding", this._resetNodeMenu, true); 1.123 + 1.124 + this.onNewSelection = this.onNewSelection.bind(this); 1.125 + this.selection.on("new-node-front", this.onNewSelection); 1.126 + this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this); 1.127 + this.selection.on("before-new-node-front", this.onBeforeNewSelection); 1.128 + this.onDetached = this.onDetached.bind(this); 1.129 + this.selection.on("detached-front", this.onDetached); 1.130 + 1.131 + this.breadcrumbs = new HTMLBreadcrumbs(this); 1.132 + 1.133 + if (this.target.isLocalTab) { 1.134 + this.browser = this.target.tab.linkedBrowser; 1.135 + this.scheduleLayoutChange = this.scheduleLayoutChange.bind(this); 1.136 + this.browser.addEventListener("resize", this.scheduleLayoutChange, true); 1.137 + 1.138 + // Show a warning when the debugger is paused. 1.139 + // We show the warning only when the inspector 1.140 + // is selected. 1.141 + this.updateDebuggerPausedWarning = function() { 1.142 + let notificationBox = this._toolbox.getNotificationBox(); 1.143 + let notification = notificationBox.getNotificationWithValue("inspector-script-paused"); 1.144 + if (!notification && this._toolbox.currentToolId == "inspector" && 1.145 + this.target.isThreadPaused) { 1.146 + let message = this.strings.GetStringFromName("debuggerPausedWarning.message"); 1.147 + notificationBox.appendNotification(message, 1.148 + "inspector-script-paused", "", notificationBox.PRIORITY_WARNING_HIGH); 1.149 + } 1.150 + 1.151 + if (notification && this._toolbox.currentToolId != "inspector") { 1.152 + notificationBox.removeNotification(notification); 1.153 + } 1.154 + 1.155 + if (notification && !this.target.isThreadPaused) { 1.156 + notificationBox.removeNotification(notification); 1.157 + } 1.158 + 1.159 + }.bind(this); 1.160 + this.target.on("thread-paused", this.updateDebuggerPausedWarning); 1.161 + this.target.on("thread-resumed", this.updateDebuggerPausedWarning); 1.162 + this._toolbox.on("select", this.updateDebuggerPausedWarning); 1.163 + this.updateDebuggerPausedWarning(); 1.164 + } 1.165 + 1.166 + this._initMarkup(); 1.167 + this.isReady = false; 1.168 + 1.169 + this.once("markuploaded", function() { 1.170 + this.isReady = true; 1.171 + 1.172 + // All the components are initialized. Let's select a node. 1.173 + this.selection.setNodeFront(defaultSelection, "inspector-open"); 1.174 + 1.175 + this.markup.expandNode(this.selection.nodeFront); 1.176 + 1.177 + this.emit("ready"); 1.178 + deferred.resolve(this); 1.179 + }.bind(this)); 1.180 + 1.181 + this.setupSearchBox(); 1.182 + this.setupSidebar(); 1.183 + 1.184 + return deferred.promise; 1.185 + }, 1.186 + 1.187 + _onBeforeNavigate: function() { 1.188 + this._defaultNode = null; 1.189 + this.selection.setNodeFront(null); 1.190 + this._destroyMarkup(); 1.191 + this.isDirty = false; 1.192 + }, 1.193 + 1.194 + _getPageStyle: function() { 1.195 + return this._toolbox.inspector.getPageStyle().then(pageStyle => { 1.196 + this.pageStyle = pageStyle; 1.197 + }); 1.198 + }, 1.199 + 1.200 + /** 1.201 + * Return a promise that will resolve to the default node for selection. 1.202 + */ 1.203 + _getDefaultNodeForSelection: function() { 1.204 + if (this._defaultNode) { 1.205 + return this._defaultNode; 1.206 + } 1.207 + let walker = this.walker; 1.208 + let rootNode = null; 1.209 + 1.210 + // If available, set either the previously selected node or the body 1.211 + // as default selected, else set documentElement 1.212 + return walker.getRootNode().then(aRootNode => { 1.213 + rootNode = aRootNode; 1.214 + return walker.querySelector(rootNode, this.selectionCssSelector); 1.215 + }).then(front => { 1.216 + if (front) { 1.217 + return front; 1.218 + } 1.219 + return walker.querySelector(rootNode, "body"); 1.220 + }).then(front => { 1.221 + if (front) { 1.222 + return front; 1.223 + } 1.224 + return this.walker.documentElement(this.walker.rootNode); 1.225 + }).then(node => { 1.226 + if (walker !== this.walker) { 1.227 + promise.reject(null); 1.228 + } 1.229 + this._defaultNode = node; 1.230 + return node; 1.231 + }); 1.232 + }, 1.233 + 1.234 + /** 1.235 + * Target getter. 1.236 + */ 1.237 + get target() { 1.238 + return this._target; 1.239 + }, 1.240 + 1.241 + /** 1.242 + * Target setter. 1.243 + */ 1.244 + set target(value) { 1.245 + this._target = value; 1.246 + }, 1.247 + 1.248 + /** 1.249 + * Expose gViewSourceUtils so that other tools can make use of them. 1.250 + */ 1.251 + get viewSourceUtils() { 1.252 + return this.panelWin.gViewSourceUtils; 1.253 + }, 1.254 + 1.255 + /** 1.256 + * Indicate that a tool has modified the state of the page. Used to 1.257 + * decide whether to show the "are you sure you want to navigate" 1.258 + * notification. 1.259 + */ 1.260 + markDirty: function InspectorPanel_markDirty() { 1.261 + this.isDirty = true; 1.262 + }, 1.263 + 1.264 + /** 1.265 + * Hooks the searchbar to show result and auto completion suggestions. 1.266 + */ 1.267 + setupSearchBox: function InspectorPanel_setupSearchBox() { 1.268 + // Initiate the selectors search object. 1.269 + if (this.searchSuggestions) { 1.270 + this.searchSuggestions.destroy(); 1.271 + this.searchSuggestions = null; 1.272 + } 1.273 + this.searchBox = this.panelDoc.getElementById("inspector-searchbox"); 1.274 + this.searchSuggestions = new SelectorSearch(this, this.searchBox); 1.275 + }, 1.276 + 1.277 + /** 1.278 + * Build the sidebar. 1.279 + */ 1.280 + setupSidebar: function InspectorPanel_setupSidebar() { 1.281 + let tabbox = this.panelDoc.querySelector("#inspector-sidebar"); 1.282 + this.sidebar = new ToolSidebar(tabbox, this, "inspector"); 1.283 + 1.284 + let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar"); 1.285 + 1.286 + this._setDefaultSidebar = function(event, toolId) { 1.287 + Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId); 1.288 + }.bind(this); 1.289 + 1.290 + this.sidebar.on("select", this._setDefaultSidebar); 1.291 + 1.292 + this.sidebar.addTab("ruleview", 1.293 + "chrome://browser/content/devtools/cssruleview.xhtml", 1.294 + "ruleview" == defaultTab); 1.295 + 1.296 + this.sidebar.addTab("computedview", 1.297 + "chrome://browser/content/devtools/computedview.xhtml", 1.298 + "computedview" == defaultTab); 1.299 + 1.300 + if (Services.prefs.getBoolPref("devtools.fontinspector.enabled") && !this.target.isRemote) { 1.301 + this.sidebar.addTab("fontinspector", 1.302 + "chrome://browser/content/devtools/fontinspector/font-inspector.xhtml", 1.303 + "fontinspector" == defaultTab); 1.304 + } 1.305 + 1.306 + this.sidebar.addTab("layoutview", 1.307 + "chrome://browser/content/devtools/layoutview/view.xhtml", 1.308 + "layoutview" == defaultTab); 1.309 + 1.310 + let ruleViewTab = this.sidebar.getTab("ruleview"); 1.311 + 1.312 + this.sidebar.show(); 1.313 + }, 1.314 + 1.315 + /** 1.316 + * Reset the inspector on new root mutation. 1.317 + */ 1.318 + onNewRoot: function InspectorPanel_onNewRoot() { 1.319 + this._defaultNode = null; 1.320 + this.selection.setNodeFront(null); 1.321 + this._destroyMarkup(); 1.322 + this.isDirty = false; 1.323 + 1.324 + let onNodeSelected = defaultNode => { 1.325 + // Cancel this promise resolution as a new one had 1.326 + // been queued up. 1.327 + if (this._pendingSelection != onNodeSelected) { 1.328 + return; 1.329 + } 1.330 + this._pendingSelection = null; 1.331 + this.selection.setNodeFront(defaultNode, "navigateaway"); 1.332 + 1.333 + this._initMarkup(); 1.334 + this.once("markuploaded", () => { 1.335 + if (!this.markup) { 1.336 + return; 1.337 + } 1.338 + this.markup.expandNode(this.selection.nodeFront); 1.339 + this.setupSearchBox(); 1.340 + this.emit("new-root"); 1.341 + }); 1.342 + }; 1.343 + this._pendingSelection = onNodeSelected; 1.344 + this._getDefaultNodeForSelection().then(onNodeSelected); 1.345 + }, 1.346 + 1.347 + _selectionCssSelector: null, 1.348 + 1.349 + /** 1.350 + * Set the currently selected node unique css selector. 1.351 + * Will store the current target url along with it to allow pre-selection at 1.352 + * reload 1.353 + */ 1.354 + set selectionCssSelector(cssSelector) { 1.355 + this._selectionCssSelector = { 1.356 + selector: cssSelector, 1.357 + url: this._target.url 1.358 + }; 1.359 + }, 1.360 + 1.361 + /** 1.362 + * Get the current selection unique css selector if any, that is, if a node 1.363 + * is actually selected and that node has been selected while on the same url 1.364 + */ 1.365 + get selectionCssSelector() { 1.366 + if (this._selectionCssSelector && 1.367 + this._selectionCssSelector.url === this._target.url) { 1.368 + return this._selectionCssSelector.selector; 1.369 + } else { 1.370 + return null; 1.371 + } 1.372 + }, 1.373 + 1.374 + /** 1.375 + * When a new node is selected. 1.376 + */ 1.377 + onNewSelection: function InspectorPanel_onNewSelection(event, value, reason) { 1.378 + if (reason === "selection-destroy") { 1.379 + return; 1.380 + } 1.381 + 1.382 + this.cancelLayoutChange(); 1.383 + 1.384 + // Wait for all the known tools to finish updating and then let the 1.385 + // client know. 1.386 + let selection = this.selection.nodeFront; 1.387 + 1.388 + // On any new selection made by the user, store the unique css selector 1.389 + // of the selected node so it can be restored after reload of the same page 1.390 + if (reason !== "navigateaway" && 1.391 + this.selection.node && 1.392 + this.selection.isElementNode()) { 1.393 + this.selectionCssSelector = CssLogic.findCssSelector(this.selection.node); 1.394 + } 1.395 + 1.396 + let selfUpdate = this.updating("inspector-panel"); 1.397 + Services.tm.mainThread.dispatch(() => { 1.398 + try { 1.399 + selfUpdate(selection); 1.400 + } catch(ex) { 1.401 + console.error(ex); 1.402 + } 1.403 + }, Ci.nsIThread.DISPATCH_NORMAL); 1.404 + }, 1.405 + 1.406 + /** 1.407 + * Delay the "inspector-updated" notification while a tool 1.408 + * is updating itself. Returns a function that must be 1.409 + * invoked when the tool is done updating with the node 1.410 + * that the tool is viewing. 1.411 + */ 1.412 + updating: function(name) { 1.413 + if (this._updateProgress && this._updateProgress.node != this.selection.nodeFront) { 1.414 + this.cancelUpdate(); 1.415 + } 1.416 + 1.417 + if (!this._updateProgress) { 1.418 + // Start an update in progress. 1.419 + var self = this; 1.420 + this._updateProgress = { 1.421 + node: this.selection.nodeFront, 1.422 + outstanding: new Set(), 1.423 + checkDone: function() { 1.424 + if (this !== self._updateProgress) { 1.425 + return; 1.426 + } 1.427 + if (this.node !== self.selection.nodeFront) { 1.428 + self.cancelUpdate(); 1.429 + return; 1.430 + } 1.431 + if (this.outstanding.size !== 0) { 1.432 + return; 1.433 + } 1.434 + 1.435 + self._updateProgress = null; 1.436 + self.emit("inspector-updated", name); 1.437 + }, 1.438 + }; 1.439 + } 1.440 + 1.441 + let progress = this._updateProgress; 1.442 + let done = function() { 1.443 + progress.outstanding.delete(done); 1.444 + progress.checkDone(); 1.445 + }; 1.446 + progress.outstanding.add(done); 1.447 + return done; 1.448 + }, 1.449 + 1.450 + /** 1.451 + * Cancel notification of inspector updates. 1.452 + */ 1.453 + cancelUpdate: function() { 1.454 + this._updateProgress = null; 1.455 + }, 1.456 + 1.457 + /** 1.458 + * When a new node is selected, before the selection has changed. 1.459 + */ 1.460 + onBeforeNewSelection: function InspectorPanel_onBeforeNewSelection(event, 1.461 + node) { 1.462 + if (this.breadcrumbs.indexOf(node) == -1) { 1.463 + // only clear locks if we'd have to update breadcrumbs 1.464 + this.clearPseudoClasses(); 1.465 + } 1.466 + }, 1.467 + 1.468 + /** 1.469 + * When a node is deleted, select its parent node or the defaultNode if no 1.470 + * parent is found (may happen when deleting an iframe inside which the 1.471 + * node was selected). 1.472 + */ 1.473 + onDetached: function InspectorPanel_onDetached(event, parentNode) { 1.474 + this.cancelLayoutChange(); 1.475 + this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode)); 1.476 + this.selection.setNodeFront(parentNode ? parentNode : this._defaultNode, "detached"); 1.477 + }, 1.478 + 1.479 + /** 1.480 + * Destroy the inspector. 1.481 + */ 1.482 + destroy: function InspectorPanel__destroy() { 1.483 + if (this._panelDestroyer) { 1.484 + return this._panelDestroyer; 1.485 + } 1.486 + 1.487 + if (this.walker) { 1.488 + this.walker.off("new-root", this.onNewRoot); 1.489 + this.pageStyle = null; 1.490 + } 1.491 + 1.492 + this.cancelUpdate(); 1.493 + this.cancelLayoutChange(); 1.494 + 1.495 + if (this.browser) { 1.496 + this.browser.removeEventListener("resize", this.scheduleLayoutChange, true); 1.497 + this.browser = null; 1.498 + } 1.499 + 1.500 + this.target.off("will-navigate", this._onBeforeNavigate); 1.501 + 1.502 + this.target.off("thread-paused", this.updateDebuggerPausedWarning); 1.503 + this.target.off("thread-resumed", this.updateDebuggerPausedWarning); 1.504 + this._toolbox.off("select", this.updateDebuggerPausedWarning); 1.505 + 1.506 + this.sidebar.off("select", this._setDefaultSidebar); 1.507 + this.sidebar.destroy(); 1.508 + this.sidebar = null; 1.509 + 1.510 + this.nodemenu.removeEventListener("popupshowing", this._setupNodeMenu, true); 1.511 + this.nodemenu.removeEventListener("popuphiding", this._resetNodeMenu, true); 1.512 + this.breadcrumbs.destroy(); 1.513 + this.searchSuggestions.destroy(); 1.514 + this.searchBox = null; 1.515 + this.selection.off("new-node-front", this.onNewSelection); 1.516 + this.selection.off("before-new-node", this.onBeforeNewSelection); 1.517 + this.selection.off("before-new-node-front", this.onBeforeNewSelection); 1.518 + this.selection.off("detached-front", this.onDetached); 1.519 + this._panelDestroyer = this._destroyMarkup(); 1.520 + this.panelWin.inspector = null; 1.521 + this.target = null; 1.522 + this.panelDoc = null; 1.523 + this.panelWin = null; 1.524 + this.breadcrumbs = null; 1.525 + this.searchSuggestions = null; 1.526 + this.lastNodemenuItem = null; 1.527 + this.nodemenu = null; 1.528 + this._toolbox = null; 1.529 + 1.530 + return this._panelDestroyer; 1.531 + }, 1.532 + 1.533 + /** 1.534 + * Show the node menu. 1.535 + */ 1.536 + showNodeMenu: function InspectorPanel_showNodeMenu(aButton, aPosition, aExtraItems) { 1.537 + if (aExtraItems) { 1.538 + for (let item of aExtraItems) { 1.539 + this.nodemenu.appendChild(item); 1.540 + } 1.541 + } 1.542 + this.nodemenu.openPopup(aButton, aPosition, 0, 0, true, false); 1.543 + }, 1.544 + 1.545 + hideNodeMenu: function InspectorPanel_hideNodeMenu() { 1.546 + this.nodemenu.hidePopup(); 1.547 + }, 1.548 + 1.549 + /** 1.550 + * Disable the delete item if needed. Update the pseudo classes. 1.551 + */ 1.552 + _setupNodeMenu: function InspectorPanel_setupNodeMenu() { 1.553 + let isSelectionElement = this.selection.isElementNode(); 1.554 + 1.555 + // Set the pseudo classes 1.556 + for (let name of ["hover", "active", "focus"]) { 1.557 + let menu = this.panelDoc.getElementById("node-menu-pseudo-" + name); 1.558 + 1.559 + if (isSelectionElement) { 1.560 + let checked = this.selection.nodeFront.hasPseudoClassLock(":" + name); 1.561 + menu.setAttribute("checked", checked); 1.562 + menu.removeAttribute("disabled"); 1.563 + } else { 1.564 + menu.setAttribute("disabled", "true"); 1.565 + } 1.566 + } 1.567 + 1.568 + // Disable delete item if needed 1.569 + let deleteNode = this.panelDoc.getElementById("node-menu-delete"); 1.570 + if (this.selection.isRoot() || this.selection.isDocumentTypeNode()) { 1.571 + deleteNode.setAttribute("disabled", "true"); 1.572 + } else { 1.573 + deleteNode.removeAttribute("disabled"); 1.574 + } 1.575 + 1.576 + // Disable / enable "Copy Unique Selector", "Copy inner HTML" & 1.577 + // "Copy outer HTML" as appropriate 1.578 + let unique = this.panelDoc.getElementById("node-menu-copyuniqueselector"); 1.579 + let copyInnerHTML = this.panelDoc.getElementById("node-menu-copyinner"); 1.580 + let copyOuterHTML = this.panelDoc.getElementById("node-menu-copyouter"); 1.581 + if (isSelectionElement) { 1.582 + unique.removeAttribute("disabled"); 1.583 + copyInnerHTML.removeAttribute("disabled"); 1.584 + copyOuterHTML.removeAttribute("disabled"); 1.585 + } else { 1.586 + unique.setAttribute("disabled", "true"); 1.587 + copyInnerHTML.setAttribute("disabled", "true"); 1.588 + copyOuterHTML.setAttribute("disabled", "true"); 1.589 + } 1.590 + 1.591 + // Enable the "edit HTML" item if the selection is an element and the root 1.592 + // actor has the appropriate trait (isOuterHTMLEditable) 1.593 + let editHTML = this.panelDoc.getElementById("node-menu-edithtml"); 1.594 + if (this.isOuterHTMLEditable && isSelectionElement) { 1.595 + editHTML.removeAttribute("disabled"); 1.596 + } else { 1.597 + editHTML.setAttribute("disabled", "true"); 1.598 + } 1.599 + 1.600 + // Enable the "copy image data-uri" item if the selection is previewable 1.601 + // which essentially checks if it's an image or canvas tag 1.602 + let copyImageData = this.panelDoc.getElementById("node-menu-copyimagedatauri"); 1.603 + let markupContainer = this.markup.getContainer(this.selection.nodeFront); 1.604 + if (markupContainer && markupContainer.isPreviewable()) { 1.605 + copyImageData.removeAttribute("disabled"); 1.606 + } else { 1.607 + copyImageData.setAttribute("disabled", "true"); 1.608 + } 1.609 + }, 1.610 + 1.611 + _resetNodeMenu: function InspectorPanel_resetNodeMenu() { 1.612 + // Remove any extra items 1.613 + while (this.lastNodemenuItem.nextSibling) { 1.614 + let toDelete = this.lastNodemenuItem.nextSibling; 1.615 + toDelete.parentNode.removeChild(toDelete); 1.616 + } 1.617 + }, 1.618 + 1.619 + _initMarkup: function InspectorPanel_initMarkup() { 1.620 + let doc = this.panelDoc; 1.621 + 1.622 + this._markupBox = doc.getElementById("markup-box"); 1.623 + 1.624 + // create tool iframe 1.625 + this._markupFrame = doc.createElement("iframe"); 1.626 + this._markupFrame.setAttribute("flex", "1"); 1.627 + this._markupFrame.setAttribute("tooltip", "aHTMLTooltip"); 1.628 + this._markupFrame.setAttribute("context", "inspector-node-popup"); 1.629 + 1.630 + // This is needed to enable tooltips inside the iframe document. 1.631 + this._boundMarkupFrameLoad = this._onMarkupFrameLoad.bind(this); 1.632 + this._markupFrame.addEventListener("load", this._boundMarkupFrameLoad, true); 1.633 + 1.634 + this._markupBox.setAttribute("collapsed", true); 1.635 + this._markupBox.appendChild(this._markupFrame); 1.636 + this._markupFrame.setAttribute("src", "chrome://browser/content/devtools/markup-view.xhtml"); 1.637 + }, 1.638 + 1.639 + _onMarkupFrameLoad: function InspectorPanel__onMarkupFrameLoad() { 1.640 + this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true); 1.641 + delete this._boundMarkupFrameLoad; 1.642 + 1.643 + this._markupFrame.contentWindow.focus(); 1.644 + 1.645 + this._markupBox.removeAttribute("collapsed"); 1.646 + 1.647 + let controllerWindow = this._toolbox.doc.defaultView; 1.648 + this.markup = new MarkupView(this, this._markupFrame, controllerWindow); 1.649 + 1.650 + this.emit("markuploaded"); 1.651 + }, 1.652 + 1.653 + _destroyMarkup: function InspectorPanel__destroyMarkup() { 1.654 + let destroyPromise; 1.655 + 1.656 + if (this._boundMarkupFrameLoad) { 1.657 + this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true); 1.658 + this._boundMarkupFrameLoad = null; 1.659 + } 1.660 + 1.661 + if (this.markup) { 1.662 + destroyPromise = this.markup.destroy(); 1.663 + this.markup = null; 1.664 + } else { 1.665 + destroyPromise = promise.resolve(); 1.666 + } 1.667 + 1.668 + if (this._markupFrame) { 1.669 + this._markupFrame.parentNode.removeChild(this._markupFrame); 1.670 + this._markupFrame = null; 1.671 + } 1.672 + 1.673 + this._markupBox = null; 1.674 + 1.675 + return destroyPromise; 1.676 + }, 1.677 + 1.678 + /** 1.679 + * Toggle a pseudo class. 1.680 + */ 1.681 + togglePseudoClass: function InspectorPanel_togglePseudoClass(aPseudo) { 1.682 + if (this.selection.isElementNode()) { 1.683 + let node = this.selection.nodeFront; 1.684 + if (node.hasPseudoClassLock(aPseudo)) { 1.685 + return this.walker.removePseudoClassLock(node, aPseudo, {parents: true}); 1.686 + } 1.687 + 1.688 + let hierarchical = aPseudo == ":hover" || aPseudo == ":active"; 1.689 + return this.walker.addPseudoClassLock(node, aPseudo, {parents: hierarchical}); 1.690 + } 1.691 + }, 1.692 + 1.693 + /** 1.694 + * Clear any pseudo-class locks applied to the current hierarchy. 1.695 + */ 1.696 + clearPseudoClasses: function InspectorPanel_clearPseudoClasses() { 1.697 + if (!this.walker) { 1.698 + return; 1.699 + } 1.700 + return this.walker.clearPseudoClassLocks().then(null, console.error); 1.701 + }, 1.702 + 1.703 + /** 1.704 + * Edit the outerHTML of the selected Node. 1.705 + */ 1.706 + editHTML: function InspectorPanel_editHTML() 1.707 + { 1.708 + if (!this.selection.isNode()) { 1.709 + return; 1.710 + } 1.711 + if (this.markup) { 1.712 + this.markup.beginEditingOuterHTML(this.selection.nodeFront); 1.713 + } 1.714 + }, 1.715 + 1.716 + /** 1.717 + * Copy the innerHTML of the selected Node to the clipboard. 1.718 + */ 1.719 + copyInnerHTML: function InspectorPanel_copyInnerHTML() 1.720 + { 1.721 + if (!this.selection.isNode()) { 1.722 + return; 1.723 + } 1.724 + this._copyLongStr(this.walker.innerHTML(this.selection.nodeFront)); 1.725 + }, 1.726 + 1.727 + /** 1.728 + * Copy the outerHTML of the selected Node to the clipboard. 1.729 + */ 1.730 + copyOuterHTML: function InspectorPanel_copyOuterHTML() 1.731 + { 1.732 + if (!this.selection.isNode()) { 1.733 + return; 1.734 + } 1.735 + 1.736 + this._copyLongStr(this.walker.outerHTML(this.selection.nodeFront)); 1.737 + }, 1.738 + 1.739 + /** 1.740 + * Copy the data-uri for the currently selected image in the clipboard. 1.741 + */ 1.742 + copyImageDataUri: function InspectorPanel_copyImageDataUri() 1.743 + { 1.744 + let container = this.markup.getContainer(this.selection.nodeFront); 1.745 + if (container && container.isPreviewable()) { 1.746 + container.copyImageDataUri(); 1.747 + } 1.748 + }, 1.749 + 1.750 + _copyLongStr: function InspectorPanel_copyLongStr(promise) 1.751 + { 1.752 + return promise.then(longstr => { 1.753 + return longstr.string().then(toCopy => { 1.754 + longstr.release().then(null, console.error); 1.755 + clipboardHelper.copyString(toCopy); 1.756 + }); 1.757 + }).then(null, console.error); 1.758 + }, 1.759 + 1.760 + /** 1.761 + * Copy a unique selector of the selected Node to the clipboard. 1.762 + */ 1.763 + copyUniqueSelector: function InspectorPanel_copyUniqueSelector() 1.764 + { 1.765 + if (!this.selection.isNode()) { 1.766 + return; 1.767 + } 1.768 + 1.769 + let toCopy = CssLogic.findCssSelector(this.selection.node); 1.770 + if (toCopy) { 1.771 + clipboardHelper.copyString(toCopy); 1.772 + } 1.773 + }, 1.774 + 1.775 + /** 1.776 + * Delete the selected node. 1.777 + */ 1.778 + deleteNode: function IUI_deleteNode() { 1.779 + if (!this.selection.isNode() || 1.780 + this.selection.isRoot()) { 1.781 + return; 1.782 + } 1.783 + 1.784 + // If the markup panel is active, use the markup panel to delete 1.785 + // the node, making this an undoable action. 1.786 + if (this.markup) { 1.787 + this.markup.deleteNode(this.selection.nodeFront); 1.788 + } else { 1.789 + // remove the node from content 1.790 + this.walker.removeNode(this.selection.nodeFront); 1.791 + } 1.792 + }, 1.793 + 1.794 + /** 1.795 + * Trigger a high-priority layout change for things that need to be 1.796 + * updated immediately 1.797 + */ 1.798 + immediateLayoutChange: function Inspector_immediateLayoutChange() 1.799 + { 1.800 + this.emit("layout-change"); 1.801 + }, 1.802 + 1.803 + /** 1.804 + * Schedule a low-priority change event for things like paint 1.805 + * and resize. 1.806 + */ 1.807 + scheduleLayoutChange: function Inspector_scheduleLayoutChange(event) 1.808 + { 1.809 + // Filter out non browser window resize events (i.e. triggered by iframes) 1.810 + if (this.browser.contentWindow === event.target) { 1.811 + if (this._timer) { 1.812 + return null; 1.813 + } 1.814 + this._timer = this.panelWin.setTimeout(function() { 1.815 + this.emit("layout-change"); 1.816 + this._timer = null; 1.817 + }.bind(this), LAYOUT_CHANGE_TIMER); 1.818 + } 1.819 + }, 1.820 + 1.821 + /** 1.822 + * Cancel a pending low-priority change event if any is 1.823 + * scheduled. 1.824 + */ 1.825 + cancelLayoutChange: function Inspector_cancelLayoutChange() 1.826 + { 1.827 + if (this._timer) { 1.828 + this.panelWin.clearTimeout(this._timer); 1.829 + delete this._timer; 1.830 + } 1.831 + } 1.832 +}; 1.833 + 1.834 +///////////////////////////////////////////////////////////////////////// 1.835 +//// Initializers 1.836 + 1.837 +loader.lazyGetter(InspectorPanel.prototype, "strings", 1.838 + function () { 1.839 + return Services.strings.createBundle( 1.840 + "chrome://browser/locale/devtools/inspector.properties"); 1.841 + }); 1.842 + 1.843 +loader.lazyGetter(this, "clipboardHelper", function() { 1.844 + return Cc["@mozilla.org/widget/clipboardhelper;1"]. 1.845 + getService(Ci.nsIClipboardHelper); 1.846 +}); 1.847 + 1.848 + 1.849 +loader.lazyGetter(this, "DOMUtils", function () { 1.850 + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); 1.851 +});