Bug 895561 - 'Edit As HTML' option in the markup view - toolkit changes, r=fitzgen
authorBrian Grinstead <bgrinstead@mozilla.com>
Wed, 23 Oct 2013 11:53:39 -0500
changeset 151952 9c44473817b5ad6555e0e08794e131880a011f8b
parent 151951 45a18b183676f8f6ded4466030d3fc4c35a0ca51
child 151953 8693c78697ae83fb6302aada797e96c537f18699
push id3207
push userscrapmachines@gmail.com
push dateThu, 24 Oct 2013 19:02:26 +0000
treeherderfx-team@8693c78697ae [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfitzgen
bugs895561
milestone27.0a1
Bug 895561 - 'Edit As HTML' option in the markup view - toolkit changes, r=fitzgen
toolkit/devtools/client/dbg-client.jsm
toolkit/devtools/server/actors/inspector.js
toolkit/devtools/server/actors/root.js
toolkit/devtools/server/tests/mochitest/inspector-traversal-data.html
toolkit/devtools/server/tests/mochitest/test_inspector-traversal.html
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -236,16 +236,17 @@ this.DebuggerClient = function DebuggerC
 
   this._pendingRequests = [];
   this._activeRequests = new Map;
   this._eventsEnabled = true;
 
   this.compat = new ProtocolCompatibility(this, [
     new SourcesShim(),
   ]);
+  this.traits = {};
 
   this.request = this.request.bind(this);
   this.localTransport = this._transport.onOutputStreamReady === undefined;
 
   /*
    * As the first thing on the connection, expect a greeting packet from
    * the connection's root actor.
    */
@@ -355,21 +356,22 @@ DebuggerClient.prototype = {
   /**
    * Connect to the server and start exchanging protocol messages.
    *
    * @param aOnConnected function
    *        If specified, will be called when the greeting packet is
    *        received from the debugging server.
    */
   connect: function DC_connect(aOnConnected) {
-    if (aOnConnected) {
-      this.addOneTimeListener("connected", function(aName, aApplicationType, aTraits) {
+    this.addOneTimeListener("connected", (aName, aApplicationType, aTraits) => {
+      this.traits = aTraits;
+      if (aOnConnected) {
         aOnConnected(aApplicationType, aTraits);
-      });
-    }
+      }
+    });
 
     this._transport.ready();
   },
 
   /**
    * Shut down communication with the debugging server.
    *
    * @param aOnClosed function
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -71,16 +71,20 @@ const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-
 const HIGHLIGHTED_TIMEOUT = 2000;
 
 let HELPER_SHEET = ".__fx-devtools-hide-shortcut__ { visibility: hidden !important } ";
 HELPER_SHEET += ":-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px!important } ";
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 
+loader.lazyGetter(this, "DOMParser", function() {
+ return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
+});
+
 exports.register = function(handle) {
   handle.addTabActor(InspectorActor, "inspectorActor");
 };
 
 exports.unregister = function(handle) {
   handle.removeTabActor(InspectorActor);
 };
 
@@ -138,16 +142,21 @@ var NodeActor = protocol.ActorClass({
   },
 
   /**
    * Instead of storing a connection object, the NodeActor gets its connection
    * from its associated walker.
    */
   get conn() this.walker.conn,
 
+  isDocumentElement: function() {
+    return this.rawNode.ownerDocument &&
+        this.rawNode.ownerDocument.documentElement === this.rawNode;
+  },
+
   // Returns the JSON representation of this object over the wire.
   form: function(detail) {
     if (detail === "actorid") {
       return this.actorID;
     }
 
     let parentNode = this.walker.parentNode(this);
 
@@ -172,18 +181,17 @@ var NodeActor = protocol.ActorClass({
       publicId: this.rawNode.publicId,
       systemId: this.rawNode.systemId,
 
       attrs: this.writeAttrs(),
 
       pseudoClassLocks: this.writePseudoClassLocks(),
     };
 
-    if (this.rawNode.ownerDocument &&
-        this.rawNode.ownerDocument.documentElement === this.rawNode) {
+    if (this.isDocumentElement()) {
       form.isDocumentElement = true;
     }
 
     if (this.rawNode.nodeValue) {
       // We only include a short version of the value if it's longer than
       // gValueSummaryLength
       if (this.rawNode.nodeValue.length > gValueSummaryLength) {
         form.shortValue = this.rawNode.nodeValue.substring(0, gValueSummaryLength);
@@ -1543,16 +1551,72 @@ var WalkerActor = protocol.ActorClass({
       node: Arg(0, "domnode")
     },
     response: {
       value: RetVal("longstring")
     }
   }),
 
   /**
+   * Set a node's outerHTML property.
+   */
+  setOuterHTML: method(function(node, value) {
+    let parsedDOM = DOMParser.parseFromString(value, "text/html");
+    let rawNode = node.rawNode;
+    let parentNode = rawNode.parentNode;
+
+    // Special case for head and body.  Setting document.body.outerHTML
+    // creates an extra <head> tag, and document.head.outerHTML creates
+    // an extra <body>.  So instead we will call replaceChild with the
+    // parsed DOM, assuming that they aren't trying to set both tags at once.
+    if (rawNode.tagName === "BODY") {
+      if (parsedDOM.head.innerHTML === "") {
+        parentNode.replaceChild(parsedDOM.body, rawNode);
+      } else {
+        rawNode.outerHTML = value;
+      }
+    } else if (rawNode.tagName === "HEAD") {
+      if (parsedDOM.body.innerHTML === "") {
+        parentNode.replaceChild(parsedDOM.head, rawNode);
+      } else {
+        rawNode.outerHTML = value;
+      }
+    } else if (node.isDocumentElement()) {
+      // Unable to set outerHTML on the document element.  Fall back by
+      // setting attributes manually, then replace the body and head elements.
+      let finalAttributeModifications = [];
+      let attributeModifications = {};
+      for (let attribute of rawNode.attributes) {
+        attributeModifications[attribute.name] = null;
+      }
+      for (let attribute of parsedDOM.documentElement.attributes) {
+        attributeModifications[attribute.name] = attribute.value;
+      }
+      for (let key in attributeModifications) {
+        finalAttributeModifications.push({
+          attributeName: key,
+          newValue: attributeModifications[key]
+        });
+      }
+      node.modifyAttributes(finalAttributeModifications);
+      rawNode.replaceChild(parsedDOM.head, rawNode.querySelector("head"));
+      rawNode.replaceChild(parsedDOM.body, rawNode.querySelector("body"));
+    } else {
+      rawNode.outerHTML = value;
+    }
+  }, {
+    request: {
+      node: Arg(0, "domnode"),
+      value: Arg(1),
+    },
+    response: {
+    }
+  }),
+
+  /**
    * Removes a node from its parent node.
    *
    * @returns The node's nextSibling before it was removed.
    */
   removeNode: method(function(node) {
     if ((node.rawNode.ownerDocument &&
          node.rawNode.ownerDocument.documentElement === this.rawNode) ||
          node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
--- a/toolkit/devtools/server/actors/root.js
+++ b/toolkit/devtools/server/actors/root.js
@@ -170,17 +170,18 @@ RootActor.prototype = {
    */
   sayHello: function() {
     return {
       from: this.actorID,
       applicationType: this.applicationType,
       /* This is not in the spec, but it's used by tests. */
       testConnectionPrefix: this.conn.prefix,
       traits: {
-        sources: true
+        sources: true,
+        editOuterHTML: true
       }
     };
   },
 
   /**
    * This is true for the root actor only, used by some child actors
    */
   get isRootActor() true,
--- a/toolkit/devtools/server/tests/mochitest/inspector-traversal-data.html
+++ b/toolkit/devtools/server/tests/mochitest/inspector-traversal-data.html
@@ -45,10 +45,11 @@
     <div id="w">w</div>
     <div id="x">x</div>
     <div id="y">y</div>
     <div id="z">z</div>
   </div>
   <div id="longlist-sibling">
     <div id="longlist-sibling-firstchild"></div>
   </div>
+  <p id="edit-html"></p>
 </body>
 </html>
--- a/toolkit/devtools/server/tests/mochitest/test_inspector-traversal.html
+++ b/toolkit/devtools/server/tests/mochitest/test_inspector-traversal.html
@@ -68,16 +68,35 @@ addTest(function testOuterHTML() {
     return gWalker.outerHTML(docElement);
   }).then(longstring => {
     return longstring.string();
   }).then(outerHTML => {
     ok(outerHTML === gInspectee.documentElement.outerHTML, "outerHTML should match");
   }).then(runNextTest));
 });
 
+addTest(function testSetOuterHTMLNode() {
+  let newHTML = "<p id=\"edit-html-done\">after edit</p>";
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#edit-html").then(node => {
+    return gWalker.setOuterHTML(node, newHTML);
+  }).then(() => {
+    return gWalker.querySelector(gWalker.rootNode, "#edit-html-done");
+  }).then(node => {
+    return gWalker.outerHTML(node);
+  }).then(longstring => {
+    return longstring.string();
+  }).then(outerHTML => {
+    is(outerHTML, newHTML, "outerHTML has been updated");
+  }).then(() => {
+    return gWalker.querySelector(gWalker.rootNode, "#edit-html");
+  }).then(node => {
+    ok(!node, "The node with the old ID cannot be selected anymore");
+  }).then(runNextTest));
+});
+
 addTest(function testQuerySelector() {
   promiseDone(gWalker.querySelector(gWalker.rootNode, "#longlist").then(node => {
     is(node.getAttribute("data-test"), "exists", "should have found the right node");
     assertOwnership();
   }).then(() => {
     return gWalker.querySelector(gWalker.rootNode, "unknownqueryselector").then(node => {
       ok(!node, "Should not find a node here.");
       assertOwnership();