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 165911 9c44473817b5ad6555e0e08794e131880a011f8b
parent 165910 45a18b183676f8f6ded4466030d3fc4c35a0ca51
child 165912 8693c78697ae83fb6302aada797e96c537f18699
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfitzgen
bugs895561
milestone27.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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();