Bug 1265796 - replace inIDOMUtils.getSubpropertiesForCSSProperty draft
authorGreg Tatum <tatum.creative@gmail.com>
Mon, 27 Jun 2016 11:12:10 -0500
changeset 409215 bd1d03aa536635fdf7769f465b3b82615ebe1ba1
parent 398620 a3fb4eb11fcf840b9e0d65a06be6cf64584add08
child 409216 f055a214952efa973575f8b207fe0b884ca60228
push id28433
push userbmo:gtatum@mozilla.com
push dateFri, 02 Sep 2016 15:08:28 +0000
bugs1265796
milestone51.0a1
Bug 1265796 - replace inIDOMUtils.getSubpropertiesForCSSProperty MozReview-Commit-ID: LevzomOfnti
devtools/client/inspector/rules/models/element-style.js
devtools/client/inspector/rules/models/rule.js
devtools/client/inspector/rules/models/text-property.js
devtools/client/inspector/rules/rules.js
devtools/client/inspector/rules/views/rule-editor.js
devtools/client/inspector/rules/views/text-property-editor.js
devtools/server/actors/styles.js
devtools/shared/fronts/styles.js
devtools/shared/specs/styles.js
--- a/devtools/client/inspector/rules/models/element-style.js
+++ b/devtools/client/inspector/rules/models/element-style.js
@@ -6,16 +6,17 @@
 
 "use strict";
 
 const promise = require("promise");
 const {Rule} = require("devtools/client/inspector/rules/models/rule");
 const {promiseWarn} = require("devtools/client/inspector/shared/utils");
 const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
 const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+const {Task} = require("devtools/shared/task");
 
 /**
  * ElementStyle is responsible for the following:
  *   Keeps track of which properties are overridden.
  *   Maintains a list of Rule objects for a given element.
  *
  * @param {Element} element
  *        The element whose style we are viewing.
@@ -82,54 +83,55 @@ ElementStyle.prototype = {
   /**
    * Refresh the list of rules to be displayed for the active element.
    * Upon completion, this.rules[] will hold a list of Rule objects.
    *
    * Returns a promise that will be resolved when the elementStyle is
    * ready.
    */
   populate: function () {
+    let self = this;
     let populated = this.pageStyle.getApplied(this.element, {
       inherited: true,
       matchedSelectors: true,
       filter: this.showUserAgentStyles ? "ua" : undefined,
-    }).then(entries => {
-      if (this.destroyed) {
+    }).then(Task.async(function* (entries) {
+      if (self.destroyed) {
         return promise.resolve(undefined);
       }
 
-      if (this.populated !== populated) {
+      if (self.populated !== populated) {
         // Don't care anymore.
         return promise.resolve(undefined);
       }
 
       // Store the current list of rules (if any) during the population
       // process.  They will be reused if possible.
-      let existingRules = this.rules;
+      let existingRules = self.rules;
 
-      this.rules = [];
+      self.rules = [];
 
       for (let entry of entries) {
-        this._maybeAddRule(entry, existingRules);
+        yield self._maybeAddRule(entry, existingRules);
       }
 
       // Mark overridden computed styles.
-      this.markOverriddenAll();
+      self.markOverriddenAll();
 
-      this._sortRulesForPseudoElement();
+      self._sortRulesForPseudoElement();
 
       // We're done with the previous list of rules.
       for (let r of existingRules) {
         if (r && r.editor) {
           r.editor.destroy();
         }
       }
 
       return undefined;
-    }).then(null, e => {
+    })).then(null, e => {
       // populate is often called after a setTimeout,
       // the connection may already be closed.
       if (this.destroyed) {
         return promise.resolve(undefined);
       }
       return promiseWarn(e);
     });
     this.populated = populated;
@@ -149,56 +151,56 @@ ElementStyle.prototype = {
    * Add a rule if it's one we care about.  Filters out duplicates and
    * inherited styles with no inherited properties.
    *
    * @param {Object} options
    *        Options for creating the Rule, see the Rule constructor.
    * @param {Array} existingRules
    *        Rules to reuse if possible.  If a rule is reused, then it
    *        it will be deleted from this array.
-   * @return {Boolean} true if we added the rule.
+   * @return {Promise} Resolves when done skipping or adding the rule.
    */
-  _maybeAddRule: function (options, existingRules) {
+  _maybeAddRule: Task.async(function* (options, existingRules) {
     // If we've already included this domRule (for example, when a
     // common selector is inherited), ignore it.
     if (options.rule &&
         this.rules.some(rule => rule.domRule === options.rule)) {
-      return false;
+      return;
     }
 
     if (options.system) {
-      return false;
+      return;
     }
 
     let rule = null;
 
     // If we're refreshing and the rule previously existed, reuse the
     // Rule object.
     if (existingRules) {
       let ruleIndex = existingRules.findIndex((r) => r.matches(options));
       if (ruleIndex >= 0) {
         rule = existingRules[ruleIndex];
-        rule.refresh(options);
+        yield rule.refresh(options);
         existingRules.splice(ruleIndex, 1);
       }
     }
 
     // If this is a new rule, create its Rule object.
     if (!rule) {
       rule = new Rule(this, options);
+      yield Promise.all(rule.textProps.map(prop => prop.updateComputed()));
     }
 
     // Ignore inherited rules with no visible properties.
     if (options.inherited && !rule.hasAnyVisibleProperties()) {
-      return false;
+      return;
     }
 
     this.rules.push(rule);
-    return true;
-  },
+  }),
 
   /**
    * Calls markOverridden with all supported pseudo elements
    */
   markOverriddenAll: function () {
     this.markOverridden();
     for (let pseudo of this.cssProperties.pseudoElements) {
       this.markOverridden(pseudo);
@@ -231,17 +233,19 @@ ElementStyle.prototype = {
         }
       }
     }
 
     // Gather all the computed properties applied by those text
     // properties.
     let computedProps = [];
     for (let textProp of textProps) {
-      computedProps = computedProps.concat(textProp.computed);
+      if (textProp.computed) {
+        computedProps = computedProps.concat(textProp.computed);
+      }
     }
 
     // Walk over the computed properties.  As we see a property name
     // for the first time, mark that property's name as taken by this
     // property.
     //
     // If we come across a property whose name is already taken, check
     // its priority against the property that was found first:
@@ -312,28 +316,31 @@ ElementStyle.prototype = {
    * @param {TextProperty} prop
    *        The text property to update.
    * @return {Boolean} true if the TextProperty's overridden state (or any of
    *         its computed properties overridden state) changed.
    */
   _updatePropertyOverridden: function (prop) {
     let overridden = true;
     let dirty = false;
-    for (let computedProp of prop.computed) {
-      if (!computedProp.overridden) {
-        overridden = false;
+    if (prop.computed) {
+      for (let computedProp of prop.computed) {
+        if (!computedProp.overridden) {
+          overridden = false;
+        }
+        dirty = computedProp._overriddenDirty || dirty;
+        delete computedProp._overriddenDirty;
       }
-      dirty = computedProp._overriddenDirty || dirty;
-      delete computedProp._overriddenDirty;
     }
 
     dirty = (!!prop.overridden !== overridden) || dirty;
     prop.overridden = overridden;
     return dirty;
   }
+
 };
 
 /**
  * Store of CSSStyleDeclarations mapped to properties that have been changed by
  * the user.
  */
 function UserProperties() {
   this.map = new Map();
--- a/devtools/client/inspector/rules/models/rule.js
+++ b/devtools/client/inspector/rules/models/rule.js
@@ -9,16 +9,17 @@
 const promise = require("promise");
 const CssLogic = require("devtools/shared/inspector/css-logic");
 const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
 const {TextProperty} =
       require("devtools/client/inspector/rules/models/text-property");
 const {promiseWarn} = require("devtools/client/inspector/shared/utils");
 const {parseDeclarations} = require("devtools/shared/css-parsing-utils");
 const Services = require("Services");
+const {Task} = require("devtools/shared/task");
 
 /**
  * Rule is responsible for the following:
  *   Manages a single style declaration or rule.
  *   Applies changes to the properties in a rule.
  *   Maintains a list of TextProperty objects.
  *
  * @param {ElementStyle} elementStyle
@@ -219,17 +220,17 @@ Rule.prototype = {
         continue;
       }
       if (prop.value.trim() === "") {
         continue;
       }
 
       modifications.setProperty(-1, prop.name, prop.value, prop.priority);
 
-      prop.updateComputed();
+      return prop.updateComputed();
     }
 
     // Store disabled properties in the disabled store.
     let disabled = this.elementStyle.store.disabled;
     if (disabledProps.length > 0) {
       disabled.set(this.style, disabledProps);
     } else {
       disabled.delete(this.style);
@@ -264,34 +265,34 @@ Rule.prototype = {
     });
   },
 
   /**
    * A helper for applyProperties that applies properties in the "as
    * authored" case; that is, when the StyleRuleActor supports
    * setRuleText.
    */
-  _applyPropertiesAuthored: function (modifications) {
-    return modifications.apply().then(() => {
-      // The rewriting may have required some other property values to
-      // change, e.g., to insert some needed terminators.  Update the
-      // relevant properties here.
-      for (let index in modifications.changedDeclarations) {
-        let newValue = modifications.changedDeclarations[index];
-        this.textProps[index].noticeNewValue(newValue);
+  _applyPropertiesAuthored: Task.async(function* (modifications) {
+    yield modifications.apply();
+
+    // The rewriting may have required some other property values to
+    // change, e.g., to insert some needed terminators.  Update the
+    // relevant properties here.
+    for (let index in modifications.changedDeclarations) {
+      let newValue = modifications.changedDeclarations[index];
+      this.textProps[index].noticeNewValue(newValue);
+    }
+    // Recompute and redisplay the computed properties.
+    for (let prop of this.textProps) {
+      if (!prop.invisible && prop.enabled) {
+        yield prop.updateComputed();
+        prop.updateEditor();
       }
-      // Recompute and redisplay the computed properties.
-      for (let prop of this.textProps) {
-        if (!prop.invisible && prop.enabled) {
-          prop.updateComputed();
-          prop.updateEditor();
-        }
-      }
-    });
-  },
+    }
+  }),
 
   /**
    * Reapply all the properties in this rule, and update their
    * computed styles.  Will re-mark overridden properties.  Sets the
    * |_applyingModifications| property to a promise which will resolve
    * when the edit has completed.
    *
    * @param {Function} modifier a function that takes a RuleModificationList
@@ -490,28 +491,29 @@ Rule.prototype = {
 
     return textProps;
   },
 
   /**
    * Reread the current state of the rules and rebuild text
    * properties as needed.
    */
-  refresh: function (options) {
+  refresh: Task.async(function* (options) {
     this.matchedSelectors = options.matchedSelectors || [];
     let newTextProps = this._getTextProperties();
 
     // Update current properties for each property present on the style.
     // This will mark any touched properties with _visited so we
     // can detect properties that weren't touched (because they were
     // removed from the style).
     // Also keep track of properties that didn't exist in the current set
     // of properties.
     let brandNewProps = [];
     for (let newProp of newTextProps) {
+      yield newProp.updateComputed();
       if (!this._updateTextProperty(newProp)) {
         brandNewProps.push(newProp);
       }
     }
 
     // Refresh editors and disabled state for all the properties that
     // were updated.
     for (let prop of this.textProps) {
@@ -527,17 +529,17 @@ Rule.prototype = {
 
     // Add brand new properties.
     this.textProps = this.textProps.concat(brandNewProps);
 
     // Refresh the editor if one already exists.
     if (this.editor) {
       this.editor.populate();
     }
-  },
+  }),
 
   /**
    * Update the current TextProperties that match a given property
    * from the authoredText.  Will choose one existing TextProperty to update
    * with the new property's value, and will disable all others.
    *
    * When choosing the best match to reuse, properties will be chosen
    * by assigning a rank and choosing the highest-ranked property:
--- a/devtools/client/inspector/rules/models/text-property.js
+++ b/devtools/client/inspector/rules/models/text-property.js
@@ -9,16 +9,17 @@
 /* eslint-disable mozilla/reject-some-requires */
 const {Cc, Ci} = require("chrome");
 /* eslint-enable mozilla/reject-some-requires */
 const {escapeCSSComment} = require("devtools/shared/css-parsing-utils");
 const {getCssProperties} = require("devtools/shared/fronts/css-properties");
 /* eslint-disable mozilla/reject-some-requires */
 const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
 /* eslint-enable mozilla/reject-some-requires */
+const {Task} = require("devtools/shared/task");
 
 XPCOMUtils.defineLazyGetter(this, "domUtils", function () {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
 
 /**
  * TextProperty is responsible for the following:
  *   Manages a single property from the authoredText attribute of the
@@ -47,70 +48,47 @@ XPCOMUtils.defineLazyGetter(this, "domUt
 function TextProperty(rule, name, value, priority, enabled = true,
                       invisible = false) {
   this.rule = rule;
   this.name = name;
   this.value = value;
   this.priority = priority;
   this.enabled = !!enabled;
   this.invisible = invisible;
-  this.updateComputed();
-
   const toolbox = this.rule.elementStyle.ruleView.inspector.toolbox;
   this.cssProperties = getCssProperties(toolbox);
+  this.pageStyle = this.rule.elementStyle.pageStyle;
 }
 
 TextProperty.prototype = {
   /**
    * Update the editor associated with this text property,
    * if any.
    */
   updateEditor: function () {
     if (this.editor) {
       this.editor.update();
     }
   },
 
   /**
    * Update the list of computed properties for this text property.
    */
-  updateComputed: function () {
+  updateComputed: Task.async(function* () {
     if (!this.name) {
-      return;
+      return Promise.resolve();
     }
-
-    // This is a bit funky.  To get the list of computed properties
-    // for this text property, we'll set the property on a dummy element
-    // and see what the computed style looks like.
-    let dummyElement = this.rule.elementStyle.ruleView.dummyElement;
-    let dummyStyle = dummyElement.style;
-    dummyStyle.cssText = "";
-    dummyStyle.setProperty(this.name, this.value, this.priority);
-
-    this.computed = [];
-
-    try {
-      // Manually get all the properties that are set when setting a value on
-      // this.name and check the computed style on dummyElement for each one.
-      // If we just read dummyStyle, it would skip properties when value === "".
-      let subProps = domUtils.getSubpropertiesForCSSProperty(this.name);
-
-      for (let prop of subProps) {
-        this.computed.push({
-          textProp: this,
-          name: prop,
-          value: dummyStyle.getPropertyValue(prop),
-          priority: dummyStyle.getPropertyPriority(prop),
-        });
-      }
-    } catch (e) {
-      // This is a partial property name, probably from cutting and pasting
-      // text. At this point don't check for computed properties.
-    }
-  },
+    return this.pageStyle.getComputedProperties(this).then(
+      computed => {
+        this.computed = computed;
+      },
+      // Do nothing if a request didn't complete, this can happen if the
+      // inspector is destroyed before the request is fulfilled.
+      () => {});
+  }),
 
   /**
    * Set all the values from another TextProperty instance into
    * this TextProperty instance.
    *
    * @param {TextProperty} prop
    *        The other TextProperty instance.
    */
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -89,57 +89,16 @@ const FILTER_STRICT_RE = /\s*`(.*?)`\s*$
  * TextPropertyEditor:
  *   Owns a TextProperty object.
  *   Manages changes to the TextProperty.
  *   Can be expanded to display computed properties.
  *   Can mark a property disabled or enabled.
  */
 
 /**
- * To figure out how shorthand properties are interpreted by the
- * engine, we will set properties on a dummy element and observe
- * how their .style attribute reflects them as computed values.
- * This function creates the document in which those dummy elements
- * will be created.
- */
-var gDummyPromise;
-function createDummyDocument() {
-  if (gDummyPromise) {
-    return gDummyPromise;
-  }
-  const { getDocShell, create: makeFrame } = require("sdk/frame/utils");
-
-  let frame = makeFrame(Services.appShell.hiddenDOMWindow.document, {
-    nodeName: "iframe",
-    namespaceURI: "http://www.w3.org/1999/xhtml",
-    allowJavascript: false,
-    allowPlugins: false,
-    allowAuth: false
-  });
-  let docShell = getDocShell(frame);
-  let eventTarget = docShell.chromeEventHandler;
-  let ssm = Services.scriptSecurityManager;
-
-  // We probably need to call InheritFromDocShellToDoc to get the correct origin
-  // attributes, but right now we can't call it from JS.
-  let nullPrincipal = ssm.createNullPrincipal(docShell.getOriginAttributes());
-  docShell.createAboutBlankContentViewer(nullPrincipal);
-  let window = docShell.contentViewer.DOMDocument.defaultView;
-  window.location = "data:text/html,<html></html>";
-  let deferred = defer();
-  eventTarget.addEventListener("DOMContentLoaded", function handler() {
-    eventTarget.removeEventListener("DOMContentLoaded", handler, false);
-    deferred.resolve(window.document);
-    frame.remove();
-  }, false);
-  gDummyPromise = deferred.promise;
-  return gDummyPromise;
-}
-
-/**
  * CssRuleView is a view of the style rules and declarations that
  * apply to a given element.  After construction, the 'element'
  * property will be available with the user interface.
  *
  * @param {Inspector} inspector
  *        Inspector toolbox panel
  * @param {Document} document
  *        The document that will contain the rule view.
@@ -240,25 +199,16 @@ function CssRuleView(inspector, document
 
 CssRuleView.prototype = {
   // The element that we're inspecting.
   _viewedElement: null,
 
   // Used for cancelling timeouts in the style filter.
   _filterChangedTimeout: null,
 
-  // Empty, unconnected element of the same type as this node, used
-  // to figure out how shorthand properties will be parsed.
-  _dummyElement: null,
-
-  // Get the dummy elemenet.
-  get dummyElement() {
-    return this._dummyElement;
-  },
-
   // Get the filter search value.
   get searchValue() {
     return this.searchField.value.toLowerCase();
   },
 
   /**
    * Get an instance of SelectorHighlighter (used to highlight nodes that match
    * selectors in the rule-view). A new instance is only created the first time
@@ -726,20 +676,16 @@ CssRuleView.prototype = {
 
     return false;
   },
 
   destroy: function () {
     this.isDestroyed = true;
     this.clear();
 
-    this._dummyElement = null;
-    this.dummyElementPromise = null;
-    gDummyPromise = null;
-
     this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
     this._prefObserver.off(PREF_UA_STYLES, this._handlePrefChange);
     this._prefObserver.off(PREF_DEFAULT_COLOR_UNIT, this._handlePrefChange);
     this._prefObserver.destroy();
 
     this._outputParser = null;
 
     // Remove context menu
@@ -832,36 +778,28 @@ CssRuleView.prototype = {
     if (!this._viewedElement) {
       this._stopSelectingElement();
       this._clearRules();
       this._showEmpty();
       this.refreshPseudoClassPanel();
       return promise.resolve(undefined);
     }
 
-    // To figure out how shorthand properties are interpreted by the
-    // engine, we will set properties on a dummy element and observe
-    // how their .style attribute reflects them as computed values.
-    this.dummyElementPromise = createDummyDocument().then(document => {
-      // ::before and ::after do not have a namespaceURI
-      let namespaceURI = this.element.namespaceURI ||
-          document.documentElement.namespaceURI;
-      this._dummyElement = document.createElementNS(namespaceURI,
-                                                   this.element.tagName);
-      document.documentElement.appendChild(this._dummyElement);
-      return this._dummyElement;
-    }).then(null, promiseWarn);
+    // Select the current element so that the actor can calculate style
+    // properties off of it.
+    let selectElementPromise = this.pageStyle.selectElement(
+      element.tagName, element.namespaceURI);
 
     let elementStyle = new ElementStyle(element, this, this.store,
       this.pageStyle, this.showUserAgentStyles);
     this._elementStyle = elementStyle;
 
     this._startSelectingElement();
 
-    return this.dummyElementPromise.then(() => {
+    return selectElementPromise.then(() => {
       if (this._elementStyle === elementStyle) {
         return this._populate();
       }
       return undefined;
     }).then(() => {
       if (this._elementStyle === elementStyle) {
         if (!refresh) {
           this.element.scrollTop = 0;
@@ -1638,17 +1576,25 @@ RuleViewTool.prototype = {
       let done = this.inspector.updating("rule-view");
       this.view.selectElement(this.inspector.selection.nodeFront)
         .then(done, done);
     }
   },
 
   refresh: function () {
     if (this.isSidebarActive()) {
-      this.view.refreshPanel();
+      this.view.refreshPanel().then(() => {
+        if(this.view.element) {
+          // If there is an editor, make sure it has retained focus.
+          const el = this.view.element.querySelector('input, textarea');
+          if (el) {
+            el.focus();
+          }
+        }
+      });
     }
   },
 
   clearUserProperties: function () {
     if (this.view && this.view.store && this.view.store.userProperties) {
       this.view.store.userProperties.clear();
     }
   },
--- a/devtools/client/inspector/rules/views/rule-editor.js
+++ b/devtools/client/inspector/rules/views/rule-editor.js
@@ -25,16 +25,17 @@ const {
   parsePseudoClassesAndAttributes,
   SELECTOR_ATTRIBUTE,
   SELECTOR_ELEMENT,
   SELECTOR_PSEUDO_CLASS
 } = require("devtools/shared/css-parsing-utils");
 const promise = require("promise");
 const Services = require("Services");
 const EventEmitter = require("devtools/shared/event-emitter");
+const {Task} = require("devtools/shared/task");
 
 XPCOMUtils.defineLazyGetter(this, "_strings", function () {
   return Services.strings.createBundle(
     "chrome://devtools-shared/locale/styleinspector.properties");
 });
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
@@ -359,68 +360,70 @@ RuleEditor.prototype = {
    *        Property priority.
    * @param {Boolean} enabled
    *        True if the property should be enabled.
    * @param {TextProperty} siblingProp
    *        Optional, property next to which the new property will be added.
    * @return {TextProperty}
    *        The new property
    */
-  addProperty: function (name, value, priority, enabled, siblingProp) {
+  addProperty: Task.async(function* (name, value, priority, enabled, siblingProp) {
     let prop = this.rule.createProperty(name, value, priority, enabled,
       siblingProp);
+    yield prop.updateComputed();
+
     let index = this.rule.textProps.indexOf(prop);
     let editor = new TextPropertyEditor(this, prop);
 
     // Insert this node before the DOM node that is currently at its new index
     // in the property list.  There is currently one less node in the DOM than
     // in the property list, so this causes it to appear after siblingProp.
     // If there is no node at its index, as is the case where this is the last
     // node being inserted, then this behaves as appendChild.
     this.propertyList.insertBefore(editor.element,
       this.propertyList.children[index]);
 
     return prop;
-  },
+  }),
 
   /**
    * Programatically add a list of new properties to the rule.  Focus the UI
    * to the proper location after adding (either focus the value on the
    * last property if it is empty, or create a new property and focus it).
    *
    * @param {Array} properties
    *        Array of properties, which are objects with this signature:
    *        {
    *          name: {string},
    *          value: {string},
    *          priority: {string}
    *        }
    * @param {TextProperty} siblingProp
    *        Optional, the property next to which all new props should be added.
    */
-  addProperties: function (properties, siblingProp) {
+  addProperties: Task.async(function* (properties, siblingProp) {
     if (!properties || !properties.length) {
       return;
     }
 
     let lastProp = siblingProp;
     for (let p of properties) {
       let isCommented = Boolean(p.commentOffsets);
       let enabled = !isCommented;
-      lastProp = this.addProperty(p.name, p.value, p.priority, enabled,
+      lastProp = yield this.addProperty(p.name, p.value, p.priority, enabled,
         lastProp);
     }
 
     // Either focus on the last value if incomplete, or start a new one.
     if (lastProp && lastProp.value.trim() === "") {
       lastProp.editor.valueSpan.click();
     } else {
       this.newProperty();
     }
-  },
+  }),
 
   /**
    * Create a text input for a property name.  If a non-empty property
    * name is given, we'll create a real TextProperty and add it to the
    * rule.
    */
   newProperty: function () {
     // If we're already creating a new property, ignore this.
@@ -485,31 +488,31 @@ RuleEditor.prototype = {
   },
 
   /**
    * Called when the new property editor is destroyed.
    * This is where the properties (type TextProperty) are actually being
    * added, since we want to wait until after the inplace editor `destroy`
    * event has been fired to keep consistent UI state.
    */
-  _newPropertyDestroy: function () {
+  _newPropertyDestroy: Task.async(function* () {
     // We're done, make the close brace focusable again.
     this.closeBrace.setAttribute("tabindex", "0");
 
     this.propertyList.removeChild(this.newPropItem);
     delete this.newPropItem;
     delete this.newPropSpan;
 
     // If properties were added, we want to focus the proper element.
     // If the last new property has no value, focus the value on it.
     // Otherwise, start a new property and focus that field.
     if (this.multipleAddedProperties && this.multipleAddedProperties.length) {
-      this.addProperties(this.multipleAddedProperties);
+      yield this.addProperties(this.multipleAddedProperties);
     }
-  },
+  }),
 
   /**
    * Called when the selector's inplace editor is closed.
    * Ignores the change if the user pressed escape, otherwise
    * commits it.
    *
    * @param {String} value
    *        The value contained in the editor.
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -484,17 +484,18 @@ TextPropertyEditor.prototype = {
 
   /**
    * Update the indicator for computed styles. The computed styles themselves
    * are populated on demand, when they become visible.
    */
   _updateComputed: function () {
     this.computed.innerHTML = "";
 
-    let showExpander = this.prop.computed.some(c => c.name !== this.prop.name);
+    let showExpander = this.prop.computed &&
+      this.prop.computed.some(c => c.name !== this.prop.name);
     this.expander.style.visibility = showExpander ? "visible" : "hidden";
 
     this._populatedComputed = false;
     if (this.expander.hasAttribute("open")) {
       this._populateComputed();
     }
   },
 
--- a/devtools/server/actors/styles.js
+++ b/devtools/server/actors/styles.js
@@ -67,16 +67,38 @@ var PageStyleActor = protocol.ActorClass
     this.onFrameUnload = this.onFrameUnload.bind(this);
     this.onStyleSheetAdded = this.onStyleSheetAdded.bind(this);
 
     events.on(this.inspector.tabActor, "will-navigate", this.onFrameUnload);
     events.on(this.inspector.tabActor, "stylesheet-added", this.onStyleSheetAdded);
 
     this._styleApplied = this._styleApplied.bind(this);
     this._watchedSheets = new Set();
+
+    // A promise to the dummy element of the same tag name and namespace as
+    // the current selected element.
+    this._selectedElement = undefined;
+  },
+
+  /**
+   * Selecting an element from the client will create a dummy element on the
+   * server with the same tag name and namespace. It can be used to test how the
+   * engine interprets shorthand CSS properties, and observe how the .style
+   * attribute reflects them as computed values.
+   *
+   * @param {String} tagName
+   * @param {String} namespaceURI (optional)
+   * @return {Promise} Resolves to true.
+   */
+  selectElement: function (tagName, namespaceURI) {
+    let document = this.inspector.tabActor.window.document;
+    // ::before and ::after do not have a namespaceURI
+    namespaceURI = namespaceURI || document.documentElement.namespaceURI;
+    this.dummyElement = document.createElementNS(namespaceURI, tagName);
+    return true;
   },
 
   destroy: function () {
     if (!this.walker) {
       return;
     }
     protocol.Actor.prototype.destroy.call(this);
     events.off(this.inspector.tabActor, "will-navigate", this.onFrameUnload);
@@ -219,16 +241,58 @@ var PageStyleActor = protocol.ActorClass
         }
       }
     }
 
     return ret;
   },
 
   /**
+   * Take a CSS property, and figure out how it is computed on the selected
+   * element. For instance "margin: 1em !important" would break down to four
+   * computed values of "margin-top", "margin-right", "margin-bottom",
+   * "margin-left". One of these computed property objects would look like:
+   * { name: "margin-top", value: "1em", priority: "important" }
+   *
+   * @param {String} name
+   * @param {String} value
+   * @param {String} priority
+   * @return {Array} An array of objects that represent the computed properties.
+   */
+  getComputedProperties: function (name, value, priority) {
+    // This is a bit funky.  To get the list of computed properties
+    // for this text property, we'll set the property on a dummy element
+    // and see what the computed style looks like.
+    let dummyStyle = this.dummyElement.style;
+    dummyStyle.cssText = "";
+    dummyStyle.setProperty(name, value, priority);
+
+    let computed = [];
+
+    try {
+      // Manually get all the properties that are set when setting a value on
+      // name and check the computed style on dummyElement for each one.
+      // If we just read dummyStyle, it would skip properties when value === "".
+      let subProps = DOMUtils.getSubpropertiesForCSSProperty(name);
+
+      for (let prop of subProps) {
+        computed.push({
+          name: prop,
+          value: dummyStyle.getPropertyValue(prop),
+          priority: dummyStyle.getPropertyPriority(prop),
+        });
+      }
+    } catch (e) {
+      // This is a partial property name, probably from cutting and pasting
+      // text. At this point don't check for computed properties.
+    }
+    return computed;
+  },
+
+  /**
    * Get all the fonts from a page.
    *
    * @param object options
    *   `includePreviews`: Whether to also return image previews of the fonts.
    *   `previewText`: The text to display in the previews.
    *   `previewFontSize`: The font size of the text in the previews.
    *
    * @returns object
--- a/devtools/shared/fronts/styles.js
+++ b/devtools/shared/fronts/styles.js
@@ -46,16 +46,29 @@ const PageStyleFront = FrontClassWithSpe
   get walker() {
     return this.inspector.walker;
   },
 
   get supportsAuthoredStyles() {
     return this._form.traits && this._form.traits.authoredStyles;
   },
 
+  getComputedProperties: custom(function (textProp) {
+    const {name, value, priority} = textProp;
+    return this._getComputedProperties(name, value, priority)
+      .then(computed => {
+        for (let computedProp of computed) {
+          computedProp.textProp = textProp;
+        }
+        return computed;
+      });
+  }, {
+    impl: "_getComputedProperties"
+  }),
+
   getMatchedSelectors: custom(function (node, property, options) {
     return this._getMatchedSelectors(node, property, options).then(ret => {
       return ret.matched;
     });
   }, {
     impl: "_getMatchedSelectors"
   }),
 
--- a/devtools/shared/specs/styles.js
+++ b/devtools/shared/specs/styles.js
@@ -45,16 +45,22 @@ types.addDictType("matchedselector", {
 });
 
 types.addDictType("appliedStylesReturn", {
   entries: "array:appliedstyle",
   rules: "array:domstylerule",
   sheets: "array:stylesheet"
 });
 
+types.addDictType("computedproperty", {
+  name: "string",
+  value: "string",
+  priority: "string"
+});
+
 types.addDictType("modifiedStylesReturn", {
   isMatching: RetVal("boolean"),
   ruleProps: RetVal("nullable:appliedStylesReturn")
 });
 
 types.addDictType("fontpreview", {
   data: "nullable:longstring",
   size: "json"
@@ -100,27 +106,40 @@ const pageStyleSpec = generateActorSpec(
         previewText: Option(0, "string"),
         previewFontSize: Option(0, "string"),
         previewFillStyle: Option(0, "string")
       },
       response: {
         fontFaces: RetVal("array:fontface")
       }
     },
+    selectElement: {
+      request: {
+        tagName: Arg(0, "string"),
+        namespaceURI: Arg(1, "string")
+      },
+      response: { isSelected: RetVal("boolean") },
+    },
     getUsedFontFaces: {
       request: {
         node: Arg(0, "domnode"),
         includePreviews: Option(1, "boolean"),
         previewText: Option(1, "string"),
         previewFontSize: Option(1, "string"),
         previewFillStyle: Option(1, "string")
       },
-      response: {
-        fontFaces: RetVal("array:fontface")
-      }
+      response: RetVal("json")
+    },
+    getComputedProperties: {
+      request: {
+        name: Arg(0, "string"),
+        value: Arg(1, "string"),
+        priority: Arg(2, "string")
+      },
+      response: RetVal("array:computedproperty")
     },
     getMatchedSelectors: {
       request: {
         node: Arg(0, "domnode"),
         property: Arg(1, "string"),
         filter: Option(2, "string")
       },
       response: RetVal(types.addDictType("matchedselectorresponse", {