--- 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", {