browser/devtools/inspector/inspector-panel.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
michael@0 3 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6
michael@0 7 const {Cc, Ci, Cu, Cr} = require("chrome");
michael@0 8
michael@0 9 Cu.import("resource://gre/modules/Services.jsm");
michael@0 10
michael@0 11 let promise = require("devtools/toolkit/deprecated-sync-thenables");
michael@0 12 let EventEmitter = require("devtools/toolkit/event-emitter");
michael@0 13 let {CssLogic} = require("devtools/styleinspector/css-logic");
michael@0 14
michael@0 15 loader.lazyGetter(this, "MarkupView", () => require("devtools/markupview/markup-view").MarkupView);
michael@0 16 loader.lazyGetter(this, "HTMLBreadcrumbs", () => require("devtools/inspector/breadcrumbs").HTMLBreadcrumbs);
michael@0 17 loader.lazyGetter(this, "ToolSidebar", () => require("devtools/framework/sidebar").ToolSidebar);
michael@0 18 loader.lazyGetter(this, "SelectorSearch", () => require("devtools/inspector/selector-search").SelectorSearch);
michael@0 19
michael@0 20 const LAYOUT_CHANGE_TIMER = 250;
michael@0 21
michael@0 22 /**
michael@0 23 * Represents an open instance of the Inspector for a tab.
michael@0 24 * The inspector controls the breadcrumbs, the markup view, and the sidebar
michael@0 25 * (computed view, rule view, font view and layout view).
michael@0 26 *
michael@0 27 * Events:
michael@0 28 * - ready
michael@0 29 * Fired when the inspector panel is opened for the first time and ready to
michael@0 30 * use
michael@0 31 * - new-root
michael@0 32 * Fired after a new root (navigation to a new page) event was fired by
michael@0 33 * the walker, and taken into account by the inspector (after the markup
michael@0 34 * view has been reloaded)
michael@0 35 * - markuploaded
michael@0 36 * Fired when the markup-view frame has loaded
michael@0 37 * - layout-change
michael@0 38 * Fired when the layout of the inspector changes
michael@0 39 * - breadcrumbs-updated
michael@0 40 * Fired when the breadcrumb widget updates to a new node
michael@0 41 * - layoutview-updated
michael@0 42 * Fired when the layoutview (box model) updates to a new node
michael@0 43 * - markupmutation
michael@0 44 * Fired after markup mutations have been processed by the markup-view
michael@0 45 * - computed-view-refreshed
michael@0 46 * Fired when the computed rules view updates to a new node
michael@0 47 * - computed-view-property-expanded
michael@0 48 * Fired when a property is expanded in the computed rules view
michael@0 49 * - computed-view-property-collapsed
michael@0 50 * Fired when a property is collapsed in the computed rules view
michael@0 51 * - rule-view-refreshed
michael@0 52 * Fired when the rule view updates to a new node
michael@0 53 */
michael@0 54 function InspectorPanel(iframeWindow, toolbox) {
michael@0 55 this._toolbox = toolbox;
michael@0 56 this._target = toolbox._target;
michael@0 57 this.panelDoc = iframeWindow.document;
michael@0 58 this.panelWin = iframeWindow;
michael@0 59 this.panelWin.inspector = this;
michael@0 60 this._inspector = null;
michael@0 61
michael@0 62 this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
michael@0 63 this._target.on("will-navigate", this._onBeforeNavigate);
michael@0 64
michael@0 65 EventEmitter.decorate(this);
michael@0 66 }
michael@0 67
michael@0 68 exports.InspectorPanel = InspectorPanel;
michael@0 69
michael@0 70 InspectorPanel.prototype = {
michael@0 71 /**
michael@0 72 * open is effectively an asynchronous constructor
michael@0 73 */
michael@0 74 open: function InspectorPanel_open() {
michael@0 75 return this.target.makeRemote().then(() => {
michael@0 76 return this._getPageStyle();
michael@0 77 }).then(() => {
michael@0 78 return this._getDefaultNodeForSelection();
michael@0 79 }).then(defaultSelection => {
michael@0 80 return this._deferredOpen(defaultSelection);
michael@0 81 }).then(null, console.error);
michael@0 82 },
michael@0 83
michael@0 84 get toolbox() {
michael@0 85 return this._toolbox;
michael@0 86 },
michael@0 87
michael@0 88 get inspector() {
michael@0 89 return this._toolbox.inspector;
michael@0 90 },
michael@0 91
michael@0 92 get walker() {
michael@0 93 return this._toolbox.walker;
michael@0 94 },
michael@0 95
michael@0 96 get selection() {
michael@0 97 return this._toolbox.selection;
michael@0 98 },
michael@0 99
michael@0 100 get isOuterHTMLEditable() {
michael@0 101 return this._target.client.traits.editOuterHTML;
michael@0 102 },
michael@0 103
michael@0 104 get hasUrlToImageDataResolver() {
michael@0 105 return this._target.client.traits.urlToImageDataResolver;
michael@0 106 },
michael@0 107
michael@0 108 _deferredOpen: function(defaultSelection) {
michael@0 109 let deferred = promise.defer();
michael@0 110
michael@0 111 this.onNewRoot = this.onNewRoot.bind(this);
michael@0 112 this.walker.on("new-root", this.onNewRoot);
michael@0 113
michael@0 114 this.nodemenu = this.panelDoc.getElementById("inspector-node-popup");
michael@0 115 this.lastNodemenuItem = this.nodemenu.lastChild;
michael@0 116 this._setupNodeMenu = this._setupNodeMenu.bind(this);
michael@0 117 this._resetNodeMenu = this._resetNodeMenu.bind(this);
michael@0 118 this.nodemenu.addEventListener("popupshowing", this._setupNodeMenu, true);
michael@0 119 this.nodemenu.addEventListener("popuphiding", this._resetNodeMenu, true);
michael@0 120
michael@0 121 this.onNewSelection = this.onNewSelection.bind(this);
michael@0 122 this.selection.on("new-node-front", this.onNewSelection);
michael@0 123 this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this);
michael@0 124 this.selection.on("before-new-node-front", this.onBeforeNewSelection);
michael@0 125 this.onDetached = this.onDetached.bind(this);
michael@0 126 this.selection.on("detached-front", this.onDetached);
michael@0 127
michael@0 128 this.breadcrumbs = new HTMLBreadcrumbs(this);
michael@0 129
michael@0 130 if (this.target.isLocalTab) {
michael@0 131 this.browser = this.target.tab.linkedBrowser;
michael@0 132 this.scheduleLayoutChange = this.scheduleLayoutChange.bind(this);
michael@0 133 this.browser.addEventListener("resize", this.scheduleLayoutChange, true);
michael@0 134
michael@0 135 // Show a warning when the debugger is paused.
michael@0 136 // We show the warning only when the inspector
michael@0 137 // is selected.
michael@0 138 this.updateDebuggerPausedWarning = function() {
michael@0 139 let notificationBox = this._toolbox.getNotificationBox();
michael@0 140 let notification = notificationBox.getNotificationWithValue("inspector-script-paused");
michael@0 141 if (!notification && this._toolbox.currentToolId == "inspector" &&
michael@0 142 this.target.isThreadPaused) {
michael@0 143 let message = this.strings.GetStringFromName("debuggerPausedWarning.message");
michael@0 144 notificationBox.appendNotification(message,
michael@0 145 "inspector-script-paused", "", notificationBox.PRIORITY_WARNING_HIGH);
michael@0 146 }
michael@0 147
michael@0 148 if (notification && this._toolbox.currentToolId != "inspector") {
michael@0 149 notificationBox.removeNotification(notification);
michael@0 150 }
michael@0 151
michael@0 152 if (notification && !this.target.isThreadPaused) {
michael@0 153 notificationBox.removeNotification(notification);
michael@0 154 }
michael@0 155
michael@0 156 }.bind(this);
michael@0 157 this.target.on("thread-paused", this.updateDebuggerPausedWarning);
michael@0 158 this.target.on("thread-resumed", this.updateDebuggerPausedWarning);
michael@0 159 this._toolbox.on("select", this.updateDebuggerPausedWarning);
michael@0 160 this.updateDebuggerPausedWarning();
michael@0 161 }
michael@0 162
michael@0 163 this._initMarkup();
michael@0 164 this.isReady = false;
michael@0 165
michael@0 166 this.once("markuploaded", function() {
michael@0 167 this.isReady = true;
michael@0 168
michael@0 169 // All the components are initialized. Let's select a node.
michael@0 170 this.selection.setNodeFront(defaultSelection, "inspector-open");
michael@0 171
michael@0 172 this.markup.expandNode(this.selection.nodeFront);
michael@0 173
michael@0 174 this.emit("ready");
michael@0 175 deferred.resolve(this);
michael@0 176 }.bind(this));
michael@0 177
michael@0 178 this.setupSearchBox();
michael@0 179 this.setupSidebar();
michael@0 180
michael@0 181 return deferred.promise;
michael@0 182 },
michael@0 183
michael@0 184 _onBeforeNavigate: function() {
michael@0 185 this._defaultNode = null;
michael@0 186 this.selection.setNodeFront(null);
michael@0 187 this._destroyMarkup();
michael@0 188 this.isDirty = false;
michael@0 189 },
michael@0 190
michael@0 191 _getPageStyle: function() {
michael@0 192 return this._toolbox.inspector.getPageStyle().then(pageStyle => {
michael@0 193 this.pageStyle = pageStyle;
michael@0 194 });
michael@0 195 },
michael@0 196
michael@0 197 /**
michael@0 198 * Return a promise that will resolve to the default node for selection.
michael@0 199 */
michael@0 200 _getDefaultNodeForSelection: function() {
michael@0 201 if (this._defaultNode) {
michael@0 202 return this._defaultNode;
michael@0 203 }
michael@0 204 let walker = this.walker;
michael@0 205 let rootNode = null;
michael@0 206
michael@0 207 // If available, set either the previously selected node or the body
michael@0 208 // as default selected, else set documentElement
michael@0 209 return walker.getRootNode().then(aRootNode => {
michael@0 210 rootNode = aRootNode;
michael@0 211 return walker.querySelector(rootNode, this.selectionCssSelector);
michael@0 212 }).then(front => {
michael@0 213 if (front) {
michael@0 214 return front;
michael@0 215 }
michael@0 216 return walker.querySelector(rootNode, "body");
michael@0 217 }).then(front => {
michael@0 218 if (front) {
michael@0 219 return front;
michael@0 220 }
michael@0 221 return this.walker.documentElement(this.walker.rootNode);
michael@0 222 }).then(node => {
michael@0 223 if (walker !== this.walker) {
michael@0 224 promise.reject(null);
michael@0 225 }
michael@0 226 this._defaultNode = node;
michael@0 227 return node;
michael@0 228 });
michael@0 229 },
michael@0 230
michael@0 231 /**
michael@0 232 * Target getter.
michael@0 233 */
michael@0 234 get target() {
michael@0 235 return this._target;
michael@0 236 },
michael@0 237
michael@0 238 /**
michael@0 239 * Target setter.
michael@0 240 */
michael@0 241 set target(value) {
michael@0 242 this._target = value;
michael@0 243 },
michael@0 244
michael@0 245 /**
michael@0 246 * Expose gViewSourceUtils so that other tools can make use of them.
michael@0 247 */
michael@0 248 get viewSourceUtils() {
michael@0 249 return this.panelWin.gViewSourceUtils;
michael@0 250 },
michael@0 251
michael@0 252 /**
michael@0 253 * Indicate that a tool has modified the state of the page. Used to
michael@0 254 * decide whether to show the "are you sure you want to navigate"
michael@0 255 * notification.
michael@0 256 */
michael@0 257 markDirty: function InspectorPanel_markDirty() {
michael@0 258 this.isDirty = true;
michael@0 259 },
michael@0 260
michael@0 261 /**
michael@0 262 * Hooks the searchbar to show result and auto completion suggestions.
michael@0 263 */
michael@0 264 setupSearchBox: function InspectorPanel_setupSearchBox() {
michael@0 265 // Initiate the selectors search object.
michael@0 266 if (this.searchSuggestions) {
michael@0 267 this.searchSuggestions.destroy();
michael@0 268 this.searchSuggestions = null;
michael@0 269 }
michael@0 270 this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
michael@0 271 this.searchSuggestions = new SelectorSearch(this, this.searchBox);
michael@0 272 },
michael@0 273
michael@0 274 /**
michael@0 275 * Build the sidebar.
michael@0 276 */
michael@0 277 setupSidebar: function InspectorPanel_setupSidebar() {
michael@0 278 let tabbox = this.panelDoc.querySelector("#inspector-sidebar");
michael@0 279 this.sidebar = new ToolSidebar(tabbox, this, "inspector");
michael@0 280
michael@0 281 let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");
michael@0 282
michael@0 283 this._setDefaultSidebar = function(event, toolId) {
michael@0 284 Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
michael@0 285 }.bind(this);
michael@0 286
michael@0 287 this.sidebar.on("select", this._setDefaultSidebar);
michael@0 288
michael@0 289 this.sidebar.addTab("ruleview",
michael@0 290 "chrome://browser/content/devtools/cssruleview.xhtml",
michael@0 291 "ruleview" == defaultTab);
michael@0 292
michael@0 293 this.sidebar.addTab("computedview",
michael@0 294 "chrome://browser/content/devtools/computedview.xhtml",
michael@0 295 "computedview" == defaultTab);
michael@0 296
michael@0 297 if (Services.prefs.getBoolPref("devtools.fontinspector.enabled") && !this.target.isRemote) {
michael@0 298 this.sidebar.addTab("fontinspector",
michael@0 299 "chrome://browser/content/devtools/fontinspector/font-inspector.xhtml",
michael@0 300 "fontinspector" == defaultTab);
michael@0 301 }
michael@0 302
michael@0 303 this.sidebar.addTab("layoutview",
michael@0 304 "chrome://browser/content/devtools/layoutview/view.xhtml",
michael@0 305 "layoutview" == defaultTab);
michael@0 306
michael@0 307 let ruleViewTab = this.sidebar.getTab("ruleview");
michael@0 308
michael@0 309 this.sidebar.show();
michael@0 310 },
michael@0 311
michael@0 312 /**
michael@0 313 * Reset the inspector on new root mutation.
michael@0 314 */
michael@0 315 onNewRoot: function InspectorPanel_onNewRoot() {
michael@0 316 this._defaultNode = null;
michael@0 317 this.selection.setNodeFront(null);
michael@0 318 this._destroyMarkup();
michael@0 319 this.isDirty = false;
michael@0 320
michael@0 321 let onNodeSelected = defaultNode => {
michael@0 322 // Cancel this promise resolution as a new one had
michael@0 323 // been queued up.
michael@0 324 if (this._pendingSelection != onNodeSelected) {
michael@0 325 return;
michael@0 326 }
michael@0 327 this._pendingSelection = null;
michael@0 328 this.selection.setNodeFront(defaultNode, "navigateaway");
michael@0 329
michael@0 330 this._initMarkup();
michael@0 331 this.once("markuploaded", () => {
michael@0 332 if (!this.markup) {
michael@0 333 return;
michael@0 334 }
michael@0 335 this.markup.expandNode(this.selection.nodeFront);
michael@0 336 this.setupSearchBox();
michael@0 337 this.emit("new-root");
michael@0 338 });
michael@0 339 };
michael@0 340 this._pendingSelection = onNodeSelected;
michael@0 341 this._getDefaultNodeForSelection().then(onNodeSelected);
michael@0 342 },
michael@0 343
michael@0 344 _selectionCssSelector: null,
michael@0 345
michael@0 346 /**
michael@0 347 * Set the currently selected node unique css selector.
michael@0 348 * Will store the current target url along with it to allow pre-selection at
michael@0 349 * reload
michael@0 350 */
michael@0 351 set selectionCssSelector(cssSelector) {
michael@0 352 this._selectionCssSelector = {
michael@0 353 selector: cssSelector,
michael@0 354 url: this._target.url
michael@0 355 };
michael@0 356 },
michael@0 357
michael@0 358 /**
michael@0 359 * Get the current selection unique css selector if any, that is, if a node
michael@0 360 * is actually selected and that node has been selected while on the same url
michael@0 361 */
michael@0 362 get selectionCssSelector() {
michael@0 363 if (this._selectionCssSelector &&
michael@0 364 this._selectionCssSelector.url === this._target.url) {
michael@0 365 return this._selectionCssSelector.selector;
michael@0 366 } else {
michael@0 367 return null;
michael@0 368 }
michael@0 369 },
michael@0 370
michael@0 371 /**
michael@0 372 * When a new node is selected.
michael@0 373 */
michael@0 374 onNewSelection: function InspectorPanel_onNewSelection(event, value, reason) {
michael@0 375 if (reason === "selection-destroy") {
michael@0 376 return;
michael@0 377 }
michael@0 378
michael@0 379 this.cancelLayoutChange();
michael@0 380
michael@0 381 // Wait for all the known tools to finish updating and then let the
michael@0 382 // client know.
michael@0 383 let selection = this.selection.nodeFront;
michael@0 384
michael@0 385 // On any new selection made by the user, store the unique css selector
michael@0 386 // of the selected node so it can be restored after reload of the same page
michael@0 387 if (reason !== "navigateaway" &&
michael@0 388 this.selection.node &&
michael@0 389 this.selection.isElementNode()) {
michael@0 390 this.selectionCssSelector = CssLogic.findCssSelector(this.selection.node);
michael@0 391 }
michael@0 392
michael@0 393 let selfUpdate = this.updating("inspector-panel");
michael@0 394 Services.tm.mainThread.dispatch(() => {
michael@0 395 try {
michael@0 396 selfUpdate(selection);
michael@0 397 } catch(ex) {
michael@0 398 console.error(ex);
michael@0 399 }
michael@0 400 }, Ci.nsIThread.DISPATCH_NORMAL);
michael@0 401 },
michael@0 402
michael@0 403 /**
michael@0 404 * Delay the "inspector-updated" notification while a tool
michael@0 405 * is updating itself. Returns a function that must be
michael@0 406 * invoked when the tool is done updating with the node
michael@0 407 * that the tool is viewing.
michael@0 408 */
michael@0 409 updating: function(name) {
michael@0 410 if (this._updateProgress && this._updateProgress.node != this.selection.nodeFront) {
michael@0 411 this.cancelUpdate();
michael@0 412 }
michael@0 413
michael@0 414 if (!this._updateProgress) {
michael@0 415 // Start an update in progress.
michael@0 416 var self = this;
michael@0 417 this._updateProgress = {
michael@0 418 node: this.selection.nodeFront,
michael@0 419 outstanding: new Set(),
michael@0 420 checkDone: function() {
michael@0 421 if (this !== self._updateProgress) {
michael@0 422 return;
michael@0 423 }
michael@0 424 if (this.node !== self.selection.nodeFront) {
michael@0 425 self.cancelUpdate();
michael@0 426 return;
michael@0 427 }
michael@0 428 if (this.outstanding.size !== 0) {
michael@0 429 return;
michael@0 430 }
michael@0 431
michael@0 432 self._updateProgress = null;
michael@0 433 self.emit("inspector-updated", name);
michael@0 434 },
michael@0 435 };
michael@0 436 }
michael@0 437
michael@0 438 let progress = this._updateProgress;
michael@0 439 let done = function() {
michael@0 440 progress.outstanding.delete(done);
michael@0 441 progress.checkDone();
michael@0 442 };
michael@0 443 progress.outstanding.add(done);
michael@0 444 return done;
michael@0 445 },
michael@0 446
michael@0 447 /**
michael@0 448 * Cancel notification of inspector updates.
michael@0 449 */
michael@0 450 cancelUpdate: function() {
michael@0 451 this._updateProgress = null;
michael@0 452 },
michael@0 453
michael@0 454 /**
michael@0 455 * When a new node is selected, before the selection has changed.
michael@0 456 */
michael@0 457 onBeforeNewSelection: function InspectorPanel_onBeforeNewSelection(event,
michael@0 458 node) {
michael@0 459 if (this.breadcrumbs.indexOf(node) == -1) {
michael@0 460 // only clear locks if we'd have to update breadcrumbs
michael@0 461 this.clearPseudoClasses();
michael@0 462 }
michael@0 463 },
michael@0 464
michael@0 465 /**
michael@0 466 * When a node is deleted, select its parent node or the defaultNode if no
michael@0 467 * parent is found (may happen when deleting an iframe inside which the
michael@0 468 * node was selected).
michael@0 469 */
michael@0 470 onDetached: function InspectorPanel_onDetached(event, parentNode) {
michael@0 471 this.cancelLayoutChange();
michael@0 472 this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode));
michael@0 473 this.selection.setNodeFront(parentNode ? parentNode : this._defaultNode, "detached");
michael@0 474 },
michael@0 475
michael@0 476 /**
michael@0 477 * Destroy the inspector.
michael@0 478 */
michael@0 479 destroy: function InspectorPanel__destroy() {
michael@0 480 if (this._panelDestroyer) {
michael@0 481 return this._panelDestroyer;
michael@0 482 }
michael@0 483
michael@0 484 if (this.walker) {
michael@0 485 this.walker.off("new-root", this.onNewRoot);
michael@0 486 this.pageStyle = null;
michael@0 487 }
michael@0 488
michael@0 489 this.cancelUpdate();
michael@0 490 this.cancelLayoutChange();
michael@0 491
michael@0 492 if (this.browser) {
michael@0 493 this.browser.removeEventListener("resize", this.scheduleLayoutChange, true);
michael@0 494 this.browser = null;
michael@0 495 }
michael@0 496
michael@0 497 this.target.off("will-navigate", this._onBeforeNavigate);
michael@0 498
michael@0 499 this.target.off("thread-paused", this.updateDebuggerPausedWarning);
michael@0 500 this.target.off("thread-resumed", this.updateDebuggerPausedWarning);
michael@0 501 this._toolbox.off("select", this.updateDebuggerPausedWarning);
michael@0 502
michael@0 503 this.sidebar.off("select", this._setDefaultSidebar);
michael@0 504 this.sidebar.destroy();
michael@0 505 this.sidebar = null;
michael@0 506
michael@0 507 this.nodemenu.removeEventListener("popupshowing", this._setupNodeMenu, true);
michael@0 508 this.nodemenu.removeEventListener("popuphiding", this._resetNodeMenu, true);
michael@0 509 this.breadcrumbs.destroy();
michael@0 510 this.searchSuggestions.destroy();
michael@0 511 this.searchBox = null;
michael@0 512 this.selection.off("new-node-front", this.onNewSelection);
michael@0 513 this.selection.off("before-new-node", this.onBeforeNewSelection);
michael@0 514 this.selection.off("before-new-node-front", this.onBeforeNewSelection);
michael@0 515 this.selection.off("detached-front", this.onDetached);
michael@0 516 this._panelDestroyer = this._destroyMarkup();
michael@0 517 this.panelWin.inspector = null;
michael@0 518 this.target = null;
michael@0 519 this.panelDoc = null;
michael@0 520 this.panelWin = null;
michael@0 521 this.breadcrumbs = null;
michael@0 522 this.searchSuggestions = null;
michael@0 523 this.lastNodemenuItem = null;
michael@0 524 this.nodemenu = null;
michael@0 525 this._toolbox = null;
michael@0 526
michael@0 527 return this._panelDestroyer;
michael@0 528 },
michael@0 529
michael@0 530 /**
michael@0 531 * Show the node menu.
michael@0 532 */
michael@0 533 showNodeMenu: function InspectorPanel_showNodeMenu(aButton, aPosition, aExtraItems) {
michael@0 534 if (aExtraItems) {
michael@0 535 for (let item of aExtraItems) {
michael@0 536 this.nodemenu.appendChild(item);
michael@0 537 }
michael@0 538 }
michael@0 539 this.nodemenu.openPopup(aButton, aPosition, 0, 0, true, false);
michael@0 540 },
michael@0 541
michael@0 542 hideNodeMenu: function InspectorPanel_hideNodeMenu() {
michael@0 543 this.nodemenu.hidePopup();
michael@0 544 },
michael@0 545
michael@0 546 /**
michael@0 547 * Disable the delete item if needed. Update the pseudo classes.
michael@0 548 */
michael@0 549 _setupNodeMenu: function InspectorPanel_setupNodeMenu() {
michael@0 550 let isSelectionElement = this.selection.isElementNode();
michael@0 551
michael@0 552 // Set the pseudo classes
michael@0 553 for (let name of ["hover", "active", "focus"]) {
michael@0 554 let menu = this.panelDoc.getElementById("node-menu-pseudo-" + name);
michael@0 555
michael@0 556 if (isSelectionElement) {
michael@0 557 let checked = this.selection.nodeFront.hasPseudoClassLock(":" + name);
michael@0 558 menu.setAttribute("checked", checked);
michael@0 559 menu.removeAttribute("disabled");
michael@0 560 } else {
michael@0 561 menu.setAttribute("disabled", "true");
michael@0 562 }
michael@0 563 }
michael@0 564
michael@0 565 // Disable delete item if needed
michael@0 566 let deleteNode = this.panelDoc.getElementById("node-menu-delete");
michael@0 567 if (this.selection.isRoot() || this.selection.isDocumentTypeNode()) {
michael@0 568 deleteNode.setAttribute("disabled", "true");
michael@0 569 } else {
michael@0 570 deleteNode.removeAttribute("disabled");
michael@0 571 }
michael@0 572
michael@0 573 // Disable / enable "Copy Unique Selector", "Copy inner HTML" &
michael@0 574 // "Copy outer HTML" as appropriate
michael@0 575 let unique = this.panelDoc.getElementById("node-menu-copyuniqueselector");
michael@0 576 let copyInnerHTML = this.panelDoc.getElementById("node-menu-copyinner");
michael@0 577 let copyOuterHTML = this.panelDoc.getElementById("node-menu-copyouter");
michael@0 578 if (isSelectionElement) {
michael@0 579 unique.removeAttribute("disabled");
michael@0 580 copyInnerHTML.removeAttribute("disabled");
michael@0 581 copyOuterHTML.removeAttribute("disabled");
michael@0 582 } else {
michael@0 583 unique.setAttribute("disabled", "true");
michael@0 584 copyInnerHTML.setAttribute("disabled", "true");
michael@0 585 copyOuterHTML.setAttribute("disabled", "true");
michael@0 586 }
michael@0 587
michael@0 588 // Enable the "edit HTML" item if the selection is an element and the root
michael@0 589 // actor has the appropriate trait (isOuterHTMLEditable)
michael@0 590 let editHTML = this.panelDoc.getElementById("node-menu-edithtml");
michael@0 591 if (this.isOuterHTMLEditable && isSelectionElement) {
michael@0 592 editHTML.removeAttribute("disabled");
michael@0 593 } else {
michael@0 594 editHTML.setAttribute("disabled", "true");
michael@0 595 }
michael@0 596
michael@0 597 // Enable the "copy image data-uri" item if the selection is previewable
michael@0 598 // which essentially checks if it's an image or canvas tag
michael@0 599 let copyImageData = this.panelDoc.getElementById("node-menu-copyimagedatauri");
michael@0 600 let markupContainer = this.markup.getContainer(this.selection.nodeFront);
michael@0 601 if (markupContainer && markupContainer.isPreviewable()) {
michael@0 602 copyImageData.removeAttribute("disabled");
michael@0 603 } else {
michael@0 604 copyImageData.setAttribute("disabled", "true");
michael@0 605 }
michael@0 606 },
michael@0 607
michael@0 608 _resetNodeMenu: function InspectorPanel_resetNodeMenu() {
michael@0 609 // Remove any extra items
michael@0 610 while (this.lastNodemenuItem.nextSibling) {
michael@0 611 let toDelete = this.lastNodemenuItem.nextSibling;
michael@0 612 toDelete.parentNode.removeChild(toDelete);
michael@0 613 }
michael@0 614 },
michael@0 615
michael@0 616 _initMarkup: function InspectorPanel_initMarkup() {
michael@0 617 let doc = this.panelDoc;
michael@0 618
michael@0 619 this._markupBox = doc.getElementById("markup-box");
michael@0 620
michael@0 621 // create tool iframe
michael@0 622 this._markupFrame = doc.createElement("iframe");
michael@0 623 this._markupFrame.setAttribute("flex", "1");
michael@0 624 this._markupFrame.setAttribute("tooltip", "aHTMLTooltip");
michael@0 625 this._markupFrame.setAttribute("context", "inspector-node-popup");
michael@0 626
michael@0 627 // This is needed to enable tooltips inside the iframe document.
michael@0 628 this._boundMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
michael@0 629 this._markupFrame.addEventListener("load", this._boundMarkupFrameLoad, true);
michael@0 630
michael@0 631 this._markupBox.setAttribute("collapsed", true);
michael@0 632 this._markupBox.appendChild(this._markupFrame);
michael@0 633 this._markupFrame.setAttribute("src", "chrome://browser/content/devtools/markup-view.xhtml");
michael@0 634 },
michael@0 635
michael@0 636 _onMarkupFrameLoad: function InspectorPanel__onMarkupFrameLoad() {
michael@0 637 this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true);
michael@0 638 delete this._boundMarkupFrameLoad;
michael@0 639
michael@0 640 this._markupFrame.contentWindow.focus();
michael@0 641
michael@0 642 this._markupBox.removeAttribute("collapsed");
michael@0 643
michael@0 644 let controllerWindow = this._toolbox.doc.defaultView;
michael@0 645 this.markup = new MarkupView(this, this._markupFrame, controllerWindow);
michael@0 646
michael@0 647 this.emit("markuploaded");
michael@0 648 },
michael@0 649
michael@0 650 _destroyMarkup: function InspectorPanel__destroyMarkup() {
michael@0 651 let destroyPromise;
michael@0 652
michael@0 653 if (this._boundMarkupFrameLoad) {
michael@0 654 this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true);
michael@0 655 this._boundMarkupFrameLoad = null;
michael@0 656 }
michael@0 657
michael@0 658 if (this.markup) {
michael@0 659 destroyPromise = this.markup.destroy();
michael@0 660 this.markup = null;
michael@0 661 } else {
michael@0 662 destroyPromise = promise.resolve();
michael@0 663 }
michael@0 664
michael@0 665 if (this._markupFrame) {
michael@0 666 this._markupFrame.parentNode.removeChild(this._markupFrame);
michael@0 667 this._markupFrame = null;
michael@0 668 }
michael@0 669
michael@0 670 this._markupBox = null;
michael@0 671
michael@0 672 return destroyPromise;
michael@0 673 },
michael@0 674
michael@0 675 /**
michael@0 676 * Toggle a pseudo class.
michael@0 677 */
michael@0 678 togglePseudoClass: function InspectorPanel_togglePseudoClass(aPseudo) {
michael@0 679 if (this.selection.isElementNode()) {
michael@0 680 let node = this.selection.nodeFront;
michael@0 681 if (node.hasPseudoClassLock(aPseudo)) {
michael@0 682 return this.walker.removePseudoClassLock(node, aPseudo, {parents: true});
michael@0 683 }
michael@0 684
michael@0 685 let hierarchical = aPseudo == ":hover" || aPseudo == ":active";
michael@0 686 return this.walker.addPseudoClassLock(node, aPseudo, {parents: hierarchical});
michael@0 687 }
michael@0 688 },
michael@0 689
michael@0 690 /**
michael@0 691 * Clear any pseudo-class locks applied to the current hierarchy.
michael@0 692 */
michael@0 693 clearPseudoClasses: function InspectorPanel_clearPseudoClasses() {
michael@0 694 if (!this.walker) {
michael@0 695 return;
michael@0 696 }
michael@0 697 return this.walker.clearPseudoClassLocks().then(null, console.error);
michael@0 698 },
michael@0 699
michael@0 700 /**
michael@0 701 * Edit the outerHTML of the selected Node.
michael@0 702 */
michael@0 703 editHTML: function InspectorPanel_editHTML()
michael@0 704 {
michael@0 705 if (!this.selection.isNode()) {
michael@0 706 return;
michael@0 707 }
michael@0 708 if (this.markup) {
michael@0 709 this.markup.beginEditingOuterHTML(this.selection.nodeFront);
michael@0 710 }
michael@0 711 },
michael@0 712
michael@0 713 /**
michael@0 714 * Copy the innerHTML of the selected Node to the clipboard.
michael@0 715 */
michael@0 716 copyInnerHTML: function InspectorPanel_copyInnerHTML()
michael@0 717 {
michael@0 718 if (!this.selection.isNode()) {
michael@0 719 return;
michael@0 720 }
michael@0 721 this._copyLongStr(this.walker.innerHTML(this.selection.nodeFront));
michael@0 722 },
michael@0 723
michael@0 724 /**
michael@0 725 * Copy the outerHTML of the selected Node to the clipboard.
michael@0 726 */
michael@0 727 copyOuterHTML: function InspectorPanel_copyOuterHTML()
michael@0 728 {
michael@0 729 if (!this.selection.isNode()) {
michael@0 730 return;
michael@0 731 }
michael@0 732
michael@0 733 this._copyLongStr(this.walker.outerHTML(this.selection.nodeFront));
michael@0 734 },
michael@0 735
michael@0 736 /**
michael@0 737 * Copy the data-uri for the currently selected image in the clipboard.
michael@0 738 */
michael@0 739 copyImageDataUri: function InspectorPanel_copyImageDataUri()
michael@0 740 {
michael@0 741 let container = this.markup.getContainer(this.selection.nodeFront);
michael@0 742 if (container && container.isPreviewable()) {
michael@0 743 container.copyImageDataUri();
michael@0 744 }
michael@0 745 },
michael@0 746
michael@0 747 _copyLongStr: function InspectorPanel_copyLongStr(promise)
michael@0 748 {
michael@0 749 return promise.then(longstr => {
michael@0 750 return longstr.string().then(toCopy => {
michael@0 751 longstr.release().then(null, console.error);
michael@0 752 clipboardHelper.copyString(toCopy);
michael@0 753 });
michael@0 754 }).then(null, console.error);
michael@0 755 },
michael@0 756
michael@0 757 /**
michael@0 758 * Copy a unique selector of the selected Node to the clipboard.
michael@0 759 */
michael@0 760 copyUniqueSelector: function InspectorPanel_copyUniqueSelector()
michael@0 761 {
michael@0 762 if (!this.selection.isNode()) {
michael@0 763 return;
michael@0 764 }
michael@0 765
michael@0 766 let toCopy = CssLogic.findCssSelector(this.selection.node);
michael@0 767 if (toCopy) {
michael@0 768 clipboardHelper.copyString(toCopy);
michael@0 769 }
michael@0 770 },
michael@0 771
michael@0 772 /**
michael@0 773 * Delete the selected node.
michael@0 774 */
michael@0 775 deleteNode: function IUI_deleteNode() {
michael@0 776 if (!this.selection.isNode() ||
michael@0 777 this.selection.isRoot()) {
michael@0 778 return;
michael@0 779 }
michael@0 780
michael@0 781 // If the markup panel is active, use the markup panel to delete
michael@0 782 // the node, making this an undoable action.
michael@0 783 if (this.markup) {
michael@0 784 this.markup.deleteNode(this.selection.nodeFront);
michael@0 785 } else {
michael@0 786 // remove the node from content
michael@0 787 this.walker.removeNode(this.selection.nodeFront);
michael@0 788 }
michael@0 789 },
michael@0 790
michael@0 791 /**
michael@0 792 * Trigger a high-priority layout change for things that need to be
michael@0 793 * updated immediately
michael@0 794 */
michael@0 795 immediateLayoutChange: function Inspector_immediateLayoutChange()
michael@0 796 {
michael@0 797 this.emit("layout-change");
michael@0 798 },
michael@0 799
michael@0 800 /**
michael@0 801 * Schedule a low-priority change event for things like paint
michael@0 802 * and resize.
michael@0 803 */
michael@0 804 scheduleLayoutChange: function Inspector_scheduleLayoutChange(event)
michael@0 805 {
michael@0 806 // Filter out non browser window resize events (i.e. triggered by iframes)
michael@0 807 if (this.browser.contentWindow === event.target) {
michael@0 808 if (this._timer) {
michael@0 809 return null;
michael@0 810 }
michael@0 811 this._timer = this.panelWin.setTimeout(function() {
michael@0 812 this.emit("layout-change");
michael@0 813 this._timer = null;
michael@0 814 }.bind(this), LAYOUT_CHANGE_TIMER);
michael@0 815 }
michael@0 816 },
michael@0 817
michael@0 818 /**
michael@0 819 * Cancel a pending low-priority change event if any is
michael@0 820 * scheduled.
michael@0 821 */
michael@0 822 cancelLayoutChange: function Inspector_cancelLayoutChange()
michael@0 823 {
michael@0 824 if (this._timer) {
michael@0 825 this.panelWin.clearTimeout(this._timer);
michael@0 826 delete this._timer;
michael@0 827 }
michael@0 828 }
michael@0 829 };
michael@0 830
michael@0 831 /////////////////////////////////////////////////////////////////////////
michael@0 832 //// Initializers
michael@0 833
michael@0 834 loader.lazyGetter(InspectorPanel.prototype, "strings",
michael@0 835 function () {
michael@0 836 return Services.strings.createBundle(
michael@0 837 "chrome://browser/locale/devtools/inspector.properties");
michael@0 838 });
michael@0 839
michael@0 840 loader.lazyGetter(this, "clipboardHelper", function() {
michael@0 841 return Cc["@mozilla.org/widget/clipboardhelper;1"].
michael@0 842 getService(Ci.nsIClipboardHelper);
michael@0 843 });
michael@0 844
michael@0 845
michael@0 846 loader.lazyGetter(this, "DOMUtils", function () {
michael@0 847 return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
michael@0 848 });

mercurial