browser/devtools/styleinspector/rule-view.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     2 /* vim: set ts=2 et sw=2 tw=80: */
     3 /* This Source Code Form is subject to the terms of the Mozilla Public
     4  * License, v. 2.0. If a copy of the MPL was not distributed with this
     5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     7 "use strict";
     9 const {Cc, Ci, Cu} = require("chrome");
    10 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
    11 const {CssLogic} = require("devtools/styleinspector/css-logic");
    12 const {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor");
    13 const {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles");
    14 const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
    15 const {Tooltip, SwatchColorPickerTooltip} = require("devtools/shared/widgets/Tooltip");
    16 const {OutputParser} = require("devtools/output-parser");
    17 const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils");
    18 const {parseSingleValue, parseDeclarations} = require("devtools/styleinspector/css-parsing-utils");
    20 Cu.import("resource://gre/modules/Services.jsm");
    21 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    23 const HTML_NS = "http://www.w3.org/1999/xhtml";
    24 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    26 /**
    27  * These regular expressions are adapted from firebug's css.js, and are
    28  * used to parse CSSStyleDeclaration's cssText attribute.
    29  */
    31 // Used to split on css line separators
    32 const CSS_LINE_RE = /(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g;
    34 // Used to parse a single property line.
    35 const CSS_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*(?:! (important))?;?$/;
    37 // Used to parse an external resource from a property value
    38 const CSS_RESOURCE_RE = /url\([\'\"]?(.*?)[\'\"]?\)/;
    40 const IOService = Cc["@mozilla.org/network/io-service;1"]
    41                   .getService(Ci.nsIIOService);
    43 function promiseWarn(err) {
    44   console.error(err);
    45   return promise.reject(err);
    46 }
    48 /**
    49  * To figure out how shorthand properties are interpreted by the
    50  * engine, we will set properties on a dummy element and observe
    51  * how their .style attribute reflects them as computed values.
    52  * This function creates the document in which those dummy elements
    53  * will be created.
    54  */
    55 var gDummyPromise;
    56 function createDummyDocument() {
    57   if (gDummyPromise) {
    58     return gDummyPromise;
    59   }
    60   const { getDocShell, create: makeFrame } = require("sdk/frame/utils");
    62   let frame = makeFrame(Services.appShell.hiddenDOMWindow.document, {
    63     nodeName: "iframe",
    64     namespaceURI: "http://www.w3.org/1999/xhtml",
    65     allowJavascript: false,
    66     allowPlugins: false,
    67     allowAuth: false
    68   });
    69   let docShell = getDocShell(frame);
    70   let eventTarget = docShell.chromeEventHandler;
    71   docShell.createAboutBlankContentViewer(Cc["@mozilla.org/nullprincipal;1"].createInstance(Ci.nsIPrincipal));
    72   let window = docShell.contentViewer.DOMDocument.defaultView;
    73   window.location = "data:text/html,<html></html>";
    74   let deferred = promise.defer();
    75   eventTarget.addEventListener("DOMContentLoaded", function handler(event) {
    76     eventTarget.removeEventListener("DOMContentLoaded", handler, false);
    77     deferred.resolve(window.document);
    78     frame.remove();
    79   }, false);
    80   gDummyPromise = deferred.promise;
    81   return gDummyPromise;
    82 }
    84 /**
    85  * Our model looks like this:
    86  *
    87  * ElementStyle:
    88  *   Responsible for keeping track of which properties are overridden.
    89  *   Maintains a list of Rule objects that apply to the element.
    90  * Rule:
    91  *   Manages a single style declaration or rule.
    92  *   Responsible for applying changes to the properties in a rule.
    93  *   Maintains a list of TextProperty objects.
    94  * TextProperty:
    95  *   Manages a single property from the cssText attribute of the
    96  *     relevant declaration.
    97  *   Maintains a list of computed properties that come from this
    98  *     property declaration.
    99  *   Changes to the TextProperty are sent to its related Rule for
   100  *     application.
   101  */
   103 /**
   104  * ElementStyle maintains a list of Rule objects for a given element.
   105  *
   106  * @param {Element} aElement
   107  *        The element whose style we are viewing.
   108  * @param {object} aStore
   109  *        The ElementStyle can use this object to store metadata
   110  *        that might outlast the rule view, particularly the current
   111  *        set of disabled properties.
   112  * @param {PageStyleFront} aPageStyle
   113  *        Front for the page style actor that will be providing
   114  *        the style information.
   115  *
   116  * @constructor
   117  */
   118 function ElementStyle(aElement, aStore, aPageStyle) {
   119   this.element = aElement;
   120   this.store = aStore || {};
   121   this.pageStyle = aPageStyle;
   123   // We don't want to overwrite this.store.userProperties so we only create it
   124   // if it doesn't already exist.
   125   if (!("userProperties" in this.store)) {
   126     this.store.userProperties = new UserProperties();
   127   }
   129   if (!("disabled" in this.store)) {
   130     this.store.disabled = new WeakMap();
   131   }
   132 }
   134 // We're exporting _ElementStyle for unit tests.
   135 exports._ElementStyle = ElementStyle;
   137 ElementStyle.prototype = {
   138   // The element we're looking at.
   139   element: null,
   141   // Empty, unconnected element of the same type as this node, used
   142   // to figure out how shorthand properties will be parsed.
   143   dummyElement: null,
   145   init: function()
   146   {
   147     // To figure out how shorthand properties are interpreted by the
   148     // engine, we will set properties on a dummy element and observe
   149     // how their .style attribute reflects them as computed values.
   150     return this.dummyElementPromise = createDummyDocument().then(document => {
   151       this.dummyElement = document.createElementNS(this.element.namespaceURI,
   152                                                    this.element.tagName);
   153       document.documentElement.appendChild(this.dummyElement);
   154       return this.dummyElement;
   155     }).then(null, promiseWarn);
   156   },
   158   destroy: function() {
   159     this.dummyElement = null;
   160     this.dummyElementPromise.then(dummyElement => {
   161       if (dummyElement.parentNode) {
   162         dummyElement.parentNode.removeChild(dummyElement);
   163       }
   164       this.dummyElementPromise = null;
   165     });
   166   },
   168   /**
   169    * Called by the Rule object when it has been changed through the
   170    * setProperty* methods.
   171    */
   172   _changed: function() {
   173     if (this.onChanged) {
   174       this.onChanged();
   175     }
   176   },
   178   /**
   179    * Refresh the list of rules to be displayed for the active element.
   180    * Upon completion, this.rules[] will hold a list of Rule objects.
   181    *
   182    * Returns a promise that will be resolved when the elementStyle is
   183    * ready.
   184    */
   185   populate: function() {
   186     let populated = this.pageStyle.getApplied(this.element, {
   187       inherited: true,
   188       matchedSelectors: true
   189     }).then(entries => {
   190       // Make sure the dummy element has been created before continuing...
   191       return this.dummyElementPromise.then(() => {
   192         if (this.populated != populated) {
   193           // Don't care anymore.
   194           return promise.reject("unused");
   195         }
   197         // Store the current list of rules (if any) during the population
   198         // process.  They will be reused if possible.
   199         this._refreshRules = this.rules;
   201         this.rules = [];
   203         for (let entry of entries) {
   204           this._maybeAddRule(entry);
   205         }
   207         // Mark overridden computed styles.
   208         this.markOverriddenAll();
   210         this._sortRulesForPseudoElement();
   212         // We're done with the previous list of rules.
   213         delete this._refreshRules;
   215         return null;
   216       });
   217     }).then(null, promiseWarn);
   218     this.populated = populated;
   219     return this.populated;
   220   },
   222   /**
   223    * Put pseudo elements in front of others.
   224    */
   225    _sortRulesForPseudoElement: function() {
   226       this.rules = this.rules.sort((a, b) => {
   227         return (a.pseudoElement || "z") > (b.pseudoElement || "z");
   228       });
   229    },
   231   /**
   232    * Add a rule if it's one we care about.  Filters out duplicates and
   233    * inherited styles with no inherited properties.
   234    *
   235    * @param {object} aOptions
   236    *        Options for creating the Rule, see the Rule constructor.
   237    *
   238    * @return {bool} true if we added the rule.
   239    */
   240   _maybeAddRule: function(aOptions) {
   241     // If we've already included this domRule (for example, when a
   242     // common selector is inherited), ignore it.
   243     if (aOptions.rule &&
   244         this.rules.some(function(rule) rule.domRule === aOptions.rule)) {
   245       return false;
   246     }
   248     if (aOptions.system) {
   249       return false;
   250     }
   252     let rule = null;
   254     // If we're refreshing and the rule previously existed, reuse the
   255     // Rule object.
   256     if (this._refreshRules) {
   257       for (let r of this._refreshRules) {
   258         if (r.matches(aOptions)) {
   259           rule = r;
   260           rule.refresh(aOptions);
   261           break;
   262         }
   263       }
   264     }
   266     // If this is a new rule, create its Rule object.
   267     if (!rule) {
   268       rule = new Rule(this, aOptions);
   269     }
   271     // Ignore inherited rules with no properties.
   272     if (aOptions.inherited && rule.textProps.length == 0) {
   273       return false;
   274     }
   276     this.rules.push(rule);
   277     return true;
   278   },
   280   /**
   281    * Calls markOverridden with all supported pseudo elements
   282    */
   283   markOverriddenAll: function() {
   284     this.markOverridden();
   285     for (let pseudo of PSEUDO_ELEMENTS) {
   286       this.markOverridden(pseudo);
   287     }
   288   },
   290   /**
   291    * Mark the properties listed in this.rules for a given pseudo element
   292    * with an overridden flag if an earlier property overrides it.
   293    * @param {string} pseudo
   294    *        Which pseudo element to flag as overridden.
   295    *        Empty string or undefined will default to no pseudo element.
   296    */
   297   markOverridden: function(pseudo="") {
   298     // Gather all the text properties applied by these rules, ordered
   299     // from more- to less-specific.
   300     let textProps = [];
   301     for (let rule of this.rules) {
   302       if (rule.pseudoElement == pseudo) {
   303         textProps = textProps.concat(rule.textProps.slice(0).reverse());
   304       }
   305     }
   307     // Gather all the computed properties applied by those text
   308     // properties.
   309     let computedProps = [];
   310     for (let textProp of textProps) {
   311       computedProps = computedProps.concat(textProp.computed);
   312     }
   314     // Walk over the computed properties.  As we see a property name
   315     // for the first time, mark that property's name as taken by this
   316     // property.
   317     //
   318     // If we come across a property whose name is already taken, check
   319     // its priority against the property that was found first:
   320     //
   321     //   If the new property is a higher priority, mark the old
   322     //   property overridden and mark the property name as taken by
   323     //   the new property.
   324     //
   325     //   If the new property is a lower or equal priority, mark it as
   326     //   overridden.
   327     //
   328     // _overriddenDirty will be set on each prop, indicating whether its
   329     // dirty status changed during this pass.
   330     let taken = {};
   331     for (let computedProp of computedProps) {
   332       let earlier = taken[computedProp.name];
   333       let overridden;
   334       if (earlier &&
   335           computedProp.priority === "important" &&
   336           earlier.priority !== "important") {
   337         // New property is higher priority.  Mark the earlier property
   338         // overridden (which will reverse its dirty state).
   339         earlier._overriddenDirty = !earlier._overriddenDirty;
   340         earlier.overridden = true;
   341         overridden = false;
   342       } else {
   343         overridden = !!earlier;
   344       }
   346       computedProp._overriddenDirty = (!!computedProp.overridden != overridden);
   347       computedProp.overridden = overridden;
   348       if (!computedProp.overridden && computedProp.textProp.enabled) {
   349         taken[computedProp.name] = computedProp;
   350       }
   351     }
   353     // For each TextProperty, mark it overridden if all of its
   354     // computed properties are marked overridden.  Update the text
   355     // property's associated editor, if any.  This will clear the
   356     // _overriddenDirty state on all computed properties.
   357     for (let textProp of textProps) {
   358       // _updatePropertyOverridden will return true if the
   359       // overridden state has changed for the text property.
   360       if (this._updatePropertyOverridden(textProp)) {
   361         textProp.updateEditor();
   362       }
   363     }
   364   },
   366   /**
   367    * Mark a given TextProperty as overridden or not depending on the
   368    * state of its computed properties.  Clears the _overriddenDirty state
   369    * on all computed properties.
   370    *
   371    * @param {TextProperty} aProp
   372    *        The text property to update.
   373    *
   374    * @return {bool} true if the TextProperty's overridden state (or any of its
   375    *         computed properties overridden state) changed.
   376    */
   377   _updatePropertyOverridden: function(aProp) {
   378     let overridden = true;
   379     let dirty = false;
   380     for each (let computedProp in aProp.computed) {
   381       if (!computedProp.overridden) {
   382         overridden = false;
   383       }
   384       dirty = computedProp._overriddenDirty || dirty;
   385       delete computedProp._overriddenDirty;
   386     }
   388     dirty = (!!aProp.overridden != overridden) || dirty;
   389     aProp.overridden = overridden;
   390     return dirty;
   391   }
   392 };
   394 /**
   395  * A single style rule or declaration.
   396  *
   397  * @param {ElementStyle} aElementStyle
   398  *        The ElementStyle to which this rule belongs.
   399  * @param {object} aOptions
   400  *        The information used to construct this rule.  Properties include:
   401  *          rule: A StyleRuleActor
   402  *          inherited: An element this rule was inherited from.  If omitted,
   403  *            the rule applies directly to the current element.
   404  * @constructor
   405  */
   406 function Rule(aElementStyle, aOptions) {
   407   this.elementStyle = aElementStyle;
   408   this.domRule = aOptions.rule || null;
   409   this.style = aOptions.rule;
   410   this.matchedSelectors = aOptions.matchedSelectors || [];
   411   this.pseudoElement = aOptions.pseudoElement || "";
   413   this.inherited = aOptions.inherited || null;
   414   this._modificationDepth = 0;
   416   if (this.domRule) {
   417     let parentRule = this.domRule.parentRule;
   418     if (parentRule && parentRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE) {
   419       this.mediaText = parentRule.mediaText;
   420     }
   421   }
   423   // Populate the text properties with the style's current cssText
   424   // value, and add in any disabled properties from the store.
   425   this.textProps = this._getTextProperties();
   426   this.textProps = this.textProps.concat(this._getDisabledProperties());
   427 }
   429 Rule.prototype = {
   430   mediaText: "",
   432   get title() {
   433     if (this._title) {
   434       return this._title;
   435     }
   436     this._title = CssLogic.shortSource(this.sheet);
   437     if (this.domRule.type !== ELEMENT_STYLE) {
   438       this._title += ":" + this.ruleLine;
   439     }
   441     this._title = this._title + (this.mediaText ? " @media " + this.mediaText : "");
   442     return this._title;
   443   },
   445   get inheritedSource() {
   446     if (this._inheritedSource) {
   447       return this._inheritedSource;
   448     }
   449     this._inheritedSource = "";
   450     if (this.inherited) {
   451       let eltText = this.inherited.tagName.toLowerCase();
   452       if (this.inherited.id) {
   453         eltText += "#" + this.inherited.id;
   454       }
   455       this._inheritedSource =
   456         CssLogic._strings.formatStringFromName("rule.inheritedFrom", [eltText], 1);
   457     }
   458     return this._inheritedSource;
   459   },
   461   get selectorText() {
   462     return this.domRule.selectors ? this.domRule.selectors.join(", ") : CssLogic.l10n("rule.sourceElement");
   463   },
   465   /**
   466    * The rule's stylesheet.
   467    */
   468   get sheet() {
   469     return this.domRule ? this.domRule.parentStyleSheet : null;
   470   },
   472   /**
   473    * The rule's line within a stylesheet
   474    */
   475   get ruleLine() {
   476     return this.domRule ? this.domRule.line : null;
   477   },
   479   /**
   480    * The rule's column within a stylesheet
   481    */
   482   get ruleColumn() {
   483     return this.domRule ? this.domRule.column : null;
   484   },
   486   /**
   487    * Get display name for this rule based on the original source
   488    * for this rule's style sheet.
   489    *
   490    * @return {Promise}
   491    *         Promise which resolves with location as an object containing
   492    *         both the full and short version of the source string.
   493    */
   494   getOriginalSourceStrings: function() {
   495     if (this._originalSourceStrings) {
   496       return promise.resolve(this._originalSourceStrings);
   497     }
   498     return this.domRule.getOriginalLocation().then(({href, line}) => {
   499       let sourceStrings = {
   500         full: href + ":" + line,
   501         short: CssLogic.shortSource({href: href}) + ":" + line
   502       };
   504       this._originalSourceStrings = sourceStrings;
   505       return sourceStrings;
   506     });
   507   },
   509   /**
   510    * Returns true if the rule matches the creation options
   511    * specified.
   512    *
   513    * @param {object} aOptions
   514    *        Creation options.  See the Rule constructor for documentation.
   515    */
   516   matches: function(aOptions) {
   517     return this.style === aOptions.rule;
   518   },
   520   /**
   521    * Create a new TextProperty to include in the rule.
   522    *
   523    * @param {string} aName
   524    *        The text property name (such as "background" or "border-top").
   525    * @param {string} aValue
   526    *        The property's value (not including priority).
   527    * @param {string} aPriority
   528    *        The property's priority (either "important" or an empty string).
   529    * @param {TextProperty} aSiblingProp
   530    *        Optional, property next to which the new property will be added.
   531    */
   532   createProperty: function(aName, aValue, aPriority, aSiblingProp) {
   533     let prop = new TextProperty(this, aName, aValue, aPriority);
   535     if (aSiblingProp) {
   536       let ind = this.textProps.indexOf(aSiblingProp);
   537       this.textProps.splice(ind + 1, 0, prop);
   538     }
   539     else {
   540       this.textProps.push(prop);
   541     }
   543     this.applyProperties();
   544     return prop;
   545   },
   547   /**
   548    * Reapply all the properties in this rule, and update their
   549    * computed styles. Store disabled properties in the element
   550    * style's store. Will re-mark overridden properties.
   551    *
   552    * @param {string} [aName]
   553    *        A text property name (such as "background" or "border-top") used
   554    *        when calling from setPropertyValue & setPropertyName to signify
   555    *        that the property should be saved in store.userProperties.
   556    */
   557   applyProperties: function(aModifications, aName) {
   558     this.elementStyle.markOverriddenAll();
   560     if (!aModifications) {
   561       aModifications = this.style.startModifyingProperties();
   562     }
   563     let disabledProps = [];
   564     let store = this.elementStyle.store;
   566     for (let prop of this.textProps) {
   567       if (!prop.enabled) {
   568         disabledProps.push({
   569           name: prop.name,
   570           value: prop.value,
   571           priority: prop.priority
   572         });
   573         continue;
   574       }
   575       if (prop.value.trim() === "") {
   576         continue;
   577       }
   579       aModifications.setProperty(prop.name, prop.value, prop.priority);
   581       prop.updateComputed();
   582     }
   584     // Store disabled properties in the disabled store.
   585     let disabled = this.elementStyle.store.disabled;
   586     if (disabledProps.length > 0) {
   587       disabled.set(this.style, disabledProps);
   588     } else {
   589       disabled.delete(this.style);
   590     }
   592     let promise = aModifications.apply().then(() => {
   593       let cssProps = {};
   594       for (let cssProp of parseDeclarations(this.style.cssText)) {
   595         cssProps[cssProp.name] = cssProp;
   596       }
   598       for (let textProp of this.textProps) {
   599         if (!textProp.enabled) {
   600           continue;
   601         }
   602         let cssProp = cssProps[textProp.name];
   604         if (!cssProp) {
   605           cssProp = {
   606             name: textProp.name,
   607             value: "",
   608             priority: ""
   609           };
   610         }
   612         if (aName && textProp.name == aName) {
   613           store.userProperties.setProperty(
   614             this.style,
   615             textProp.name,
   616             textProp.value);
   617         }
   618         textProp.priority = cssProp.priority;
   619       }
   621       this.elementStyle.markOverriddenAll();
   623       if (promise === this._applyingModifications) {
   624         this._applyingModifications = null;
   625       }
   627       this.elementStyle._changed();
   628     }).then(null, promiseWarn);
   630     this._applyingModifications = promise;
   631     return promise;
   632   },
   634   /**
   635    * Renames a property.
   636    *
   637    * @param {TextProperty} aProperty
   638    *        The property to rename.
   639    * @param {string} aName
   640    *        The new property name (such as "background" or "border-top").
   641    */
   642   setPropertyName: function(aProperty, aName) {
   643     if (aName === aProperty.name) {
   644       return;
   645     }
   646     let modifications = this.style.startModifyingProperties();
   647     modifications.removeProperty(aProperty.name);
   648     aProperty.name = aName;
   649     this.applyProperties(modifications, aName);
   650   },
   652   /**
   653    * Sets the value and priority of a property, then reapply all properties.
   654    *
   655    * @param {TextProperty} aProperty
   656    *        The property to manipulate.
   657    * @param {string} aValue
   658    *        The property's value (not including priority).
   659    * @param {string} aPriority
   660    *        The property's priority (either "important" or an empty string).
   661    */
   662   setPropertyValue: function(aProperty, aValue, aPriority) {
   663     if (aValue === aProperty.value && aPriority === aProperty.priority) {
   664       return;
   665     }
   667     aProperty.value = aValue;
   668     aProperty.priority = aPriority;
   669     this.applyProperties(null, aProperty.name);
   670   },
   672   /**
   673    * Just sets the value and priority of a property, in order to preview its
   674    * effect on the content document.
   675    *
   676    * @param {TextProperty} aProperty
   677    *        The property which value will be previewed
   678    * @param {String} aValue
   679    *        The value to be used for the preview
   680    * @param {String} aPriority
   681    *        The property's priority (either "important" or an empty string).
   682    */
   683   previewPropertyValue: function(aProperty, aValue, aPriority) {
   684     let modifications = this.style.startModifyingProperties();
   685     modifications.setProperty(aProperty.name, aValue, aPriority);
   686     modifications.apply();
   687   },
   689   /**
   690    * Disables or enables given TextProperty.
   691    *
   692    * @param {TextProperty} aProperty
   693    *        The property to enable/disable
   694    * @param {Boolean} aValue
   695    */
   696   setPropertyEnabled: function(aProperty, aValue) {
   697     aProperty.enabled = !!aValue;
   698     let modifications = this.style.startModifyingProperties();
   699     if (!aProperty.enabled) {
   700       modifications.removeProperty(aProperty.name);
   701     }
   702     this.applyProperties(modifications);
   703   },
   705   /**
   706    * Remove a given TextProperty from the rule and update the rule
   707    * accordingly.
   708    *
   709    * @param {TextProperty} aProperty
   710    *        The property to be removed
   711    */
   712   removeProperty: function(aProperty) {
   713     this.textProps = this.textProps.filter(function(prop) prop != aProperty);
   714     let modifications = this.style.startModifyingProperties();
   715     modifications.removeProperty(aProperty.name);
   716     // Need to re-apply properties in case removing this TextProperty
   717     // exposes another one.
   718     this.applyProperties(modifications);
   719   },
   721   /**
   722    * Get the list of TextProperties from the style.  Needs
   723    * to parse the style's cssText.
   724    */
   725   _getTextProperties: function() {
   726     let textProps = [];
   727     let store = this.elementStyle.store;
   728     let props = parseDeclarations(this.style.cssText);
   729     for (let prop of props) {
   730       let name = prop.name;
   731       if (this.inherited && !domUtils.isInheritedProperty(name)) {
   732         continue;
   733       }
   734       let value = store.userProperties.getProperty(this.style, name, prop.value);
   735       let textProp = new TextProperty(this, name, value, prop.priority);
   736       textProps.push(textProp);
   737     }
   739     return textProps;
   740   },
   742   /**
   743    * Return the list of disabled properties from the store for this rule.
   744    */
   745   _getDisabledProperties: function() {
   746     let store = this.elementStyle.store;
   748     // Include properties from the disabled property store, if any.
   749     let disabledProps = store.disabled.get(this.style);
   750     if (!disabledProps) {
   751       return [];
   752     }
   754     let textProps = [];
   756     for each (let prop in disabledProps) {
   757       let value = store.userProperties.getProperty(this.style, prop.name, prop.value);
   758       let textProp = new TextProperty(this, prop.name, value, prop.priority);
   759       textProp.enabled = false;
   760       textProps.push(textProp);
   761     }
   763     return textProps;
   764   },
   766   /**
   767    * Reread the current state of the rules and rebuild text
   768    * properties as needed.
   769    */
   770   refresh: function(aOptions) {
   771     this.matchedSelectors = aOptions.matchedSelectors || [];
   772     let newTextProps = this._getTextProperties();
   774     // Update current properties for each property present on the style.
   775     // This will mark any touched properties with _visited so we
   776     // can detect properties that weren't touched (because they were
   777     // removed from the style).
   778     // Also keep track of properties that didn't exist in the current set
   779     // of properties.
   780     let brandNewProps = [];
   781     for (let newProp of newTextProps) {
   782       if (!this._updateTextProperty(newProp)) {
   783         brandNewProps.push(newProp);
   784       }
   785     }
   787     // Refresh editors and disabled state for all the properties that
   788     // were updated.
   789     for (let prop of this.textProps) {
   790       // Properties that weren't touched during the update
   791       // process must no longer exist on the node.  Mark them disabled.
   792       if (!prop._visited) {
   793         prop.enabled = false;
   794         prop.updateEditor();
   795       } else {
   796         delete prop._visited;
   797       }
   798     }
   800     // Add brand new properties.
   801     this.textProps = this.textProps.concat(brandNewProps);
   803     // Refresh the editor if one already exists.
   804     if (this.editor) {
   805       this.editor.populate();
   806     }
   807   },
   809   /**
   810    * Update the current TextProperties that match a given property
   811    * from the cssText.  Will choose one existing TextProperty to update
   812    * with the new property's value, and will disable all others.
   813    *
   814    * When choosing the best match to reuse, properties will be chosen
   815    * by assigning a rank and choosing the highest-ranked property:
   816    *   Name, value, and priority match, enabled. (6)
   817    *   Name, value, and priority match, disabled. (5)
   818    *   Name and value match, enabled. (4)
   819    *   Name and value match, disabled. (3)
   820    *   Name matches, enabled. (2)
   821    *   Name matches, disabled. (1)
   822    *
   823    * If no existing properties match the property, nothing happens.
   824    *
   825    * @param {TextProperty} aNewProp
   826    *        The current version of the property, as parsed from the
   827    *        cssText in Rule._getTextProperties().
   828    *
   829    * @return {bool} true if a property was updated, false if no properties
   830    *         were updated.
   831    */
   832   _updateTextProperty: function(aNewProp) {
   833     let match = { rank: 0, prop: null };
   835     for each (let prop in this.textProps) {
   836       if (prop.name != aNewProp.name)
   837         continue;
   839       // Mark this property visited.
   840       prop._visited = true;
   842       // Start at rank 1 for matching name.
   843       let rank = 1;
   845       // Value and Priority matches add 2 to the rank.
   846       // Being enabled adds 1.  This ranks better matches higher,
   847       // with priority breaking ties.
   848       if (prop.value === aNewProp.value) {
   849         rank += 2;
   850         if (prop.priority === aNewProp.priority) {
   851           rank += 2;
   852         }
   853       }
   855       if (prop.enabled) {
   856         rank += 1;
   857       }
   859       if (rank > match.rank) {
   860         if (match.prop) {
   861           // We outrank a previous match, disable it.
   862           match.prop.enabled = false;
   863           match.prop.updateEditor();
   864         }
   865         match.rank = rank;
   866         match.prop = prop;
   867       } else if (rank) {
   868         // A previous match outranks us, disable ourself.
   869         prop.enabled = false;
   870         prop.updateEditor();
   871       }
   872     }
   874     // If we found a match, update its value with the new text property
   875     // value.
   876     if (match.prop) {
   877       match.prop.set(aNewProp);
   878       return true;
   879     }
   881     return false;
   882   },
   884   /**
   885    * Jump between editable properties in the UI.  Will begin editing the next
   886    * name, if possible.  If this is the last element in the set, then begin
   887    * editing the previous value.  If this is the *only* element in the set,
   888    * then settle for focusing the new property editor.
   889    *
   890    * @param {TextProperty} aTextProperty
   891    *        The text property that will be left to focus on a sibling.
   892    *
   893    */
   894   editClosestTextProperty: function(aTextProperty) {
   895     let index = this.textProps.indexOf(aTextProperty);
   896     let previous = false;
   898     // If this is the last element, move to the previous instead of next
   899     if (index === this.textProps.length - 1) {
   900       index = index - 1;
   901       previous = true;
   902     }
   903     else {
   904       index = index + 1;
   905     }
   907     let nextProp = this.textProps[index];
   909     // If possible, begin editing the next name or previous value.
   910     // Otherwise, settle for focusing the new property element.
   911     if (nextProp) {
   912       if (previous) {
   913         nextProp.editor.valueSpan.click();
   914       } else {
   915         nextProp.editor.nameSpan.click();
   916       }
   917     } else {
   918       aTextProperty.rule.editor.closeBrace.focus();
   919     }
   920   }
   921 };
   923 /**
   924  * A single property in a rule's cssText.
   925  *
   926  * @param {Rule} aRule
   927  *        The rule this TextProperty came from.
   928  * @param {string} aName
   929  *        The text property name (such as "background" or "border-top").
   930  * @param {string} aValue
   931  *        The property's value (not including priority).
   932  * @param {string} aPriority
   933  *        The property's priority (either "important" or an empty string).
   934  *
   935  */
   936 function TextProperty(aRule, aName, aValue, aPriority) {
   937   this.rule = aRule;
   938   this.name = aName;
   939   this.value = aValue;
   940   this.priority = aPriority;
   941   this.enabled = true;
   942   this.updateComputed();
   943 }
   945 TextProperty.prototype = {
   946   /**
   947    * Update the editor associated with this text property,
   948    * if any.
   949    */
   950   updateEditor: function() {
   951     if (this.editor) {
   952       this.editor.update();
   953     }
   954   },
   956   /**
   957    * Update the list of computed properties for this text property.
   958    */
   959   updateComputed: function() {
   960     if (!this.name) {
   961       return;
   962     }
   964     // This is a bit funky.  To get the list of computed properties
   965     // for this text property, we'll set the property on a dummy element
   966     // and see what the computed style looks like.
   967     let dummyElement = this.rule.elementStyle.dummyElement;
   968     let dummyStyle = dummyElement.style;
   969     dummyStyle.cssText = "";
   970     dummyStyle.setProperty(this.name, this.value, this.priority);
   972     this.computed = [];
   973     for (let i = 0, n = dummyStyle.length; i < n; i++) {
   974       let prop = dummyStyle.item(i);
   975       this.computed.push({
   976         textProp: this,
   977         name: prop,
   978         value: dummyStyle.getPropertyValue(prop),
   979         priority: dummyStyle.getPropertyPriority(prop),
   980       });
   981     }
   982   },
   984   /**
   985    * Set all the values from another TextProperty instance into
   986    * this TextProperty instance.
   987    *
   988    * @param {TextProperty} aOther
   989    *        The other TextProperty instance.
   990    */
   991   set: function(aOther) {
   992     let changed = false;
   993     for (let item of ["name", "value", "priority", "enabled"]) {
   994       if (this[item] != aOther[item]) {
   995         this[item] = aOther[item];
   996         changed = true;
   997       }
   998     }
  1000     if (changed) {
  1001       this.updateEditor();
  1003   },
  1005   setValue: function(aValue, aPriority) {
  1006     this.rule.setPropertyValue(this, aValue, aPriority);
  1007     this.updateEditor();
  1008   },
  1010   setName: function(aName) {
  1011     this.rule.setPropertyName(this, aName);
  1012     this.updateEditor();
  1013   },
  1015   setEnabled: function(aValue) {
  1016     this.rule.setPropertyEnabled(this, aValue);
  1017     this.updateEditor();
  1018   },
  1020   remove: function() {
  1021     this.rule.removeProperty(this);
  1023 };
  1026 /**
  1027  * View hierarchy mostly follows the model hierarchy.
  1029  * CssRuleView:
  1030  *   Owns an ElementStyle and creates a list of RuleEditors for its
  1031  *    Rules.
  1032  * RuleEditor:
  1033  *   Owns a Rule object and creates a list of TextPropertyEditors
  1034  *     for its TextProperties.
  1035  *   Manages creation of new text properties.
  1036  * TextPropertyEditor:
  1037  *   Owns a TextProperty object.
  1038  *   Manages changes to the TextProperty.
  1039  *   Can be expanded to display computed properties.
  1040  *   Can mark a property disabled or enabled.
  1041  */
  1043 /**
  1044  * CssRuleView is a view of the style rules and declarations that
  1045  * apply to a given element.  After construction, the 'element'
  1046  * property will be available with the user interface.
  1048  * @param {Inspector} aInspector
  1049  * @param {Document} aDoc
  1050  *        The document that will contain the rule view.
  1051  * @param {object} aStore
  1052  *        The CSS rule view can use this object to store metadata
  1053  *        that might outlast the rule view, particularly the current
  1054  *        set of disabled properties.
  1055  * @param {PageStyleFront} aPageStyle
  1056  *        The PageStyleFront for communicating with the remote server.
  1057  * @constructor
  1058  */
  1059 function CssRuleView(aInspector, aDoc, aStore, aPageStyle) {
  1060   this.inspector = aInspector;
  1061   this.doc = aDoc;
  1062   this.store = aStore || {};
  1063   this.pageStyle = aPageStyle;
  1064   this.element = this.doc.createElementNS(HTML_NS, "div");
  1065   this.element.className = "ruleview devtools-monospace";
  1066   this.element.flex = 1;
  1068   this._outputParser = new OutputParser();
  1070   this._buildContextMenu = this._buildContextMenu.bind(this);
  1071   this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
  1072   this._onSelectAll = this._onSelectAll.bind(this);
  1073   this._onCopy = this._onCopy.bind(this);
  1074   this._onToggleOrigSources = this._onToggleOrigSources.bind(this);
  1076   this.element.addEventListener("copy", this._onCopy);
  1078   this._handlePrefChange = this._handlePrefChange.bind(this);
  1079   gDevTools.on("pref-changed", this._handlePrefChange);
  1081   this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this);
  1082   this._prefObserver = new PrefObserver("devtools.");
  1083   this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
  1085   let options = {
  1086     autoSelect: true,
  1087     theme: "auto"
  1088   };
  1089   this.popup = new AutocompletePopup(aDoc.defaultView.parent.document, options);
  1091   // Create a tooltip for previewing things in the rule view (images for now)
  1092   this.previewTooltip = new Tooltip(this.inspector.panelDoc);
  1093   this.previewTooltip.startTogglingOnHover(this.element,
  1094     this._onTooltipTargetHover.bind(this));
  1096   // Also create a more complex tooltip for editing colors with the spectrum
  1097   // color picker
  1098   this.colorPicker = new SwatchColorPickerTooltip(this.inspector.panelDoc);
  1100   this._buildContextMenu();
  1101   this._showEmpty();
  1104 exports.CssRuleView = CssRuleView;
  1106 CssRuleView.prototype = {
  1107   // The element that we're inspecting.
  1108   _viewedElement: null,
  1110   /**
  1111    * Build the context menu.
  1112    */
  1113   _buildContextMenu: function() {
  1114     let doc = this.doc.defaultView.parent.document;
  1116     this._contextmenu = doc.createElementNS(XUL_NS, "menupopup");
  1117     this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate);
  1118     this._contextmenu.id = "rule-view-context-menu";
  1120     this.menuitemSelectAll = createMenuItem(this._contextmenu, {
  1121       label: "ruleView.contextmenu.selectAll",
  1122       accesskey: "ruleView.contextmenu.selectAll.accessKey",
  1123       command: this._onSelectAll
  1124     });
  1125     this.menuitemCopy = createMenuItem(this._contextmenu, {
  1126       label: "ruleView.contextmenu.copy",
  1127       accesskey: "ruleView.contextmenu.copy.accessKey",
  1128       command: this._onCopy
  1129     });
  1130     this.menuitemSources= createMenuItem(this._contextmenu, {
  1131       label: "ruleView.contextmenu.showOrigSources",
  1132       accesskey: "ruleView.contextmenu.showOrigSources.accessKey",
  1133       command: this._onToggleOrigSources
  1134     });
  1136     let popupset = doc.documentElement.querySelector("popupset");
  1137     if (!popupset) {
  1138       popupset = doc.createElementNS(XUL_NS, "popupset");
  1139       doc.documentElement.appendChild(popupset);
  1142     popupset.appendChild(this._contextmenu);
  1143   },
  1145   /**
  1146    * Which type of hover-tooltip should be shown for the given element?
  1147    * This depends on the element: does it contain an image URL, a CSS transform,
  1148    * a font-family, ...
  1149    * @param {DOMNode} el The element to test
  1150    * @return {String} The type of hover-tooltip
  1151    */
  1152   _getHoverTooltipTypeForTarget: function(el) {
  1153     let prop = el.textProperty;
  1155     // Test for css transform
  1156     if (prop && prop.name === "transform") {
  1157       return "transform";
  1160     // Test for image
  1161     let isUrl = el.classList.contains("theme-link") &&
  1162                 el.parentNode.classList.contains("ruleview-propertyvalue");
  1163     if (this.inspector.hasUrlToImageDataResolver && isUrl) {
  1164       return "image";
  1167     // Test for font-family
  1168     let propertyRoot = el.parentNode;
  1169     let propertyNameNode = propertyRoot.querySelector(".ruleview-propertyname");
  1170     if (!propertyNameNode) {
  1171       propertyRoot = propertyRoot.parentNode;
  1172       propertyNameNode = propertyRoot.querySelector(".ruleview-propertyname");
  1174     let propertyName;
  1175     if (propertyNameNode) {
  1176       propertyName = propertyNameNode.textContent;
  1178     if (propertyName === "font-family" && el.classList.contains("ruleview-propertyvalue")) {
  1179       return "font";
  1181   },
  1183   /**
  1184    * Executed by the tooltip when the pointer hovers over an element of the view.
  1185    * Used to decide whether the tooltip should be shown or not and to actually
  1186    * put content in it.
  1187    * Checks if the hovered target is a css value we support tooltips for.
  1188    * @param {DOMNode} target
  1189    * @return {Boolean|Promise} Either a boolean or a promise, used by the
  1190    * Tooltip class to wait for the content to be put in the tooltip and finally
  1191    * decide whether or not the tooltip should be shown.
  1192    */
  1193   _onTooltipTargetHover: function(target) {
  1194     let tooltipType = this._getHoverTooltipTypeForTarget(target);
  1195     if (!tooltipType) {
  1196       return false;
  1199     if (this.colorPicker.tooltip.isShown()) {
  1200       this.colorPicker.revert();
  1201       this.colorPicker.hide();
  1204     if (tooltipType === "transform") {
  1205       return this.previewTooltip.setCssTransformContent(target.textProperty.value,
  1206         this.pageStyle, this._viewedElement);
  1208     if (tooltipType === "image") {
  1209       let prop = target.parentNode.textProperty;
  1210       let dim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
  1211       let uri = CssLogic.getBackgroundImageUriFromProperty(prop.value, prop.rule.domRule.href);
  1212       return this.previewTooltip.setRelativeImageContent(uri, this.inspector.inspector, dim);
  1214     if (tooltipType === "font") {
  1215       this.previewTooltip.setFontFamilyContent(target.textContent);
  1216       return true;
  1219     return false;
  1220   },
  1222   /**
  1223    * Update the context menu. This means enabling or disabling menuitems as
  1224    * appropriate.
  1225    */
  1226   _contextMenuUpdate: function() {
  1227     let win = this.doc.defaultView;
  1229     // Copy selection.
  1230     let selection = win.getSelection();
  1231     let copy;
  1233     if (selection.toString()) {
  1234       // Panel text selected
  1235       copy = true;
  1236     } else if (selection.anchorNode) {
  1237       // input type="text"
  1238       let { selectionStart, selectionEnd } = this.doc.popupNode;
  1240       if (isFinite(selectionStart) && isFinite(selectionEnd) &&
  1241           selectionStart !== selectionEnd) {
  1242         copy = true;
  1244     } else {
  1245       // No text selected, disable copy.
  1246       copy = false;
  1249     this.menuitemCopy.disabled = !copy;
  1251     let label = "ruleView.contextmenu.showOrigSources";
  1252     if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
  1253       label = "ruleView.contextmenu.showCSSSources";
  1255     this.menuitemSources.setAttribute("label",
  1256                                       _strings.GetStringFromName(label));
  1258     let accessKey = label + ".accessKey";
  1259     this.menuitemSources.setAttribute("accesskey",
  1260                                       _strings.GetStringFromName(accessKey));
  1261   },
  1263   /**
  1264    * Select all text.
  1265    */
  1266   _onSelectAll: function() {
  1267     let win = this.doc.defaultView;
  1268     let selection = win.getSelection();
  1270     selection.selectAllChildren(this.doc.documentElement);
  1271   },
  1273   /**
  1274    * Copy selected text from the rule view.
  1276    * @param {Event} event
  1277    *        The event object.
  1278    */
  1279   _onCopy: function(event) {
  1280     try {
  1281       let target = event.target;
  1282       let text;
  1284       if (event.target.nodeName === "menuitem") {
  1285         target = this.doc.popupNode;
  1288       if (target.nodeName == "input") {
  1289         let start = Math.min(target.selectionStart, target.selectionEnd);
  1290         let end = Math.max(target.selectionStart, target.selectionEnd);
  1291         let count = end - start;
  1292         text = target.value.substr(start, count);
  1293       } else {
  1294         let win = this.doc.defaultView;
  1295         let selection = win.getSelection();
  1297         text = selection.toString();
  1299         // Remove any double newlines.
  1300         text = text.replace(/(\r?\n)\r?\n/g, "$1");
  1302         // Remove "inline"
  1303         let inline = _strings.GetStringFromName("rule.sourceInline");
  1304         let rx = new RegExp("^" + inline + "\\r?\\n?", "g");
  1305         text = text.replace(rx, "");
  1308       clipboardHelper.copyString(text, this.doc);
  1309       event.preventDefault();
  1310     } catch(e) {
  1311       console.error(e);
  1313   },
  1315   /**
  1316    *  Toggle the original sources pref.
  1317    */
  1318   _onToggleOrigSources: function() {
  1319     let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
  1320     Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
  1321   },
  1323   setPageStyle: function(aPageStyle) {
  1324     this.pageStyle = aPageStyle;
  1325   },
  1327   /**
  1328    * Return {bool} true if the rule view currently has an input editor visible.
  1329    */
  1330   get isEditing() {
  1331     return this.element.querySelectorAll(".styleinspector-propertyeditor").length > 0
  1332       || this.colorPicker.tooltip.isShown();
  1333   },
  1335   _handlePrefChange: function(event, data) {
  1336     if (data.pref == "devtools.defaultColorUnit") {
  1337       let element = this._viewedElement;
  1338       this._viewedElement = null;
  1339       this.highlight(element);
  1341   },
  1343   _onSourcePrefChanged: function() {
  1344     if (this.menuitemSources) {
  1345       let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
  1346       this.menuitemSources.setAttribute("checked", isEnabled);
  1349     // update text of source links
  1350     for (let rule of this._elementStyle.rules) {
  1351       if (rule.editor) {
  1352         rule.editor.updateSourceLink();
  1355   },
  1357   destroy: function() {
  1358     this.clear();
  1360     gDummyPromise = null;
  1361     gDevTools.off("pref-changed", this._handlePrefChange);
  1363     this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
  1364     this._prefObserver.destroy();
  1366     this.element.removeEventListener("copy", this._onCopy);
  1367     delete this._onCopy;
  1369     delete this._outputParser;
  1371     // Remove context menu
  1372     if (this._contextmenu) {
  1373       // Destroy the Select All menuitem.
  1374       this.menuitemSelectAll.removeEventListener("command", this._onSelectAll);
  1375       this.menuitemSelectAll = null;
  1377       // Destroy the Copy menuitem.
  1378       this.menuitemCopy.removeEventListener("command", this._onCopy);
  1379       this.menuitemCopy = null;
  1381       this.menuitemSources.removeEventListener("command", this._onToggleOrigSources);
  1382       this.menuitemSources = null;
  1384       // Destroy the context menu.
  1385       this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate);
  1386       this._contextmenu.parentNode.removeChild(this._contextmenu);
  1387       this._contextmenu = null;
  1390     // We manage the popupNode ourselves so we also need to destroy it.
  1391     this.doc.popupNode = null;
  1393     this.previewTooltip.stopTogglingOnHover(this.element);
  1394     this.previewTooltip.destroy();
  1395     this.colorPicker.destroy();
  1397     if (this.element.parentNode) {
  1398       this.element.parentNode.removeChild(this.element);
  1401     if (this.elementStyle) {
  1402       this.elementStyle.destroy();
  1405     this.popup.destroy();
  1406   },
  1408   /**
  1409    * Update the highlighted element.
  1411    * @param {NodeActor} aElement
  1412    *        The node whose style rules we'll inspect.
  1413    */
  1414   highlight: function(aElement) {
  1415     if (this._viewedElement === aElement) {
  1416       return promise.resolve(undefined);
  1419     this.clear();
  1421     if (this._elementStyle) {
  1422       delete this._elementStyle;
  1425     this._viewedElement = aElement;
  1426     if (!this._viewedElement) {
  1427       this._showEmpty();
  1428       return promise.resolve(undefined);
  1431     this._elementStyle = new ElementStyle(aElement, this.store, this.pageStyle);
  1432     return this._elementStyle.init().then(() => {
  1433       return this._populate();
  1434     }).then(() => {
  1435       // A new node may already be selected, in which this._elementStyle will
  1436       // be null.
  1437       if (this._elementStyle) {
  1438         this._elementStyle.onChanged = () => {
  1439           this._changed();
  1440         };
  1442     }).then(null, console.error);
  1443   },
  1445   /**
  1446    * Update the rules for the currently highlighted element.
  1447    */
  1448   nodeChanged: function() {
  1449     // Ignore refreshes during editing or when no element is selected.
  1450     if (this.isEditing || !this._elementStyle) {
  1451       return;
  1454     this._clearRules();
  1456     // Repopulate the element style.
  1457     this._populate();
  1458   },
  1460   _populate: function() {
  1461     let elementStyle = this._elementStyle;
  1462     return this._elementStyle.populate().then(() => {
  1463       if (this._elementStyle != elementStyle) {
  1464         return;
  1466       this._createEditors();
  1468       // Notify anyone that cares that we refreshed.
  1469       var evt = this.doc.createEvent("Events");
  1470       evt.initEvent("CssRuleViewRefreshed", true, false);
  1471       this.element.dispatchEvent(evt);
  1472       return undefined;
  1473     }).then(null, promiseWarn);
  1474   },
  1476   /**
  1477    * Show the user that the rule view has no node selected.
  1478    */
  1479   _showEmpty: function() {
  1480     if (this.doc.getElementById("noResults") > 0) {
  1481       return;
  1484     createChild(this.element, "div", {
  1485       id: "noResults",
  1486       textContent: CssLogic.l10n("rule.empty")
  1487     });
  1488   },
  1490   /**
  1491    * Clear the rules.
  1492    */
  1493   _clearRules: function() {
  1494     while (this.element.hasChildNodes()) {
  1495       this.element.removeChild(this.element.lastChild);
  1497   },
  1499   /**
  1500    * Clear the rule view.
  1501    */
  1502   clear: function() {
  1503     this._clearRules();
  1504     this._viewedElement = null;
  1505     this._elementStyle = null;
  1507     this.previewTooltip.hide();
  1508     this.colorPicker.hide();
  1509   },
  1511   /**
  1512    * Called when the user has made changes to the ElementStyle.
  1513    * Emits an event that clients can listen to.
  1514    */
  1515   _changed: function() {
  1516     var evt = this.doc.createEvent("Events");
  1517     evt.initEvent("CssRuleViewChanged", true, false);
  1518     this.element.dispatchEvent(evt);
  1519   },
  1521   /**
  1522    * Text for header that shows above rules for this element
  1523    */
  1524   get selectedElementLabel() {
  1525     if (this._selectedElementLabel) {
  1526       return this._selectedElementLabel;
  1528     this._selectedElementLabel = CssLogic.l10n("rule.selectedElement");
  1529     return this._selectedElementLabel;
  1530   },
  1532   /**
  1533    * Text for header that shows above rules for pseudo elements
  1534    */
  1535   get pseudoElementLabel() {
  1536     if (this._pseudoElementLabel) {
  1537       return this._pseudoElementLabel;
  1539     this._pseudoElementLabel = CssLogic.l10n("rule.pseudoElement");
  1540     return this._pseudoElementLabel;
  1541   },
  1543   togglePseudoElementVisibility: function(value) {
  1544     this._showPseudoElements = !!value;
  1545     let isOpen = this.showPseudoElements;
  1547     Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements",
  1548       isOpen);
  1550     this.element.classList.toggle("show-pseudo-elements", isOpen);
  1552     if (this.pseudoElementTwisty) {
  1553       if (isOpen) {
  1554         this.pseudoElementTwisty.setAttribute("open", "true");
  1556       else {
  1557         this.pseudoElementTwisty.removeAttribute("open");
  1560   },
  1562   get showPseudoElements() {
  1563     if (this._showPseudoElements === undefined) {
  1564       this._showPseudoElements =
  1565         Services.prefs.getBoolPref("devtools.inspector.show_pseudo_elements");
  1567     return this._showPseudoElements;
  1568   },
  1570   _getRuleViewHeaderClassName: function(isPseudo) {
  1571     let baseClassName = "theme-gutter ruleview-header";
  1572     return isPseudo ? baseClassName + " ruleview-expandable-header" : baseClassName;
  1573   },
  1575   /**
  1576    * Creates editor UI for each of the rules in _elementStyle.
  1577    */
  1578   _createEditors: function() {
  1579     // Run through the current list of rules, attaching
  1580     // their editors in order.  Create editors if needed.
  1581     let lastInheritedSource = "";
  1582     let seenPseudoElement = false;
  1583     let seenNormalElement = false;
  1585     for (let rule of this._elementStyle.rules) {
  1586       if (rule.domRule.system) {
  1587         continue;
  1590       // Only print header for this element if there are pseudo elements
  1591       if (seenPseudoElement && !seenNormalElement && !rule.pseudoElement) {
  1592         seenNormalElement = true;
  1593         let div = this.doc.createElementNS(HTML_NS, "div");
  1594         div.className = this._getRuleViewHeaderClassName();
  1595         div.textContent = this.selectedElementLabel;
  1596         this.element.appendChild(div);
  1599       let inheritedSource = rule.inheritedSource;
  1600       if (inheritedSource != lastInheritedSource) {
  1601         let div = this.doc.createElementNS(HTML_NS, "div");
  1602         div.className = this._getRuleViewHeaderClassName();
  1603         div.textContent = inheritedSource;
  1604         lastInheritedSource = inheritedSource;
  1605         this.element.appendChild(div);
  1608       if (!seenPseudoElement && rule.pseudoElement) {
  1609         seenPseudoElement = true;
  1611         let div = this.doc.createElementNS(HTML_NS, "div");
  1612         div.className = this._getRuleViewHeaderClassName(true);
  1613         div.textContent = this.pseudoElementLabel;
  1614         div.addEventListener("dblclick", () => {
  1615           this.togglePseudoElementVisibility(!this.showPseudoElements);
  1616         }, false);
  1618         let twisty = this.pseudoElementTwisty =
  1619           this.doc.createElementNS(HTML_NS, "span");
  1620         twisty.className = "ruleview-expander theme-twisty";
  1621         twisty.addEventListener("click", () => {
  1622           this.togglePseudoElementVisibility(!this.showPseudoElements);
  1623         }, false);
  1625         div.insertBefore(twisty, div.firstChild);
  1626         this.element.appendChild(div);
  1629       if (!rule.editor) {
  1630         rule.editor = new RuleEditor(this, rule);
  1633       this.element.appendChild(rule.editor.element);
  1636     this.togglePseudoElementVisibility(this.showPseudoElements);
  1638 };
  1640 /**
  1641  * Create a RuleEditor.
  1643  * @param {CssRuleView} aRuleView
  1644  *        The CssRuleView containg the document holding this rule editor.
  1645  * @param {Rule} aRule
  1646  *        The Rule object we're editing.
  1647  * @constructor
  1648  */
  1649 function RuleEditor(aRuleView, aRule) {
  1650   this.ruleView = aRuleView;
  1651   this.doc = this.ruleView.doc;
  1652   this.rule = aRule;
  1654   this._onNewProperty = this._onNewProperty.bind(this);
  1655   this._newPropertyDestroy = this._newPropertyDestroy.bind(this);
  1657   this._create();
  1660 RuleEditor.prototype = {
  1661   _create: function() {
  1662     this.element = this.doc.createElementNS(HTML_NS, "div");
  1663     this.element.className = "ruleview-rule theme-separator";
  1664     this.element._ruleEditor = this;
  1665     if (this.rule.pseudoElement) {
  1666       this.element.classList.add("ruleview-rule-pseudo-element");
  1669     // Give a relative position for the inplace editor's measurement
  1670     // span to be placed absolutely against.
  1671     this.element.style.position = "relative";
  1673     // Add the source link.
  1674     let source = createChild(this.element, "div", {
  1675       class: "ruleview-rule-source theme-link"
  1676     });
  1677     source.addEventListener("click", function() {
  1678       let rule = this.rule.domRule;
  1679       let evt = this.doc.createEvent("CustomEvent");
  1680       evt.initCustomEvent("CssRuleViewCSSLinkClicked", true, false, {
  1681         rule: rule,
  1682       });
  1683       this.element.dispatchEvent(evt);
  1684     }.bind(this));
  1685     let sourceLabel = this.doc.createElementNS(XUL_NS, "label");
  1686     sourceLabel.setAttribute("crop", "center");
  1687     sourceLabel.classList.add("source-link-label");
  1688     source.appendChild(sourceLabel);
  1690     this.updateSourceLink();
  1692     let code = createChild(this.element, "div", {
  1693       class: "ruleview-code"
  1694     });
  1696     let header = createChild(code, "div", {});
  1698     this.selectorText = createChild(header, "span", {
  1699       class: "ruleview-selector theme-fg-color3"
  1700     });
  1702     this.openBrace = createChild(header, "span", {
  1703       class: "ruleview-ruleopen",
  1704       textContent: " {"
  1705     });
  1707     code.addEventListener("click", function() {
  1708       let selection = this.doc.defaultView.getSelection();
  1709       if (selection.isCollapsed) {
  1710         this.newProperty();
  1712     }.bind(this), false);
  1714     this.element.addEventListener("mousedown", function() {
  1715       this.doc.defaultView.focus();
  1716     }.bind(this), false);
  1718     this.element.addEventListener("contextmenu", event => {
  1719       try {
  1720         // In the sidebar we do not have this.doc.popupNode so we need to save
  1721         // the node ourselves.
  1722         this.doc.popupNode = event.explicitOriginalTarget;
  1723         let win = this.doc.defaultView;
  1724         win.focus();
  1726         this.ruleView._contextmenu.openPopupAtScreen(
  1727           event.screenX, event.screenY, true);
  1729       } catch(e) {
  1730         console.error(e);
  1732     }, false);
  1734     this.propertyList = createChild(code, "ul", {
  1735       class: "ruleview-propertylist"
  1736     });
  1738     this.populate();
  1740     this.closeBrace = createChild(code, "div", {
  1741       class: "ruleview-ruleclose",
  1742       tabindex: "0",
  1743       textContent: "}"
  1744     });
  1746     // Create a property editor when the close brace is clicked.
  1747     editableItem({ element: this.closeBrace }, (aElement) => {
  1748       this.newProperty();
  1749     });
  1750   },
  1752   updateSourceLink: function RuleEditor_updateSourceLink()
  1754     let sourceLabel = this.element.querySelector(".source-link-label");
  1755     sourceLabel.setAttribute("value", this.rule.title);
  1757     let sourceHref = (this.rule.sheet && this.rule.sheet.href) ?
  1758       this.rule.sheet.href : this.rule.title;
  1760     sourceLabel.setAttribute("tooltiptext", sourceHref);
  1762     let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
  1763     if (showOrig && this.rule.domRule.type != ELEMENT_STYLE) {
  1764       this.rule.getOriginalSourceStrings().then((strings) => {
  1765         sourceLabel.setAttribute("value", strings.short);
  1766         sourceLabel.setAttribute("tooltiptext", strings.full);
  1767       })
  1769   },
  1771   /**
  1772    * Update the rule editor with the contents of the rule.
  1773    */
  1774   populate: function() {
  1775     // Clear out existing viewers.
  1776     while (this.selectorText.hasChildNodes()) {
  1777       this.selectorText.removeChild(this.selectorText.lastChild);
  1780     // If selector text comes from a css rule, highlight selectors that
  1781     // actually match.  For custom selector text (such as for the 'element'
  1782     // style, just show the text directly.
  1783     if (this.rule.domRule.type === ELEMENT_STYLE) {
  1784       this.selectorText.textContent = this.rule.selectorText;
  1785     } else {
  1786       this.rule.domRule.selectors.forEach((selector, i) => {
  1787         if (i != 0) {
  1788           createChild(this.selectorText, "span", {
  1789             class: "ruleview-selector-separator",
  1790             textContent: ", "
  1791           });
  1793         let cls;
  1794         if (this.rule.matchedSelectors.indexOf(selector) > -1) {
  1795           cls = "ruleview-selector-matched";
  1796         } else {
  1797           cls = "ruleview-selector-unmatched";
  1799         createChild(this.selectorText, "span", {
  1800           class: cls,
  1801           textContent: selector
  1802         });
  1803       });
  1806     for (let prop of this.rule.textProps) {
  1807       if (!prop.editor) {
  1808         let editor = new TextPropertyEditor(this, prop);
  1809         this.propertyList.appendChild(editor.element);
  1812   },
  1814   /**
  1815    * Programatically add a new property to the rule.
  1817    * @param {string} aName
  1818    *        Property name.
  1819    * @param {string} aValue
  1820    *        Property value.
  1821    * @param {string} aPriority
  1822    *        Property priority.
  1823    * @param {TextProperty} aSiblingProp
  1824    *        Optional, property next to which the new property will be added.
  1825    * @return {TextProperty}
  1826    *        The new property
  1827    */
  1828   addProperty: function(aName, aValue, aPriority, aSiblingProp) {
  1829     let prop = this.rule.createProperty(aName, aValue, aPriority, aSiblingProp);
  1830     let index = this.rule.textProps.indexOf(prop);
  1831     let editor = new TextPropertyEditor(this, prop);
  1833     // Insert this node before the DOM node that is currently at its new index
  1834     // in the property list.  There is currently one less node in the DOM than
  1835     // in the property list, so this causes it to appear after aSiblingProp.
  1836     // If there is no node at its index, as is the case where this is the last
  1837     // node being inserted, then this behaves as appendChild.
  1838     this.propertyList.insertBefore(editor.element,
  1839       this.propertyList.children[index]);
  1841     return prop;
  1842   },
  1844   /**
  1845    * Programatically add a list of new properties to the rule.  Focus the UI
  1846    * to the proper location after adding (either focus the value on the
  1847    * last property if it is empty, or create a new property and focus it).
  1849    * @param {Array} aProperties
  1850    *        Array of properties, which are objects with this signature:
  1851    *        {
  1852    *          name: {string},
  1853    *          value: {string},
  1854    *          priority: {string}
  1855    *        }
  1856    * @param {TextProperty} aSiblingProp
  1857    *        Optional, the property next to which all new props should be added.
  1858    */
  1859   addProperties: function(aProperties, aSiblingProp) {
  1860     if (!aProperties || !aProperties.length) {
  1861       return;
  1864     let lastProp = aSiblingProp;
  1865     for (let p of aProperties) {
  1866       lastProp = this.addProperty(p.name, p.value, p.priority, lastProp);
  1869     // Either focus on the last value if incomplete, or start a new one.
  1870     if (lastProp && lastProp.value.trim() === "") {
  1871       lastProp.editor.valueSpan.click();
  1872     } else {
  1873       this.newProperty();
  1875   },
  1877   /**
  1878    * Create a text input for a property name.  If a non-empty property
  1879    * name is given, we'll create a real TextProperty and add it to the
  1880    * rule.
  1881    */
  1882   newProperty: function() {
  1883     // If we're already creating a new property, ignore this.
  1884     if (!this.closeBrace.hasAttribute("tabindex")) {
  1885       return;
  1888     // While we're editing a new property, it doesn't make sense to
  1889     // start a second new property editor, so disable focusing the
  1890     // close brace for now.
  1891     this.closeBrace.removeAttribute("tabindex");
  1893     this.newPropItem = createChild(this.propertyList, "li", {
  1894       class: "ruleview-property ruleview-newproperty",
  1895     });
  1897     this.newPropSpan = createChild(this.newPropItem, "span", {
  1898       class: "ruleview-propertyname",
  1899       tabindex: "0"
  1900     });
  1902     this.multipleAddedProperties = null;
  1904     this.editor = new InplaceEditor({
  1905       element: this.newPropSpan,
  1906       done: this._onNewProperty,
  1907       destroy: this._newPropertyDestroy,
  1908       advanceChars: ":",
  1909       contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
  1910       popup: this.ruleView.popup
  1911     });
  1913     // Auto-close the input if multiple rules get pasted into new property.
  1914     this.editor.input.addEventListener("paste",
  1915       blurOnMultipleProperties, false);
  1916   },
  1918   /**
  1919    * Called when the new property input has been dismissed.
  1921    * @param {string} aValue
  1922    *        The value in the editor.
  1923    * @param {bool} aCommit
  1924    *        True if the value should be committed.
  1925    */
  1926   _onNewProperty: function(aValue, aCommit) {
  1927     if (!aValue || !aCommit) {
  1928       return;
  1931     // parseDeclarations allows for name-less declarations, but in the present
  1932     // case, we're creating a new declaration, it doesn't make sense to accept
  1933     // these entries
  1934     this.multipleAddedProperties = parseDeclarations(aValue).filter(d => d.name);
  1936     // Blur the editor field now and deal with adding declarations later when
  1937     // the field gets destroyed (see _newPropertyDestroy)
  1938     this.editor.input.blur();
  1939   },
  1941   /**
  1942    * Called when the new property editor is destroyed.
  1943    * This is where the properties (type TextProperty) are actually being
  1944    * added, since we want to wait until after the inplace editor `destroy`
  1945    * event has been fired to keep consistent UI state.
  1946    */
  1947   _newPropertyDestroy: function() {
  1948     // We're done, make the close brace focusable again.
  1949     this.closeBrace.setAttribute("tabindex", "0");
  1951     this.propertyList.removeChild(this.newPropItem);
  1952     delete this.newPropItem;
  1953     delete this.newPropSpan;
  1955     // If properties were added, we want to focus the proper element.
  1956     // If the last new property has no value, focus the value on it.
  1957     // Otherwise, start a new property and focus that field.
  1958     if (this.multipleAddedProperties && this.multipleAddedProperties.length) {
  1959       this.addProperties(this.multipleAddedProperties);
  1962 };
  1964 /**
  1965  * Create a TextPropertyEditor.
  1967  * @param {RuleEditor} aRuleEditor
  1968  *        The rule editor that owns this TextPropertyEditor.
  1969  * @param {TextProperty} aProperty
  1970  *        The text property to edit.
  1971  * @constructor
  1972  */
  1973 function TextPropertyEditor(aRuleEditor, aProperty) {
  1974   this.ruleEditor = aRuleEditor;
  1975   this.doc = this.ruleEditor.doc;
  1976   this.popup = this.ruleEditor.ruleView.popup;
  1977   this.prop = aProperty;
  1978   this.prop.editor = this;
  1979   this.browserWindow = this.doc.defaultView.top;
  1980   this.removeOnRevert = this.prop.value === "";
  1982   this._onEnableClicked = this._onEnableClicked.bind(this);
  1983   this._onExpandClicked = this._onExpandClicked.bind(this);
  1984   this._onStartEditing = this._onStartEditing.bind(this);
  1985   this._onNameDone = this._onNameDone.bind(this);
  1986   this._onValueDone = this._onValueDone.bind(this);
  1987   this._onValidate = throttle(this._previewValue, 10, this);
  1988   this.update = this.update.bind(this);
  1990   this._create();
  1991   this.update();
  1994 TextPropertyEditor.prototype = {
  1995   /**
  1996    * Boolean indicating if the name or value is being currently edited.
  1997    */
  1998   get editing() {
  1999     return !!(this.nameSpan.inplaceEditor || this.valueSpan.inplaceEditor ||
  2000       this.ruleEditor.ruleView.colorPicker.tooltip.isShown() ||
  2001       this.ruleEditor.ruleView.colorPicker.eyedropperOpen) ||
  2002       this.popup.isOpen;
  2003   },
  2005   /**
  2006    * Create the property editor's DOM.
  2007    */
  2008   _create: function() {
  2009     this.element = this.doc.createElementNS(HTML_NS, "li");
  2010     this.element.classList.add("ruleview-property");
  2012     // The enable checkbox will disable or enable the rule.
  2013     this.enable = createChild(this.element, "div", {
  2014       class: "ruleview-enableproperty theme-checkbox",
  2015       tabindex: "-1"
  2016     });
  2017     this.enable.addEventListener("click", this._onEnableClicked, true);
  2019     // Click to expand the computed properties of the text property.
  2020     this.expander = createChild(this.element, "span", {
  2021       class: "ruleview-expander theme-twisty"
  2022     });
  2023     this.expander.addEventListener("click", this._onExpandClicked, true);
  2025     this.nameContainer = createChild(this.element, "span", {
  2026       class: "ruleview-namecontainer"
  2027     });
  2028     this.nameContainer.addEventListener("click", (aEvent) => {
  2029       // Clicks within the name shouldn't propagate any further.
  2030       aEvent.stopPropagation();
  2031       if (aEvent.target === propertyContainer) {
  2032         this.nameSpan.click();
  2034     }, false);
  2036     // Property name, editable when focused.  Property name
  2037     // is committed when the editor is unfocused.
  2038     this.nameSpan = createChild(this.nameContainer, "span", {
  2039       class: "ruleview-propertyname theme-fg-color5",
  2040       tabindex: "0",
  2041     });
  2043     editableField({
  2044       start: this._onStartEditing,
  2045       element: this.nameSpan,
  2046       done: this._onNameDone,
  2047       destroy: this.update,
  2048       advanceChars: ':',
  2049       contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
  2050       popup: this.popup
  2051     });
  2053     // Auto blur name field on multiple CSS rules get pasted in.
  2054     this.nameContainer.addEventListener("paste",
  2055       blurOnMultipleProperties, false);
  2057     appendText(this.nameContainer, ": ");
  2059     // Create a span that will hold the property and semicolon.
  2060     // Use this span to create a slightly larger click target
  2061     // for the value.
  2062     let propertyContainer = createChild(this.element, "span", {
  2063       class: "ruleview-propertycontainer"
  2064     });
  2066     propertyContainer.addEventListener("click", (aEvent) => {
  2067       // Clicks within the value shouldn't propagate any further.
  2068       aEvent.stopPropagation();
  2070       if (aEvent.target === propertyContainer) {
  2071         this.valueSpan.click();
  2073     }, false);
  2075     // Property value, editable when focused.  Changes to the
  2076     // property value are applied as they are typed, and reverted
  2077     // if the user presses escape.
  2078     this.valueSpan = createChild(propertyContainer, "span", {
  2079       class: "ruleview-propertyvalue theme-fg-color1",
  2080       tabindex: "0",
  2081     });
  2083     this.valueSpan.addEventListener("click", (event) => {
  2084       let target = event.target;
  2086       if (target.nodeName === "a") {
  2087         event.stopPropagation();
  2088         event.preventDefault();
  2089         this.browserWindow.openUILinkIn(target.href, "tab");
  2091     }, false);
  2093     // Storing the TextProperty on the valuespan for easy access
  2094     // (for instance by the tooltip)
  2095     this.valueSpan.textProperty = this.prop;
  2097     // Save the initial value as the last committed value,
  2098     // for restoring after pressing escape.
  2099     this.committed = { name: this.prop.name,
  2100                        value: this.prop.value,
  2101                        priority: this.prop.priority };
  2103     appendText(propertyContainer, ";");
  2105     this.warning = createChild(this.element, "div", {
  2106       class: "ruleview-warning",
  2107       hidden: "",
  2108       title: CssLogic.l10n("rule.warning.title"),
  2109     });
  2111     // Holds the viewers for the computed properties.
  2112     // will be populated in |_updateComputed|.
  2113     this.computed = createChild(this.element, "ul", {
  2114       class: "ruleview-computedlist",
  2115     });
  2117     editableField({
  2118       start: this._onStartEditing,
  2119       element: this.valueSpan,
  2120       done: this._onValueDone,
  2121       destroy: this.update,
  2122       validate: this._onValidate,
  2123       advanceChars: ';',
  2124       contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
  2125       property: this.prop,
  2126       popup: this.popup
  2127     });
  2128   },
  2130   /**
  2131    * Get the path from which to resolve requests for this
  2132    * rule's stylesheet.
  2133    * @return {string} the stylesheet's href.
  2134    */
  2135   get sheetHref() {
  2136     let domRule = this.prop.rule.domRule;
  2137     if (domRule) {
  2138       return domRule.href || domRule.nodeHref;
  2140   },
  2142   /**
  2143    * Get the URI from which to resolve relative requests for
  2144    * this rule's stylesheet.
  2145    * @return {nsIURI} A URI based on the the stylesheet's href.
  2146    */
  2147   get sheetURI() {
  2148     if (this._sheetURI === undefined) {
  2149       if (this.sheetHref) {
  2150         this._sheetURI = IOService.newURI(this.sheetHref, null, null);
  2151       } else {
  2152         this._sheetURI = null;
  2156     return this._sheetURI;
  2157   },
  2159   /**
  2160    * Resolve a URI based on the rule stylesheet
  2161    * @param {string} relativePath the path to resolve
  2162    * @return {string} the resolved path.
  2163    */
  2164   resolveURI: function(relativePath) {
  2165     if (this.sheetURI) {
  2166       relativePath = this.sheetURI.resolve(relativePath);
  2168     return relativePath;
  2169   },
  2171   /**
  2172    * Check the property value to find an external resource (if any).
  2173    * @return {string} the URI in the property value, or null if there is no match.
  2174    */
  2175   getResourceURI: function() {
  2176     let val = this.prop.value;
  2177     let uriMatch = CSS_RESOURCE_RE.exec(val);
  2178     let uri = null;
  2180     if (uriMatch && uriMatch[1]) {
  2181       uri = uriMatch[1];
  2184     return uri;
  2185   },
  2187   /**
  2188    * Populate the span based on changes to the TextProperty.
  2189    */
  2190   update: function() {
  2191     if (this.prop.enabled) {
  2192       this.enable.style.removeProperty("visibility");
  2193       this.enable.setAttribute("checked", "");
  2194     } else {
  2195       this.enable.style.visibility = "visible";
  2196       this.enable.removeAttribute("checked");
  2199     this.warning.hidden = this.editing || this.isValid();
  2201     if ((this.prop.overridden || !this.prop.enabled) && !this.editing) {
  2202       this.element.classList.add("ruleview-overridden");
  2203     } else {
  2204       this.element.classList.remove("ruleview-overridden");
  2207     let name = this.prop.name;
  2208     this.nameSpan.textContent = name;
  2210     // Combine the property's value and priority into one string for
  2211     // the value.
  2212     let val = this.prop.value;
  2213     if (this.prop.priority) {
  2214       val += " !" + this.prop.priority;
  2217     let store = this.prop.rule.elementStyle.store;
  2218     let propDirty = store.userProperties.contains(this.prop.rule.style, name);
  2220     if (propDirty) {
  2221       this.element.setAttribute("dirty", "");
  2222     } else {
  2223       this.element.removeAttribute("dirty");
  2226     let swatchClass = "ruleview-colorswatch";
  2227     let outputParser = this.ruleEditor.ruleView._outputParser;
  2228     let frag = outputParser.parseCssProperty(name, val, {
  2229       colorSwatchClass: swatchClass,
  2230       colorClass: "ruleview-color",
  2231       defaultColorType: !propDirty,
  2232       urlClass: "theme-link",
  2233       baseURI: this.sheetURI
  2234     });
  2235     this.valueSpan.innerHTML = "";
  2236     this.valueSpan.appendChild(frag);
  2238     // Attach the color picker tooltip to the color swatches
  2239     this._swatchSpans = this.valueSpan.querySelectorAll("." + swatchClass);
  2240     for (let span of this._swatchSpans) {
  2241       // Capture the original declaration value to be able to revert later
  2242       let originalValue = this.valueSpan.textContent;
  2243       // Adding this swatch to the list of swatches our colorpicker knows about
  2244       this.ruleEditor.ruleView.colorPicker.addSwatch(span, {
  2245         onPreview: () => this._previewValue(this.valueSpan.textContent),
  2246         onCommit: () => this._applyNewValue(this.valueSpan.textContent),
  2247         onRevert: () => this._applyNewValue(originalValue)
  2248       });
  2251     // Populate the computed styles.
  2252     this._updateComputed();
  2253   },
  2255   _onStartEditing: function() {
  2256     this.element.classList.remove("ruleview-overridden");
  2257     this._previewValue(this.prop.value);
  2258   },
  2260   /**
  2261    * Populate the list of computed styles.
  2262    */
  2263   _updateComputed: function () {
  2264     // Clear out existing viewers.
  2265     while (this.computed.hasChildNodes()) {
  2266       this.computed.removeChild(this.computed.lastChild);
  2269     let showExpander = false;
  2270     for each (let computed in this.prop.computed) {
  2271       // Don't bother to duplicate information already
  2272       // shown in the text property.
  2273       if (computed.name === this.prop.name) {
  2274         continue;
  2277       showExpander = true;
  2279       let li = createChild(this.computed, "li", {
  2280         class: "ruleview-computed"
  2281       });
  2283       if (computed.overridden) {
  2284         li.classList.add("ruleview-overridden");
  2287       createChild(li, "span", {
  2288         class: "ruleview-propertyname theme-fg-color5",
  2289         textContent: computed.name
  2290       });
  2291       appendText(li, ": ");
  2293       let outputParser = this.ruleEditor.ruleView._outputParser;
  2294       let frag = outputParser.parseCssProperty(
  2295         computed.name, computed.value, {
  2296           colorSwatchClass: "ruleview-colorswatch",
  2297           urlClass: "theme-link",
  2298           baseURI: this.sheetURI
  2300       );
  2302       createChild(li, "span", {
  2303         class: "ruleview-propertyvalue theme-fg-color1",
  2304         child: frag
  2305       });
  2307       appendText(li, ";");
  2310     // Show or hide the expander as needed.
  2311     if (showExpander) {
  2312       this.expander.style.visibility = "visible";
  2313     } else {
  2314       this.expander.style.visibility = "hidden";
  2316   },
  2318   /**
  2319    * Handles clicks on the disabled property.
  2320    */
  2321   _onEnableClicked: function(aEvent) {
  2322     let checked = this.enable.hasAttribute("checked");
  2323     if (checked) {
  2324       this.enable.removeAttribute("checked");
  2325     } else {
  2326       this.enable.setAttribute("checked", "");
  2328     this.prop.setEnabled(!checked);
  2329     aEvent.stopPropagation();
  2330   },
  2332   /**
  2333    * Handles clicks on the computed property expander.
  2334    */
  2335   _onExpandClicked: function(aEvent) {
  2336     this.computed.classList.toggle("styleinspector-open");
  2337     if (this.computed.classList.contains("styleinspector-open")) {
  2338       this.expander.setAttribute("open", "true");
  2339     } else {
  2340       this.expander.removeAttribute("open");
  2342     aEvent.stopPropagation();
  2343   },
  2345   /**
  2346    * Called when the property name's inplace editor is closed.
  2347    * Ignores the change if the user pressed escape, otherwise
  2348    * commits it.
  2350    * @param {string} aValue
  2351    *        The value contained in the editor.
  2352    * @param {boolean} aCommit
  2353    *        True if the change should be applied.
  2354    */
  2355   _onNameDone: function(aValue, aCommit) {
  2356     if (aCommit) {
  2357       // Unlike the value editor, if a name is empty the entire property
  2358       // should always be removed.
  2359       if (aValue.trim() === "") {
  2360         this.remove();
  2361       } else {
  2362         // Adding multiple rules inside of name field overwrites the current
  2363         // property with the first, then adds any more onto the property list.
  2364         let properties = parseDeclarations(aValue);
  2366         if (properties.length) {
  2367           this.prop.setName(properties[0].name);
  2368           if (properties.length > 1) {
  2369             this.prop.setValue(properties[0].value, properties[0].priority);
  2370             this.ruleEditor.addProperties(properties.slice(1), this.prop);
  2375   },
  2377   /**
  2378    * Remove property from style and the editors from DOM.
  2379    * Begin editing next available property.
  2380    */
  2381   remove: function() {
  2382     if (this._swatchSpans && this._swatchSpans.length) {
  2383       for (let span of this._swatchSpans) {
  2384         this.ruleEditor.ruleView.colorPicker.removeSwatch(span);
  2388     this.element.parentNode.removeChild(this.element);
  2389     this.ruleEditor.rule.editClosestTextProperty(this.prop);
  2390     this.valueSpan.textProperty = null;
  2391     this.prop.remove();
  2392   },
  2394   /**
  2395    * Called when a value editor closes.  If the user pressed escape,
  2396    * revert to the value this property had before editing.
  2398    * @param {string} aValue
  2399    *        The value contained in the editor.
  2400    * @param {bool} aCommit
  2401    *        True if the change should be applied.
  2402    */
  2403    _onValueDone: function(aValue, aCommit) {
  2404     if (!aCommit) {
  2405        // A new property should be removed when escape is pressed.
  2406        if (this.removeOnRevert) {
  2407          this.remove();
  2408        } else {
  2409          this.prop.setValue(this.committed.value, this.committed.priority);
  2411        return;
  2414     let {propertiesToAdd,firstValue} = this._getValueAndExtraProperties(aValue);
  2416     // First, set this property value (common case, only modified a property)
  2417     let val = parseSingleValue(firstValue);
  2418     this.prop.setValue(val.value, val.priority);
  2419     this.removeOnRevert = false;
  2420     this.committed.value = this.prop.value;
  2421     this.committed.priority = this.prop.priority;
  2423     // If needed, add any new properties after this.prop.
  2424     this.ruleEditor.addProperties(propertiesToAdd, this.prop);
  2426     // If the name or value is not actively being edited, and the value is
  2427     // empty, then remove the whole property.
  2428     // A timeout is used here to accurately check the state, since the inplace
  2429     // editor `done` and `destroy` events fire before the next editor
  2430     // is focused.
  2431     if (val.value.trim() === "") {
  2432       setTimeout(() => {
  2433         if (!this.editing) {
  2434           this.remove();
  2436       }, 0);
  2438   },
  2440   /**
  2441    * Parse a value string and break it into pieces, starting with the
  2442    * first value, and into an array of additional properties (if any).
  2444    * Example: Calling with "red; width: 100px" would return
  2445    * { firstValue: "red", propertiesToAdd: [{ name: "width", value: "100px" }] }
  2447    * @param {string} aValue
  2448    *        The string to parse
  2449    * @return {object} An object with the following properties:
  2450    *        firstValue: A string containing a simple value, like
  2451    *                    "red" or "100px!important"
  2452    *        propertiesToAdd: An array with additional properties, following the
  2453    *                         parseDeclarations format of {name,value,priority}
  2454    */
  2455   _getValueAndExtraProperties: function(aValue) {
  2456     // The inplace editor will prevent manual typing of multiple properties,
  2457     // but we need to deal with the case during a paste event.
  2458     // Adding multiple properties inside of value editor sets value with the
  2459     // first, then adds any more onto the property list (below this property).
  2460     let firstValue = aValue;
  2461     let propertiesToAdd = [];
  2463     let properties = parseDeclarations(aValue);
  2465     // Check to see if the input string can be parsed as multiple properties
  2466     if (properties.length) {
  2467       // Get the first property value (if any), and any remaining properties (if any)
  2468       if (!properties[0].name && properties[0].value) {
  2469         firstValue = properties[0].value;
  2470         propertiesToAdd = properties.slice(1);
  2472       // In some cases, the value could be a property:value pair itself.
  2473       // Join them as one value string and append potentially following properties
  2474       else if (properties[0].name && properties[0].value) {
  2475         firstValue = properties[0].name + ": " + properties[0].value;
  2476         propertiesToAdd = properties.slice(1);
  2480     return {
  2481       propertiesToAdd: propertiesToAdd,
  2482       firstValue: firstValue
  2483     };
  2484   },
  2486   _applyNewValue: function(aValue) {
  2487     let val = parseSingleValue(aValue);
  2489     this.prop.setValue(val.value, val.priority);
  2490     this.removeOnRevert = false;
  2491     this.committed.value = this.prop.value;
  2492     this.committed.priority = this.prop.priority;
  2493   },
  2495   /**
  2496    * Live preview this property, without committing changes.
  2497    * @param {string} aValue The value to set the current property to.
  2498    */
  2499   _previewValue: function(aValue) {
  2500     // Since function call is throttled, we need to make sure we are still editing
  2501     if (!this.editing) {
  2502       return;
  2505     let val = parseSingleValue(aValue);
  2506     this.ruleEditor.rule.previewPropertyValue(this.prop, val.value, val.priority);
  2507   },
  2509   /**
  2510    * Validate this property. Does it make sense for this value to be assigned
  2511    * to this property name? This does not apply the property value
  2513    * @param {string} [aValue]
  2514    *        The property value used for validation.
  2515    *        Defaults to the current value for this.prop
  2517    * @return {bool} true if the property value is valid, false otherwise.
  2518    */
  2519   isValid: function(aValue) {
  2520     let name = this.prop.name;
  2521     let value = typeof aValue == "undefined" ? this.prop.value : aValue;
  2522     let val = parseSingleValue(value);
  2524     let style = this.doc.createElementNS(HTML_NS, "div").style;
  2525     let prefs = Services.prefs;
  2527     // We toggle output of errors whilst the user is typing a property value.
  2528     let prefVal = prefs.getBoolPref("layout.css.report_errors");
  2529     prefs.setBoolPref("layout.css.report_errors", false);
  2531     let validValue = false;
  2532     try {
  2533       style.setProperty(name, val.value, val.priority);
  2534       validValue = style.getPropertyValue(name) !== "" || val.value === "";
  2535     } finally {
  2536       prefs.setBoolPref("layout.css.report_errors", prefVal);
  2538     return validValue;
  2540 };
  2542 /**
  2543  * Store of CSSStyleDeclarations mapped to properties that have been changed by
  2544  * the user.
  2545  */
  2546 function UserProperties() {
  2547   this.map = new Map();
  2550 UserProperties.prototype = {
  2551   /**
  2552    * Get a named property for a given CSSStyleDeclaration.
  2554    * @param {CSSStyleDeclaration} aStyle
  2555    *        The CSSStyleDeclaration against which the property is mapped.
  2556    * @param {string} aName
  2557    *        The name of the property to get.
  2558    * @param {string} aDefault
  2559    *        The value to return if the property is has been changed outside of
  2560    *        the rule view.
  2561    * @return {string}
  2562    *          The property value if it has previously been set by the user, null
  2563    *          otherwise.
  2564    */
  2565   getProperty: function(aStyle, aName, aDefault) {
  2566     let key = this.getKey(aStyle);
  2567     let entry = this.map.get(key, null);
  2569     if (entry && aName in entry) {
  2570       let item = entry[aName];
  2571       if (item != aDefault) {
  2572         delete entry[aName];
  2573         return aDefault;
  2575       return item;
  2577     return aDefault;
  2578   },
  2580   /**
  2581    * Set a named property for a given CSSStyleDeclaration.
  2583    * @param {CSSStyleDeclaration} aStyle
  2584    *        The CSSStyleDeclaration against which the property is to be mapped.
  2585    * @param {String} aName
  2586    *        The name of the property to set.
  2587    * @param {String} aUserValue
  2588    *        The value of the property to set.
  2589    */
  2590   setProperty: function(aStyle, aName, aUserValue) {
  2591     let key = this.getKey(aStyle);
  2592     let entry = this.map.get(key, null);
  2593     if (entry) {
  2594       entry[aName] = aUserValue;
  2595     } else {
  2596       let props = {};
  2597       props[aName] = aUserValue;
  2598       this.map.set(key, props);
  2600   },
  2602   /**
  2603    * Check whether a named property for a given CSSStyleDeclaration is stored.
  2605    * @param {CSSStyleDeclaration} aStyle
  2606    *        The CSSStyleDeclaration against which the property would be mapped.
  2607    * @param {String} aName
  2608    *        The name of the property to check.
  2609    */
  2610   contains: function(aStyle, aName) {
  2611     let key = this.getKey(aStyle);
  2612     let entry = this.map.get(key, null);
  2613     return !!entry && aName in entry;
  2614   },
  2616   getKey: function(aStyle) {
  2617     return aStyle.href + ":" + aStyle.line;
  2619 };
  2621 /**
  2622  * Helper functions
  2623  */
  2625 /**
  2626  * Create a child element with a set of attributes.
  2628  * @param {Element} aParent
  2629  *        The parent node.
  2630  * @param {string} aTag
  2631  *        The tag name.
  2632  * @param {object} aAttributes
  2633  *        A set of attributes to set on the node.
  2634  */
  2635 function createChild(aParent, aTag, aAttributes) {
  2636   let elt = aParent.ownerDocument.createElementNS(HTML_NS, aTag);
  2637   for (let attr in aAttributes) {
  2638     if (aAttributes.hasOwnProperty(attr)) {
  2639       if (attr === "textContent") {
  2640         elt.textContent = aAttributes[attr];
  2641       } else if(attr === "child") {
  2642         elt.appendChild(aAttributes[attr]);
  2643       } else {
  2644         elt.setAttribute(attr, aAttributes[attr]);
  2648   aParent.appendChild(elt);
  2649   return elt;
  2652 function createMenuItem(aMenu, aAttributes) {
  2653   let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem");
  2655   item.setAttribute("label", _strings.GetStringFromName(aAttributes.label));
  2656   item.setAttribute("accesskey", _strings.GetStringFromName(aAttributes.accesskey));
  2657   item.addEventListener("command", aAttributes.command);
  2659   aMenu.appendChild(item);
  2661   return item;
  2664 function setTimeout() {
  2665   let window = Services.appShell.hiddenDOMWindow;
  2666   return window.setTimeout.apply(window, arguments);
  2669 function clearTimeout() {
  2670   let window = Services.appShell.hiddenDOMWindow;
  2671   return window.clearTimeout.apply(window, arguments);
  2674 function throttle(func, wait, scope) {
  2675   var timer = null;
  2676   return function() {
  2677     if(timer) {
  2678       clearTimeout(timer);
  2680     var args = arguments;
  2681     timer = setTimeout(function() {
  2682       timer = null;
  2683       func.apply(scope, args);
  2684     }, wait);
  2685   };
  2688 /**
  2689  * Event handler that causes a blur on the target if the input has
  2690  * multiple CSS properties as the value.
  2691  */
  2692 function blurOnMultipleProperties(e) {
  2693   setTimeout(() => {
  2694     let props = parseDeclarations(e.target.value);
  2695     if (props.length > 1) {
  2696       e.target.blur();
  2698   }, 0);
  2701 /**
  2702  * Append a text node to an element.
  2703  */
  2704 function appendText(aParent, aText) {
  2705   aParent.appendChild(aParent.ownerDocument.createTextNode(aText));
  2708 XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
  2709   return Cc["@mozilla.org/widget/clipboardhelper;1"].
  2710     getService(Ci.nsIClipboardHelper);
  2711 });
  2713 XPCOMUtils.defineLazyGetter(this, "_strings", function() {
  2714   return Services.strings.createBundle(
  2715     "chrome://global/locale/devtools/styleinspector.properties");
  2716 });
  2718 XPCOMUtils.defineLazyGetter(this, "domUtils", function() {
  2719   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
  2720 });
  2722 loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup);

mercurial