Bug 845822 - Inplace editor to be moved to devtools/shared/InplaceEditor.jsm r=jwalker
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Wed, 20 Mar 2013 12:11:50 +0000
changeset 125735 672608b227c3ca1d778cb581ac5297bc214cdcd0
parent 125734 8caaaf8e18946ecb65051148872775beac244393
child 125736 f6e7195e87dbf93f65e36b0214f9ff15c65474ed
push id25087
push usereakhgari@mozilla.com
push dateThu, 21 Mar 2013 12:27:40 +0000
treeherdermozilla-inbound@be6da6dbf632 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker
bugs845822
milestone22.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 845822 - Inplace editor to be moved to devtools/shared/InplaceEditor.jsm r=jwalker
browser/devtools/markupview/MarkupView.jsm
browser/devtools/markupview/test/browser_inspector_markup_edit.js
browser/devtools/shared/InplaceEditor.jsm
browser/devtools/styleinspector/CssRuleView.jsm
browser/devtools/styleinspector/test/browser_bug722691_rule_view_increment.js
browser/devtools/styleinspector/test/browser_ruleview_bug_703643_context_menu_copy.js
browser/devtools/styleinspector/test/browser_ruleview_editor.js
browser/devtools/styleinspector/test/browser_ruleview_editor_changedvalues.js
browser/devtools/styleinspector/test/browser_ruleview_focus.js
browser/devtools/styleinspector/test/browser_ruleview_inherit.js
browser/devtools/styleinspector/test/browser_ruleview_manipulation.js
browser/devtools/styleinspector/test/browser_ruleview_override.js
browser/devtools/styleinspector/test/browser_ruleview_ui.js
browser/devtools/styleinspector/test/browser_ruleview_update.js
browser/devtools/styleinspector/test/head.js
--- a/browser/devtools/markupview/MarkupView.jsm
+++ b/browser/devtools/markupview/MarkupView.jsm
@@ -13,16 +13,17 @@ const PAGE_SIZE = 10;
 
 const PREVIEW_AREA = 700;
 const DEFAULT_MAX_CHILDREN = 100;
 
 this.EXPORTED_SYMBOLS = ["MarkupView"];
 
 Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource:///modules/devtools/CssRuleView.jsm");
+Cu.import("resource:///modules/devtools/InplaceEditor.jsm");
 Cu.import("resource:///modules/devtools/Templater.jsm");
 Cu.import("resource:///modules/devtools/Undo.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 /**
  * Vocabulary for the purposes of this file:
  *
@@ -957,17 +958,17 @@ function DoctypeEditor(aContainer, aNode
  * @param string aTemplate The template id to use to build the editor.
  */
 function TextEditor(aContainer, aNode, aTemplate)
 {
   this.node = aNode;
 
   aContainer.markup.template(aTemplate, this);
 
-  _editableField({
+  editableField({
     element: this.value,
     stopOnReturn: true,
     trigger: "dblclick",
     multiline: true,
     done: function TE_done(aVal, aCommit) {
       if (!aCommit) {
         return;
       }
@@ -1026,26 +1027,26 @@ function ElementEditor(aContainer, aNode
   }
 
   // Create the closing tag
   this.template("elementClose", this);
 
   // Make the tag name editable (unless this is a document element)
   if (aNode != aNode.ownerDocument.documentElement) {
     this.tag.setAttribute("tabindex", "0");
-    _editableField({
+    editableField({
       element: this.tag,
       trigger: "dblclick",
       stopOnReturn: true,
       done: this.onTagEdit.bind(this),
     });
   }
 
   // Make the new attribute space editable.
-  _editableField({
+  editableField({
     element: this.newAttr,
     trigger: "dblclick",
     stopOnReturn: true,
     done: function EE_onNew(aVal, aCommit) {
       if (!aCommit) {
         return;
       }
 
@@ -1115,17 +1116,17 @@ ElementEditor.prototype = {
         before = this.attrList.firstChild;
       } else if (aAttr.name == "class") {
         let idNode = this.attrs["id"];
         before = idNode ? idNode.nextSibling : this.attrList.firstChild;
       }
       this.attrList.insertBefore(attr, before);
 
       // Make the attribute editable.
-      _editableField({
+      editableField({
         element: inner,
         trigger: "dblclick",
         stopOnReturn: true,
         selectAll: false,
         start: function EE_editAttribute_start(aEditor, aEvent) {
           // If the editing was started inside the name or value areas,
           // select accordingly.
           if (aEvent && aEvent.target === name) {
--- a/browser/devtools/markupview/test/browser_inspector_markup_edit.js
+++ b/browser/devtools/markupview/test/browser_inspector_markup_edit.js
@@ -13,20 +13,19 @@ http://creativecommons.org/publicdomain/
  *
  * This test mostly tries to verify that the editor makes changes to the
  * underlying DOM, not that the UI updates - UI updates are based on
  * underlying DOM changes, and the mutation tests should cover those cases.
  */
 
 function test() {
   let inspector;
-  let tempScope = {}
-  Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
-
-  let inplaceEditor = tempScope._getInplaceEditorForSpan;
+  let {
+    getInplaceEditorForSpan: inplaceEditor
+  } = Cu.import("resource:///modules/devtools/InplaceEditor.jsm", {});
 
   waitForExplicitFinish();
 
   // Will hold the doc we're viewing
   let doc;
 
   // Holds the MarkupTool object we're testing.
   let markup;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/InplaceEditor.jsm
@@ -0,0 +1,849 @@
+/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+
+/**
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * Basic use:
+ * let spanToEdit = document.getElementById("somespan");
+ *
+ * editableField({
+ *   element: spanToEdit,
+ *   done: function(value, commit) {
+ *     if (commit) {
+ *       spanToEdit.textContent = value;
+ *     }
+ *   },
+ *   trigger: "dblclick"
+ * });
+ *
+ * See editableField() for more options.
+ */
+
+"use strict";
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const FOCUS_FORWARD = Ci.nsIFocusManager.MOVEFOCUS_FORWARD;
+const FOCUS_BACKWARD = Ci.nsIFocusManager.MOVEFOCUS_BACKWARD;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["editableItem",
+                         "editableField",
+                         "getInplaceEditorForSpan",
+                         "InplaceEditor"];
+
+/**
+ * Mark a span editable.  |editableField| will listen for the span to
+ * be focused and create an InlineEditor to handle text input.
+ * Changes will be committed when the InlineEditor's input is blurred
+ * or dropped when the user presses escape.
+ *
+ * @param {object} aOptions
+ *    Options for the editable field, including:
+ *    {Element} element:
+ *      (required) The span to be edited on focus.
+ *    {function} canEdit:
+ *       Will be called before creating the inplace editor.  Editor
+ *       won't be created if canEdit returns false.
+ *    {function} start:
+ *       Will be called when the inplace editor is initialized.
+ *    {function} change:
+ *       Will be called when the text input changes.  Will be called
+ *       with the current value of the text input.
+ *    {function} done:
+ *       Called when input is committed or blurred.  Called with
+ *       current value and a boolean telling the caller whether to
+ *       commit the change.  This function is called before the editor
+ *       has been torn down.
+ *    {function} destroy:
+ *       Called when the editor is destroyed and has been torn down.
+ *    {string} advanceChars:
+ *       If any characters in advanceChars are typed, focus will advance
+ *       to the next element.
+ *    {boolean} stopOnReturn:
+ *       If true, the return key will not advance the editor to the next
+ *       focusable element.
+ *    {string} trigger: The DOM event that should trigger editing,
+ *      defaults to "click"
+ */
+function editableField(aOptions)
+{
+  return editableItem(aOptions, function(aElement, aEvent) {
+    new InplaceEditor(aOptions, aEvent);
+  });
+}
+
+/**
+ * Handle events for an element that should respond to
+ * clicks and sit in the editing tab order, and call
+ * a callback when it is activated.
+ *
+ * @param {object} aOptions
+ *    The options for this editor, including:
+ *    {Element} element: The DOM element.
+ *    {string} trigger: The DOM event that should trigger editing,
+ *      defaults to "click"
+ * @param {function} aCallback
+ *        Called when the editor is activated.
+ */
+this.editableItem = function editableItem(aOptions, aCallback)
+{
+  let trigger = aOptions.trigger || "click"
+  let element = aOptions.element;
+  element.addEventListener(trigger, function(evt) {
+    let win = this.ownerDocument.defaultView;
+    let selection = win.getSelection();
+    if (trigger != "click" || selection.isCollapsed) {
+      aCallback(element, evt);
+    }
+    evt.stopPropagation();
+  }, false);
+
+  // If focused by means other than a click, start editing by
+  // pressing enter or space.
+  element.addEventListener("keypress", function(evt) {
+    if (evt.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN ||
+        evt.charCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
+      aCallback(element);
+    }
+  }, true);
+
+  // Ugly workaround - the element is focused on mousedown but
+  // the editor is activated on click/mouseup.  This leads
+  // to an ugly flash of the focus ring before showing the editor.
+  // So hide the focus ring while the mouse is down.
+  element.addEventListener("mousedown", function(evt) {
+    let cleanup = function() {
+      element.style.removeProperty("outline-style");
+      element.removeEventListener("mouseup", cleanup, false);
+      element.removeEventListener("mouseout", cleanup, false);
+    };
+    element.style.setProperty("outline-style", "none");
+    element.addEventListener("mouseup", cleanup, false);
+    element.addEventListener("mouseout", cleanup, false);
+  }, false);
+
+  // Mark the element editable field for tab
+  // navigation while editing.
+  element._editable = true;
+}
+
+/*
+ * Various API consumers (especially tests) sometimes want to grab the
+ * inplaceEditor expando off span elements. However, when each global has its
+ * own compartment, those expandos live on Xray wrappers that are only visible
+ * within this JSM. So we provide a little workaround here.
+ */
+this.getInplaceEditorForSpan = function getInplaceEditorForSpan(aSpan)
+{
+  return aSpan.inplaceEditor;
+};
+
+function InplaceEditor(aOptions, aEvent)
+{
+  this.elt = aOptions.element;
+  let doc = this.elt.ownerDocument;
+  this.doc = doc;
+  this.elt.inplaceEditor = this;
+
+  this.change = aOptions.change;
+  this.done = aOptions.done;
+  this.destroy = aOptions.destroy;
+  this.initial = aOptions.initial ? aOptions.initial : this.elt.textContent;
+  this.multiline = aOptions.multiline || false;
+  this.stopOnReturn = !!aOptions.stopOnReturn;
+
+  this._onBlur = this._onBlur.bind(this);
+  this._onKeyPress = this._onKeyPress.bind(this);
+  this._onInput = this._onInput.bind(this);
+  this._onKeyup = this._onKeyup.bind(this);
+
+  this._createInput();
+  this._autosize();
+
+  // Pull out character codes for advanceChars, listing the
+  // characters that should trigger a blur.
+  this._advanceCharCodes = {};
+  let advanceChars = aOptions.advanceChars || '';
+  for (let i = 0; i < advanceChars.length; i++) {
+    this._advanceCharCodes[advanceChars.charCodeAt(i)] = true;
+  }
+
+  // Hide the provided element and add our editor.
+  this.originalDisplay = this.elt.style.display;
+  this.elt.style.display = "none";
+  this.elt.parentNode.insertBefore(this.input, this.elt);
+
+  if (typeof(aOptions.selectAll) == "undefined" || aOptions.selectAll) {
+    this.input.select();
+  }
+  this.input.focus();
+
+  this.input.addEventListener("blur", this._onBlur, false);
+  this.input.addEventListener("keypress", this._onKeyPress, false);
+  this.input.addEventListener("input", this._onInput, false);
+  this.input.addEventListener("mousedown", function(aEvt) {
+                                             aEvt.stopPropagation();
+                                           }, false);
+
+  this.warning = aOptions.warning;
+  this.validate = aOptions.validate;
+
+  if (this.warning && this.validate) {
+    this.input.addEventListener("keyup", this._onKeyup, false);
+  }
+
+  if (aOptions.start) {
+    aOptions.start(this, aEvent);
+  }
+}
+
+InplaceEditor.prototype = {
+  _createInput: function InplaceEditor_createEditor()
+  {
+    this.input =
+      this.doc.createElementNS(HTML_NS, this.multiline ? "textarea" : "input");
+    this.input.inplaceEditor = this;
+    this.input.classList.add("styleinspector-propertyeditor");
+    this.input.value = this.initial;
+
+    copyTextStyles(this.elt, this.input);
+  },
+
+  /**
+   * Get rid of the editor.
+   */
+  _clear: function InplaceEditor_clear()
+  {
+    if (!this.input) {
+      // Already cleared.
+      return;
+    }
+
+    this.input.removeEventListener("blur", this._onBlur, false);
+    this.input.removeEventListener("keypress", this._onKeyPress, false);
+    this.input.removeEventListener("keyup", this._onKeyup, false);
+    this.input.removeEventListener("oninput", this._onInput, false);
+    this._stopAutosize();
+
+    this.elt.style.display = this.originalDisplay;
+    this.elt.focus();
+
+    if (this.destroy) {
+      this.destroy();
+    }
+
+    this.elt.parentNode.removeChild(this.input);
+    this.input = null;
+
+    delete this.elt.inplaceEditor;
+    delete this.elt;
+  },
+
+  /**
+   * Keeps the editor close to the size of its input string.  This is pretty
+   * crappy, suggestions for improvement welcome.
+   */
+  _autosize: function InplaceEditor_autosize()
+  {
+    // Create a hidden, absolutely-positioned span to measure the text
+    // in the input.  Boo.
+
+    // We can't just measure the original element because a) we don't
+    // change the underlying element's text ourselves (we leave that
+    // up to the client), and b) without tweaking the style of the
+    // original element, it might wrap differently or something.
+    this._measurement =
+      this.doc.createElementNS(HTML_NS, this.multiline ? "pre" : "span");
+    this._measurement.className = "autosizer";
+    this.elt.parentNode.appendChild(this._measurement);
+    let style = this._measurement.style;
+    style.visibility = "hidden";
+    style.position = "absolute";
+    style.top = "0";
+    style.left = "0";
+    copyTextStyles(this.input, this._measurement);
+    this._updateSize();
+  },
+
+  /**
+   * Clean up the mess created by _autosize().
+   */
+  _stopAutosize: function InplaceEditor_stopAutosize()
+  {
+    if (!this._measurement) {
+      return;
+    }
+    this._measurement.parentNode.removeChild(this._measurement);
+    delete this._measurement;
+  },
+
+  /**
+   * Size the editor to fit its current contents.
+   */
+  _updateSize: function InplaceEditor_updateSize()
+  {
+    // Replace spaces with non-breaking spaces.  Otherwise setting
+    // the span's textContent will collapse spaces and the measurement
+    // will be wrong.
+    this._measurement.textContent = this.input.value.replace(/ /g, '\u00a0');
+
+    // We add a bit of padding to the end.  Should be enough to fit
+    // any letter that could be typed, otherwise we'll scroll before
+    // we get a chance to resize.  Yuck.
+    let width = this._measurement.offsetWidth + 10;
+
+    if (this.multiline) {
+      // Make sure there's some content in the current line.  This is a hack to
+      // account for the fact that after adding a newline the <pre> doesn't grow
+      // unless there's text content on the line.
+      width += 15;
+      this._measurement.textContent += "M";
+      this.input.style.height = this._measurement.offsetHeight + "px";
+    }
+
+    this.input.style.width = width + "px";
+  },
+
+   /**
+   * Increment property values in rule view.
+   *
+   * @param {number} increment
+   *        The amount to increase/decrease the property value.
+   * @return {bool} true if value has been incremented.
+   */
+  _incrementValue: function InplaceEditor_incrementValue(increment)
+  {
+    let value = this.input.value;
+    let selectionStart = this.input.selectionStart;
+    let selectionEnd = this.input.selectionEnd;
+
+    let newValue = this._incrementCSSValue(value, increment, selectionStart,
+                                           selectionEnd);
+
+    if (!newValue) {
+      return false;
+    }
+
+    this.input.value = newValue.value;
+    this.input.setSelectionRange(newValue.start, newValue.end);
+
+    return true;
+  },
+
+  /**
+   * Increment the property value based on the property type.
+   *
+   * @param {string} value
+   *        Property value.
+   * @param {number} increment
+   *        Amount to increase/decrease the property value.
+   * @param {number} selStart
+   *        Starting index of the value.
+   * @param {number} selEnd
+   *        Ending index of the value.
+   * @return {object} object with properties 'value', 'start', and 'end'.
+   */
+  _incrementCSSValue: function InplaceEditor_incrementCSSValue(value, increment,
+                                                               selStart, selEnd)
+  {
+    let range = this._parseCSSValue(value, selStart);
+    let type = (range && range.type) || "";
+    let rawValue = (range ? value.substring(range.start, range.end) : "");
+    let incrementedValue = null, selection;
+
+    if (type === "num") {
+      let newValue = this._incrementRawValue(rawValue, increment);
+      if (newValue !== null) {
+        incrementedValue = newValue;
+        selection = [0, incrementedValue.length];
+      }
+    } else if (type === "hex") {
+      let exprOffset = selStart - range.start;
+      let exprOffsetEnd = selEnd - range.start;
+      let newValue = this._incHexColor(rawValue, increment, exprOffset,
+                                       exprOffsetEnd);
+      if (newValue) {
+        incrementedValue = newValue.value;
+        selection = newValue.selection;
+      }
+    } else {
+      let info;
+      if (type === "rgb" || type === "hsl") {
+        info = {};
+        let part = value.substring(range.start, selStart).split(",").length - 1;
+        if (part === 3) { // alpha
+          info.minValue = 0;
+          info.maxValue = 1;
+        } else if (type === "rgb") {
+          info.minValue = 0;
+          info.maxValue = 255;
+        } else if (part !== 0) { // hsl percentage
+          info.minValue = 0;
+          info.maxValue = 100;
+
+          // select the previous number if the selection is at the end of a
+          // percentage sign.
+          if (value.charAt(selStart - 1) === "%") {
+            --selStart;
+          }
+        }
+      }
+      return this._incrementGenericValue(value, increment, selStart, selEnd, info);
+    }
+
+    if (incrementedValue === null) {
+      return;
+    }
+
+    let preRawValue = value.substr(0, range.start);
+    let postRawValue = value.substr(range.end);
+
+    return {
+      value: preRawValue + incrementedValue + postRawValue,
+      start: range.start + selection[0],
+      end: range.start + selection[1]
+    };
+  },
+
+  /**
+   * Parses the property value and type.
+   *
+   * @param {string} value
+   *        Property value.
+   * @param {number} offset
+   *        Starting index of value.
+   * @return {object} object with properties 'value', 'start', 'end', and 'type'.
+   */
+   _parseCSSValue: function InplaceEditor_parseCSSValue(value, offset)
+  {
+    const reSplitCSS = /(url\("?[^"\)]+"?\)?)|(rgba?\([^)]*\)?)|(hsla?\([^)]*\)?)|(#[\dA-Fa-f]+)|(-?\d+(\.\d+)?(%|[a-z]{1,4})?)|"([^"]*)"?|'([^']*)'?|([^,\s\/!\(\)]+)|(!(.*)?)/;
+    let start = 0;
+    let m;
+
+    // retreive values from left to right until we find the one at our offset
+    while ((m = reSplitCSS.exec(value)) &&
+          (m.index + m[0].length < offset)) {
+      value = value.substr(m.index + m[0].length);
+      start += m.index + m[0].length;
+      offset -= m.index + m[0].length;
+    }
+
+    if (!m) {
+      return;
+    }
+
+    let type;
+    if (m[1]) {
+      type = "url";
+    } else if (m[2]) {
+      type = "rgb";
+    } else if (m[3]) {
+      type = "hsl";
+    } else if (m[4]) {
+      type = "hex";
+    } else if (m[5]) {
+      type = "num";
+    }
+
+    return {
+      value: m[0],
+      start: start + m.index,
+      end: start + m.index + m[0].length,
+      type: type
+    };
+  },
+
+  /**
+   * Increment the property value for types other than
+   * number or hex, such as rgb, hsl, and file names.
+   *
+   * @param {string} value
+   *        Property value.
+   * @param {number} increment
+   *        Amount to increment/decrement.
+   * @param {number} offset
+   *        Starting index of the property value.
+   * @param {number} offsetEnd
+   *        Ending index of the property value.
+   * @param {object} info
+   *        Object with details about the property value.
+   * @return {object} object with properties 'value', 'start', and 'end'.
+   */
+  _incrementGenericValue:
+  function InplaceEditor_incrementGenericValue(value, increment, offset,
+                                               offsetEnd, info)
+  {
+    // Try to find a number around the cursor to increment.
+    let start, end;
+    // Check if we are incrementing in a non-number context (such as a URL)
+    if (/^-?[0-9.]/.test(value.substring(offset, offsetEnd)) &&
+      !(/\d/.test(value.charAt(offset - 1) + value.charAt(offsetEnd)))) {
+      // We have a number selected, possibly with a suffix, and we are not in
+      // the disallowed case of just part of a known number being selected.
+      // Use that number.
+      start = offset;
+      end = offsetEnd;
+    } else {
+      // Parse periods as belonging to the number only if we are in a known number
+      // context. (This makes incrementing the 1 in 'image1.gif' work.)
+      let pattern = "[" + (info ? "0-9." : "0-9") + "]*";
+      let before = new RegExp(pattern + "$").exec(value.substr(0, offset))[0].length;
+      let after = new RegExp("^" + pattern).exec(value.substr(offset))[0].length;
+
+      start = offset - before;
+      end = offset + after;
+
+      // Expand the number to contain an initial minus sign if it seems
+      // free-standing.
+      if (value.charAt(start - 1) === "-" &&
+         (start - 1 === 0 || /[ (:,='"]/.test(value.charAt(start - 2)))) {
+        --start;
+      }
+    }
+
+    if (start !== end)
+    {
+      // Include percentages as part of the incremented number (they are
+      // common enough).
+      if (value.charAt(end) === "%") {
+        ++end;
+      }
+
+      let first = value.substr(0, start);
+      let mid = value.substring(start, end);
+      let last = value.substr(end);
+
+      mid = this._incrementRawValue(mid, increment, info);
+
+      if (mid !== null) {
+        return {
+          value: first + mid + last,
+          start: start,
+          end: start + mid.length
+        };
+      }
+    }
+  },
+
+  /**
+   * Increment the property value for numbers.
+   *
+   * @param {string} rawValue
+   *        Raw value to increment.
+   * @param {number} increment
+   *        Amount to increase/decrease the raw value.
+   * @param {object} info
+   *        Object with info about the property value.
+   * @return {string} the incremented value.
+   */
+  _incrementRawValue:
+  function InplaceEditor_incrementRawValue(rawValue, increment, info)
+  {
+    let num = parseFloat(rawValue);
+
+    if (isNaN(num)) {
+      return null;
+    }
+
+    let number = /\d+(\.\d+)?/.exec(rawValue);
+    let units = rawValue.substr(number.index + number[0].length);
+
+    // avoid rounding errors
+    let newValue = Math.round((num + increment) * 1000) / 1000;
+
+    if (info && "minValue" in info) {
+      newValue = Math.max(newValue, info.minValue);
+    }
+    if (info && "maxValue" in info) {
+      newValue = Math.min(newValue, info.maxValue);
+    }
+
+    newValue = newValue.toString();
+
+    return newValue + units;
+  },
+
+  /**
+   * Increment the property value for hex.
+   *
+   * @param {string} value
+   *        Property value.
+   * @param {number} increment
+   *        Amount to increase/decrease the property value.
+   * @param {number} offset
+   *        Starting index of the property value.
+   * @param {number} offsetEnd
+   *        Ending index of the property value.
+   * @return {object} object with properties 'value' and 'selection'.
+   */
+  _incHexColor:
+  function InplaceEditor_incHexColor(rawValue, increment, offset, offsetEnd)
+  {
+    // Return early if no part of the rawValue is selected.
+    if (offsetEnd > rawValue.length && offset >= rawValue.length) {
+      return;
+    }
+    if (offset < 1 && offsetEnd <= 1) {
+      return;
+    }
+    // Ignore the leading #.
+    rawValue = rawValue.substr(1);
+    --offset;
+    --offsetEnd;
+
+    // Clamp the selection to within the actual value.
+    offset = Math.max(offset, 0);
+    offsetEnd = Math.min(offsetEnd, rawValue.length);
+    offsetEnd = Math.max(offsetEnd, offset);
+
+    // Normalize #ABC -> #AABBCC.
+    if (rawValue.length === 3) {
+      rawValue = rawValue.charAt(0) + rawValue.charAt(0) +
+                 rawValue.charAt(1) + rawValue.charAt(1) +
+                 rawValue.charAt(2) + rawValue.charAt(2);
+      offset *= 2;
+      offsetEnd *= 2;
+    }
+
+    if (rawValue.length !== 6) {
+      return;
+    }
+
+    // If no selection, increment an adjacent color, preferably one to the left.
+    if (offset === offsetEnd) {
+      if (offset === 0) {
+        offsetEnd = 1;
+      } else {
+        offset = offsetEnd - 1;
+      }
+    }
+
+    // Make the selection cover entire parts.
+    offset -= offset % 2;
+    offsetEnd += offsetEnd % 2;
+
+    // Remap the increments from [0.1, 1, 10] to [1, 1, 16].
+    if (-1 < increment && increment < 1) {
+      increment = (increment < 0 ? -1 : 1);
+    }
+    if (Math.abs(increment) === 10) {
+      increment = (increment < 0 ? -16 : 16);
+    }
+
+    let isUpper = (rawValue.toUpperCase() === rawValue);
+
+    for (let pos = offset; pos < offsetEnd; pos += 2) {
+      // Increment the part in [pos, pos+2).
+      let mid = rawValue.substr(pos, 2);
+      let value = parseInt(mid, 16);
+
+      if (isNaN(value)) {
+        return;
+      }
+
+      mid = Math.min(Math.max(value + increment, 0), 255).toString(16);
+
+      while (mid.length < 2) {
+        mid = "0" + mid;
+      }
+      if (isUpper) {
+        mid = mid.toUpperCase();
+      }
+
+      rawValue = rawValue.substr(0, pos) + mid + rawValue.substr(pos + 2);
+    }
+
+    return {
+      value: "#" + rawValue,
+      selection: [offset + 1, offsetEnd + 1]
+    };
+  },
+
+  /**
+   * Call the client's done handler and clear out.
+   */
+  _apply: function InplaceEditor_apply(aEvent)
+  {
+    if (this._applied) {
+      return;
+    }
+
+    this._applied = true;
+
+    if (this.done) {
+      let val = this.input.value.trim();
+      return this.done(this.cancelled ? this.initial : val, !this.cancelled);
+    }
+
+    return null;
+  },
+
+  /**
+   * Handle loss of focus by calling done if it hasn't been called yet.
+   */
+  _onBlur: function InplaceEditor_onBlur(aEvent, aDoNotClear)
+  {
+    this._apply();
+    if (!aDoNotClear) {
+      this._clear();
+    }
+  },
+
+  /**
+   * Handle the input field's keypress event.
+   */
+  _onKeyPress: function InplaceEditor_onKeyPress(aEvent)
+  {
+    let prevent = false;
+
+    const largeIncrement = 100;
+    const mediumIncrement = 10;
+    const smallIncrement = 0.1;
+
+    let increment = 0;
+
+    if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP
+       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP) {
+      increment = 1;
+    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DOWN
+       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
+      increment = -1;
+    }
+
+    if (aEvent.shiftKey && !aEvent.altKey) {
+      if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP
+           ||  aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
+        increment *= largeIncrement;
+      } else {
+        increment *= mediumIncrement;
+      }
+    } else if (aEvent.altKey && !aEvent.shiftKey) {
+      increment *= smallIncrement;
+    }
+
+    if (increment && this._incrementValue(increment) ) {
+      this._updateSize();
+      prevent = true;
+    }
+
+    if (this.multiline &&
+        aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN &&
+        aEvent.shiftKey) {
+      prevent = false;
+    } else if (aEvent.charCode in this._advanceCharCodes
+       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN
+       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB) {
+      prevent = true;
+
+      let direction = FOCUS_FORWARD;
+      if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB &&
+          aEvent.shiftKey) {
+        this.cancelled = true;
+        direction = FOCUS_BACKWARD;
+      }
+      if (this.stopOnReturn && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
+        direction = null;
+      }
+
+      let input = this.input;
+
+      this._apply();
+
+      if (direction !== null && focusManager.focusedElement === input) {
+        // If the focused element wasn't changed by the done callback,
+        // move the focus as requested.
+        let next = moveFocus(this.doc.defaultView, direction);
+
+        // If the next node to be focused has been tagged as an editable
+        // node, send it a click event to trigger
+        if (next && next.ownerDocument === this.doc && next._editable) {
+          next.click();
+        }
+      }
+
+      this._clear();
+    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE) {
+      // Cancel and blur ourselves.
+      prevent = true;
+      this.cancelled = true;
+      this._apply();
+      this._clear();
+      aEvent.stopPropagation();
+    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
+      // No need for leading spaces here.  This is particularly
+      // noticable when adding a property: it's very natural to type
+      // <name>: (which advances to the next property) then spacebar.
+      prevent = !this.input.value;
+    }
+
+    if (prevent) {
+      aEvent.preventDefault();
+    }
+  },
+
+  /**
+   * Handle the input field's keyup event.
+   */
+  _onKeyup: function(aEvent) {
+    // Validate the entered value.
+    this.warning.hidden = this.validate(this.input.value);
+    this._applied = false;
+    this._onBlur(null, true);
+  },
+
+  /**
+   * Handle changes to the input text.
+   */
+  _onInput: function InplaceEditor_onInput(aEvent)
+  {
+    // Validate the entered value.
+    if (this.warning && this.validate) {
+      this.warning.hidden = this.validate(this.input.value);
+    }
+
+    // Update size if we're autosizing.
+    if (this._measurement) {
+      this._updateSize();
+    }
+
+    // Call the user's change handler if available.
+    if (this.change) {
+      this.change(this.input.value.trim());
+    }
+  }
+};
+
+/**
+ * Copy text-related styles from one element to another.
+ */
+function copyTextStyles(aFrom, aTo)
+{
+  let win = aFrom.ownerDocument.defaultView;
+  let style = win.getComputedStyle(aFrom);
+  aTo.style.fontFamily = style.getPropertyCSSValue("font-family").cssText;
+  aTo.style.fontSize = style.getPropertyCSSValue("font-size").cssText;
+  aTo.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText;
+  aTo.style.fontStyle = style.getPropertyCSSValue("font-style").cssText;
+}
+
+/**
+ * Trigger a focus change similar to pressing tab/shift-tab.
+ */
+function moveFocus(aWin, aDirection)
+{
+  return focusManager.moveFocus(aWin, null, aDirection, 0);
+}
+
+
+XPCOMUtils.defineLazyGetter(this, "focusManager", function() {
+  return Services.focus;
+});
--- a/browser/devtools/styleinspector/CssRuleView.jsm
+++ b/browser/devtools/styleinspector/CssRuleView.jsm
@@ -8,39 +8,34 @@
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
-const FOCUS_FORWARD = Ci.nsIFocusManager.MOVEFOCUS_FORWARD;
-const FOCUS_BACKWARD = Ci.nsIFocusManager.MOVEFOCUS_BACKWARD;
-
 /**
  * These regular expressions are adapted from firebug's css.js, and are
  * used to parse CSSStyleDeclaration's cssText attribute.
  */
 
 // Used to split on css line separators
 const CSS_LINE_RE = /(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g;
 
 // Used to parse a single property line.
 const CSS_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*(?:! (important))?;?$/;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/CssLogic.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/InplaceEditor.jsm");
 
 this.EXPORTED_SYMBOLS = ["CssRuleView",
-                         "_ElementStyle",
-                         "editableItem",
-                         "_editableField",
-                         "_getInplaceEditorForSpan"];
+                         "_ElementStyle"];
 
 /**
  * Our model looks like this:
  *
  * ElementStyle:
  *   Responsible for keeping track of which properties are overridden.
  *   Maintains a list of Rule objects that apply to the element.
  * Rule:
@@ -1906,791 +1901,16 @@ TextPropertyEditor.prototype = {
     } finally {
       prefs.setBoolPref("layout.css.report_errors", prefVal);
     }
     return !!style.getPropertyValue(name);
   },
 };
 
 /**
- * Mark a span editable.  |editableField| will listen for the span to
- * be focused and create an InlineEditor to handle text input.
- * Changes will be committed when the InlineEditor's input is blurred
- * or dropped when the user presses escape.
- *
- * @param {object} aOptions
- *    Options for the editable field, including:
- *    {Element} element:
- *      (required) The span to be edited on focus.
- *    {function} canEdit:
- *       Will be called before creating the inplace editor.  Editor
- *       won't be created if canEdit returns false.
- *    {function} start:
- *       Will be called when the inplace editor is initialized.
- *    {function} change:
- *       Will be called when the text input changes.  Will be called
- *       with the current value of the text input.
- *    {function} done:
- *       Called when input is committed or blurred.  Called with
- *       current value and a boolean telling the caller whether to
- *       commit the change.  This function is called before the editor
- *       has been torn down.
- *    {function} destroy:
- *       Called when the editor is destroyed and has been torn down.
- *    {string} advanceChars:
- *       If any characters in advanceChars are typed, focus will advance
- *       to the next element.
- *    {boolean} stopOnReturn:
- *       If true, the return key will not advance the editor to the next
- *       focusable element.
- *    {string} trigger: The DOM event that should trigger editing,
- *      defaults to "click"
- */
-function editableField(aOptions)
-{
-  return editableItem(aOptions, function(aElement, aEvent) {
-    new InplaceEditor(aOptions, aEvent);
-  });
-}
-
-/**
- * Handle events for an element that should respond to
- * clicks and sit in the editing tab order, and call
- * a callback when it is activated.
- *
- * @param {object} aOptions
- *    The options for this editor, including:
- *    {Element} element: The DOM element.
- *    {string} trigger: The DOM event that should trigger editing,
- *      defaults to "click"
- * @param {function} aCallback
- *        Called when the editor is activated.
- */
-this.editableItem = function editableItem(aOptions, aCallback)
-{
-  let trigger = aOptions.trigger || "click"
-  let element = aOptions.element;
-  element.addEventListener(trigger, function(evt) {
-    let win = this.ownerDocument.defaultView;
-    let selection = win.getSelection();
-    if (trigger != "click" || selection.isCollapsed) {
-      aCallback(element, evt);
-    }
-    evt.stopPropagation();
-  }, false);
-
-  // If focused by means other than a click, start editing by
-  // pressing enter or space.
-  element.addEventListener("keypress", function(evt) {
-    if (evt.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN ||
-        evt.charCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
-      aCallback(element);
-    }
-  }, true);
-
-  // Ugly workaround - the element is focused on mousedown but
-  // the editor is activated on click/mouseup.  This leads
-  // to an ugly flash of the focus ring before showing the editor.
-  // So hide the focus ring while the mouse is down.
-  element.addEventListener("mousedown", function(evt) {
-    let cleanup = function() {
-      element.style.removeProperty("outline-style");
-      element.removeEventListener("mouseup", cleanup, false);
-      element.removeEventListener("mouseout", cleanup, false);
-    };
-    element.style.setProperty("outline-style", "none");
-    element.addEventListener("mouseup", cleanup, false);
-    element.addEventListener("mouseout", cleanup, false);
-  }, false);
-
-  // Mark the element editable field for tab
-  // navigation while editing.
-  element._editable = true;
-}
-
-this._editableField = editableField;
-
-function InplaceEditor(aOptions, aEvent)
-{
-  this.elt = aOptions.element;
-  let doc = this.elt.ownerDocument;
-  this.doc = doc;
-  this.elt.inplaceEditor = this;
-
-  this.change = aOptions.change;
-  this.done = aOptions.done;
-  this.destroy = aOptions.destroy;
-  this.initial = aOptions.initial ? aOptions.initial : this.elt.textContent;
-  this.multiline = aOptions.multiline || false;
-  this.stopOnReturn = !!aOptions.stopOnReturn;
-
-  this._onBlur = this._onBlur.bind(this);
-  this._onKeyPress = this._onKeyPress.bind(this);
-  this._onInput = this._onInput.bind(this);
-  this._onKeyup = this._onKeyup.bind(this);
-
-  this._createInput();
-  this._autosize();
-
-  // Pull out character codes for advanceChars, listing the
-  // characters that should trigger a blur.
-  this._advanceCharCodes = {};
-  let advanceChars = aOptions.advanceChars || '';
-  for (let i = 0; i < advanceChars.length; i++) {
-    this._advanceCharCodes[advanceChars.charCodeAt(i)] = true;
-  }
-
-  // Hide the provided element and add our editor.
-  this.originalDisplay = this.elt.style.display;
-  this.elt.style.display = "none";
-  this.elt.parentNode.insertBefore(this.input, this.elt);
-
-  if (typeof(aOptions.selectAll) == "undefined" || aOptions.selectAll) {
-    this.input.select();
-  }
-  this.input.focus();
-
-  this.input.addEventListener("blur", this._onBlur, false);
-  this.input.addEventListener("keypress", this._onKeyPress, false);
-  this.input.addEventListener("input", this._onInput, false);
-  this.input.addEventListener("mousedown", function(aEvt) { aEvt.stopPropagation(); }, false);
-
-  this.warning = aOptions.warning;
-  this.validate = aOptions.validate;
-
-  if (this.warning && this.validate) {
-    this.input.addEventListener("keyup", this._onKeyup, false);
-  }
-
-  if (aOptions.start) {
-    aOptions.start(this, aEvent);
-  }
-}
-
-InplaceEditor.prototype = {
-  _createInput: function InplaceEditor_createEditor()
-  {
-    this.input = this.doc.createElementNS(HTML_NS, this.multiline ? "textarea" : "input");
-    this.input.inplaceEditor = this;
-    this.input.classList.add("styleinspector-propertyeditor");
-    this.input.value = this.initial;
-
-    copyTextStyles(this.elt, this.input);
-  },
-
-  /**
-   * Get rid of the editor.
-   */
-  _clear: function InplaceEditor_clear()
-  {
-    if (!this.input) {
-      // Already cleared.
-      return;
-    }
-
-    this.input.removeEventListener("blur", this._onBlur, false);
-    this.input.removeEventListener("keypress", this._onKeyPress, false);
-    this.input.removeEventListener("keyup", this._onKeyup, false);
-    this.input.removeEventListener("oninput", this._onInput, false);
-    this._stopAutosize();
-
-    this.elt.style.display = this.originalDisplay;
-    this.elt.focus();
-
-    if (this.destroy) {
-      this.destroy();
-    }
-
-    this.elt.parentNode.removeChild(this.input);
-    this.input = null;
-
-    delete this.elt.inplaceEditor;
-    delete this.elt;
-  },
-
-  /**
-   * Keeps the editor close to the size of its input string.  This is pretty
-   * crappy, suggestions for improvement welcome.
-   */
-  _autosize: function InplaceEditor_autosize()
-  {
-    // Create a hidden, absolutely-positioned span to measure the text
-    // in the input.  Boo.
-
-    // We can't just measure the original element because a) we don't
-    // change the underlying element's text ourselves (we leave that
-    // up to the client), and b) without tweaking the style of the
-    // original element, it might wrap differently or something.
-    this._measurement = this.doc.createElementNS(HTML_NS, this.multiline ? "pre" : "span");
-    this._measurement.className = "autosizer";
-    this.elt.parentNode.appendChild(this._measurement);
-    let style = this._measurement.style;
-    style.visibility = "hidden";
-    style.position = "absolute";
-    style.top = "0";
-    style.left = "0";
-    copyTextStyles(this.input, this._measurement);
-    this._updateSize();
-  },
-
-  /**
-   * Clean up the mess created by _autosize().
-   */
-  _stopAutosize: function InplaceEditor_stopAutosize()
-  {
-    if (!this._measurement) {
-      return;
-    }
-    this._measurement.parentNode.removeChild(this._measurement);
-    delete this._measurement;
-  },
-
-  /**
-   * Size the editor to fit its current contents.
-   */
-  _updateSize: function InplaceEditor_updateSize()
-  {
-    // Replace spaces with non-breaking spaces.  Otherwise setting
-    // the span's textContent will collapse spaces and the measurement
-    // will be wrong.
-    this._measurement.textContent = this.input.value.replace(/ /g, '\u00a0');
-
-    // We add a bit of padding to the end.  Should be enough to fit
-    // any letter that could be typed, otherwise we'll scroll before
-    // we get a chance to resize.  Yuck.
-    let width = this._measurement.offsetWidth + 10;
-
-    if (this.multiline) {
-      // Make sure there's some content in the current line.  This is a hack to account
-      // for the fact that after adding a newline the <pre> doesn't grow unless there's
-      // text content on the line.
-      width += 15;
-      this._measurement.textContent += "M";
-      this.input.style.height = this._measurement.offsetHeight + "px";
-    }
-
-    this.input.style.width = width + "px";
-  },
-
-   /**
-   * Increment property values in rule view.
-   *
-   * @param {number} increment 
-   *        The amount to increase/decrease the property value.
-   * @return {bool} true if value has been incremented.
-   */
-  _incrementValue: function InplaceEditor_incrementValue(increment)
-  {
-    let value = this.input.value;
-    let selectionStart = this.input.selectionStart;
-    let selectionEnd = this.input.selectionEnd;
-
-    let newValue = this._incrementCSSValue(value, increment, selectionStart, selectionEnd);
-
-    if (!newValue) {
-      return false;
-    }
-
-    this.input.value = newValue.value;
-    this.input.setSelectionRange(newValue.start, newValue.end);
-
-    return true;
-  },
-
-  /**
-   * Increment the property value based on the property type.
-   *
-   * @param {string} value
-   *        Property value.
-   * @param {number} increment
-   *        Amount to increase/decrease the property value.
-   * @param {number} selStart
-   *        Starting index of the value.
-   * @param {number} selEnd
-   *        Ending index of the value.
-   * @return {object} object with properties 'value', 'start', and 'end'.
-   */
-  _incrementCSSValue: function InplaceEditor_incrementCSSValue(value, increment, selStart, 
-                                                               selEnd)
-  {
-    let range = this._parseCSSValue(value, selStart);
-    let type = (range && range.type) || "";
-    let rawValue = (range ? value.substring(range.start, range.end) : "");
-    let incrementedValue = null, selection;
-
-    if (type === "num") {
-      let newValue = this._incrementRawValue(rawValue, increment);
-      if (newValue !== null) {
-        incrementedValue = newValue;
-        selection = [0, incrementedValue.length];
-      }
-    } else if (type === "hex") {
-      let exprOffset = selStart - range.start;
-      let exprOffsetEnd = selEnd - range.start;
-      let newValue = this._incHexColor(rawValue, increment, exprOffset, exprOffsetEnd);
-      if (newValue) {
-        incrementedValue = newValue.value;
-        selection = newValue.selection;
-      }
-    } else {
-      let info;
-      if (type === "rgb" || type === "hsl") {
-        info = {};
-        let part = value.substring(range.start, selStart).split(",").length - 1;
-        if (part === 3) { // alpha
-          info.minValue = 0;
-          info.maxValue = 1;
-        } else if (type === "rgb") {
-          info.minValue = 0;
-          info.maxValue = 255;
-        } else if (part !== 0) { // hsl percentage
-          info.minValue = 0;
-          info.maxValue = 100;
-
-          // select the previous number if the selection is at the end of a percentage sign
-          if (value.charAt(selStart - 1) === "%") {
-            --selStart;
-          }
-        }
-      }
-      return this._incrementGenericValue(value, increment, selStart, selEnd, info);
-    }
-
-    if (incrementedValue === null) {
-      return;
-    }
-
-    let preRawValue = value.substr(0, range.start);
-    let postRawValue = value.substr(range.end);
-
-    return {
-      value: preRawValue + incrementedValue + postRawValue,
-      start: range.start + selection[0],
-      end: range.start + selection[1]
-    };
-  },
-
-  /**
-   * Parses the property value and type.
-   *
-   * @param {string} value 
-   *        Property value.
-   * @param {number} offset 
-   *        Starting index of value.
-   * @return {object} object with properties 'value', 'start', 'end', and 'type'.
-   */
-   _parseCSSValue: function InplaceEditor_parseCSSValue(value, offset)
-  {
-    const reSplitCSS = /(url\("?[^"\)]+"?\)?)|(rgba?\([^)]*\)?)|(hsla?\([^)]*\)?)|(#[\dA-Fa-f]+)|(-?\d+(\.\d+)?(%|[a-z]{1,4})?)|"([^"]*)"?|'([^']*)'?|([^,\s\/!\(\)]+)|(!(.*)?)/;
-    let start = 0;
-    let m;
-
-    // retreive values from left to right until we find the one at our offset
-    while ((m = reSplitCSS.exec(value)) &&
-          (m.index + m[0].length < offset)) {
-      value = value.substr(m.index + m[0].length);
-      start += m.index + m[0].length;
-      offset -= m.index + m[0].length;
-    }
-
-    if (!m) {
-      return;
-    }
-
-    let type;
-    if (m[1]) {
-      type = "url";
-    } else if (m[2]) {
-      type = "rgb";
-    } else if (m[3]) {
-      type = "hsl";
-    } else if (m[4]) {
-      type = "hex";
-    } else if (m[5]) {
-      type = "num";
-    }
-
-    return {
-      value: m[0],
-      start: start + m.index,
-      end: start + m.index + m[0].length,
-      type: type
-    };
-  },
-
-  /**
-   * Increment the property value for types other than
-   * number or hex, such as rgb, hsl, and file names.
-   *
-   * @param {string} value 
-   *        Property value.
-   * @param {number} increment 
-   *        Amount to increment/decrement.
-   * @param {number} offset 
-   *        Starting index of the property value.
-   * @param {number} offsetEnd 
-   *        Ending index of the property value.
-   * @param {object} info 
-   *        Object with details about the property value.
-   * @return {object} object with properties 'value', 'start', and 'end'.
-   */
-  _incrementGenericValue: function InplaceEditor_incrementGenericValue(value, increment, offset,
-                                                                       offsetEnd, info)
-  {
-    // Try to find a number around the cursor to increment.
-    let start, end;
-    // Check if we are incrementing in a non-number context (such as a URL)
-    if (/^-?[0-9.]/.test(value.substring(offset, offsetEnd)) &&
-      !(/\d/.test(value.charAt(offset - 1) + value.charAt(offsetEnd)))) {
-      // We have a number selected, possibly with a suffix, and we are not in
-      // the disallowed case of just part of a known number being selected.
-      // Use that number.
-      start = offset;
-      end = offsetEnd;
-    } else {
-      // Parse periods as belonging to the number only if we are in a known number
-      // context. (This makes incrementing the 1 in 'image1.gif' work.)
-      let pattern = "[" + (info ? "0-9." : "0-9") + "]*";
-      let before = new RegExp(pattern + "$").exec(value.substr(0, offset))[0].length;
-      let after = new RegExp("^" + pattern).exec(value.substr(offset))[0].length;
-
-      start = offset - before;
-      end = offset + after;
-
-      // Expand the number to contain an initial minus sign if it seems
-      // free-standing.
-      if (value.charAt(start - 1) === "-" &&
-         (start - 1 === 0 || /[ (:,='"]/.test(value.charAt(start - 2)))) {
-        --start;
-      }
-    }
-
-    if (start !== end)
-    {
-      // Include percentages as part of the incremented number (they are
-      // common enough).
-      if (value.charAt(end) === "%") {
-        ++end;
-      }
-
-      let first = value.substr(0, start);
-      let mid = value.substring(start, end);
-      let last = value.substr(end);
-
-      mid = this._incrementRawValue(mid, increment, info);
-
-      if (mid !== null) {
-        return {
-          value: first + mid + last,
-          start: start,
-          end: start + mid.length
-        };
-      }
-    }
-  },
-
-  /**
-   * Increment the property value for numbers.
-   *
-   * @param {string} rawValue 
-   *        Raw value to increment.
-   * @param {number} increment 
-   *        Amount to increase/decrease the raw value.
-   * @param {object} info 
-   *        Object with info about the property value.
-   * @return {string} the incremented value.
-   */
-  _incrementRawValue: function InplaceEditor_incrementRawValue(rawValue, increment, info)
-  {
-    let num = parseFloat(rawValue);
-
-    if (isNaN(num)) {
-      return null;
-    }
-
-    let number = /\d+(\.\d+)?/.exec(rawValue);
-    let units = rawValue.substr(number.index + number[0].length);
-
-    // avoid rounding errors
-    let newValue = Math.round((num + increment) * 1000) / 1000;
-
-    if (info && "minValue" in info) {
-      newValue = Math.max(newValue, info.minValue);
-    }
-    if (info && "maxValue" in info) {
-      newValue = Math.min(newValue, info.maxValue);
-    }
-
-    newValue = newValue.toString();
-
-    return newValue + units;
-  },
-
-  /**
-   * Increment the property value for hex.
-   *
-   * @param {string} value 
-   *        Property value.
-   * @param {number} increment 
-   *        Amount to increase/decrease the property value.
-   * @param {number} offset 
-   *        Starting index of the property value.
-   * @param {number} offsetEnd 
-   *        Ending index of the property value.
-   * @return {object} object with properties 'value' and 'selection'.
-   */
-  _incHexColor: function InplaceEditor_incHexColor(rawValue, increment, offset, offsetEnd)
-  {
-    // Return early if no part of the rawValue is selected.
-    if (offsetEnd > rawValue.length && offset >= rawValue.length) {
-      return;
-    }
-    if (offset < 1 && offsetEnd <= 1) {
-      return;
-    }
-    // Ignore the leading #.
-    rawValue = rawValue.substr(1);
-    --offset;
-    --offsetEnd;
-
-    // Clamp the selection to within the actual value.
-    offset = Math.max(offset, 0);
-    offsetEnd = Math.min(offsetEnd, rawValue.length);
-    offsetEnd = Math.max(offsetEnd, offset);
-
-    // Normalize #ABC -> #AABBCC.
-    if (rawValue.length === 3) {
-      rawValue = rawValue.charAt(0) + rawValue.charAt(0) +
-                 rawValue.charAt(1) + rawValue.charAt(1) +
-                 rawValue.charAt(2) + rawValue.charAt(2);
-      offset *= 2;
-      offsetEnd *= 2;
-    }
-
-    if (rawValue.length !== 6) {
-      return;
-    }
-
-    // If no selection, increment an adjacent color, preferably one to the left.
-    if (offset === offsetEnd) {
-      if (offset === 0) {
-        offsetEnd = 1;
-      } else {
-        offset = offsetEnd - 1;
-      }
-    }
-
-    // Make the selection cover entire parts.
-    offset -= offset % 2;
-    offsetEnd += offsetEnd % 2;
-
-    // Remap the increments from [0.1, 1, 10] to [1, 1, 16].
-    if (-1 < increment && increment < 1) {
-      increment = (increment < 0 ? -1 : 1);
-    }
-    if (Math.abs(increment) === 10) {
-      increment = (increment < 0 ? -16 : 16);
-    }
-
-    let isUpper = (rawValue.toUpperCase() === rawValue);
-
-    for (let pos = offset; pos < offsetEnd; pos += 2) {
-      // Increment the part in [pos, pos+2).
-      let mid = rawValue.substr(pos, 2);
-      let value = parseInt(mid, 16);
-
-      if (isNaN(value)) {
-        return;
-      }
-
-      mid = Math.min(Math.max(value + increment, 0), 255).toString(16);
-
-      while (mid.length < 2) {
-        mid = "0" + mid;
-      }
-      if (isUpper) {
-        mid = mid.toUpperCase();
-      }
-
-      rawValue = rawValue.substr(0, pos) + mid + rawValue.substr(pos + 2);
-    }
-
-    return {
-      value: "#" + rawValue,
-      selection: [offset + 1, offsetEnd + 1]
-    };
-  },
-
-  /**
-   * Call the client's done handler and clear out.
-   */
-  _apply: function InplaceEditor_apply(aEvent)
-  {
-    if (this._applied) {
-      return;
-    }
-
-    this._applied = true;
-
-    if (this.done) {
-      let val = this.input.value.trim();
-      return this.done(this.cancelled ? this.initial : val, !this.cancelled);
-    }
-
-    return null;
-  },
-
-  /**
-   * Handle loss of focus by calling done if it hasn't been called yet.
-   */
-  _onBlur: function InplaceEditor_onBlur(aEvent, aDoNotClear)
-  {
-    this._apply();
-    if (!aDoNotClear) {
-      this._clear();
-    }
-  },
-
-  /**
-   * Handle the input field's keypress event.
-   */
-  _onKeyPress: function InplaceEditor_onKeyPress(aEvent)
-  {
-    let prevent = false;
-
-    const largeIncrement = 100;
-    const mediumIncrement = 10;
-    const smallIncrement = 0.1;
-
-    let increment = 0;
-
-    if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP
-       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP) {
-      increment = 1;
-    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DOWN
-       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
-      increment = -1;
-    }
-
-    if (aEvent.shiftKey && !aEvent.altKey) {
-      if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP
-           ||  aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
-        increment *= largeIncrement;
-      } else {
-        increment *= mediumIncrement;
-      }
-    } else if (aEvent.altKey && !aEvent.shiftKey) {
-      increment *= smallIncrement;
-    }
-
-    if (increment && this._incrementValue(increment) ) {
-      this._updateSize();
-      prevent = true;
-    }
-
-    if (this.multiline &&
-        aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN &&
-        aEvent.shiftKey) {
-      prevent = false;
-    } else if (aEvent.charCode in this._advanceCharCodes
-       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN
-       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB) {
-      prevent = true;
-
-      let direction = FOCUS_FORWARD;
-      if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB &&
-          aEvent.shiftKey) {
-        this.cancelled = true;
-        direction = FOCUS_BACKWARD;
-      }
-      if (this.stopOnReturn && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
-        direction = null;
-      }
-
-      let input = this.input;
-
-      this._apply();
-
-      let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
-      if (direction !== null && fm.focusedElement === input) {
-        // If the focused element wasn't changed by the done callback,
-        // move the focus as requested.
-        let next = moveFocus(this.doc.defaultView, direction);
-
-        // If the next node to be focused has been tagged as an editable
-        // node, send it a click event to trigger
-        if (next && next.ownerDocument === this.doc && next._editable) {
-          next.click();
-        }
-      }
-
-      this._clear();
-    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE) {
-      // Cancel and blur ourselves.
-      prevent = true;
-      this.cancelled = true;
-      this._apply();
-      this._clear();
-      aEvent.stopPropagation();
-    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
-      // No need for leading spaces here.  This is particularly
-      // noticable when adding a property: it's very natural to type
-      // <name>: (which advances to the next property) then spacebar.
-      prevent = !this.input.value;
-    }
-
-    if (prevent) {
-      aEvent.preventDefault();
-    }
-  },
-
-  /**
-   * Handle the input field's keyup event.
-   */
-  _onKeyup: function(aEvent) {
-    // Validate the entered value.
-    this.warning.hidden = this.validate(this.input.value);
-    this._applied = false;
-    this._onBlur(null, true);
-  },
-
-  /**
-   * Handle changes to the input text.
-   */
-  _onInput: function InplaceEditor_onInput(aEvent)
-  {
-    // Validate the entered value.
-    if (this.warning && this.validate) {
-      this.warning.hidden = this.validate(this.input.value);
-    }
-
-    // Update size if we're autosizing.
-    if (this._measurement) {
-      this._updateSize();
-    }
-
-    // Call the user's change handler if available.
-    if (this.change) {
-      this.change(this.input.value.trim());
-    }
-  }
-};
-
-/*
- * Various API consumers (especially tests) sometimes want to grab the
- * inplaceEditor expando off span elements. However, when each global has its
- * own compartment, those expandos live on Xray wrappers that are only visible
- * within this JSM. So we provide a little workaround here.
- */
-this._getInplaceEditorForSpan = function _getInplaceEditorForSpan(aSpan)
-{
-  return aSpan.inplaceEditor;
-};
-
-/**
  * Store of CSSStyleDeclarations mapped to properties that have been changed by
  * the user.
  */
 function UserProperties()
 {
   // FIXME: This should be a WeakMap once bug 753517 is fixed.
   // See Bug 777373 for details.
   this.map = new Map();
@@ -2811,38 +2031,16 @@ function createMenuItem(aMenu, aAttribut
 /**
  * Append a text node to an element.
  */
 function appendText(aParent, aText)
 {
   aParent.appendChild(aParent.ownerDocument.createTextNode(aText));
 }
 
-/**
- * Copy text-related styles from one element to another.
- */
-function copyTextStyles(aFrom, aTo)
-{
-  let win = aFrom.ownerDocument.defaultView;
-  let style = win.getComputedStyle(aFrom);
-  aTo.style.fontFamily = style.getPropertyCSSValue("font-family").cssText;
-  aTo.style.fontSize = style.getPropertyCSSValue("font-size").cssText;
-  aTo.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText;
-  aTo.style.fontStyle = style.getPropertyCSSValue("font-style").cssText;
-}
-
-/**
- * Trigger a focus change similar to pressing tab/shift-tab.
- */
-function moveFocus(aWin, aDirection)
-{
-  let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
-  return fm.moveFocus(aWin, null, aDirection, 0);
-}
-
 XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
   return Cc["@mozilla.org/widget/clipboardhelper;1"].
     getService(Ci.nsIClipboardHelper);
 });
 
 XPCOMUtils.defineLazyGetter(this, "_strings", function() {
   return Services.strings.createBundle(
     "chrome://browser/locale/devtools/styleinspector.properties");
--- a/browser/devtools/styleinspector/test/browser_bug722691_rule_view_increment.js
+++ b/browser/devtools/styleinspector/test/browser_bug722691_rule_view_increment.js
@@ -4,18 +4,16 @@
 
 // Test that increasing/decreasing values in rule view using
 // arrow keys works correctly.
 
 let tempScope = {};
 Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
 let CssRuleView = tempScope.CssRuleView;
 let _ElementStyle = tempScope._ElementStyle;
-let _editableField = tempScope._editableField;
-let inplaceEditor = tempScope._getInplaceEditorForSpan;
 
 let doc;
 let ruleDialog;
 let ruleView;
 
 function setUpTests()
 {
   doc.body.innerHTML = '<div id="test" style="' +
--- a/browser/devtools/styleinspector/test/browser_ruleview_bug_703643_context_menu_copy.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_bug_703643_context_menu_copy.js
@@ -1,16 +1,13 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let doc;
-let tempScope = {};
-Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
-let inplaceEditor = tempScope._getInplaceEditorForSpan;
 let inspector;
 let win;
 
 XPCOMUtils.defineLazyGetter(this, "osString", function() {
   return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
 });
 
 function createDocument()
--- a/browser/devtools/styleinspector/test/browser_ruleview_editor.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_editor.js
@@ -1,18 +1,16 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let tempScope = {}
 Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
 let CssRuleView = tempScope.CssRuleView;
 let _ElementStyle = tempScope._ElementStyle;
-let _editableField = tempScope._editableField;
-let inplaceEditor = tempScope._getInplaceEditorForSpan;
 
 let doc = content.document;
 
 function expectDone(aValue, aCommit, aNext)
 {
   return function(aDoneValue, aDoneCommit) {
     dump("aDoneValue: " + aDoneValue + " commit: " + aDoneCommit + "\n");
 
@@ -35,50 +33,50 @@ function createSpan()
   doc.body.appendChild(span);
   return span;
 }
 
 function testReturnCommit()
 {
   clearBody();
   let span = createSpan();
-  _editableField({
+  editableField({
     element: span,
     initial: "explicit initial",
     start: function() {
       is(inplaceEditor(span).input.value, "explicit initial", "Explicit initial value should be used.");
       inplaceEditor(span).input.value = "Test Value";
       EventUtils.sendKey("return");
     },
     done: expectDone("Test Value", true, testBlurCommit)
   });
   span.click();
 }
 
 function testBlurCommit()
 {
   clearBody();
   let span = createSpan();
-  _editableField({
+  editableField({
     element: span,
     start: function() {
       is(inplaceEditor(span).input.value, "Edit Me!", "textContent of the span used.");
       inplaceEditor(span).input.value = "Test Value";
       inplaceEditor(span).input.blur();
     },
     done: expectDone("Test Value", true, testAdvanceCharCommit)
   });
   span.click();
 }
 
 function testAdvanceCharCommit()
 {
   clearBody();
   let span = createSpan();
-  _editableField({
+  editableField({
     element: span,
     advanceChars: ":",
     start: function() {
       let input = inplaceEditor(span).input;
       for each (let ch in "Test:") {
         EventUtils.sendChar(ch);
       }
     },
@@ -86,17 +84,17 @@ function testAdvanceCharCommit()
   });
   span.click();
 }
 
 function testEscapeCancel()
 {
   clearBody();
   let span = createSpan();
-  _editableField({
+  editableField({
     element: span,
     initial: "initial text",
     start: function() {
       inplaceEditor(span).input.value = "Test Value";
       EventUtils.sendKey("escape");
     },
     done: expectDone("initial text", false, finishTest)
   });
--- a/browser/devtools/styleinspector/test/browser_ruleview_editor_changedvalues.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_editor_changedvalues.js
@@ -1,18 +1,16 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let tempScope = {};
 Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
 let CssRuleView = tempScope.CssRuleView;
 let _ElementStyle = tempScope._ElementStyle;
-let _editableField = tempScope._editableField;
-let inplaceEditor = tempScope._getInplaceEditorForSpan;
 
 let doc;
 let ruleDialog;
 let ruleView;
 
 var gRuleViewChanged = false;
 function ruleViewChanged()
 {
@@ -52,30 +50,28 @@ function startTest()
     waitForFocus(testCancelNew, ruleDialog);
   }, true);
 }
 
 function testCancelNew()
 {
   // Start at the beginning: start to add a rule to the element's style
   // declaration, but leave it empty.
-
   let elementRuleEditor = ruleView.element.children[0]._ruleEditor;
   waitForEditorFocus(elementRuleEditor.element, function onNewElement(aEditor) {
     is(inplaceEditor(elementRuleEditor.newPropSpan), aEditor, "Next focused editor should be the new property editor.");
     let input = aEditor.input;
     waitForEditorBlur(aEditor, function () {
       ok(!gRuleViewChanged, "Shouldn't get a change event after a cancel.");
       is(elementRuleEditor.rule.textProps.length,  0, "Should have canceled creating a new text property.");
       ok(!elementRuleEditor.propertyList.hasChildNodes(), "Should not have any properties.");
       testCreateNew();
     });
     aEditor.input.blur();
   });
-
   EventUtils.synthesizeMouse(elementRuleEditor.closeBrace, 1, 1,
                              { },
                              ruleDialog);
 }
 
 function testCreateNew()
 {
   // Create a new property.
--- a/browser/devtools/styleinspector/test/browser_ruleview_focus.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_focus.js
@@ -1,18 +1,15 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test that focus doesn't leave the style editor when adding a property
 // (bug 719916)
 
-let tempScope = {};
-Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
-let inplaceEditor = tempScope._getInplaceEditorForSpan;
 let doc;
 let inspector;
 let stylePanel;
 
 function openRuleView()
 {
   var target = TargetFactory.forTab(gBrowser.selectedTab);
   gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
--- a/browser/devtools/styleinspector/test/browser_ruleview_inherit.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_inherit.js
@@ -1,17 +1,16 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let tempScope = {}
 Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
 let CssRuleView = tempScope.CssRuleView;
 let _ElementStyle = tempScope._ElementStyle;
-let _editableField = tempScope._editableField;
 
 let doc;
 
 function simpleInherit()
 {
   let style = '' +
     '#test2 {' +
     '  background-color: green;' +
--- a/browser/devtools/styleinspector/test/browser_ruleview_manipulation.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_manipulation.js
@@ -1,17 +1,16 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let tempScope = {}
 Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
 let CssRuleView = tempScope.CssRuleView;
 let _ElementStyle = tempScope._ElementStyle;
-let _editableField = tempScope._editableField;
 
 let doc;
 
 function simpleOverride()
 {
   doc.body.innerHTML = '<div id="testid">Styled Node</div>';
   let element = doc.getElementById("testid");
   let elementStyle = new _ElementStyle(element);
--- a/browser/devtools/styleinspector/test/browser_ruleview_override.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_override.js
@@ -1,17 +1,16 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let tempScope = {}
 Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
 let CssRuleView = tempScope.CssRuleView;
 let _ElementStyle = tempScope._ElementStyle;
-let _editableField = tempScope._editableField;
 
 let doc;
 
 function simpleOverride()
 {
   let style = '' +
     '#testid {' +
     '  background-color: blue;' +
--- a/browser/devtools/styleinspector/test/browser_ruleview_ui.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_ui.js
@@ -1,18 +1,16 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let tempScope = {}
 Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
 let CssRuleView = tempScope.CssRuleView;
 let _ElementStyle = tempScope._ElementStyle;
-let _editableField = tempScope._editableField;
-let inplaceEditor = tempScope._getInplaceEditorForSpan;
 
 let doc;
 let ruleDialog;
 let ruleView;
 
 var gRuleViewChanged = false;
 function ruleViewChanged()
 {
--- a/browser/devtools/styleinspector/test/browser_ruleview_update.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_update.js
@@ -1,18 +1,16 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let tempScope = {}
 Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
 let CssRuleView = tempScope.CssRuleView;
 let _ElementStyle = tempScope._ElementStyle;
-let _editableField = tempScope._editableField;
-let inplaceEditor = tempScope._getInplaceEditorForSpan;
 
 let doc;
 let ruleDialog;
 let ruleView;
 let testElement;
 
 function startTest()
 {
--- a/browser/devtools/styleinspector/test/head.js
+++ b/browser/devtools/styleinspector/test/head.js
@@ -8,16 +8,20 @@ Cu.import("resource:///modules/devtools/
 Cu.import("resource:///modules/devtools/CssHtmlTree.jsm", tempScope);
 Cu.import("resource:///modules/devtools/gDevTools.jsm", tempScope);
 let ConsoleUtils = tempScope.ConsoleUtils;
 let CssLogic = tempScope.CssLogic;
 let CssHtmlTree = tempScope.CssHtmlTree;
 let gDevTools = tempScope.gDevTools;
 Cu.import("resource:///modules/devtools/Target.jsm", tempScope);
 let TargetFactory = tempScope.TargetFactory;
+let {
+  editableField,
+  getInplaceEditorForSpan: inplaceEditor
+} = Cu.import("resource:///modules/devtools/InplaceEditor.jsm", {});
 Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope);
 let console = tempScope.console;
 
 let browser, hudId, hud, hudBox, filterBox, outputNode, cs;
 
 function addTab(aURL)
 {
   gBrowser.selectedTab = gBrowser.addTab();