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.

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

mercurial