Bug 575234 - Create attribute-value editor for HTML inspector; f=pwalton r=rcampbell,gavin.sharp
authorKyle Simpson <getify@mozilla.com>
Wed, 17 Aug 2011 09:16:15 -0300
changeset 75470 a2cc2d7fa5473f42c92ec2b8d99ccb90b92c5647
parent 75469 4c3632a6d26b052fd19fba1491607045a2392ede
child 75471 75eeb6cb7050af0c027302d7d76f228a888aa1bf
push id21027
push userrcampbell@mozilla.com
push dateThu, 18 Aug 2011 19:32:20 +0000
treeherdermozilla-central@932c8414512f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrcampbell, gavin.sharp
bugs575234
milestone9.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 575234 - Create attribute-value editor for HTML inspector; f=pwalton r=rcampbell,gavin.sharp
browser/base/content/domplate.jsm
browser/base/content/inspector.html
browser/base/content/inspector.js
browser/base/content/test/inspector/Makefile.in
browser/base/content/test/inspector/browser_inspector_editor.js
browser/base/content/test/inspector/browser_inspector_treePanel_result.html
browser/themes/gnomestripe/browser/inspector.css
browser/themes/pinstripe/browser/inspector.css
browser/themes/winstripe/browser/inspector.css
--- a/browser/base/content/domplate.jsm
+++ b/browser/base/content/domplate.jsm
@@ -1626,17 +1626,17 @@ BaseTemplates.Element = domplate(BaseTem
 ///////////////////////////////////////////////////////////////////////////
 //// HTMLTemplates.tags
 
 BaseTemplates.AttrTag =
   domplate.SPAN({"class": "nodeAttr editGroup"},
     "&nbsp;",
     domplate.SPAN({"class": "nodeName editable"}, "$attr.nodeName"),
     "=&quot;",
-    domplate.SPAN({"class": "nodeValue editable"}, "$attr.nodeValue"),
+    domplate.SPAN({"class": "nodeValue editable", "data-attributeName": "$attr.nodeName"}, "$attr.nodeValue"),
     "&quot;");
 
 BaseTemplates.TextTag =
   domplate.SPAN({"class": "nodeText editable"},
     domplate.FOR("chr", "$object|getNodeTextGroups",
       domplate.SPAN({"class": "$chr.class $chr.extra"},
         "$chr.str")));
 
--- a/browser/base/content/inspector.html
+++ b/browser/base/content/inspector.html
@@ -2,10 +2,13 @@
   "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
 
 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
 <head>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
   <link rel="stylesheet" href="chrome://browser/skin/inspector.css" type="text/css"/>
 </head>
 <body role="application">
+  <div id="attribute-editor">
+    <input id="attribute-editor-input" />
+  </div>
 </body>
 </html>
--- a/browser/base/content/inspector.js
+++ b/browser/base/content/inspector.js
@@ -21,16 +21,17 @@
  * Portions created by the Initial Developer are Copyright (C) 2010
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *   Rob Campbell <rcampbell@mozilla.com> (original author)
  *   Mihai Șucan <mihai.sucan@gmail.com>
  *   Julian Viereck <jviereck@mozilla.com>
  *   Paul Rouget <paul@mozilla.com>
+ *   Kyle Simpson <ksimpson@mozilla.com> 
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -65,16 +66,21 @@ const INSPECTOR_NOTIFICATIONS = {
   UNHIGHLIGHTING: "inspector-unhighlighting",
 
   // Fires once the Inspector completes the initialization and opens up on
   // screen.
   OPENED: "inspector-opened",
 
   // Fires once the Inspector is closed.
   CLOSED: "inspector-closed",
+
+  // Event notifications for the attribute-value editor
+  EDITOR_OPENED: "inspector-editor-opened",
+  EDITOR_CLOSED: "inspector-editor-closed",
+  EDITOR_SAVED: "inspector-editor-saved",
 };
 
 ///////////////////////////////////////////////////////////////////////////
 //// Highlighter
 
 /**
  * A highlighter mechanism.
  *
@@ -586,16 +592,18 @@ var InspectorUI = {
   {
     this.treeBrowserDocument = this.treeIFrame.contentDocument;
     this.treePanelDiv = this.treeBrowserDocument.createElement("div");
     this.treeBrowserDocument.body.appendChild(this.treePanelDiv);
     this.treePanelDiv.ownerPanel = this;
     this.ioBox = new InsideOutBox(this, this.treePanelDiv);
     this.ioBox.createObjectBox(this.win.document.documentElement);
     this.treeLoaded = true;
+    this.editingContext = null;
+    this.editingEvents = {};
 
     // initialize the highlighter
     this.initializeHighlighter();
   },
 
   /**
    * Open the inspector's tree panel and initialize it.
    */
@@ -609,16 +617,17 @@ var InspectorUI = {
     this.treeIFrame = document.getElementById("inspector-tree-iframe");
     if (!this.treeIFrame) {
       let resizerBox = document.getElementById("tree-panel-resizer-box");
       this.treeIFrame = document.createElement("iframe");
       this.treeIFrame.setAttribute("id", "inspector-tree-iframe");
       this.treeIFrame.setAttribute("flex", "1");
       this.treeIFrame.setAttribute("type", "content");
       this.treeIFrame.setAttribute("onclick", "InspectorUI.onTreeClick(event)");
+      this.treeIFrame.setAttribute("ondblclick", "InspectorUI.onTreeDblClick(event);");
       this.treeIFrame = this.treePanel.insertBefore(this.treeIFrame, resizerBox);
     }
 
     this.treePanel.addEventListener("popupshown", function treePanelShown() {
       InspectorUI.treePanel.removeEventListener("popupshown",
         treePanelShown, false);
 
         InspectorUI.treeIFrame.addEventListener("load",
@@ -812,16 +821,21 @@ var InspectorUI = {
    *
    * @param boolean aKeepStore
    *        Tells if you want the store associated to the current tab/window to
    *        be cleared or not. Set this to true to not clear the store, or false
    *        otherwise.
    */
   closeInspectorUI: function IUI_closeInspectorUI(aKeepStore)
   {
+    // if currently editing an attribute value, closing the
+    // highlighter/HTML panel dismisses the editor
+    if (this.editingContext)
+      this.closeEditor();
+
     if (this.closing || !this.win || !this.browser) {
       return;
     }
 
     this.closing = true;
     this.toolbar.hidden = true;
 
     if (!aKeepStore) {
@@ -888,16 +902,21 @@ var InspectorUI = {
   },
 
   /**
    * Begin inspecting webpage, attach page event listeners, activate
    * highlighter event listeners.
    */
   startInspecting: function IUI_startInspecting()
   {
+    // if currently editing an attribute value, starting
+    // "live inspection" mode closes the editor
+    if (this.editingContext)
+      this.closeEditor();
+
     document.getElementById("inspector-inspect-toolbutton").checked = true;
     this.attachPageListeners();
     this.inspecting = true;
     this.highlighter.veilTransparentBox.removeAttribute("locked");
   },
 
   /**
    * Stop inspecting webpage, detach page listeners, disable highlighter
@@ -928,16 +947,21 @@ var InspectorUI = {
    *        node to inspect
    * @param forceUpdate
    *        force an update?
    * @param aScroll
    *        force scroll?
    */
   select: function IUI_select(aNode, forceUpdate, aScroll)
   {
+    // if currently editing an attribute value, using the
+    // highlighter dismisses the editor
+    if (this.editingContext)
+      this.closeEditor();
+
     if (!aNode)
       aNode = this.defaultSelection;
 
     if (forceUpdate || aNode != this.selection) {
       this.selection = aNode;
       if (!this.inspecting) {
         this.highlighter.highlightNode(this.selection);
       }
@@ -1039,16 +1063,27 @@ var InspectorUI = {
 
   /**
    * Handle click events in the html tree panel.
    * @param aEvent
    *        The mouse event.
    */
   onTreeClick: function IUI_onTreeClick(aEvent)
   {
+    // if currently editing an attribute value, clicking outside
+    // the editor dismisses the editor
+    if (this.editingContext) {
+      this.closeEditor();
+
+      // clicking outside the editor ONLY closes the editor
+      // so, cancel the rest of the processing of this event
+      aEvent.preventDefault();
+      return;
+    }
+
     let node;
     let target = aEvent.target;
     let hitTwisty = false;
     if (this.hasClass(target, "twisty")) {
       node = this.getRepObject(aEvent.target.nextSibling);
       hitTwisty = true;
     } else {
       node = this.getRepObject(aEvent.target);
@@ -1064,16 +1099,230 @@ var InspectorUI = {
           this.select(node, true, false);
           this.highlighter.highlightNode(node);
         }
       }
     }
   },
 
   /**
+   * Handle double-click events in the html tree panel.
+   * (double-clicking an attribute value allows it to be edited)
+   * @param aEvent
+   *        The mouse event.
+   */
+  onTreeDblClick: function IUI_onTreeDblClick(aEvent)
+  {
+    // if already editing an attribute value, double-clicking elsewhere
+    // in the tree is the same as a click, which dismisses the editor
+    if (this.editingContext)
+      this.closeEditor();
+
+    let target = aEvent.target;
+
+    if (this.hasClass(target, "nodeValue")) {
+      let repObj = this.getRepObject(target);
+      let attrName = target.getAttribute("data-attributeName");
+      let attrVal = target.innerHTML;
+
+      this.editAttributeValue(target, repObj, attrName, attrVal);
+    }
+  },
+
+  /**
+   * Starts the editor for an attribute value.
+   * @param aAttrObj
+   *        The DOM object representing the attribute value in the HTML Tree
+   * @param aRepObj
+   *        The original DOM (target) object being inspected/edited
+   * @param aAttrName
+   *        The name of the attribute being edited
+   * @param aAttrVal
+   *        The current value of the attribute being edited
+   */
+  editAttributeValue: 
+  function IUI_editAttributeValue(aAttrObj, aRepObj, aAttrName, aAttrVal)
+  {
+    let editor = this.treeBrowserDocument.getElementById("attribute-editor");
+    let editorInput = 
+      this.treeBrowserDocument.getElementById("attribute-editor-input");
+    let attrDims = aAttrObj.getBoundingClientRect();
+    // figure out actual viewable viewport dimensions (sans scrollbars)
+    let viewportWidth = this.treeBrowserDocument.documentElement.clientWidth;
+    let viewportHeight = this.treeBrowserDocument.documentElement.clientHeight;
+
+    // saves the editing context for use when the editor is saved/closed
+    this.editingContext = {
+      attrObj: aAttrObj,
+      repObj: aRepObj,
+      attrName: aAttrName
+    };
+
+    // highlight attribute-value node in tree while editing
+    this.addClass(aAttrObj, "editingAttributeValue");
+
+    // show the editor
+    this.addClass(editor, "editing");
+
+    // offset the editor below the attribute-value node being edited
+    let editorVeritcalOffset = 2;
+
+    // keep the editor comfortably within the bounds of the viewport
+    let editorViewportBoundary = 5;
+
+    // outer editor is sized based on the <input> box inside it
+    editorInput.style.width = Math.min(attrDims.width, viewportWidth - 
+                                editorViewportBoundary) + "px";
+    editorInput.style.height = Math.min(attrDims.height, viewportHeight - 
+                                editorViewportBoundary) + "px";
+    let editorDims = editor.getBoundingClientRect();
+
+    // calculate position for the editor according to the attribute node
+    let editorLeft = attrDims.left + this.treeIFrame.contentWindow.scrollX -
+                    // center the editor against the attribute value    
+                    ((editorDims.width - attrDims.width) / 2); 
+    let editorTop = attrDims.top + this.treeIFrame.contentWindow.scrollY + 
+                    attrDims.height + editorVeritcalOffset;
+
+    // but, make sure the editor stays within the visible viewport
+    editorLeft = Math.max(0, Math.min(
+                                      (this.treeIFrame.contentWindow.scrollX + 
+                                          viewportWidth - editorDims.width),
+                                      editorLeft)
+                          );
+    editorTop = Math.max(0, Math.min(
+                                      (this.treeIFrame.contentWindow.scrollY + 
+                                          viewportHeight - editorDims.height),
+                                      editorTop)
+                          );
+
+    // position the editor
+    editor.style.left = editorLeft + "px";
+    editor.style.top = editorTop + "px";
+
+    // set and select the text
+    editorInput.value = aAttrVal;
+    editorInput.select();
+
+    // listen for editor specific events
+    this.bindEditorEvent(editor, "click", function(aEvent) {
+      aEvent.stopPropagation();
+    });
+    this.bindEditorEvent(editor, "dblclick", function(aEvent) {
+      aEvent.stopPropagation();
+    });
+    this.bindEditorEvent(editor, "keypress", 
+                          this.handleEditorKeypress.bind(this));
+
+    // event notification    
+    Services.obs.notifyObservers(null, INSPECTOR_NOTIFICATIONS.EDITOR_OPENED, 
+                                  null);
+  },
+
+  /**
+   * Handle binding an event handler for the editor.
+   * (saves the callback for easier unbinding later)   
+   * @param aEditor
+   *        The DOM object for the editor
+   * @param aEventName
+   *        The name of the event to listen for
+   * @param aEventCallback
+   *        The callback to bind to the event (and also to save for later 
+   *          unbinding)
+   */
+  bindEditorEvent: 
+  function IUI_bindEditorEvent(aEditor, aEventName, aEventCallback)
+  {
+    this.editingEvents[aEventName] = aEventCallback;
+    aEditor.addEventListener(aEventName, aEventCallback, false);
+  },
+
+  /**
+   * Handle unbinding an event handler from the editor.
+   * (unbinds the previously bound and saved callback)   
+   * @param aEditor
+   *        The DOM object for the editor
+   * @param aEventName
+   *        The name of the event being listened for
+   */
+  unbindEditorEvent: function IUI_unbindEditorEvent(aEditor, aEventName)
+  {
+    aEditor.removeEventListener(aEventName, this.editingEvents[aEventName], 
+                                  false);
+    this.editingEvents[aEventName] = null;
+  },
+
+  /**
+   * Handle keypress events in the editor.
+   * @param aEvent
+   *        The keyboard event.
+   */
+  handleEditorKeypress: function IUI_handleEditorKeypress(aEvent)
+  {
+    if (aEvent.which == KeyEvent.DOM_VK_RETURN) {
+      this.saveEditor();
+    } else if (aEvent.keyCode == KeyEvent.DOM_VK_ESCAPE) {
+      this.closeEditor();
+    }
+  },
+
+  /**
+   * Close the editor and cleanup.
+   */
+  closeEditor: function IUI_closeEditor()
+  {
+    let editor = this.treeBrowserDocument.getElementById("attribute-editor");
+    let editorInput = 
+      this.treeBrowserDocument.getElementById("attribute-editor-input");
+
+    // remove highlight from attribute-value node in tree
+    this.removeClass(this.editingContext.attrObj, "editingAttributeValue");
+
+    // hide editor
+    this.removeClass(editor, "editing");
+
+    // stop listening for editor specific events
+    this.unbindEditorEvent(editor, "click");
+    this.unbindEditorEvent(editor, "dblclick");
+    this.unbindEditorEvent(editor, "keypress");
+
+    // clean up after the editor
+    editorInput.value = "";
+    editorInput.blur();
+    this.editingContext = null;
+    this.editingEvents = {};
+
+    // event notification    
+    Services.obs.notifyObservers(null, INSPECTOR_NOTIFICATIONS.EDITOR_CLOSED, 
+                                  null);
+  },
+
+  /**
+   * Commit the edits made in the editor, then close it.
+   */
+  saveEditor: function IUI_saveEditor()
+  {
+    let editorInput = 
+      this.treeBrowserDocument.getElementById("attribute-editor-input");
+
+    // set the new attribute value on the original target DOM element
+    this.editingContext.repObj.setAttribute(this.editingContext.attrName, 
+                                              editorInput.value);
+
+    // update the HTML tree attribute value
+    this.editingContext.attrObj.innerHTML = editorInput.value;
+
+    // event notification    
+    Services.obs.notifyObservers(null, INSPECTOR_NOTIFICATIONS.EDITOR_SAVED, 
+                                  null);
+    
+    this.closeEditor();
+  },
+
+  /**
    * Attach event listeners to content window and child windows to enable
    * highlighting and click to stop inspection.
    */
   attachPageListeners: function IUI_attachPageListeners()
   {
     this.browser.addEventListener("keypress", this, true);
     this.highlighter.attachInspectListeners();
   },
--- a/browser/base/content/test/inspector/Makefile.in
+++ b/browser/base/content/test/inspector/Makefile.in
@@ -51,12 +51,13 @@ include $(topsrcdir)/config/rules.mk
 		browser_inspector_scrolling.js \
 		browser_inspector_store.js \
 		browser_inspector_tab_switch.js \
 		browser_inspector_treePanel_output.js \
 		browser_inspector_treePanel_input.html \
 		browser_inspector_treePanel_result.html \
 		browser_inspector_registertools.js \
 		browser_inspector_bug_665880.js \
+		browser_inspector_editor.js \
 		$(NULL)
 
 libs::	$(_BROWSER_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/inspector/browser_inspector_editor.js
@@ -0,0 +1,233 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* ***** BEGIN LICENSE BLOCK *****
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ * 
+ * Contributor(s):
+ *   Rob Campbell <rcampbell@mozilla.com>
+ *   Mihai Sucan <mihai.sucan@gmail.com>
+ *   Kyle Simpson <ksimpson@mozilla.com> 
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+let doc;
+let div;
+let editorTestSteps;
+
+function doNextStep() {
+  editorTestSteps.next();
+}
+
+function setupEditorTests()
+{
+  div = doc.createElement("div");
+  div.setAttribute("id", "foobar");
+  div.setAttribute("class", "barbaz");
+  doc.body.appendChild(div);
+  
+  Services.obs.addObserver(runEditorTests, INSPECTOR_NOTIFICATIONS.OPENED, false);
+  InspectorUI.toggleInspectorUI();
+}
+
+function runEditorTests()
+{
+  Services.obs.removeObserver(runEditorTests, INSPECTOR_NOTIFICATIONS.OPENED, false);
+  InspectorUI.stopInspecting();
+  
+  // setup generator for async test steps
+  editorTestSteps = doEditorTestSteps();
+  
+  // add step listeners
+  Services.obs.addObserver(doNextStep, INSPECTOR_NOTIFICATIONS.EDITOR_OPENED, false);
+  Services.obs.addObserver(doNextStep, INSPECTOR_NOTIFICATIONS.EDITOR_CLOSED, false);
+  Services.obs.addObserver(doNextStep, INSPECTOR_NOTIFICATIONS.EDITOR_SAVED, false);
+
+  // start the tests
+  doNextStep();
+}
+
+function doEditorTestSteps()
+{
+  let editor = InspectorUI.treeBrowserDocument.getElementById("attribute-editor");
+  let editorInput = InspectorUI.treeBrowserDocument.getElementById("attribute-editor-input");
+
+  // Step 1: grab and test the attribute-value nodes in the HTML panel, then open editor
+  let attrValNode_id = InspectorUI.treeBrowserDocument.querySelectorAll(".nodeValue.editable[data-attributeName='id']")[0];
+  let attrValNode_class = InspectorUI.treeBrowserDocument.querySelectorAll(".nodeValue.editable[data-attributeName='class']")[0];
+
+  is(attrValNode_id.innerHTML, "foobar", "Step 1: we have the correct `id` attribute-value node in the HTML panel");
+  is(attrValNode_class.innerHTML, "barbaz", "we have the correct `class` attribute-value node in the HTML panel");
+  
+  // double-click the `id` attribute-value node to open the editor
+  executeSoon(function() {
+    // firing 2 clicks right in a row to simulate a double-click
+    EventUtils.synthesizeMouse(attrValNode_id, 2, 2, {clickCount: 2}, attrValNode_id.ownerDocument.defaultView);
+  });
+
+  yield; // End of Step 1
+
+
+  // Step 2: validate editing session, enter new attribute value into editor, and save input
+  ok(InspectorUI.editingContext, "Step 2: editor session started");
+
+  let editorVisible = editor.classList.contains("editing");
+  ok(editorVisible, "editor popup visible");
+
+  // check if the editor popup is "near" the correct position
+  let editorDims = editor.getBoundingClientRect();
+  let attrValNodeDims = attrValNode_id.getBoundingClientRect();
+  let editorPositionOK = (editorDims.left >= (attrValNodeDims.left - editorDims.width - 5)) &&
+                          (editorDims.right <= (attrValNodeDims.right + editorDims.width + 5)) &&
+                          (editorDims.top >= (attrValNodeDims.top - editorDims.height - 5)) &&
+                          (editorDims.bottom <= (attrValNodeDims.bottom + editorDims.height + 5));
+
+  ok(editorPositionOK, "editor position acceptable");
+
+  // check to make sure the attribute-value node being edited is properly highlighted
+  let attrValNodeHighlighted = attrValNode_id.classList.contains("editingAttributeValue");
+  ok(attrValNodeHighlighted, "`id` attribute-value node is editor-highlighted");
+
+  is(InspectorUI.editingContext.repObj, div, "editor session has correct reference to div");
+  is(InspectorUI.editingContext.attrObj, attrValNode_id, "editor session has correct reference to `id` attribute-value node in HTML panel");
+  is(InspectorUI.editingContext.attrName, "id", "editor session knows correct attribute-name");
+
+  editorInput.value = "Hello World";
+  editorInput.focus();
+  
+  // hit <enter> to save the inputted value
+  executeSoon(function() {
+    EventUtils.synthesizeKey("VK_RETURN", {}, attrValNode_id.ownerDocument.defaultView);
+  });
+
+  // two `yield` statements, to trap both the "SAVED" and "CLOSED" events that will be triggered
+  yield;
+  yield; // End of Step 2
+
+
+  // Step 3: validate that the previous editing session saved correctly, then open editor on `class` attribute value
+  ok(!InspectorUI.editingContext, "Step 3: editor session ended");
+  editorVisible = editor.classList.contains("editing");
+  ok(!editorVisible, "editor popup hidden");
+  attrValNodeHighlighted = attrValNode_id.classList.contains("editingAttributeValue");
+  ok(!attrValNodeHighlighted, "`id` attribute-value node is no longer editor-highlighted");
+  is(div.getAttribute("id"), "Hello World", "`id` attribute-value successfully updated");
+  is(attrValNode_id.innerHTML, "Hello World", "attribute-value node in HTML panel successfully updated");
+
+  // double-click the `class` attribute-value node to open the editor
+  executeSoon(function() {
+    // firing 2 clicks right in a row to simulate a double-click
+    EventUtils.synthesizeMouse(attrValNode_class, 2, 2, {clickCount: 2}, attrValNode_class.ownerDocument.defaultView);
+  });
+
+  yield; // End of Step 3
+
+
+  // Step 4: enter value into editor, then hit <escape> to discard it
+  ok(InspectorUI.editingContext, "Step 4: editor session started");
+  editorVisible = editor.classList.contains("editing");
+  ok(editorVisible, "editor popup visible");
+  
+  is(InspectorUI.editingContext.attrObj, attrValNode_class, "editor session has correct reference to `class` attribute-value node in HTML panel");
+  is(InspectorUI.editingContext.attrName, "class", "editor session knows correct attribute-name");
+
+  editorInput.value = "Hello World";
+  editorInput.focus();
+  
+  // hit <escape> to discard the inputted value
+  executeSoon(function() {
+    EventUtils.synthesizeKey("VK_ESCAPE", {}, attrValNode_class.ownerDocument.defaultView);
+  });
+
+  yield; // End of Step 4
+
+
+  // Step 5: validate that the previous editing session discarded correctly, then open editor on `id` attribute value again
+  ok(!InspectorUI.editingContext, "Step 5: editor session ended");
+  editorVisible = editor.classList.contains("editing");
+  ok(!editorVisible, "editor popup hidden");
+  is(div.getAttribute("class"), "barbaz", "`class` attribute-value *not* updated");
+  is(attrValNode_class.innerHTML, "barbaz", "attribute-value node in HTML panel *not* updated");
+
+  // double-click the `id` attribute-value node to open the editor
+  executeSoon(function() {
+    // firing 2 clicks right in a row to simulate a double-click
+    EventUtils.synthesizeMouse(attrValNode_id, 2, 2, {clickCount: 2}, attrValNode_id.ownerDocument.defaultView);
+  });
+
+  yield; // End of Step 5
+
+
+  // Step 6: validate that editor opened again, then test double-click inside of editor (should do nothing)
+  ok(InspectorUI.editingContext, "Step 6: editor session started");
+  editorVisible = editor.classList.contains("editing");
+  ok(editorVisible, "editor popup visible");
+  
+  // double-click on the editor input box
+  executeSoon(function() {
+    // firing 2 clicks right in a row to simulate a double-click
+    EventUtils.synthesizeMouse(editorInput, 2, 2, {clickCount: 2}, editorInput.ownerDocument.defaultView);
+    
+    // since the previous double-click is supposed to do nothing,
+    // wait a brief moment, then move on to the next step
+    executeSoon(function() {
+      doNextStep();
+    });
+  });
+
+  yield; // End of Step 6
+
+
+  // Step 7: validate that editing session is still correct, then enter a value and try a click 
+  //         outside of editor (should cancel the editing session)
+  ok(InspectorUI.editingContext, "Step 7: editor session still going");
+  editorVisible = editor.classList.contains("editing");
+  ok(editorVisible, "editor popup still visible");
+  
+  editorInput.value = "all your base are belong to us";
+
+  // single-click the `class` attribute-value node
+  executeSoon(function() {
+    EventUtils.synthesizeMouse(attrValNode_class, 2, 2, {}, attrValNode_class.ownerDocument.defaultView);
+  });
+
+  yield; // End of Step 7
+
+
+  // Step 8: validate that the editor was closed and that the editing was not saved
+  ok(!InspectorUI.editingContext, "Step 8: editor session ended");
+  editorVisible = editor.classList.contains("editing");
+  ok(!editorVisible, "editor popup hidden");
+  is(div.getAttribute("id"), "Hello World", "`id` attribute-value *not* updated");
+  is(attrValNode_id.innerHTML, "Hello World", "attribute-value node in HTML panel *not* updated");
+  
+  // End of Step 8
+
+  // end of all steps, so clean up
+  Services.obs.removeObserver(doNextStep, INSPECTOR_NOTIFICATIONS.EDITOR_OPENED, false);
+  Services.obs.removeObserver(doNextStep, INSPECTOR_NOTIFICATIONS.EDITOR_CLOSED, false);
+  Services.obs.removeObserver(doNextStep, INSPECTOR_NOTIFICATIONS.EDITOR_SAVED, false);
+
+  executeSoon(finishUp);
+}
+
+function finishUp() {
+  doc = div = null;
+  InspectorUI.closeInspectorUI();
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function test()
+{
+  waitForExplicitFinish();
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function() {
+    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+    doc = content.document;
+    waitForFocus(setupEditorTests, content);
+  }, true);
+
+  content.location = "data:text/html,basic tests for html panel attribute-value editor";
+}
+
--- a/browser/base/content/test/inspector/browser_inspector_treePanel_result.html
+++ b/browser/base/content/test/inspector/browser_inspector_treePanel_result.html
@@ -1,8 +1,8 @@
-<div role="presentation" class="nodeBox htmlNodeBox containerNodeBox  repIgnore open"><div class="docType ">&lt;!DOCTYPE html&gt;</div><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">html</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">xml:lang</span>="<span class="nodeValue editable ">en</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">head</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox emptyNodeBox  repIgnore"><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">meta</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">charset</span>="<span class="nodeValue editable ">utf-8</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div></div><div role="presentation" class="nodeBox textNodeBox  repIgnore "><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">title</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  ">Inspector tree panel test</span></span>&lt;/<span class="nodeTag ">title</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">style</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">type</span>="<span class="nodeValue editable ">text/css</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox "><span class="nodeText editable "><span class="  ">&lt;!--
+<div role="presentation" class="nodeBox htmlNodeBox containerNodeBox  repIgnore open"><div class="docType ">&lt;!DOCTYPE html&gt;</div><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">html</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">xml:lang</span>="<span data-attributename="xml:lang" class="nodeValue editable ">en</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">head</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox emptyNodeBox  repIgnore"><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">meta</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">charset</span>="<span data-attributename="charset" class="nodeValue editable ">utf-8</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div></div><div role="presentation" class="nodeBox textNodeBox  repIgnore "><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">title</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  ">Inspector tree panel test</span></span>&lt;/<span class="nodeTag ">title</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">style</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">type</span>="<span data-attributename="type" class="nodeValue editable ">text/css</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox "><span class="nodeText editable "><span class="  ">&lt;!--
 #duplicate { color: green }
---&gt;</span></span></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">style</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">script</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">type</span>="<span class="nodeValue editable ">text/javascript</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox "><span class="nodeText editable "><span class="  ">&lt;!--
+--&gt;</span></span></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">style</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">script</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">type</span>="<span data-attributename="type" class="nodeValue editable ">text/javascript</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox "><span class="nodeText editable "><span class="  ">&lt;!--
 function fooBarBaz(arg1) {
 return true; // do nothing
 }
-// --&gt;</span></span></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">script</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">head</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">body</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">arbitrary:attribute</span>="<span class="nodeValue editable ">value</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox textNodeBox  repIgnore "><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">p</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  ">Inspector tree panel test.</span></span>&lt;/<span class="nodeTag ">p</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">div</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">id</span>="<span class="nodeValue editable ">foo</span>"</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">class</span>="<span class="nodeValue editable ">foo bar baz</span>"</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">style</span>="<span class="nodeValue editable ">border:1px solid red;
-unknownProperty: unknownValue; color: withUnkownValue</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">unknowntag</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">unknownattribute</span>="<span class="nodeValue editable ">fooBar</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox textNodeBox  repIgnore"><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">p</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">data-test1</span>="<span class="nodeValue editable ">value</span>"</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">unknownattribute</span>="<span class="nodeValue editable ">fooBar</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  ">hello world!</span></span>&lt;/<span class="nodeTag ">p</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">unknowntag</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">div</span>&gt;</span></div></div><div role="presentation" class="nodeBox textNodeBox  repIgnore "><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">div</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">id</span>="<span class="nodeValue editable ">duplicate</span>"</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">class</span>="<span class="nodeValue editable ">test</span>"</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">foobar</span>="<span class="nodeValue editable ">baz</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  ">test</span></span>&lt;/<span class="nodeTag ">div</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">iframe</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">src</span>="<span class="nodeValue editable ">data:text/html,&lt;div&gt;hello from an iframe!&lt;/div&gt;</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">html</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox textNodeBox  repIgnore "><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">head</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  "></span></span>&lt;/<span class="nodeTag ">head</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">body</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox textNodeBox  repIgnore selected"><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">div</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  ">hello from an iframe!</span></span>&lt;/<span class="nodeTag ">div</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">body</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">html</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">iframe</span>&gt;</span></div></div><div role="presentation" class="nodeBox nodeComment ">&lt;!--<span class="nodeComment editable "> hello world from a comment! </span>--&gt;</div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">body</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">html</span>&gt;</span></div></div>
+// --&gt;</span></span></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">script</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">head</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">body</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">arbitrary:attribute</span>="<span data-attributename="arbitrary:attribute" class="nodeValue editable ">value</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox textNodeBox  repIgnore "><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">p</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  ">Inspector tree panel test.</span></span>&lt;/<span class="nodeTag ">p</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">div</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">id</span>="<span data-attributename="id" class="nodeValue editable ">foo</span>"</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">class</span>="<span data-attributename="class" class="nodeValue editable ">foo bar baz</span>"</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">style</span>="<span data-attributename="style" class="nodeValue editable ">border:1px solid red;
+unknownProperty: unknownValue; color: withUnkownValue</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">unknowntag</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">unknownattribute</span>="<span data-attributename="unknownattribute" class="nodeValue editable ">fooBar</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox textNodeBox  repIgnore"><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">p</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">data-test1</span>="<span data-attributename="data-test1" class="nodeValue editable ">value</span>"</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">unknownattribute</span>="<span data-attributename="unknownattribute" class="nodeValue editable ">fooBar</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  ">hello world!</span></span>&lt;/<span class="nodeTag ">p</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">unknowntag</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">div</span>&gt;</span></div></div><div role="presentation" class="nodeBox textNodeBox  repIgnore "><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">div</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">id</span>="<span data-attributename="id" class="nodeValue editable ">duplicate</span>"</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">class</span>="<span data-attributename="class" class="nodeValue editable ">test</span>"</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">foobar</span>="<span data-attributename="foobar" class="nodeValue editable ">baz</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  ">test</span></span>&lt;/<span class="nodeTag ">div</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">iframe</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">src</span>="<span data-attributename="src" class="nodeValue editable ">data:text/html,&lt;div&gt;hello from an iframe!&lt;/div&gt;</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">html</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox textNodeBox  repIgnore "><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">head</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  "></span></span>&lt;/<span class="nodeTag ">head</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">body</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox textNodeBox  repIgnore selected"><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">div</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  ">hello from an iframe!</span></span>&lt;/<span class="nodeTag ">div</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">body</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">html</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">iframe</span>&gt;</span></div></div><div role="presentation" class="nodeBox nodeComment ">&lt;!--<span class="nodeComment editable "> hello world from a comment! </span>--&gt;</div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">body</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">html</span>&gt;</span></div></div>
\ No newline at end of file
--- a/browser/themes/gnomestripe/browser/inspector.css
+++ b/browser/themes/gnomestripe/browser/inspector.css
@@ -370,8 +370,31 @@ code {
   border-color: #3875d7;
   background-color: #3875d7;
   color: #FFFFFF !important;
 }
 
 .nodeBox.highlighted > .nodeLabel {
   border-color: #3875d7 !important;
 }
+
+.editingAttributeValue {
+  background-color: #492;
+}
+
+#attribute-editor {
+  visibility: hidden;
+  position: absolute;
+  z-index: 5000;
+  background-color: #fff;
+  border: 1px solid #000;
+}
+
+#attribute-editor.editing {
+  visibility: visible;
+}
+
+#attribute-editor-input {
+  border: none;
+  padding: 2px 5px;
+  font-family: Menlo, Andale Mono, monospace;
+  font-size: 11px;
+}
--- a/browser/themes/pinstripe/browser/inspector.css
+++ b/browser/themes/pinstripe/browser/inspector.css
@@ -358,8 +358,31 @@ code {
   border-color: #3875d7;
   background-color: #3875d7;
   color: #FFFFFF !important;
 }
 
 .nodeBox.highlighted > .nodeLabel {
   border-color: #3875d7 !important;
 }
+
+.editingAttributeValue {
+  background-color: #492;
+}
+
+#attribute-editor {
+  visibility: hidden;
+  position: absolute;
+  z-index: 5000;
+  background-color: #fff;
+  border: 1px solid #000;
+}
+
+#attribute-editor.editing {
+  visibility: visible;
+}
+
+#attribute-editor-input {
+  border: none;
+  padding: 2px 5px;
+  font-family: Menlo, Andale Mono, monospace;
+  font-size: 11px;
+}
--- a/browser/themes/winstripe/browser/inspector.css
+++ b/browser/themes/winstripe/browser/inspector.css
@@ -343,8 +343,30 @@ code {
 }
 
 .nodeBox.highlightOpen > .nodeLabel > .twisty,
 .nodeBox.open > .nodeLabel > .twisty
 {
   background-image: url("chrome://global/skin/tree/twisty-open.png") !important;
 }
 
+.editingAttributeValue {
+  background-color: #492;
+}
+
+#attribute-editor {
+  visibility: hidden;
+  position: absolute;
+  z-index: 5000;
+  background-color: #fff;
+  border: 1px solid #000;
+}
+
+#attribute-editor.editing {
+  visibility: visible;
+}
+
+#attribute-editor-input {
+  border: none;
+  padding: 2px 5px;
+  font-family: Menlo, Andale Mono, monospace;
+  font-size: 11px;
+}