browser/devtools/markupview/markup-view.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, Cu, Ci} = require("chrome");
michael@0 8
michael@0 9 // Page size for pageup/pagedown
michael@0 10 const PAGE_SIZE = 10;
michael@0 11 const PREVIEW_AREA = 700;
michael@0 12 const DEFAULT_MAX_CHILDREN = 100;
michael@0 13 const COLLAPSE_ATTRIBUTE_LENGTH = 120;
michael@0 14 const COLLAPSE_DATA_URL_REGEX = /^data.+base64/;
michael@0 15 const COLLAPSE_DATA_URL_LENGTH = 60;
michael@0 16 const CONTAINER_FLASHING_DURATION = 500;
michael@0 17 const NEW_SELECTION_HIGHLIGHTER_TIMER = 1000;
michael@0 18
michael@0 19 const {UndoStack} = require("devtools/shared/undo");
michael@0 20 const {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
michael@0 21 const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
michael@0 22 const {HTMLEditor} = require("devtools/markupview/html-editor");
michael@0 23 const promise = require("devtools/toolkit/deprecated-sync-thenables");
michael@0 24 const {Tooltip} = require("devtools/shared/widgets/Tooltip");
michael@0 25 const EventEmitter = require("devtools/toolkit/event-emitter");
michael@0 26
michael@0 27 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
michael@0 28 Cu.import("resource://gre/modules/devtools/Templater.jsm");
michael@0 29 Cu.import("resource://gre/modules/Services.jsm");
michael@0 30 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 31
michael@0 32 loader.lazyGetter(this, "DOMParser", function() {
michael@0 33 return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
michael@0 34 });
michael@0 35 loader.lazyGetter(this, "AutocompletePopup", () => {
michael@0 36 return require("devtools/shared/autocomplete-popup").AutocompletePopup
michael@0 37 });
michael@0 38
michael@0 39 /**
michael@0 40 * Vocabulary for the purposes of this file:
michael@0 41 *
michael@0 42 * MarkupContainer - the structure that holds an editor and its
michael@0 43 * immediate children in the markup panel.
michael@0 44 * Node - A content node.
michael@0 45 * object.elt - A UI element in the markup panel.
michael@0 46 */
michael@0 47
michael@0 48 /**
michael@0 49 * The markup tree. Manages the mapping of nodes to MarkupContainers,
michael@0 50 * updating based on mutations, and the undo/redo bindings.
michael@0 51 *
michael@0 52 * @param Inspector aInspector
michael@0 53 * The inspector we're watching.
michael@0 54 * @param iframe aFrame
michael@0 55 * An iframe in which the caller has kindly loaded markup-view.xhtml.
michael@0 56 */
michael@0 57 function MarkupView(aInspector, aFrame, aControllerWindow) {
michael@0 58 this._inspector = aInspector;
michael@0 59 this.walker = this._inspector.walker;
michael@0 60 this._frame = aFrame;
michael@0 61 this.doc = this._frame.contentDocument;
michael@0 62 this._elt = this.doc.querySelector("#root");
michael@0 63 this.htmlEditor = new HTMLEditor(this.doc);
michael@0 64
michael@0 65 this.layoutHelpers = new LayoutHelpers(this.doc.defaultView);
michael@0 66
michael@0 67 try {
michael@0 68 this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize");
michael@0 69 } catch(ex) {
michael@0 70 this.maxChildren = DEFAULT_MAX_CHILDREN;
michael@0 71 }
michael@0 72
michael@0 73 // Creating the popup to be used to show CSS suggestions.
michael@0 74 let options = {
michael@0 75 autoSelect: true,
michael@0 76 theme: "auto"
michael@0 77 };
michael@0 78 this.popup = new AutocompletePopup(this.doc.defaultView.parent.document, options);
michael@0 79
michael@0 80 this.undo = new UndoStack();
michael@0 81 this.undo.installController(aControllerWindow);
michael@0 82
michael@0 83 this._containers = new Map();
michael@0 84
michael@0 85 this._boundMutationObserver = this._mutationObserver.bind(this);
michael@0 86 this.walker.on("mutations", this._boundMutationObserver);
michael@0 87
michael@0 88 this._boundOnNewSelection = this._onNewSelection.bind(this);
michael@0 89 this._inspector.selection.on("new-node-front", this._boundOnNewSelection);
michael@0 90 this._onNewSelection();
michael@0 91
michael@0 92 this._boundKeyDown = this._onKeyDown.bind(this);
michael@0 93 this._frame.contentWindow.addEventListener("keydown", this._boundKeyDown, false);
michael@0 94
michael@0 95 this._boundFocus = this._onFocus.bind(this);
michael@0 96 this._frame.addEventListener("focus", this._boundFocus, false);
michael@0 97
michael@0 98 this._initPreview();
michael@0 99 this._initTooltips();
michael@0 100 this._initHighlighter();
michael@0 101
michael@0 102 EventEmitter.decorate(this);
michael@0 103 }
michael@0 104
michael@0 105 exports.MarkupView = MarkupView;
michael@0 106
michael@0 107 MarkupView.prototype = {
michael@0 108 _selectedContainer: null,
michael@0 109
michael@0 110 _initTooltips: function() {
michael@0 111 this.tooltip = new Tooltip(this._inspector.panelDoc);
michael@0 112 this.tooltip.startTogglingOnHover(this._elt,
michael@0 113 this._isImagePreviewTarget.bind(this));
michael@0 114 },
michael@0 115
michael@0 116 _initHighlighter: function() {
michael@0 117 // Show the box model on markup-view mousemove
michael@0 118 this._onMouseMove = this._onMouseMove.bind(this);
michael@0 119 this._elt.addEventListener("mousemove", this._onMouseMove, false);
michael@0 120 this._onMouseLeave = this._onMouseLeave.bind(this);
michael@0 121 this._elt.addEventListener("mouseleave", this._onMouseLeave, false);
michael@0 122
michael@0 123 // Show markup-containers as hovered on toolbox "picker-node-hovered" event
michael@0 124 // which happens when the "pick" button is pressed
michael@0 125 this._onToolboxPickerHover = (event, nodeFront) => {
michael@0 126 this.showNode(nodeFront, true).then(() => {
michael@0 127 this._showContainerAsHovered(nodeFront);
michael@0 128 });
michael@0 129 }
michael@0 130 this._inspector.toolbox.on("picker-node-hovered", this._onToolboxPickerHover);
michael@0 131 },
michael@0 132
michael@0 133 _onMouseMove: function(event) {
michael@0 134 let target = event.target;
michael@0 135
michael@0 136 // Search target for a markupContainer reference, if not found, walk up
michael@0 137 while (!target.container) {
michael@0 138 if (target.tagName.toLowerCase() === "body") {
michael@0 139 return;
michael@0 140 }
michael@0 141 target = target.parentNode;
michael@0 142 }
michael@0 143
michael@0 144 let container = target.container;
michael@0 145 if (this._hoveredNode !== container.node) {
michael@0 146 if (container.node.nodeType !== Ci.nsIDOMNode.TEXT_NODE) {
michael@0 147 this._showBoxModel(container.node);
michael@0 148 } else {
michael@0 149 this._hideBoxModel();
michael@0 150 }
michael@0 151 }
michael@0 152 this._showContainerAsHovered(container.node);
michael@0 153 },
michael@0 154
michael@0 155 _hoveredNode: null,
michael@0 156 _showContainerAsHovered: function(nodeFront) {
michael@0 157 if (this._hoveredNode !== nodeFront) {
michael@0 158 if (this._hoveredNode) {
michael@0 159 this._containers.get(this._hoveredNode).hovered = false;
michael@0 160 }
michael@0 161 this._containers.get(nodeFront).hovered = true;
michael@0 162
michael@0 163 this._hoveredNode = nodeFront;
michael@0 164 }
michael@0 165 },
michael@0 166
michael@0 167 _onMouseLeave: function() {
michael@0 168 this._hideBoxModel(true);
michael@0 169 if (this._hoveredNode) {
michael@0 170 this._containers.get(this._hoveredNode).hovered = false;
michael@0 171 }
michael@0 172 this._hoveredNode = null;
michael@0 173 },
michael@0 174
michael@0 175 _showBoxModel: function(nodeFront, options={}) {
michael@0 176 this._inspector.toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
michael@0 177 },
michael@0 178
michael@0 179 _hideBoxModel: function(forceHide) {
michael@0 180 return this._inspector.toolbox.highlighterUtils.unhighlight(forceHide);
michael@0 181 },
michael@0 182
michael@0 183 _briefBoxModelTimer: null,
michael@0 184 _brieflyShowBoxModel: function(nodeFront, options) {
michael@0 185 let win = this._frame.contentWindow;
michael@0 186
michael@0 187 if (this._briefBoxModelTimer) {
michael@0 188 win.clearTimeout(this._briefBoxModelTimer);
michael@0 189 this._briefBoxModelTimer = null;
michael@0 190 }
michael@0 191
michael@0 192 this._showBoxModel(nodeFront, options);
michael@0 193
michael@0 194 this._briefBoxModelTimer = this._frame.contentWindow.setTimeout(() => {
michael@0 195 this._hideBoxModel();
michael@0 196 }, NEW_SELECTION_HIGHLIGHTER_TIMER);
michael@0 197 },
michael@0 198
michael@0 199 template: function(aName, aDest, aOptions={stack: "markup-view.xhtml"}) {
michael@0 200 let node = this.doc.getElementById("template-" + aName).cloneNode(true);
michael@0 201 node.removeAttribute("id");
michael@0 202 template(node, aDest, aOptions);
michael@0 203 return node;
michael@0 204 },
michael@0 205
michael@0 206 /**
michael@0 207 * Get the MarkupContainer object for a given node, or undefined if
michael@0 208 * none exists.
michael@0 209 */
michael@0 210 getContainer: function(aNode) {
michael@0 211 return this._containers.get(aNode);
michael@0 212 },
michael@0 213
michael@0 214 update: function() {
michael@0 215 let updateChildren = function(node) {
michael@0 216 this.getContainer(node).update();
michael@0 217 for (let child of node.treeChildren()) {
michael@0 218 updateChildren(child);
michael@0 219 }
michael@0 220 }.bind(this);
michael@0 221
michael@0 222 // Start with the documentElement
michael@0 223 let documentElement;
michael@0 224 for (let node of this._rootNode.treeChildren()) {
michael@0 225 if (node.isDocumentElement === true) {
michael@0 226 documentElement = node;
michael@0 227 break;
michael@0 228 }
michael@0 229 }
michael@0 230
michael@0 231 // Recursively update each node starting with documentElement.
michael@0 232 updateChildren(documentElement);
michael@0 233 },
michael@0 234
michael@0 235 /**
michael@0 236 * Executed when the mouse hovers over a target in the markup-view and is used
michael@0 237 * to decide whether this target should be used to display an image preview
michael@0 238 * tooltip.
michael@0 239 * Delegates the actual decision to the corresponding MarkupContainer instance
michael@0 240 * if one is found.
michael@0 241 * @return the promise returned by MarkupContainer._isImagePreviewTarget
michael@0 242 */
michael@0 243 _isImagePreviewTarget: function(target) {
michael@0 244 // From the target passed here, let's find the parent MarkupContainer
michael@0 245 // and ask it if the tooltip should be shown
michael@0 246 let parent = target, container;
michael@0 247 while (parent !== this.doc.body) {
michael@0 248 if (parent.container) {
michael@0 249 container = parent.container;
michael@0 250 break;
michael@0 251 }
michael@0 252 parent = parent.parentNode;
michael@0 253 }
michael@0 254
michael@0 255 if (container) {
michael@0 256 // With the newly found container, delegate the tooltip content creation
michael@0 257 // and decision to show or not the tooltip
michael@0 258 return container._isImagePreviewTarget(target, this.tooltip);
michael@0 259 }
michael@0 260 },
michael@0 261
michael@0 262 /**
michael@0 263 * Given the known reason, should the current selection be briefly highlighted
michael@0 264 * In a few cases, we don't want to highlight the node:
michael@0 265 * - If the reason is null (used to reset the selection),
michael@0 266 * - if it's "inspector-open" (when the inspector opens up, let's not highlight
michael@0 267 * the default node)
michael@0 268 * - if it's "navigateaway" (since the page is being navigated away from)
michael@0 269 * - if it's "test" (this is a special case for mochitest. In tests, we often
michael@0 270 * need to select elements but don't necessarily want the highlighter to come
michael@0 271 * and go after a delay as this might break test scenarios)
michael@0 272 * We also do not want to start a brief highlight timeout if the node is already
michael@0 273 * being hovered over, since in that case it will already be highlighted.
michael@0 274 */
michael@0 275 _shouldNewSelectionBeHighlighted: function() {
michael@0 276 let reason = this._inspector.selection.reason;
michael@0 277 let unwantedReasons = ["inspector-open", "navigateaway", "test"];
michael@0 278 let isHighlitNode = this._hoveredNode === this._inspector.selection.nodeFront;
michael@0 279 return !isHighlitNode && reason && unwantedReasons.indexOf(reason) === -1;
michael@0 280 },
michael@0 281
michael@0 282 /**
michael@0 283 * Highlight the inspector selected node.
michael@0 284 */
michael@0 285 _onNewSelection: function() {
michael@0 286 let selection = this._inspector.selection;
michael@0 287
michael@0 288 this.htmlEditor.hide();
michael@0 289 let done = this._inspector.updating("markup-view");
michael@0 290 if (selection.isNode()) {
michael@0 291 if (this._shouldNewSelectionBeHighlighted()) {
michael@0 292 this._brieflyShowBoxModel(selection.nodeFront, {});
michael@0 293 }
michael@0 294
michael@0 295 this.showNode(selection.nodeFront, true).then(() => {
michael@0 296 if (selection.reason !== "treepanel") {
michael@0 297 this.markNodeAsSelected(selection.nodeFront);
michael@0 298 }
michael@0 299 done();
michael@0 300 }, (e) => {
michael@0 301 console.error(e);
michael@0 302 done();
michael@0 303 });
michael@0 304 } else {
michael@0 305 this.unmarkSelectedNode();
michael@0 306 done();
michael@0 307 }
michael@0 308 },
michael@0 309
michael@0 310 /**
michael@0 311 * Create a TreeWalker to find the next/previous
michael@0 312 * node for selection.
michael@0 313 */
michael@0 314 _selectionWalker: function(aStart) {
michael@0 315 let walker = this.doc.createTreeWalker(
michael@0 316 aStart || this._elt,
michael@0 317 Ci.nsIDOMNodeFilter.SHOW_ELEMENT,
michael@0 318 function(aElement) {
michael@0 319 if (aElement.container &&
michael@0 320 aElement.container.elt === aElement &&
michael@0 321 aElement.container.visible) {
michael@0 322 return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
michael@0 323 }
michael@0 324 return Ci.nsIDOMNodeFilter.FILTER_SKIP;
michael@0 325 }
michael@0 326 );
michael@0 327 walker.currentNode = this._selectedContainer.elt;
michael@0 328 return walker;
michael@0 329 },
michael@0 330
michael@0 331 /**
michael@0 332 * Key handling.
michael@0 333 */
michael@0 334 _onKeyDown: function(aEvent) {
michael@0 335 let handled = true;
michael@0 336
michael@0 337 // Ignore keystrokes that originated in editors.
michael@0 338 if (aEvent.target.tagName.toLowerCase() === "input" ||
michael@0 339 aEvent.target.tagName.toLowerCase() === "textarea") {
michael@0 340 return;
michael@0 341 }
michael@0 342
michael@0 343 switch(aEvent.keyCode) {
michael@0 344 case Ci.nsIDOMKeyEvent.DOM_VK_H:
michael@0 345 let node = this._selectedContainer.node;
michael@0 346 if (node.hidden) {
michael@0 347 this.walker.unhideNode(node).then(() => this.nodeChanged(node));
michael@0 348 } else {
michael@0 349 this.walker.hideNode(node).then(() => this.nodeChanged(node));
michael@0 350 }
michael@0 351 break;
michael@0 352 case Ci.nsIDOMKeyEvent.DOM_VK_DELETE:
michael@0 353 case Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE:
michael@0 354 this.deleteNode(this._selectedContainer.node);
michael@0 355 break;
michael@0 356 case Ci.nsIDOMKeyEvent.DOM_VK_HOME:
michael@0 357 let rootContainer = this._containers.get(this._rootNode);
michael@0 358 this.navigate(rootContainer.children.firstChild.container);
michael@0 359 break;
michael@0 360 case Ci.nsIDOMKeyEvent.DOM_VK_LEFT:
michael@0 361 if (this._selectedContainer.expanded) {
michael@0 362 this.collapseNode(this._selectedContainer.node);
michael@0 363 } else {
michael@0 364 let parent = this._selectionWalker().parentNode();
michael@0 365 if (parent) {
michael@0 366 this.navigate(parent.container);
michael@0 367 }
michael@0 368 }
michael@0 369 break;
michael@0 370 case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT:
michael@0 371 if (!this._selectedContainer.expanded &&
michael@0 372 this._selectedContainer.hasChildren) {
michael@0 373 this._expandContainer(this._selectedContainer);
michael@0 374 } else {
michael@0 375 let next = this._selectionWalker().nextNode();
michael@0 376 if (next) {
michael@0 377 this.navigate(next.container);
michael@0 378 }
michael@0 379 }
michael@0 380 break;
michael@0 381 case Ci.nsIDOMKeyEvent.DOM_VK_UP:
michael@0 382 let prev = this._selectionWalker().previousNode();
michael@0 383 if (prev) {
michael@0 384 this.navigate(prev.container);
michael@0 385 }
michael@0 386 break;
michael@0 387 case Ci.nsIDOMKeyEvent.DOM_VK_DOWN:
michael@0 388 let next = this._selectionWalker().nextNode();
michael@0 389 if (next) {
michael@0 390 this.navigate(next.container);
michael@0 391 }
michael@0 392 break;
michael@0 393 case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP: {
michael@0 394 let walker = this._selectionWalker();
michael@0 395 let selection = this._selectedContainer;
michael@0 396 for (let i = 0; i < PAGE_SIZE; i++) {
michael@0 397 let prev = walker.previousNode();
michael@0 398 if (!prev) {
michael@0 399 break;
michael@0 400 }
michael@0 401 selection = prev.container;
michael@0 402 }
michael@0 403 this.navigate(selection);
michael@0 404 break;
michael@0 405 }
michael@0 406 case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN: {
michael@0 407 let walker = this._selectionWalker();
michael@0 408 let selection = this._selectedContainer;
michael@0 409 for (let i = 0; i < PAGE_SIZE; i++) {
michael@0 410 let next = walker.nextNode();
michael@0 411 if (!next) {
michael@0 412 break;
michael@0 413 }
michael@0 414 selection = next.container;
michael@0 415 }
michael@0 416 this.navigate(selection);
michael@0 417 break;
michael@0 418 }
michael@0 419 case Ci.nsIDOMKeyEvent.DOM_VK_F2: {
michael@0 420 this.beginEditingOuterHTML(this._selectedContainer.node);
michael@0 421 break;
michael@0 422 }
michael@0 423 default:
michael@0 424 handled = false;
michael@0 425 }
michael@0 426 if (handled) {
michael@0 427 aEvent.stopPropagation();
michael@0 428 aEvent.preventDefault();
michael@0 429 }
michael@0 430 },
michael@0 431
michael@0 432 /**
michael@0 433 * Delete a node from the DOM.
michael@0 434 * This is an undoable action.
michael@0 435 */
michael@0 436 deleteNode: function(aNode) {
michael@0 437 if (aNode.isDocumentElement ||
michael@0 438 aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
michael@0 439 return;
michael@0 440 }
michael@0 441
michael@0 442 let container = this._containers.get(aNode);
michael@0 443
michael@0 444 // Retain the node so we can undo this...
michael@0 445 this.walker.retainNode(aNode).then(() => {
michael@0 446 let parent = aNode.parentNode();
michael@0 447 let sibling = null;
michael@0 448 this.undo.do(() => {
michael@0 449 if (container.selected) {
michael@0 450 this.navigate(this._containers.get(parent));
michael@0 451 }
michael@0 452 this.walker.removeNode(aNode).then(nextSibling => {
michael@0 453 sibling = nextSibling;
michael@0 454 });
michael@0 455 }, () => {
michael@0 456 this.walker.insertBefore(aNode, parent, sibling);
michael@0 457 });
michael@0 458 }).then(null, console.error);
michael@0 459 },
michael@0 460
michael@0 461 /**
michael@0 462 * If an editable item is focused, select its container.
michael@0 463 */
michael@0 464 _onFocus: function(aEvent) {
michael@0 465 let parent = aEvent.target;
michael@0 466 while (!parent.container) {
michael@0 467 parent = parent.parentNode;
michael@0 468 }
michael@0 469 if (parent) {
michael@0 470 this.navigate(parent.container, true);
michael@0 471 }
michael@0 472 },
michael@0 473
michael@0 474 /**
michael@0 475 * Handle a user-requested navigation to a given MarkupContainer,
michael@0 476 * updating the inspector's currently-selected node.
michael@0 477 *
michael@0 478 * @param MarkupContainer aContainer
michael@0 479 * The container we're navigating to.
michael@0 480 * @param aIgnoreFocus aIgnoreFocus
michael@0 481 * If falsy, keyboard focus will be moved to the container too.
michael@0 482 */
michael@0 483 navigate: function(aContainer, aIgnoreFocus) {
michael@0 484 if (!aContainer) {
michael@0 485 return;
michael@0 486 }
michael@0 487
michael@0 488 let node = aContainer.node;
michael@0 489 this.markNodeAsSelected(node, "treepanel");
michael@0 490
michael@0 491 if (!aIgnoreFocus) {
michael@0 492 aContainer.focus();
michael@0 493 }
michael@0 494 },
michael@0 495
michael@0 496 /**
michael@0 497 * Make sure a node is included in the markup tool.
michael@0 498 *
michael@0 499 * @param DOMNode aNode
michael@0 500 * The node in the content document.
michael@0 501 * @param boolean aFlashNode
michael@0 502 * Whether the newly imported node should be flashed
michael@0 503 * @returns MarkupContainer The MarkupContainer object for this element.
michael@0 504 */
michael@0 505 importNode: function(aNode, aFlashNode) {
michael@0 506 if (!aNode) {
michael@0 507 return null;
michael@0 508 }
michael@0 509
michael@0 510 if (this._containers.has(aNode)) {
michael@0 511 return this._containers.get(aNode);
michael@0 512 }
michael@0 513
michael@0 514 if (aNode === this.walker.rootNode) {
michael@0 515 var container = new RootContainer(this, aNode);
michael@0 516 this._elt.appendChild(container.elt);
michael@0 517 this._rootNode = aNode;
michael@0 518 } else {
michael@0 519 var container = new MarkupContainer(this, aNode, this._inspector);
michael@0 520 if (aFlashNode) {
michael@0 521 container.flashMutation();
michael@0 522 }
michael@0 523 }
michael@0 524
michael@0 525 this._containers.set(aNode, container);
michael@0 526 container.childrenDirty = true;
michael@0 527
michael@0 528 this._updateChildren(container);
michael@0 529
michael@0 530 return container;
michael@0 531 },
michael@0 532
michael@0 533 /**
michael@0 534 * Mutation observer used for included nodes.
michael@0 535 */
michael@0 536 _mutationObserver: function(aMutations) {
michael@0 537 let requiresLayoutChange = false;
michael@0 538 let reselectParent;
michael@0 539 let reselectChildIndex;
michael@0 540
michael@0 541 for (let mutation of aMutations) {
michael@0 542 let type = mutation.type;
michael@0 543 let target = mutation.target;
michael@0 544
michael@0 545 if (mutation.type === "documentUnload") {
michael@0 546 // Treat this as a childList change of the child (maybe the protocol
michael@0 547 // should do this).
michael@0 548 type = "childList";
michael@0 549 target = mutation.targetParent;
michael@0 550 if (!target) {
michael@0 551 continue;
michael@0 552 }
michael@0 553 }
michael@0 554
michael@0 555 let container = this._containers.get(target);
michael@0 556 if (!container) {
michael@0 557 // Container might not exist if this came from a load event for a node
michael@0 558 // we're not viewing.
michael@0 559 continue;
michael@0 560 }
michael@0 561 if (type === "attributes" || type === "characterData") {
michael@0 562 container.update();
michael@0 563
michael@0 564 // Auto refresh style properties on selected node when they change.
michael@0 565 if (type === "attributes" && container.selected) {
michael@0 566 requiresLayoutChange = true;
michael@0 567 }
michael@0 568 } else if (type === "childList") {
michael@0 569 let isFromOuterHTML = mutation.removed.some((n) => {
michael@0 570 return n === this._outerHTMLNode;
michael@0 571 });
michael@0 572
michael@0 573 // Keep track of which node should be reselected after mutations.
michael@0 574 if (isFromOuterHTML) {
michael@0 575 reselectParent = target;
michael@0 576 reselectChildIndex = this._outerHTMLChildIndex;
michael@0 577
michael@0 578 delete this._outerHTMLNode;
michael@0 579 delete this._outerHTMLChildIndex;
michael@0 580 }
michael@0 581
michael@0 582 container.childrenDirty = true;
michael@0 583 // Update the children to take care of changes in the markup view DOM.
michael@0 584 this._updateChildren(container, {flash: !isFromOuterHTML});
michael@0 585 }
michael@0 586 }
michael@0 587
michael@0 588 if (requiresLayoutChange) {
michael@0 589 this._inspector.immediateLayoutChange();
michael@0 590 }
michael@0 591 this._waitForChildren().then((nodes) => {
michael@0 592 this._flashMutatedNodes(aMutations);
michael@0 593 this._inspector.emit("markupmutation", aMutations);
michael@0 594
michael@0 595 // Since the htmlEditor is absolutely positioned, a mutation may change
michael@0 596 // the location in which it should be shown.
michael@0 597 this.htmlEditor.refresh();
michael@0 598
michael@0 599 // If a node has had its outerHTML set, the parent node will be selected.
michael@0 600 // Reselect the original node immediately.
michael@0 601 if (this._inspector.selection.nodeFront === reselectParent) {
michael@0 602 this.walker.children(reselectParent).then((o) => {
michael@0 603 let node = o.nodes[reselectChildIndex];
michael@0 604 let container = this._containers.get(node);
michael@0 605 if (node && container) {
michael@0 606 this.markNodeAsSelected(node, "outerhtml");
michael@0 607 if (container.hasChildren) {
michael@0 608 this.expandNode(node);
michael@0 609 }
michael@0 610 }
michael@0 611 });
michael@0 612
michael@0 613 }
michael@0 614 });
michael@0 615 },
michael@0 616
michael@0 617 /**
michael@0 618 * Given a list of mutations returned by the mutation observer, flash the
michael@0 619 * corresponding containers to attract attention.
michael@0 620 */
michael@0 621 _flashMutatedNodes: function(aMutations) {
michael@0 622 let addedOrEditedContainers = new Set();
michael@0 623 let removedContainers = new Set();
michael@0 624
michael@0 625 for (let {type, target, added, removed} of aMutations) {
michael@0 626 let container = this._containers.get(target);
michael@0 627
michael@0 628 if (container) {
michael@0 629 if (type === "attributes" || type === "characterData") {
michael@0 630 addedOrEditedContainers.add(container);
michael@0 631 } else if (type === "childList") {
michael@0 632 // If there has been removals, flash the parent
michael@0 633 if (removed.length) {
michael@0 634 removedContainers.add(container);
michael@0 635 }
michael@0 636
michael@0 637 // If there has been additions, flash the nodes
michael@0 638 added.forEach(added => {
michael@0 639 let addedContainer = this._containers.get(added);
michael@0 640 addedOrEditedContainers.add(addedContainer);
michael@0 641
michael@0 642 // The node may be added as a result of an append, in which case it
michael@0 643 // it will have been removed from another container first, but in
michael@0 644 // these cases we don't want to flash both the removal and the
michael@0 645 // addition
michael@0 646 removedContainers.delete(container);
michael@0 647 });
michael@0 648 }
michael@0 649 }
michael@0 650 }
michael@0 651
michael@0 652 for (let container of removedContainers) {
michael@0 653 container.flashMutation();
michael@0 654 }
michael@0 655 for (let container of addedOrEditedContainers) {
michael@0 656 container.flashMutation();
michael@0 657 }
michael@0 658 },
michael@0 659
michael@0 660 /**
michael@0 661 * Make sure the given node's parents are expanded and the
michael@0 662 * node is scrolled on to screen.
michael@0 663 */
michael@0 664 showNode: function(aNode, centered) {
michael@0 665 let parent = aNode;
michael@0 666
michael@0 667 this.importNode(aNode);
michael@0 668
michael@0 669 while ((parent = parent.parentNode())) {
michael@0 670 this.importNode(parent);
michael@0 671 this.expandNode(parent);
michael@0 672 }
michael@0 673
michael@0 674 return this._waitForChildren().then(() => {
michael@0 675 return this._ensureVisible(aNode);
michael@0 676 }).then(() => {
michael@0 677 // Why is this not working?
michael@0 678 this.layoutHelpers.scrollIntoViewIfNeeded(this._containers.get(aNode).editor.elt, centered);
michael@0 679 });
michael@0 680 },
michael@0 681
michael@0 682 /**
michael@0 683 * Expand the container's children.
michael@0 684 */
michael@0 685 _expandContainer: function(aContainer) {
michael@0 686 return this._updateChildren(aContainer, {expand: true}).then(() => {
michael@0 687 aContainer.expanded = true;
michael@0 688 });
michael@0 689 },
michael@0 690
michael@0 691 /**
michael@0 692 * Expand the node's children.
michael@0 693 */
michael@0 694 expandNode: function(aNode) {
michael@0 695 let container = this._containers.get(aNode);
michael@0 696 this._expandContainer(container);
michael@0 697 },
michael@0 698
michael@0 699 /**
michael@0 700 * Expand the entire tree beneath a container.
michael@0 701 *
michael@0 702 * @param aContainer The container to expand.
michael@0 703 */
michael@0 704 _expandAll: function(aContainer) {
michael@0 705 return this._expandContainer(aContainer).then(() => {
michael@0 706 let child = aContainer.children.firstChild;
michael@0 707 let promises = [];
michael@0 708 while (child) {
michael@0 709 promises.push(this._expandAll(child.container));
michael@0 710 child = child.nextSibling;
michael@0 711 }
michael@0 712 return promise.all(promises);
michael@0 713 }).then(null, console.error);
michael@0 714 },
michael@0 715
michael@0 716 /**
michael@0 717 * Expand the entire tree beneath a node.
michael@0 718 *
michael@0 719 * @param aContainer The node to expand, or null
michael@0 720 * to start from the top.
michael@0 721 */
michael@0 722 expandAll: function(aNode) {
michael@0 723 aNode = aNode || this._rootNode;
michael@0 724 return this._expandAll(this._containers.get(aNode));
michael@0 725 },
michael@0 726
michael@0 727 /**
michael@0 728 * Collapse the node's children.
michael@0 729 */
michael@0 730 collapseNode: function(aNode) {
michael@0 731 let container = this._containers.get(aNode);
michael@0 732 container.expanded = false;
michael@0 733 },
michael@0 734
michael@0 735 /**
michael@0 736 * Retrieve the outerHTML for a remote node.
michael@0 737 * @param aNode The NodeFront to get the outerHTML for.
michael@0 738 * @returns A promise that will be resolved with the outerHTML.
michael@0 739 */
michael@0 740 getNodeOuterHTML: function(aNode) {
michael@0 741 let def = promise.defer();
michael@0 742 this.walker.outerHTML(aNode).then(longstr => {
michael@0 743 longstr.string().then(outerHTML => {
michael@0 744 longstr.release().then(null, console.error);
michael@0 745 def.resolve(outerHTML);
michael@0 746 });
michael@0 747 });
michael@0 748 return def.promise;
michael@0 749 },
michael@0 750
michael@0 751 /**
michael@0 752 * Retrieve the index of a child within its parent's children list.
michael@0 753 * @param aNode The NodeFront to find the index of.
michael@0 754 * @returns A promise that will be resolved with the integer index.
michael@0 755 * If the child cannot be found, returns -1
michael@0 756 */
michael@0 757 getNodeChildIndex: function(aNode) {
michael@0 758 let def = promise.defer();
michael@0 759 let parentNode = aNode.parentNode();
michael@0 760
michael@0 761 // Node may have been removed from the DOM, instead of throwing an error,
michael@0 762 // return -1 indicating that it isn't inside of its parent children list.
michael@0 763 if (!parentNode) {
michael@0 764 def.resolve(-1);
michael@0 765 } else {
michael@0 766 this.walker.children(parentNode).then(children => {
michael@0 767 def.resolve(children.nodes.indexOf(aNode));
michael@0 768 });
michael@0 769 }
michael@0 770
michael@0 771 return def.promise;
michael@0 772 },
michael@0 773
michael@0 774 /**
michael@0 775 * Retrieve the index of a child within its parent's children collection.
michael@0 776 * @param aNode The NodeFront to find the index of.
michael@0 777 * @param newValue The new outerHTML to set on the node.
michael@0 778 * @param oldValue The old outerHTML that will be reverted to find the index of.
michael@0 779 * @returns A promise that will be resolved with the integer index.
michael@0 780 * If the child cannot be found, returns -1
michael@0 781 */
michael@0 782 updateNodeOuterHTML: function(aNode, newValue, oldValue) {
michael@0 783 let container = this._containers.get(aNode);
michael@0 784 if (!container) {
michael@0 785 return;
michael@0 786 }
michael@0 787
michael@0 788 this.getNodeChildIndex(aNode).then((i) => {
michael@0 789 this._outerHTMLChildIndex = i;
michael@0 790 this._outerHTMLNode = aNode;
michael@0 791
michael@0 792 container.undo.do(() => {
michael@0 793 this.walker.setOuterHTML(aNode, newValue);
michael@0 794 }, () => {
michael@0 795 this.walker.setOuterHTML(aNode, oldValue);
michael@0 796 });
michael@0 797 });
michael@0 798 },
michael@0 799
michael@0 800 /**
michael@0 801 * Open an editor in the UI to allow editing of a node's outerHTML.
michael@0 802 * @param aNode The NodeFront to edit.
michael@0 803 */
michael@0 804 beginEditingOuterHTML: function(aNode) {
michael@0 805 this.getNodeOuterHTML(aNode).then((oldValue)=> {
michael@0 806 let container = this._containers.get(aNode);
michael@0 807 if (!container) {
michael@0 808 return;
michael@0 809 }
michael@0 810 this.htmlEditor.show(container.tagLine, oldValue);
michael@0 811 this.htmlEditor.once("popuphidden", (e, aCommit, aValue) => {
michael@0 812 // Need to focus the <html> element instead of the frame / window
michael@0 813 // in order to give keyboard focus back to doc (from editor).
michael@0 814 this._frame.contentDocument.documentElement.focus();
michael@0 815
michael@0 816 if (aCommit) {
michael@0 817 this.updateNodeOuterHTML(aNode, aValue, oldValue);
michael@0 818 }
michael@0 819 });
michael@0 820 });
michael@0 821 },
michael@0 822
michael@0 823 /**
michael@0 824 * Mark the given node expanded.
michael@0 825 * @param {NodeFront} aNode The NodeFront to mark as expanded.
michael@0 826 * @param {Boolean} aExpanded Whether the expand or collapse.
michael@0 827 * @param {Boolean} aExpandDescendants Whether to expand all descendants too
michael@0 828 */
michael@0 829 setNodeExpanded: function(aNode, aExpanded, aExpandDescendants) {
michael@0 830 if (aExpanded) {
michael@0 831 if (aExpandDescendants) {
michael@0 832 this.expandAll(aNode);
michael@0 833 } else {
michael@0 834 this.expandNode(aNode);
michael@0 835 }
michael@0 836 } else {
michael@0 837 this.collapseNode(aNode);
michael@0 838 }
michael@0 839 },
michael@0 840
michael@0 841 /**
michael@0 842 * Mark the given node selected, and update the inspector.selection
michael@0 843 * object's NodeFront to keep consistent state between UI and selection.
michael@0 844 * @param aNode The NodeFront to mark as selected.
michael@0 845 */
michael@0 846 markNodeAsSelected: function(aNode, reason) {
michael@0 847 let container = this._containers.get(aNode);
michael@0 848 if (this._selectedContainer === container) {
michael@0 849 return false;
michael@0 850 }
michael@0 851 if (this._selectedContainer) {
michael@0 852 this._selectedContainer.selected = false;
michael@0 853 }
michael@0 854 this._selectedContainer = container;
michael@0 855 if (aNode) {
michael@0 856 this._selectedContainer.selected = true;
michael@0 857 }
michael@0 858
michael@0 859 this._inspector.selection.setNodeFront(aNode, reason || "nodeselected");
michael@0 860 return true;
michael@0 861 },
michael@0 862
michael@0 863 /**
michael@0 864 * Make sure that every ancestor of the selection are updated
michael@0 865 * and included in the list of visible children.
michael@0 866 */
michael@0 867 _ensureVisible: function(node) {
michael@0 868 while (node) {
michael@0 869 let container = this._containers.get(node);
michael@0 870 let parent = node.parentNode();
michael@0 871 if (!container.elt.parentNode) {
michael@0 872 let parentContainer = this._containers.get(parent);
michael@0 873 if (parentContainer) {
michael@0 874 parentContainer.childrenDirty = true;
michael@0 875 this._updateChildren(parentContainer, {expand: node});
michael@0 876 }
michael@0 877 }
michael@0 878
michael@0 879 node = parent;
michael@0 880 }
michael@0 881 return this._waitForChildren();
michael@0 882 },
michael@0 883
michael@0 884 /**
michael@0 885 * Unmark selected node (no node selected).
michael@0 886 */
michael@0 887 unmarkSelectedNode: function() {
michael@0 888 if (this._selectedContainer) {
michael@0 889 this._selectedContainer.selected = false;
michael@0 890 this._selectedContainer = null;
michael@0 891 }
michael@0 892 },
michael@0 893
michael@0 894 /**
michael@0 895 * Called when the markup panel initiates a change on a node.
michael@0 896 */
michael@0 897 nodeChanged: function(aNode) {
michael@0 898 if (aNode === this._inspector.selection.nodeFront) {
michael@0 899 this._inspector.change("markupview");
michael@0 900 }
michael@0 901 },
michael@0 902
michael@0 903 /**
michael@0 904 * Check if the current selection is a descendent of the container.
michael@0 905 * if so, make sure it's among the visible set for the container,
michael@0 906 * and set the dirty flag if needed.
michael@0 907 * @returns The node that should be made visible, if any.
michael@0 908 */
michael@0 909 _checkSelectionVisible: function(aContainer) {
michael@0 910 let centered = null;
michael@0 911 let node = this._inspector.selection.nodeFront;
michael@0 912 while (node) {
michael@0 913 if (node.parentNode() === aContainer.node) {
michael@0 914 centered = node;
michael@0 915 break;
michael@0 916 }
michael@0 917 node = node.parentNode();
michael@0 918 }
michael@0 919
michael@0 920 return centered;
michael@0 921 },
michael@0 922
michael@0 923 /**
michael@0 924 * Make sure all children of the given container's node are
michael@0 925 * imported and attached to the container in the right order.
michael@0 926 *
michael@0 927 * Children need to be updated only in the following circumstances:
michael@0 928 * a) We just imported this node and have never seen its children.
michael@0 929 * container.childrenDirty will be set by importNode in this case.
michael@0 930 * b) We received a childList mutation on the node.
michael@0 931 * container.childrenDirty will be set in that case too.
michael@0 932 * c) We have changed the selection, and the path to that selection
michael@0 933 * wasn't loaded in a previous children request (because we only
michael@0 934 * grab a subset).
michael@0 935 * container.childrenDirty should be set in that case too!
michael@0 936 *
michael@0 937 * @param MarkupContainer aContainer
michael@0 938 * The markup container whose children need updating
michael@0 939 * @param Object options
michael@0 940 * Options are {expand:boolean,flash:boolean}
michael@0 941 * @return a promise that will be resolved when the children are ready
michael@0 942 * (which may be immediately).
michael@0 943 */
michael@0 944 _updateChildren: function(aContainer, options) {
michael@0 945 let expand = options && options.expand;
michael@0 946 let flash = options && options.flash;
michael@0 947
michael@0 948 aContainer.hasChildren = aContainer.node.hasChildren;
michael@0 949
michael@0 950 if (!this._queuedChildUpdates) {
michael@0 951 this._queuedChildUpdates = new Map();
michael@0 952 }
michael@0 953
michael@0 954 if (this._queuedChildUpdates.has(aContainer)) {
michael@0 955 return this._queuedChildUpdates.get(aContainer);
michael@0 956 }
michael@0 957
michael@0 958 if (!aContainer.childrenDirty) {
michael@0 959 return promise.resolve(aContainer);
michael@0 960 }
michael@0 961
michael@0 962 if (!aContainer.hasChildren) {
michael@0 963 while (aContainer.children.firstChild) {
michael@0 964 aContainer.children.removeChild(aContainer.children.firstChild);
michael@0 965 }
michael@0 966 aContainer.childrenDirty = false;
michael@0 967 return promise.resolve(aContainer);
michael@0 968 }
michael@0 969
michael@0 970 // If we're not expanded (or asked to update anyway), we're done for
michael@0 971 // now. Note that this will leave the childrenDirty flag set, so when
michael@0 972 // expanded we'll refresh the child list.
michael@0 973 if (!(aContainer.expanded || expand)) {
michael@0 974 return promise.resolve(aContainer);
michael@0 975 }
michael@0 976
michael@0 977 // We're going to issue a children request, make sure it includes the
michael@0 978 // centered node.
michael@0 979 let centered = this._checkSelectionVisible(aContainer);
michael@0 980
michael@0 981 // Children aren't updated yet, but clear the childrenDirty flag anyway.
michael@0 982 // If the dirty flag is re-set while we're fetching we'll need to fetch
michael@0 983 // again.
michael@0 984 aContainer.childrenDirty = false;
michael@0 985 let updatePromise = this._getVisibleChildren(aContainer, centered).then(children => {
michael@0 986 if (!this._containers) {
michael@0 987 return promise.reject("markup view destroyed");
michael@0 988 }
michael@0 989 this._queuedChildUpdates.delete(aContainer);
michael@0 990
michael@0 991 // If children are dirty, we got a change notification for this node
michael@0 992 // while the request was in progress, we need to do it again.
michael@0 993 if (aContainer.childrenDirty) {
michael@0 994 return this._updateChildren(aContainer, {expand: centered});
michael@0 995 }
michael@0 996
michael@0 997 let fragment = this.doc.createDocumentFragment();
michael@0 998
michael@0 999 for (let child of children.nodes) {
michael@0 1000 let container = this.importNode(child, flash);
michael@0 1001 fragment.appendChild(container.elt);
michael@0 1002 }
michael@0 1003
michael@0 1004 while (aContainer.children.firstChild) {
michael@0 1005 aContainer.children.removeChild(aContainer.children.firstChild);
michael@0 1006 }
michael@0 1007
michael@0 1008 if (!(children.hasFirst && children.hasLast)) {
michael@0 1009 let data = {
michael@0 1010 showing: this.strings.GetStringFromName("markupView.more.showing"),
michael@0 1011 showAll: this.strings.formatStringFromName(
michael@0 1012 "markupView.more.showAll",
michael@0 1013 [aContainer.node.numChildren.toString()], 1),
michael@0 1014 allButtonClick: () => {
michael@0 1015 aContainer.maxChildren = -1;
michael@0 1016 aContainer.childrenDirty = true;
michael@0 1017 this._updateChildren(aContainer);
michael@0 1018 }
michael@0 1019 };
michael@0 1020
michael@0 1021 if (!children.hasFirst) {
michael@0 1022 let span = this.template("more-nodes", data);
michael@0 1023 fragment.insertBefore(span, fragment.firstChild);
michael@0 1024 }
michael@0 1025 if (!children.hasLast) {
michael@0 1026 let span = this.template("more-nodes", data);
michael@0 1027 fragment.appendChild(span);
michael@0 1028 }
michael@0 1029 }
michael@0 1030
michael@0 1031 aContainer.children.appendChild(fragment);
michael@0 1032 return aContainer;
michael@0 1033 }).then(null, console.error);
michael@0 1034 this._queuedChildUpdates.set(aContainer, updatePromise);
michael@0 1035 return updatePromise;
michael@0 1036 },
michael@0 1037
michael@0 1038 _waitForChildren: function() {
michael@0 1039 if (!this._queuedChildUpdates) {
michael@0 1040 return promise.resolve(undefined);
michael@0 1041 }
michael@0 1042 return promise.all([updatePromise for (updatePromise of this._queuedChildUpdates.values())]);
michael@0 1043 },
michael@0 1044
michael@0 1045 /**
michael@0 1046 * Return a list of the children to display for this container.
michael@0 1047 */
michael@0 1048 _getVisibleChildren: function(aContainer, aCentered) {
michael@0 1049 let maxChildren = aContainer.maxChildren || this.maxChildren;
michael@0 1050 if (maxChildren == -1) {
michael@0 1051 maxChildren = undefined;
michael@0 1052 }
michael@0 1053
michael@0 1054 return this.walker.children(aContainer.node, {
michael@0 1055 maxNodes: maxChildren,
michael@0 1056 center: aCentered
michael@0 1057 });
michael@0 1058 },
michael@0 1059
michael@0 1060 /**
michael@0 1061 * Tear down the markup panel.
michael@0 1062 */
michael@0 1063 destroy: function() {
michael@0 1064 if (this._destroyer) {
michael@0 1065 return this._destroyer;
michael@0 1066 }
michael@0 1067
michael@0 1068 // Note that if the toolbox is closed, this will work fine, but will fail
michael@0 1069 // in case the browser is closed and will trigger a noSuchActor message.
michael@0 1070 this._destroyer = this._hideBoxModel();
michael@0 1071
michael@0 1072 this._hoveredNode = null;
michael@0 1073 this._inspector.toolbox.off("picker-node-hovered", this._onToolboxPickerHover);
michael@0 1074
michael@0 1075 this.htmlEditor.destroy();
michael@0 1076 this.htmlEditor = null;
michael@0 1077
michael@0 1078 this.undo.destroy();
michael@0 1079 this.undo = null;
michael@0 1080
michael@0 1081 this.popup.destroy();
michael@0 1082 this.popup = null;
michael@0 1083
michael@0 1084 this._frame.removeEventListener("focus", this._boundFocus, false);
michael@0 1085 this._boundFocus = null;
michael@0 1086
michael@0 1087 if (this._boundUpdatePreview) {
michael@0 1088 this._frame.contentWindow.removeEventListener("scroll",
michael@0 1089 this._boundUpdatePreview, true);
michael@0 1090 this._boundUpdatePreview = null;
michael@0 1091 }
michael@0 1092
michael@0 1093 if (this._boundResizePreview) {
michael@0 1094 this._frame.contentWindow.removeEventListener("resize",
michael@0 1095 this._boundResizePreview, true);
michael@0 1096 this._frame.contentWindow.removeEventListener("overflow",
michael@0 1097 this._boundResizePreview, true);
michael@0 1098 this._frame.contentWindow.removeEventListener("underflow",
michael@0 1099 this._boundResizePreview, true);
michael@0 1100 this._boundResizePreview = null;
michael@0 1101 }
michael@0 1102
michael@0 1103 this._frame.contentWindow.removeEventListener("keydown",
michael@0 1104 this._boundKeyDown, false);
michael@0 1105 this._boundKeyDown = null;
michael@0 1106
michael@0 1107 this._inspector.selection.off("new-node-front", this._boundOnNewSelection);
michael@0 1108 this._boundOnNewSelection = null;
michael@0 1109
michael@0 1110 this.walker.off("mutations", this._boundMutationObserver)
michael@0 1111 this._boundMutationObserver = null;
michael@0 1112
michael@0 1113 this._elt.removeEventListener("mousemove", this._onMouseMove, false);
michael@0 1114 this._elt.removeEventListener("mouseleave", this._onMouseLeave, false);
michael@0 1115 this._elt = null;
michael@0 1116
michael@0 1117 for (let [key, container] of this._containers) {
michael@0 1118 container.destroy();
michael@0 1119 }
michael@0 1120 this._containers = null;
michael@0 1121
michael@0 1122 this.tooltip.destroy();
michael@0 1123 this.tooltip = null;
michael@0 1124
michael@0 1125 return this._destroyer;
michael@0 1126 },
michael@0 1127
michael@0 1128 /**
michael@0 1129 * Initialize the preview panel.
michael@0 1130 */
michael@0 1131 _initPreview: function() {
michael@0 1132 this._previewEnabled = Services.prefs.getBoolPref("devtools.inspector.markupPreview");
michael@0 1133 if (!this._previewEnabled) {
michael@0 1134 return;
michael@0 1135 }
michael@0 1136
michael@0 1137 this._previewBar = this.doc.querySelector("#previewbar");
michael@0 1138 this._preview = this.doc.querySelector("#preview");
michael@0 1139 this._viewbox = this.doc.querySelector("#viewbox");
michael@0 1140
michael@0 1141 this._previewBar.classList.remove("disabled");
michael@0 1142
michael@0 1143 this._previewWidth = this._preview.getBoundingClientRect().width;
michael@0 1144
michael@0 1145 this._boundResizePreview = this._resizePreview.bind(this);
michael@0 1146 this._frame.contentWindow.addEventListener("resize",
michael@0 1147 this._boundResizePreview, true);
michael@0 1148 this._frame.contentWindow.addEventListener("overflow",
michael@0 1149 this._boundResizePreview, true);
michael@0 1150 this._frame.contentWindow.addEventListener("underflow",
michael@0 1151 this._boundResizePreview, true);
michael@0 1152
michael@0 1153 this._boundUpdatePreview = this._updatePreview.bind(this);
michael@0 1154 this._frame.contentWindow.addEventListener("scroll",
michael@0 1155 this._boundUpdatePreview, true);
michael@0 1156 this._updatePreview();
michael@0 1157 },
michael@0 1158
michael@0 1159 /**
michael@0 1160 * Move the preview viewbox.
michael@0 1161 */
michael@0 1162 _updatePreview: function() {
michael@0 1163 if (!this._previewEnabled) {
michael@0 1164 return;
michael@0 1165 }
michael@0 1166 let win = this._frame.contentWindow;
michael@0 1167
michael@0 1168 if (win.scrollMaxY == 0) {
michael@0 1169 this._previewBar.classList.add("disabled");
michael@0 1170 return;
michael@0 1171 }
michael@0 1172
michael@0 1173 this._previewBar.classList.remove("disabled");
michael@0 1174
michael@0 1175 let ratio = this._previewWidth / PREVIEW_AREA;
michael@0 1176 let width = ratio * win.innerWidth;
michael@0 1177
michael@0 1178 let height = ratio * (win.scrollMaxY + win.innerHeight);
michael@0 1179 let scrollTo
michael@0 1180 if (height >= win.innerHeight) {
michael@0 1181 scrollTo = -(height - win.innerHeight) * (win.scrollY / win.scrollMaxY);
michael@0 1182 this._previewBar.setAttribute("style", "height:" + height +
michael@0 1183 "px;transform:translateY(" + scrollTo + "px)");
michael@0 1184 } else {
michael@0 1185 this._previewBar.setAttribute("style", "height:100%");
michael@0 1186 }
michael@0 1187
michael@0 1188 let bgSize = ~~width + "px " + ~~height + "px";
michael@0 1189 this._preview.setAttribute("style", "background-size:" + bgSize);
michael@0 1190
michael@0 1191 let height = ~~(win.innerHeight * ratio) + "px";
michael@0 1192 let top = ~~(win.scrollY * ratio) + "px";
michael@0 1193 this._viewbox.setAttribute("style", "height:" + height +
michael@0 1194 ";transform: translateY(" + top + ")");
michael@0 1195 },
michael@0 1196
michael@0 1197 /**
michael@0 1198 * Hide the preview while resizing, to avoid slowness.
michael@0 1199 */
michael@0 1200 _resizePreview: function() {
michael@0 1201 if (!this._previewEnabled) {
michael@0 1202 return;
michael@0 1203 }
michael@0 1204 let win = this._frame.contentWindow;
michael@0 1205 this._previewBar.classList.add("hide");
michael@0 1206 win.clearTimeout(this._resizePreviewTimeout);
michael@0 1207
michael@0 1208 win.setTimeout(function() {
michael@0 1209 this._updatePreview();
michael@0 1210 this._previewBar.classList.remove("hide");
michael@0 1211 }.bind(this), 1000);
michael@0 1212 }
michael@0 1213 };
michael@0 1214
michael@0 1215
michael@0 1216 /**
michael@0 1217 * The main structure for storing a document node in the markup
michael@0 1218 * tree. Manages creation of the editor for the node and
michael@0 1219 * a <ul> for placing child elements, and expansion/collapsing
michael@0 1220 * of the element.
michael@0 1221 *
michael@0 1222 * @param MarkupView aMarkupView
michael@0 1223 * The markup view that owns this container.
michael@0 1224 * @param DOMNode aNode
michael@0 1225 * The node to display.
michael@0 1226 * @param Inspector aInspector
michael@0 1227 * The inspector tool container the markup-view
michael@0 1228 */
michael@0 1229 function MarkupContainer(aMarkupView, aNode, aInspector) {
michael@0 1230 this.markup = aMarkupView;
michael@0 1231 this.doc = this.markup.doc;
michael@0 1232 this.undo = this.markup.undo;
michael@0 1233 this.node = aNode;
michael@0 1234 this._inspector = aInspector;
michael@0 1235
michael@0 1236 if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE) {
michael@0 1237 this.editor = new TextEditor(this, aNode, "text");
michael@0 1238 } else if (aNode.nodeType == Ci.nsIDOMNode.COMMENT_NODE) {
michael@0 1239 this.editor = new TextEditor(this, aNode, "comment");
michael@0 1240 } else if (aNode.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
michael@0 1241 this.editor = new ElementEditor(this, aNode);
michael@0 1242 } else if (aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
michael@0 1243 this.editor = new DoctypeEditor(this, aNode);
michael@0 1244 } else {
michael@0 1245 this.editor = new GenericEditor(this, aNode);
michael@0 1246 }
michael@0 1247
michael@0 1248 // The template will fill the following properties
michael@0 1249 this.elt = null;
michael@0 1250 this.expander = null;
michael@0 1251 this.tagState = null;
michael@0 1252 this.tagLine = null;
michael@0 1253 this.children = null;
michael@0 1254 this.markup.template("container", this);
michael@0 1255 this.elt.container = this;
michael@0 1256 this.children.container = this;
michael@0 1257
michael@0 1258 // Expanding/collapsing the node on dblclick of the whole tag-line element
michael@0 1259 this._onToggle = this._onToggle.bind(this);
michael@0 1260 this.elt.addEventListener("dblclick", this._onToggle, false);
michael@0 1261 this.expander.addEventListener("click", this._onToggle, false);
michael@0 1262
michael@0 1263 // Appending the editor element and attaching event listeners
michael@0 1264 this.tagLine.appendChild(this.editor.elt);
michael@0 1265
michael@0 1266 this._onMouseDown = this._onMouseDown.bind(this);
michael@0 1267 this.elt.addEventListener("mousedown", this._onMouseDown, false);
michael@0 1268
michael@0 1269 // Prepare the image preview tooltip data if any
michael@0 1270 this._prepareImagePreview();
michael@0 1271 }
michael@0 1272
michael@0 1273 MarkupContainer.prototype = {
michael@0 1274 toString: function() {
michael@0 1275 return "[MarkupContainer for " + this.node + "]";
michael@0 1276 },
michael@0 1277
michael@0 1278 isPreviewable: function() {
michael@0 1279 if (this.node.tagName) {
michael@0 1280 let tagName = this.node.tagName.toLowerCase();
michael@0 1281 let srcAttr = this.editor.getAttributeElement("src");
michael@0 1282 let isImage = tagName === "img" && srcAttr;
michael@0 1283 let isCanvas = tagName === "canvas";
michael@0 1284
michael@0 1285 return isImage || isCanvas;
michael@0 1286 } else {
michael@0 1287 return false;
michael@0 1288 }
michael@0 1289 },
michael@0 1290
michael@0 1291 /**
michael@0 1292 * If the node is an image or canvas (@see isPreviewable), then get the
michael@0 1293 * image data uri from the server so that it can then later be previewed in
michael@0 1294 * a tooltip if needed.
michael@0 1295 * Stores a promise in this.tooltipData.data that resolves when the data has
michael@0 1296 * been retrieved
michael@0 1297 */
michael@0 1298 _prepareImagePreview: function() {
michael@0 1299 if (this.isPreviewable()) {
michael@0 1300 // Get the image data for later so that when the user actually hovers over
michael@0 1301 // the element, the tooltip does contain the image
michael@0 1302 let def = promise.defer();
michael@0 1303
michael@0 1304 this.tooltipData = {
michael@0 1305 target: this.editor.getAttributeElement("src") || this.editor.tag,
michael@0 1306 data: def.promise
michael@0 1307 };
michael@0 1308
michael@0 1309 let maxDim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
michael@0 1310 this.node.getImageData(maxDim).then(data => {
michael@0 1311 data.data.string().then(str => {
michael@0 1312 let res = {data: str, size: data.size};
michael@0 1313 // Resolving the data promise and, to always keep tooltipData.data
michael@0 1314 // as a promise, create a new one that resolves immediately
michael@0 1315 def.resolve(res);
michael@0 1316 this.tooltipData.data = promise.resolve(res);
michael@0 1317 });
michael@0 1318 }, () => {
michael@0 1319 this.tooltipData.data = promise.reject();
michael@0 1320 });
michael@0 1321 }
michael@0 1322 },
michael@0 1323
michael@0 1324 /**
michael@0 1325 * Executed by MarkupView._isImagePreviewTarget which is itself called when the
michael@0 1326 * mouse hovers over a target in the markup-view.
michael@0 1327 * Checks if the target is indeed something we want to have an image tooltip
michael@0 1328 * preview over and, if so, inserts content into the tooltip.
michael@0 1329 * @return a promise that resolves when the content has been inserted or
michael@0 1330 * rejects if no preview is required. This promise is then used by Tooltip.js
michael@0 1331 * to decide if/when to show the tooltip
michael@0 1332 */
michael@0 1333 _isImagePreviewTarget: function(target, tooltip) {
michael@0 1334 if (!this.tooltipData || this.tooltipData.target !== target) {
michael@0 1335 return promise.reject();
michael@0 1336 }
michael@0 1337
michael@0 1338 return this.tooltipData.data.then(({data, size}) => {
michael@0 1339 tooltip.setImageContent(data, size);
michael@0 1340 }, () => {
michael@0 1341 tooltip.setBrokenImageContent();
michael@0 1342 });
michael@0 1343 },
michael@0 1344
michael@0 1345 copyImageDataUri: function() {
michael@0 1346 // We need to send again a request to gettooltipData even if one was sent for
michael@0 1347 // the tooltip, because we want the full-size image
michael@0 1348 this.node.getImageData().then(data => {
michael@0 1349 data.data.string().then(str => {
michael@0 1350 clipboardHelper.copyString(str, this.markup.doc);
michael@0 1351 });
michael@0 1352 });
michael@0 1353 },
michael@0 1354
michael@0 1355 /**
michael@0 1356 * True if the current node has children. The MarkupView
michael@0 1357 * will set this attribute for the MarkupContainer.
michael@0 1358 */
michael@0 1359 _hasChildren: false,
michael@0 1360
michael@0 1361 get hasChildren() {
michael@0 1362 return this._hasChildren;
michael@0 1363 },
michael@0 1364
michael@0 1365 set hasChildren(aValue) {
michael@0 1366 this._hasChildren = aValue;
michael@0 1367 if (aValue) {
michael@0 1368 this.expander.style.visibility = "visible";
michael@0 1369 } else {
michael@0 1370 this.expander.style.visibility = "hidden";
michael@0 1371 }
michael@0 1372 },
michael@0 1373
michael@0 1374 parentContainer: function() {
michael@0 1375 return this.elt.parentNode ? this.elt.parentNode.container : null;
michael@0 1376 },
michael@0 1377
michael@0 1378 /**
michael@0 1379 * True if the node has been visually expanded in the tree.
michael@0 1380 */
michael@0 1381 get expanded() {
michael@0 1382 return !this.elt.classList.contains("collapsed");
michael@0 1383 },
michael@0 1384
michael@0 1385 set expanded(aValue) {
michael@0 1386 if (aValue && this.elt.classList.contains("collapsed")) {
michael@0 1387 // Expanding a node means cloning its "inline" closing tag into a new
michael@0 1388 // tag-line that the user can interact with and showing the children.
michael@0 1389 if (this.editor instanceof ElementEditor) {
michael@0 1390 let closingTag = this.elt.querySelector(".close");
michael@0 1391 if (closingTag) {
michael@0 1392 if (!this.closeTagLine) {
michael@0 1393 let line = this.markup.doc.createElement("div");
michael@0 1394 line.classList.add("tag-line");
michael@0 1395
michael@0 1396 let tagState = this.markup.doc.createElement("div");
michael@0 1397 tagState.classList.add("tag-state");
michael@0 1398 line.appendChild(tagState);
michael@0 1399
michael@0 1400 line.appendChild(closingTag.cloneNode(true));
michael@0 1401
michael@0 1402 this.closeTagLine = line;
michael@0 1403 }
michael@0 1404 this.elt.appendChild(this.closeTagLine);
michael@0 1405 }
michael@0 1406 }
michael@0 1407 this.elt.classList.remove("collapsed");
michael@0 1408 this.expander.setAttribute("open", "");
michael@0 1409 this.hovered = false;
michael@0 1410 } else if (!aValue) {
michael@0 1411 if (this.editor instanceof ElementEditor && this.closeTagLine) {
michael@0 1412 this.elt.removeChild(this.closeTagLine);
michael@0 1413 }
michael@0 1414 this.elt.classList.add("collapsed");
michael@0 1415 this.expander.removeAttribute("open");
michael@0 1416 }
michael@0 1417 },
michael@0 1418
michael@0 1419 _onToggle: function(event) {
michael@0 1420 this.markup.navigate(this);
michael@0 1421 if(this.hasChildren) {
michael@0 1422 this.markup.setNodeExpanded(this.node, !this.expanded, event.altKey);
michael@0 1423 }
michael@0 1424 event.stopPropagation();
michael@0 1425 },
michael@0 1426
michael@0 1427 _onMouseDown: function(event) {
michael@0 1428 let target = event.target;
michael@0 1429
michael@0 1430 // Target may be a resource link (generated by the output-parser)
michael@0 1431 if (target.nodeName === "a") {
michael@0 1432 event.stopPropagation();
michael@0 1433 event.preventDefault();
michael@0 1434 let browserWin = this.markup._inspector.target
michael@0 1435 .tab.ownerDocument.defaultView;
michael@0 1436 browserWin.openUILinkIn(target.href, "tab");
michael@0 1437 }
michael@0 1438 // Or it may be the "show more nodes" button (which already has its onclick)
michael@0 1439 // Else, it's the container itself
michael@0 1440 else if (target.nodeName !== "button") {
michael@0 1441 this.hovered = false;
michael@0 1442 this.markup.navigate(this);
michael@0 1443 event.stopPropagation();
michael@0 1444 }
michael@0 1445 },
michael@0 1446
michael@0 1447 /**
michael@0 1448 * Temporarily flash the container to attract attention.
michael@0 1449 * Used for markup mutations.
michael@0 1450 */
michael@0 1451 flashMutation: function() {
michael@0 1452 if (!this.selected) {
michael@0 1453 let contentWin = this.markup._frame.contentWindow;
michael@0 1454 this.flashed = true;
michael@0 1455 if (this._flashMutationTimer) {
michael@0 1456 contentWin.clearTimeout(this._flashMutationTimer);
michael@0 1457 this._flashMutationTimer = null;
michael@0 1458 }
michael@0 1459 this._flashMutationTimer = contentWin.setTimeout(() => {
michael@0 1460 this.flashed = false;
michael@0 1461 }, CONTAINER_FLASHING_DURATION);
michael@0 1462 }
michael@0 1463 },
michael@0 1464
michael@0 1465 set flashed(aValue) {
michael@0 1466 if (aValue) {
michael@0 1467 // Make sure the animation class is not here
michael@0 1468 this.tagState.classList.remove("flash-out");
michael@0 1469
michael@0 1470 // Change the background
michael@0 1471 this.tagState.classList.add("theme-bg-contrast");
michael@0 1472
michael@0 1473 // Change the text color
michael@0 1474 this.editor.elt.classList.add("theme-fg-contrast");
michael@0 1475 [].forEach.call(
michael@0 1476 this.editor.elt.querySelectorAll("[class*=theme-fg-color]"),
michael@0 1477 span => span.classList.add("theme-fg-contrast")
michael@0 1478 );
michael@0 1479 } else {
michael@0 1480 // Add the animation class to smoothly remove the background
michael@0 1481 this.tagState.classList.add("flash-out");
michael@0 1482
michael@0 1483 // Remove the background
michael@0 1484 this.tagState.classList.remove("theme-bg-contrast");
michael@0 1485
michael@0 1486 // Remove the text color
michael@0 1487 this.editor.elt.classList.remove("theme-fg-contrast");
michael@0 1488 [].forEach.call(
michael@0 1489 this.editor.elt.querySelectorAll("[class*=theme-fg-color]"),
michael@0 1490 span => span.classList.remove("theme-fg-contrast")
michael@0 1491 );
michael@0 1492 }
michael@0 1493 },
michael@0 1494
michael@0 1495 _hovered: false,
michael@0 1496
michael@0 1497 /**
michael@0 1498 * Highlight the currently hovered tag + its closing tag if necessary
michael@0 1499 * (that is if the tag is expanded)
michael@0 1500 */
michael@0 1501 set hovered(aValue) {
michael@0 1502 this.tagState.classList.remove("flash-out");
michael@0 1503 this._hovered = aValue;
michael@0 1504 if (aValue) {
michael@0 1505 if (!this.selected) {
michael@0 1506 this.tagState.classList.add("theme-bg-darker");
michael@0 1507 }
michael@0 1508 if (this.closeTagLine) {
michael@0 1509 this.closeTagLine.querySelector(".tag-state").classList.add(
michael@0 1510 "theme-bg-darker");
michael@0 1511 }
michael@0 1512 } else {
michael@0 1513 this.tagState.classList.remove("theme-bg-darker");
michael@0 1514 if (this.closeTagLine) {
michael@0 1515 this.closeTagLine.querySelector(".tag-state").classList.remove(
michael@0 1516 "theme-bg-darker");
michael@0 1517 }
michael@0 1518 }
michael@0 1519 },
michael@0 1520
michael@0 1521 /**
michael@0 1522 * True if the container is visible in the markup tree.
michael@0 1523 */
michael@0 1524 get visible() {
michael@0 1525 return this.elt.getBoundingClientRect().height > 0;
michael@0 1526 },
michael@0 1527
michael@0 1528 /**
michael@0 1529 * True if the container is currently selected.
michael@0 1530 */
michael@0 1531 _selected: false,
michael@0 1532
michael@0 1533 get selected() {
michael@0 1534 return this._selected;
michael@0 1535 },
michael@0 1536
michael@0 1537 set selected(aValue) {
michael@0 1538 this.tagState.classList.remove("flash-out");
michael@0 1539 this._selected = aValue;
michael@0 1540 this.editor.selected = aValue;
michael@0 1541 if (this._selected) {
michael@0 1542 this.tagLine.setAttribute("selected", "");
michael@0 1543 this.tagState.classList.add("theme-selected");
michael@0 1544 } else {
michael@0 1545 this.tagLine.removeAttribute("selected");
michael@0 1546 this.tagState.classList.remove("theme-selected");
michael@0 1547 }
michael@0 1548 },
michael@0 1549
michael@0 1550 /**
michael@0 1551 * Update the container's editor to the current state of the
michael@0 1552 * viewed node.
michael@0 1553 */
michael@0 1554 update: function() {
michael@0 1555 if (this.editor.update) {
michael@0 1556 this.editor.update();
michael@0 1557 }
michael@0 1558 },
michael@0 1559
michael@0 1560 /**
michael@0 1561 * Try to put keyboard focus on the current editor.
michael@0 1562 */
michael@0 1563 focus: function() {
michael@0 1564 let focusable = this.editor.elt.querySelector("[tabindex]");
michael@0 1565 if (focusable) {
michael@0 1566 focusable.focus();
michael@0 1567 }
michael@0 1568 },
michael@0 1569
michael@0 1570 /**
michael@0 1571 * Get rid of event listeners and references, when the container is no longer
michael@0 1572 * needed
michael@0 1573 */
michael@0 1574 destroy: function() {
michael@0 1575 // Recursively destroy children containers
michael@0 1576 let firstChild;
michael@0 1577 while (firstChild = this.children.firstChild) {
michael@0 1578 // Not all children of a container are containers themselves
michael@0 1579 // ("show more nodes" button is one example)
michael@0 1580 if (firstChild.container) {
michael@0 1581 firstChild.container.destroy();
michael@0 1582 }
michael@0 1583 this.children.removeChild(firstChild);
michael@0 1584 }
michael@0 1585
michael@0 1586 // Remove event listeners
michael@0 1587 this.elt.removeEventListener("dblclick", this._onToggle, false);
michael@0 1588 this.elt.removeEventListener("mousedown", this._onMouseDown, false);
michael@0 1589 this.expander.removeEventListener("click", this._onToggle, false);
michael@0 1590
michael@0 1591 // Destroy my editor
michael@0 1592 this.editor.destroy();
michael@0 1593 }
michael@0 1594 };
michael@0 1595
michael@0 1596
michael@0 1597 /**
michael@0 1598 * Dummy container node used for the root document element.
michael@0 1599 */
michael@0 1600 function RootContainer(aMarkupView, aNode) {
michael@0 1601 this.doc = aMarkupView.doc;
michael@0 1602 this.elt = this.doc.createElement("ul");
michael@0 1603 this.elt.container = this;
michael@0 1604 this.children = this.elt;
michael@0 1605 this.node = aNode;
michael@0 1606 this.toString = () => "[root container]";
michael@0 1607 }
michael@0 1608
michael@0 1609 RootContainer.prototype = {
michael@0 1610 hasChildren: true,
michael@0 1611 expanded: true,
michael@0 1612 update: function() {},
michael@0 1613 destroy: function() {}
michael@0 1614 };
michael@0 1615
michael@0 1616 /**
michael@0 1617 * Creates an editor for simple nodes.
michael@0 1618 */
michael@0 1619 function GenericEditor(aContainer, aNode) {
michael@0 1620 this.elt = aContainer.doc.createElement("span");
michael@0 1621 this.elt.className = "editor";
michael@0 1622 this.elt.textContent = aNode.nodeName;
michael@0 1623 }
michael@0 1624
michael@0 1625 GenericEditor.prototype = {
michael@0 1626 destroy: function() {}
michael@0 1627 };
michael@0 1628
michael@0 1629 /**
michael@0 1630 * Creates an editor for a DOCTYPE node.
michael@0 1631 *
michael@0 1632 * @param MarkupContainer aContainer The container owning this editor.
michael@0 1633 * @param DOMNode aNode The node being edited.
michael@0 1634 */
michael@0 1635 function DoctypeEditor(aContainer, aNode) {
michael@0 1636 this.elt = aContainer.doc.createElement("span");
michael@0 1637 this.elt.className = "editor comment";
michael@0 1638 this.elt.textContent = '<!DOCTYPE ' + aNode.name +
michael@0 1639 (aNode.publicId ? ' PUBLIC "' + aNode.publicId + '"': '') +
michael@0 1640 (aNode.systemId ? ' "' + aNode.systemId + '"' : '') +
michael@0 1641 '>';
michael@0 1642 }
michael@0 1643
michael@0 1644 DoctypeEditor.prototype = {
michael@0 1645 destroy: function() {}
michael@0 1646 };
michael@0 1647
michael@0 1648 /**
michael@0 1649 * Creates a simple text editor node, used for TEXT and COMMENT
michael@0 1650 * nodes.
michael@0 1651 *
michael@0 1652 * @param MarkupContainer aContainer The container owning this editor.
michael@0 1653 * @param DOMNode aNode The node being edited.
michael@0 1654 * @param string aTemplate The template id to use to build the editor.
michael@0 1655 */
michael@0 1656 function TextEditor(aContainer, aNode, aTemplate) {
michael@0 1657 this.node = aNode;
michael@0 1658 this._selected = false;
michael@0 1659
michael@0 1660 aContainer.markup.template(aTemplate, this);
michael@0 1661
michael@0 1662 editableField({
michael@0 1663 element: this.value,
michael@0 1664 stopOnReturn: true,
michael@0 1665 trigger: "dblclick",
michael@0 1666 multiline: true,
michael@0 1667 done: (aVal, aCommit) => {
michael@0 1668 if (!aCommit) {
michael@0 1669 return;
michael@0 1670 }
michael@0 1671 this.node.getNodeValue().then(longstr => {
michael@0 1672 longstr.string().then(oldValue => {
michael@0 1673 longstr.release().then(null, console.error);
michael@0 1674
michael@0 1675 aContainer.undo.do(() => {
michael@0 1676 this.node.setNodeValue(aVal).then(() => {
michael@0 1677 aContainer.markup.nodeChanged(this.node);
michael@0 1678 });
michael@0 1679 }, () => {
michael@0 1680 this.node.setNodeValue(oldValue).then(() => {
michael@0 1681 aContainer.markup.nodeChanged(this.node);
michael@0 1682 })
michael@0 1683 });
michael@0 1684 });
michael@0 1685 });
michael@0 1686 }
michael@0 1687 });
michael@0 1688
michael@0 1689 this.update();
michael@0 1690 }
michael@0 1691
michael@0 1692 TextEditor.prototype = {
michael@0 1693 get selected() this._selected,
michael@0 1694 set selected(aValue) {
michael@0 1695 if (aValue === this._selected) {
michael@0 1696 return;
michael@0 1697 }
michael@0 1698 this._selected = aValue;
michael@0 1699 this.update();
michael@0 1700 },
michael@0 1701
michael@0 1702 update: function() {
michael@0 1703 if (!this.selected || !this.node.incompleteValue) {
michael@0 1704 let text = this.node.shortValue;
michael@0 1705 // XXX: internationalize the elliding
michael@0 1706 if (this.node.incompleteValue) {
michael@0 1707 text += "…";
michael@0 1708 }
michael@0 1709 this.value.textContent = text;
michael@0 1710 } else {
michael@0 1711 let longstr = null;
michael@0 1712 this.node.getNodeValue().then(ret => {
michael@0 1713 longstr = ret;
michael@0 1714 return longstr.string();
michael@0 1715 }).then(str => {
michael@0 1716 longstr.release().then(null, console.error);
michael@0 1717 if (this.selected) {
michael@0 1718 this.value.textContent = str;
michael@0 1719 }
michael@0 1720 }).then(null, console.error);
michael@0 1721 }
michael@0 1722 },
michael@0 1723
michael@0 1724 destroy: function() {}
michael@0 1725 };
michael@0 1726
michael@0 1727 /**
michael@0 1728 * Creates an editor for an Element node.
michael@0 1729 *
michael@0 1730 * @param MarkupContainer aContainer The container owning this editor.
michael@0 1731 * @param Element aNode The node being edited.
michael@0 1732 */
michael@0 1733 function ElementEditor(aContainer, aNode) {
michael@0 1734 this.doc = aContainer.doc;
michael@0 1735 this.undo = aContainer.undo;
michael@0 1736 this.template = aContainer.markup.template.bind(aContainer.markup);
michael@0 1737 this.container = aContainer;
michael@0 1738 this.markup = this.container.markup;
michael@0 1739 this.node = aNode;
michael@0 1740
michael@0 1741 this.attrs = {};
michael@0 1742
michael@0 1743 // The templates will fill the following properties
michael@0 1744 this.elt = null;
michael@0 1745 this.tag = null;
michael@0 1746 this.closeTag = null;
michael@0 1747 this.attrList = null;
michael@0 1748 this.newAttr = null;
michael@0 1749 this.closeElt = null;
michael@0 1750
michael@0 1751 // Create the main editor
michael@0 1752 this.template("element", this);
michael@0 1753
michael@0 1754 if (aNode.isLocal_toBeDeprecated()) {
michael@0 1755 this.rawNode = aNode.rawNode();
michael@0 1756 }
michael@0 1757
michael@0 1758 // Make the tag name editable (unless this is a remote node or
michael@0 1759 // a document element)
michael@0 1760 if (this.rawNode && !aNode.isDocumentElement) {
michael@0 1761 this.tag.setAttribute("tabindex", "0");
michael@0 1762 editableField({
michael@0 1763 element: this.tag,
michael@0 1764 trigger: "dblclick",
michael@0 1765 stopOnReturn: true,
michael@0 1766 done: this.onTagEdit.bind(this),
michael@0 1767 });
michael@0 1768 }
michael@0 1769
michael@0 1770 // Make the new attribute space editable.
michael@0 1771 editableField({
michael@0 1772 element: this.newAttr,
michael@0 1773 trigger: "dblclick",
michael@0 1774 stopOnReturn: true,
michael@0 1775 contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
michael@0 1776 popup: this.markup.popup,
michael@0 1777 done: (aVal, aCommit) => {
michael@0 1778 if (!aCommit) {
michael@0 1779 return;
michael@0 1780 }
michael@0 1781
michael@0 1782 try {
michael@0 1783 let doMods = this._startModifyingAttributes();
michael@0 1784 let undoMods = this._startModifyingAttributes();
michael@0 1785 this._applyAttributes(aVal, null, doMods, undoMods);
michael@0 1786 this.undo.do(() => {
michael@0 1787 doMods.apply();
michael@0 1788 }, function() {
michael@0 1789 undoMods.apply();
michael@0 1790 });
michael@0 1791 } catch(x) {
michael@0 1792 console.error(x);
michael@0 1793 }
michael@0 1794 }
michael@0 1795 });
michael@0 1796
michael@0 1797 let tagName = this.node.nodeName.toLowerCase();
michael@0 1798 this.tag.textContent = tagName;
michael@0 1799 this.closeTag.textContent = tagName;
michael@0 1800
michael@0 1801 this.update();
michael@0 1802 }
michael@0 1803
michael@0 1804 ElementEditor.prototype = {
michael@0 1805 /**
michael@0 1806 * Update the state of the editor from the node.
michael@0 1807 */
michael@0 1808 update: function() {
michael@0 1809 let attrs = this.node.attributes;
michael@0 1810 if (!attrs) {
michael@0 1811 return;
michael@0 1812 }
michael@0 1813
michael@0 1814 // Hide all the attribute editors, they'll be re-shown if they're
michael@0 1815 // still applicable. Don't update attributes that are being
michael@0 1816 // actively edited.
michael@0 1817 let attrEditors = this.attrList.querySelectorAll(".attreditor");
michael@0 1818 for (let i = 0; i < attrEditors.length; i++) {
michael@0 1819 if (!attrEditors[i].inplaceEditor) {
michael@0 1820 attrEditors[i].style.display = "none";
michael@0 1821 }
michael@0 1822 }
michael@0 1823
michael@0 1824 // Get the attribute editor for each attribute that exists on
michael@0 1825 // the node and show it.
michael@0 1826 for (let attr of attrs) {
michael@0 1827 let attribute = this._createAttribute(attr);
michael@0 1828 if (!attribute.inplaceEditor) {
michael@0 1829 attribute.style.removeProperty("display");
michael@0 1830 }
michael@0 1831 }
michael@0 1832 },
michael@0 1833
michael@0 1834 _startModifyingAttributes: function() {
michael@0 1835 return this.node.startModifyingAttributes();
michael@0 1836 },
michael@0 1837
michael@0 1838 /**
michael@0 1839 * Get the element used for one of the attributes of this element
michael@0 1840 * @param string attrName The name of the attribute to get the element for
michael@0 1841 * @return DOMElement
michael@0 1842 */
michael@0 1843 getAttributeElement: function(attrName) {
michael@0 1844 return this.attrList.querySelector(
michael@0 1845 ".attreditor[data-attr=" + attrName + "] .attr-value");
michael@0 1846 },
michael@0 1847
michael@0 1848 _createAttribute: function(aAttr, aBefore = null) {
michael@0 1849 // Create the template editor, which will save some variables here.
michael@0 1850 let data = {
michael@0 1851 attrName: aAttr.name,
michael@0 1852 };
michael@0 1853 this.template("attribute", data);
michael@0 1854 var {attr, inner, name, val} = data;
michael@0 1855
michael@0 1856 // Double quotes need to be handled specially to prevent DOMParser failing.
michael@0 1857 // name="v"a"l"u"e" when editing -> name='v"a"l"u"e"'
michael@0 1858 // name="v'a"l'u"e" when editing -> name="v'a&quot;l'u&quot;e"
michael@0 1859 let editValueDisplayed = aAttr.value || "";
michael@0 1860 let hasDoubleQuote = editValueDisplayed.contains('"');
michael@0 1861 let hasSingleQuote = editValueDisplayed.contains("'");
michael@0 1862 let initial = aAttr.name + '="' + editValueDisplayed + '"';
michael@0 1863
michael@0 1864 // Can't just wrap value with ' since the value contains both " and '.
michael@0 1865 if (hasDoubleQuote && hasSingleQuote) {
michael@0 1866 editValueDisplayed = editValueDisplayed.replace(/\"/g, "&quot;");
michael@0 1867 initial = aAttr.name + '="' + editValueDisplayed + '"';
michael@0 1868 }
michael@0 1869
michael@0 1870 // Wrap with ' since there are no single quotes in the attribute value.
michael@0 1871 if (hasDoubleQuote && !hasSingleQuote) {
michael@0 1872 initial = aAttr.name + "='" + editValueDisplayed + "'";
michael@0 1873 }
michael@0 1874
michael@0 1875 // Make the attribute editable.
michael@0 1876 editableField({
michael@0 1877 element: inner,
michael@0 1878 trigger: "dblclick",
michael@0 1879 stopOnReturn: true,
michael@0 1880 selectAll: false,
michael@0 1881 initial: initial,
michael@0 1882 contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
michael@0 1883 popup: this.markup.popup,
michael@0 1884 start: (aEditor, aEvent) => {
michael@0 1885 // If the editing was started inside the name or value areas,
michael@0 1886 // select accordingly.
michael@0 1887 if (aEvent && aEvent.target === name) {
michael@0 1888 aEditor.input.setSelectionRange(0, name.textContent.length);
michael@0 1889 } else if (aEvent && aEvent.target === val) {
michael@0 1890 let length = editValueDisplayed.length;
michael@0 1891 let editorLength = aEditor.input.value.length;
michael@0 1892 let start = editorLength - (length + 1);
michael@0 1893 aEditor.input.setSelectionRange(start, start + length);
michael@0 1894 } else {
michael@0 1895 aEditor.input.select();
michael@0 1896 }
michael@0 1897 },
michael@0 1898 done: (aVal, aCommit) => {
michael@0 1899 if (!aCommit || aVal === initial) {
michael@0 1900 return;
michael@0 1901 }
michael@0 1902
michael@0 1903 let doMods = this._startModifyingAttributes();
michael@0 1904 let undoMods = this._startModifyingAttributes();
michael@0 1905
michael@0 1906 // Remove the attribute stored in this editor and re-add any attributes
michael@0 1907 // parsed out of the input element. Restore original attribute if
michael@0 1908 // parsing fails.
michael@0 1909 try {
michael@0 1910 this._saveAttribute(aAttr.name, undoMods);
michael@0 1911 doMods.removeAttribute(aAttr.name);
michael@0 1912 this._applyAttributes(aVal, attr, doMods, undoMods);
michael@0 1913 this.undo.do(() => {
michael@0 1914 doMods.apply();
michael@0 1915 }, () => {
michael@0 1916 undoMods.apply();
michael@0 1917 })
michael@0 1918 } catch(ex) {
michael@0 1919 console.error(ex);
michael@0 1920 }
michael@0 1921 }
michael@0 1922 });
michael@0 1923
michael@0 1924 // Figure out where we should place the attribute.
michael@0 1925 let before = aBefore;
michael@0 1926 if (aAttr.name == "id") {
michael@0 1927 before = this.attrList.firstChild;
michael@0 1928 } else if (aAttr.name == "class") {
michael@0 1929 let idNode = this.attrs["id"];
michael@0 1930 before = idNode ? idNode.nextSibling : this.attrList.firstChild;
michael@0 1931 }
michael@0 1932 this.attrList.insertBefore(attr, before);
michael@0 1933
michael@0 1934 // Remove the old version of this attribute from the DOM.
michael@0 1935 let oldAttr = this.attrs[aAttr.name];
michael@0 1936 if (oldAttr && oldAttr.parentNode) {
michael@0 1937 oldAttr.parentNode.removeChild(oldAttr);
michael@0 1938 }
michael@0 1939
michael@0 1940 this.attrs[aAttr.name] = attr;
michael@0 1941
michael@0 1942 let collapsedValue;
michael@0 1943 if (aAttr.value.match(COLLAPSE_DATA_URL_REGEX)) {
michael@0 1944 collapsedValue = truncateString(aAttr.value, COLLAPSE_DATA_URL_LENGTH);
michael@0 1945 } else {
michael@0 1946 collapsedValue = truncateString(aAttr.value, COLLAPSE_ATTRIBUTE_LENGTH);
michael@0 1947 }
michael@0 1948
michael@0 1949 name.textContent = aAttr.name;
michael@0 1950 val.textContent = collapsedValue;
michael@0 1951
michael@0 1952 return attr;
michael@0 1953 },
michael@0 1954
michael@0 1955 /**
michael@0 1956 * Parse a user-entered attribute string and apply the resulting
michael@0 1957 * attributes to the node. This operation is undoable.
michael@0 1958 *
michael@0 1959 * @param string aValue the user-entered value.
michael@0 1960 * @param Element aAttrNode the attribute editor that created this
michael@0 1961 * set of attributes, used to place new attributes where the
michael@0 1962 * user put them.
michael@0 1963 */
michael@0 1964 _applyAttributes: function(aValue, aAttrNode, aDoMods, aUndoMods) {
michael@0 1965 let attrs = parseAttributeValues(aValue, this.doc);
michael@0 1966 for (let attr of attrs) {
michael@0 1967 // Create an attribute editor next to the current attribute if needed.
michael@0 1968 this._createAttribute(attr, aAttrNode ? aAttrNode.nextSibling : null);
michael@0 1969 this._saveAttribute(attr.name, aUndoMods);
michael@0 1970 aDoMods.setAttribute(attr.name, attr.value);
michael@0 1971 }
michael@0 1972 },
michael@0 1973
michael@0 1974 /**
michael@0 1975 * Saves the current state of the given attribute into an attribute
michael@0 1976 * modification list.
michael@0 1977 */
michael@0 1978 _saveAttribute: function(aName, aUndoMods) {
michael@0 1979 let node = this.node;
michael@0 1980 if (node.hasAttribute(aName)) {
michael@0 1981 let oldValue = node.getAttribute(aName);
michael@0 1982 aUndoMods.setAttribute(aName, oldValue);
michael@0 1983 } else {
michael@0 1984 aUndoMods.removeAttribute(aName);
michael@0 1985 }
michael@0 1986 },
michael@0 1987
michael@0 1988 /**
michael@0 1989 * Called when the tag name editor has is done editing.
michael@0 1990 */
michael@0 1991 onTagEdit: function(aVal, aCommit) {
michael@0 1992 if (!aCommit || aVal == this.rawNode.tagName) {
michael@0 1993 return;
michael@0 1994 }
michael@0 1995
michael@0 1996 // Create a new element with the same attributes as the
michael@0 1997 // current element and prepare to replace the current node
michael@0 1998 // with it.
michael@0 1999 try {
michael@0 2000 var newElt = nodeDocument(this.rawNode).createElement(aVal);
michael@0 2001 } catch(x) {
michael@0 2002 // Failed to create a new element with that tag name, ignore
michael@0 2003 // the change.
michael@0 2004 return;
michael@0 2005 }
michael@0 2006
michael@0 2007 let attrs = this.rawNode.attributes;
michael@0 2008
michael@0 2009 for (let i = 0 ; i < attrs.length; i++) {
michael@0 2010 newElt.setAttribute(attrs[i].name, attrs[i].value);
michael@0 2011 }
michael@0 2012 let newFront = this.markup.walker.frontForRawNode(newElt);
michael@0 2013 let newContainer = this.markup.importNode(newFront);
michael@0 2014
michael@0 2015 // Retain the two nodes we care about here so we can undo.
michael@0 2016 let walker = this.markup.walker;
michael@0 2017 promise.all([
michael@0 2018 walker.retainNode(newFront), walker.retainNode(this.node)
michael@0 2019 ]).then(() => {
michael@0 2020 function swapNodes(aOld, aNew) {
michael@0 2021 aOld.parentNode.insertBefore(aNew, aOld);
michael@0 2022 while (aOld.firstChild) {
michael@0 2023 aNew.appendChild(aOld.firstChild);
michael@0 2024 }
michael@0 2025 aOld.parentNode.removeChild(aOld);
michael@0 2026 }
michael@0 2027
michael@0 2028 this.undo.do(() => {
michael@0 2029 swapNodes(this.rawNode, newElt);
michael@0 2030 this.markup.setNodeExpanded(newFront, this.container.expanded);
michael@0 2031 if (this.container.selected) {
michael@0 2032 this.markup.navigate(newContainer);
michael@0 2033 }
michael@0 2034 }, () => {
michael@0 2035 swapNodes(newElt, this.rawNode);
michael@0 2036 this.markup.setNodeExpanded(this.node, newContainer.expanded);
michael@0 2037 if (newContainer.selected) {
michael@0 2038 this.markup.navigate(this.container);
michael@0 2039 }
michael@0 2040 });
michael@0 2041 }).then(null, console.error);
michael@0 2042 },
michael@0 2043
michael@0 2044 destroy: function() {}
michael@0 2045 };
michael@0 2046
michael@0 2047 function nodeDocument(node) {
michael@0 2048 return node.ownerDocument ||
michael@0 2049 (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
michael@0 2050 }
michael@0 2051
michael@0 2052 function truncateString(str, maxLength) {
michael@0 2053 if (str.length <= maxLength) {
michael@0 2054 return str;
michael@0 2055 }
michael@0 2056
michael@0 2057 return str.substring(0, Math.ceil(maxLength / 2)) +
michael@0 2058 "…" +
michael@0 2059 str.substring(str.length - Math.floor(maxLength / 2));
michael@0 2060 }
michael@0 2061 /**
michael@0 2062 * Parse attribute names and values from a string.
michael@0 2063 *
michael@0 2064 * @param {String} attr
michael@0 2065 * The input string for which names/values are to be parsed.
michael@0 2066 * @param {HTMLDocument} doc
michael@0 2067 * A document that can be used to test valid attributes.
michael@0 2068 * @return {Array}
michael@0 2069 * An array of attribute names and their values.
michael@0 2070 */
michael@0 2071 function parseAttributeValues(attr, doc) {
michael@0 2072 attr = attr.trim();
michael@0 2073
michael@0 2074 // Handle bad user inputs by appending a " or ' if it fails to parse without them.
michael@0 2075 let el = DOMParser.parseFromString("<div " + attr + "></div>", "text/html").body.childNodes[0] ||
michael@0 2076 DOMParser.parseFromString("<div " + attr + "\"></div>", "text/html").body.childNodes[0] ||
michael@0 2077 DOMParser.parseFromString("<div " + attr + "'></div>", "text/html").body.childNodes[0];
michael@0 2078 let div = doc.createElement("div");
michael@0 2079
michael@0 2080 let attributes = [];
michael@0 2081 for (let attribute of el.attributes) {
michael@0 2082 // Try to set on an element in the document, throws exception on bad input.
michael@0 2083 // Prevents InvalidCharacterError - "String contains an invalid character".
michael@0 2084 try {
michael@0 2085 div.setAttribute(attribute.name, attribute.value);
michael@0 2086 attributes.push({
michael@0 2087 name: attribute.name,
michael@0 2088 value: attribute.value
michael@0 2089 });
michael@0 2090 }
michael@0 2091 catch(e) { }
michael@0 2092 }
michael@0 2093
michael@0 2094 // Attributes return from DOMParser in reverse order from how they are entered.
michael@0 2095 return attributes.reverse();
michael@0 2096 }
michael@0 2097
michael@0 2098 loader.lazyGetter(MarkupView.prototype, "strings", () => Services.strings.createBundle(
michael@0 2099 "chrome://browser/locale/devtools/inspector.properties"
michael@0 2100 ));
michael@0 2101
michael@0 2102 XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
michael@0 2103 return Cc["@mozilla.org/widget/clipboardhelper;1"].
michael@0 2104 getService(Ci.nsIClipboardHelper);
michael@0 2105 });

mercurial