michael@0: /* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {Cc, Ci, Cu} = require("chrome"); michael@0: const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); michael@0: const {CssLogic} = require("devtools/styleinspector/css-logic"); michael@0: const {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor"); michael@0: const {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles"); michael@0: const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); michael@0: const {Tooltip, SwatchColorPickerTooltip} = require("devtools/shared/widgets/Tooltip"); michael@0: const {OutputParser} = require("devtools/output-parser"); michael@0: const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils"); michael@0: const {parseSingleValue, parseDeclarations} = require("devtools/styleinspector/css-parsing-utils"); michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: const HTML_NS = "http://www.w3.org/1999/xhtml"; michael@0: const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; michael@0: michael@0: /** michael@0: * These regular expressions are adapted from firebug's css.js, and are michael@0: * used to parse CSSStyleDeclaration's cssText attribute. michael@0: */ michael@0: michael@0: // Used to split on css line separators michael@0: const CSS_LINE_RE = /(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g; michael@0: michael@0: // Used to parse a single property line. michael@0: const CSS_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*(?:! (important))?;?$/; michael@0: michael@0: // Used to parse an external resource from a property value michael@0: const CSS_RESOURCE_RE = /url\([\'\"]?(.*?)[\'\"]?\)/; michael@0: michael@0: const IOService = Cc["@mozilla.org/network/io-service;1"] michael@0: .getService(Ci.nsIIOService); michael@0: michael@0: function promiseWarn(err) { michael@0: console.error(err); michael@0: return promise.reject(err); michael@0: } michael@0: michael@0: /** michael@0: * To figure out how shorthand properties are interpreted by the michael@0: * engine, we will set properties on a dummy element and observe michael@0: * how their .style attribute reflects them as computed values. michael@0: * This function creates the document in which those dummy elements michael@0: * will be created. michael@0: */ michael@0: var gDummyPromise; michael@0: function createDummyDocument() { michael@0: if (gDummyPromise) { michael@0: return gDummyPromise; michael@0: } michael@0: const { getDocShell, create: makeFrame } = require("sdk/frame/utils"); michael@0: michael@0: let frame = makeFrame(Services.appShell.hiddenDOMWindow.document, { michael@0: nodeName: "iframe", michael@0: namespaceURI: "http://www.w3.org/1999/xhtml", michael@0: allowJavascript: false, michael@0: allowPlugins: false, michael@0: allowAuth: false michael@0: }); michael@0: let docShell = getDocShell(frame); michael@0: let eventTarget = docShell.chromeEventHandler; michael@0: docShell.createAboutBlankContentViewer(Cc["@mozilla.org/nullprincipal;1"].createInstance(Ci.nsIPrincipal)); michael@0: let window = docShell.contentViewer.DOMDocument.defaultView; michael@0: window.location = "data:text/html,"; michael@0: let deferred = promise.defer(); michael@0: eventTarget.addEventListener("DOMContentLoaded", function handler(event) { michael@0: eventTarget.removeEventListener("DOMContentLoaded", handler, false); michael@0: deferred.resolve(window.document); michael@0: frame.remove(); michael@0: }, false); michael@0: gDummyPromise = deferred.promise; michael@0: return gDummyPromise; michael@0: } michael@0: michael@0: /** michael@0: * Our model looks like this: michael@0: * michael@0: * ElementStyle: michael@0: * Responsible for keeping track of which properties are overridden. michael@0: * Maintains a list of Rule objects that apply to the element. michael@0: * Rule: michael@0: * Manages a single style declaration or rule. michael@0: * Responsible for applying changes to the properties in a rule. michael@0: * Maintains a list of TextProperty objects. michael@0: * TextProperty: michael@0: * Manages a single property from the cssText attribute of the michael@0: * relevant declaration. michael@0: * Maintains a list of computed properties that come from this michael@0: * property declaration. michael@0: * Changes to the TextProperty are sent to its related Rule for michael@0: * application. michael@0: */ michael@0: michael@0: /** michael@0: * ElementStyle maintains a list of Rule objects for a given element. michael@0: * michael@0: * @param {Element} aElement michael@0: * The element whose style we are viewing. michael@0: * @param {object} aStore michael@0: * The ElementStyle can use this object to store metadata michael@0: * that might outlast the rule view, particularly the current michael@0: * set of disabled properties. michael@0: * @param {PageStyleFront} aPageStyle michael@0: * Front for the page style actor that will be providing michael@0: * the style information. michael@0: * michael@0: * @constructor michael@0: */ michael@0: function ElementStyle(aElement, aStore, aPageStyle) { michael@0: this.element = aElement; michael@0: this.store = aStore || {}; michael@0: this.pageStyle = aPageStyle; michael@0: michael@0: // We don't want to overwrite this.store.userProperties so we only create it michael@0: // if it doesn't already exist. michael@0: if (!("userProperties" in this.store)) { michael@0: this.store.userProperties = new UserProperties(); michael@0: } michael@0: michael@0: if (!("disabled" in this.store)) { michael@0: this.store.disabled = new WeakMap(); michael@0: } michael@0: } michael@0: michael@0: // We're exporting _ElementStyle for unit tests. michael@0: exports._ElementStyle = ElementStyle; michael@0: michael@0: ElementStyle.prototype = { michael@0: // The element we're looking at. michael@0: element: null, michael@0: michael@0: // Empty, unconnected element of the same type as this node, used michael@0: // to figure out how shorthand properties will be parsed. michael@0: dummyElement: null, michael@0: michael@0: init: function() michael@0: { michael@0: // To figure out how shorthand properties are interpreted by the michael@0: // engine, we will set properties on a dummy element and observe michael@0: // how their .style attribute reflects them as computed values. michael@0: return this.dummyElementPromise = createDummyDocument().then(document => { michael@0: this.dummyElement = document.createElementNS(this.element.namespaceURI, michael@0: this.element.tagName); michael@0: document.documentElement.appendChild(this.dummyElement); michael@0: return this.dummyElement; michael@0: }).then(null, promiseWarn); michael@0: }, michael@0: michael@0: destroy: function() { michael@0: this.dummyElement = null; michael@0: this.dummyElementPromise.then(dummyElement => { michael@0: if (dummyElement.parentNode) { michael@0: dummyElement.parentNode.removeChild(dummyElement); michael@0: } michael@0: this.dummyElementPromise = null; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Called by the Rule object when it has been changed through the michael@0: * setProperty* methods. michael@0: */ michael@0: _changed: function() { michael@0: if (this.onChanged) { michael@0: this.onChanged(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Refresh the list of rules to be displayed for the active element. michael@0: * Upon completion, this.rules[] will hold a list of Rule objects. michael@0: * michael@0: * Returns a promise that will be resolved when the elementStyle is michael@0: * ready. michael@0: */ michael@0: populate: function() { michael@0: let populated = this.pageStyle.getApplied(this.element, { michael@0: inherited: true, michael@0: matchedSelectors: true michael@0: }).then(entries => { michael@0: // Make sure the dummy element has been created before continuing... michael@0: return this.dummyElementPromise.then(() => { michael@0: if (this.populated != populated) { michael@0: // Don't care anymore. michael@0: return promise.reject("unused"); michael@0: } michael@0: michael@0: // Store the current list of rules (if any) during the population michael@0: // process. They will be reused if possible. michael@0: this._refreshRules = this.rules; michael@0: michael@0: this.rules = []; michael@0: michael@0: for (let entry of entries) { michael@0: this._maybeAddRule(entry); michael@0: } michael@0: michael@0: // Mark overridden computed styles. michael@0: this.markOverriddenAll(); michael@0: michael@0: this._sortRulesForPseudoElement(); michael@0: michael@0: // We're done with the previous list of rules. michael@0: delete this._refreshRules; michael@0: michael@0: return null; michael@0: }); michael@0: }).then(null, promiseWarn); michael@0: this.populated = populated; michael@0: return this.populated; michael@0: }, michael@0: michael@0: /** michael@0: * Put pseudo elements in front of others. michael@0: */ michael@0: _sortRulesForPseudoElement: function() { michael@0: this.rules = this.rules.sort((a, b) => { michael@0: return (a.pseudoElement || "z") > (b.pseudoElement || "z"); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Add a rule if it's one we care about. Filters out duplicates and michael@0: * inherited styles with no inherited properties. michael@0: * michael@0: * @param {object} aOptions michael@0: * Options for creating the Rule, see the Rule constructor. michael@0: * michael@0: * @return {bool} true if we added the rule. michael@0: */ michael@0: _maybeAddRule: function(aOptions) { michael@0: // If we've already included this domRule (for example, when a michael@0: // common selector is inherited), ignore it. michael@0: if (aOptions.rule && michael@0: this.rules.some(function(rule) rule.domRule === aOptions.rule)) { michael@0: return false; michael@0: } michael@0: michael@0: if (aOptions.system) { michael@0: return false; michael@0: } michael@0: michael@0: let rule = null; michael@0: michael@0: // If we're refreshing and the rule previously existed, reuse the michael@0: // Rule object. michael@0: if (this._refreshRules) { michael@0: for (let r of this._refreshRules) { michael@0: if (r.matches(aOptions)) { michael@0: rule = r; michael@0: rule.refresh(aOptions); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // If this is a new rule, create its Rule object. michael@0: if (!rule) { michael@0: rule = new Rule(this, aOptions); michael@0: } michael@0: michael@0: // Ignore inherited rules with no properties. michael@0: if (aOptions.inherited && rule.textProps.length == 0) { michael@0: return false; michael@0: } michael@0: michael@0: this.rules.push(rule); michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Calls markOverridden with all supported pseudo elements michael@0: */ michael@0: markOverriddenAll: function() { michael@0: this.markOverridden(); michael@0: for (let pseudo of PSEUDO_ELEMENTS) { michael@0: this.markOverridden(pseudo); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Mark the properties listed in this.rules for a given pseudo element michael@0: * with an overridden flag if an earlier property overrides it. michael@0: * @param {string} pseudo michael@0: * Which pseudo element to flag as overridden. michael@0: * Empty string or undefined will default to no pseudo element. michael@0: */ michael@0: markOverridden: function(pseudo="") { michael@0: // Gather all the text properties applied by these rules, ordered michael@0: // from more- to less-specific. michael@0: let textProps = []; michael@0: for (let rule of this.rules) { michael@0: if (rule.pseudoElement == pseudo) { michael@0: textProps = textProps.concat(rule.textProps.slice(0).reverse()); michael@0: } michael@0: } michael@0: michael@0: // Gather all the computed properties applied by those text michael@0: // properties. michael@0: let computedProps = []; michael@0: for (let textProp of textProps) { michael@0: computedProps = computedProps.concat(textProp.computed); michael@0: } michael@0: michael@0: // Walk over the computed properties. As we see a property name michael@0: // for the first time, mark that property's name as taken by this michael@0: // property. michael@0: // michael@0: // If we come across a property whose name is already taken, check michael@0: // its priority against the property that was found first: michael@0: // michael@0: // If the new property is a higher priority, mark the old michael@0: // property overridden and mark the property name as taken by michael@0: // the new property. michael@0: // michael@0: // If the new property is a lower or equal priority, mark it as michael@0: // overridden. michael@0: // michael@0: // _overriddenDirty will be set on each prop, indicating whether its michael@0: // dirty status changed during this pass. michael@0: let taken = {}; michael@0: for (let computedProp of computedProps) { michael@0: let earlier = taken[computedProp.name]; michael@0: let overridden; michael@0: if (earlier && michael@0: computedProp.priority === "important" && michael@0: earlier.priority !== "important") { michael@0: // New property is higher priority. Mark the earlier property michael@0: // overridden (which will reverse its dirty state). michael@0: earlier._overriddenDirty = !earlier._overriddenDirty; michael@0: earlier.overridden = true; michael@0: overridden = false; michael@0: } else { michael@0: overridden = !!earlier; michael@0: } michael@0: michael@0: computedProp._overriddenDirty = (!!computedProp.overridden != overridden); michael@0: computedProp.overridden = overridden; michael@0: if (!computedProp.overridden && computedProp.textProp.enabled) { michael@0: taken[computedProp.name] = computedProp; michael@0: } michael@0: } michael@0: michael@0: // For each TextProperty, mark it overridden if all of its michael@0: // computed properties are marked overridden. Update the text michael@0: // property's associated editor, if any. This will clear the michael@0: // _overriddenDirty state on all computed properties. michael@0: for (let textProp of textProps) { michael@0: // _updatePropertyOverridden will return true if the michael@0: // overridden state has changed for the text property. michael@0: if (this._updatePropertyOverridden(textProp)) { michael@0: textProp.updateEditor(); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Mark a given TextProperty as overridden or not depending on the michael@0: * state of its computed properties. Clears the _overriddenDirty state michael@0: * on all computed properties. michael@0: * michael@0: * @param {TextProperty} aProp michael@0: * The text property to update. michael@0: * michael@0: * @return {bool} true if the TextProperty's overridden state (or any of its michael@0: * computed properties overridden state) changed. michael@0: */ michael@0: _updatePropertyOverridden: function(aProp) { michael@0: let overridden = true; michael@0: let dirty = false; michael@0: for each (let computedProp in aProp.computed) { michael@0: if (!computedProp.overridden) { michael@0: overridden = false; michael@0: } michael@0: dirty = computedProp._overriddenDirty || dirty; michael@0: delete computedProp._overriddenDirty; michael@0: } michael@0: michael@0: dirty = (!!aProp.overridden != overridden) || dirty; michael@0: aProp.overridden = overridden; michael@0: return dirty; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * A single style rule or declaration. michael@0: * michael@0: * @param {ElementStyle} aElementStyle michael@0: * The ElementStyle to which this rule belongs. michael@0: * @param {object} aOptions michael@0: * The information used to construct this rule. Properties include: michael@0: * rule: A StyleRuleActor michael@0: * inherited: An element this rule was inherited from. If omitted, michael@0: * the rule applies directly to the current element. michael@0: * @constructor michael@0: */ michael@0: function Rule(aElementStyle, aOptions) { michael@0: this.elementStyle = aElementStyle; michael@0: this.domRule = aOptions.rule || null; michael@0: this.style = aOptions.rule; michael@0: this.matchedSelectors = aOptions.matchedSelectors || []; michael@0: this.pseudoElement = aOptions.pseudoElement || ""; michael@0: michael@0: this.inherited = aOptions.inherited || null; michael@0: this._modificationDepth = 0; michael@0: michael@0: if (this.domRule) { michael@0: let parentRule = this.domRule.parentRule; michael@0: if (parentRule && parentRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE) { michael@0: this.mediaText = parentRule.mediaText; michael@0: } michael@0: } michael@0: michael@0: // Populate the text properties with the style's current cssText michael@0: // value, and add in any disabled properties from the store. michael@0: this.textProps = this._getTextProperties(); michael@0: this.textProps = this.textProps.concat(this._getDisabledProperties()); michael@0: } michael@0: michael@0: Rule.prototype = { michael@0: mediaText: "", michael@0: michael@0: get title() { michael@0: if (this._title) { michael@0: return this._title; michael@0: } michael@0: this._title = CssLogic.shortSource(this.sheet); michael@0: if (this.domRule.type !== ELEMENT_STYLE) { michael@0: this._title += ":" + this.ruleLine; michael@0: } michael@0: michael@0: this._title = this._title + (this.mediaText ? " @media " + this.mediaText : ""); michael@0: return this._title; michael@0: }, michael@0: michael@0: get inheritedSource() { michael@0: if (this._inheritedSource) { michael@0: return this._inheritedSource; michael@0: } michael@0: this._inheritedSource = ""; michael@0: if (this.inherited) { michael@0: let eltText = this.inherited.tagName.toLowerCase(); michael@0: if (this.inherited.id) { michael@0: eltText += "#" + this.inherited.id; michael@0: } michael@0: this._inheritedSource = michael@0: CssLogic._strings.formatStringFromName("rule.inheritedFrom", [eltText], 1); michael@0: } michael@0: return this._inheritedSource; michael@0: }, michael@0: michael@0: get selectorText() { michael@0: return this.domRule.selectors ? this.domRule.selectors.join(", ") : CssLogic.l10n("rule.sourceElement"); michael@0: }, michael@0: michael@0: /** michael@0: * The rule's stylesheet. michael@0: */ michael@0: get sheet() { michael@0: return this.domRule ? this.domRule.parentStyleSheet : null; michael@0: }, michael@0: michael@0: /** michael@0: * The rule's line within a stylesheet michael@0: */ michael@0: get ruleLine() { michael@0: return this.domRule ? this.domRule.line : null; michael@0: }, michael@0: michael@0: /** michael@0: * The rule's column within a stylesheet michael@0: */ michael@0: get ruleColumn() { michael@0: return this.domRule ? this.domRule.column : null; michael@0: }, michael@0: michael@0: /** michael@0: * Get display name for this rule based on the original source michael@0: * for this rule's style sheet. michael@0: * michael@0: * @return {Promise} michael@0: * Promise which resolves with location as an object containing michael@0: * both the full and short version of the source string. michael@0: */ michael@0: getOriginalSourceStrings: function() { michael@0: if (this._originalSourceStrings) { michael@0: return promise.resolve(this._originalSourceStrings); michael@0: } michael@0: return this.domRule.getOriginalLocation().then(({href, line}) => { michael@0: let sourceStrings = { michael@0: full: href + ":" + line, michael@0: short: CssLogic.shortSource({href: href}) + ":" + line michael@0: }; michael@0: michael@0: this._originalSourceStrings = sourceStrings; michael@0: return sourceStrings; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Returns true if the rule matches the creation options michael@0: * specified. michael@0: * michael@0: * @param {object} aOptions michael@0: * Creation options. See the Rule constructor for documentation. michael@0: */ michael@0: matches: function(aOptions) { michael@0: return this.style === aOptions.rule; michael@0: }, michael@0: michael@0: /** michael@0: * Create a new TextProperty to include in the rule. michael@0: * michael@0: * @param {string} aName michael@0: * The text property name (such as "background" or "border-top"). michael@0: * @param {string} aValue michael@0: * The property's value (not including priority). michael@0: * @param {string} aPriority michael@0: * The property's priority (either "important" or an empty string). michael@0: * @param {TextProperty} aSiblingProp michael@0: * Optional, property next to which the new property will be added. michael@0: */ michael@0: createProperty: function(aName, aValue, aPriority, aSiblingProp) { michael@0: let prop = new TextProperty(this, aName, aValue, aPriority); michael@0: michael@0: if (aSiblingProp) { michael@0: let ind = this.textProps.indexOf(aSiblingProp); michael@0: this.textProps.splice(ind + 1, 0, prop); michael@0: } michael@0: else { michael@0: this.textProps.push(prop); michael@0: } michael@0: michael@0: this.applyProperties(); michael@0: return prop; michael@0: }, michael@0: michael@0: /** michael@0: * Reapply all the properties in this rule, and update their michael@0: * computed styles. Store disabled properties in the element michael@0: * style's store. Will re-mark overridden properties. michael@0: * michael@0: * @param {string} [aName] michael@0: * A text property name (such as "background" or "border-top") used michael@0: * when calling from setPropertyValue & setPropertyName to signify michael@0: * that the property should be saved in store.userProperties. michael@0: */ michael@0: applyProperties: function(aModifications, aName) { michael@0: this.elementStyle.markOverriddenAll(); michael@0: michael@0: if (!aModifications) { michael@0: aModifications = this.style.startModifyingProperties(); michael@0: } michael@0: let disabledProps = []; michael@0: let store = this.elementStyle.store; michael@0: michael@0: for (let prop of this.textProps) { michael@0: if (!prop.enabled) { michael@0: disabledProps.push({ michael@0: name: prop.name, michael@0: value: prop.value, michael@0: priority: prop.priority michael@0: }); michael@0: continue; michael@0: } michael@0: if (prop.value.trim() === "") { michael@0: continue; michael@0: } michael@0: michael@0: aModifications.setProperty(prop.name, prop.value, prop.priority); michael@0: michael@0: prop.updateComputed(); michael@0: } michael@0: michael@0: // Store disabled properties in the disabled store. michael@0: let disabled = this.elementStyle.store.disabled; michael@0: if (disabledProps.length > 0) { michael@0: disabled.set(this.style, disabledProps); michael@0: } else { michael@0: disabled.delete(this.style); michael@0: } michael@0: michael@0: let promise = aModifications.apply().then(() => { michael@0: let cssProps = {}; michael@0: for (let cssProp of parseDeclarations(this.style.cssText)) { michael@0: cssProps[cssProp.name] = cssProp; michael@0: } michael@0: michael@0: for (let textProp of this.textProps) { michael@0: if (!textProp.enabled) { michael@0: continue; michael@0: } michael@0: let cssProp = cssProps[textProp.name]; michael@0: michael@0: if (!cssProp) { michael@0: cssProp = { michael@0: name: textProp.name, michael@0: value: "", michael@0: priority: "" michael@0: }; michael@0: } michael@0: michael@0: if (aName && textProp.name == aName) { michael@0: store.userProperties.setProperty( michael@0: this.style, michael@0: textProp.name, michael@0: textProp.value); michael@0: } michael@0: textProp.priority = cssProp.priority; michael@0: } michael@0: michael@0: this.elementStyle.markOverriddenAll(); michael@0: michael@0: if (promise === this._applyingModifications) { michael@0: this._applyingModifications = null; michael@0: } michael@0: michael@0: this.elementStyle._changed(); michael@0: }).then(null, promiseWarn); michael@0: michael@0: this._applyingModifications = promise; michael@0: return promise; michael@0: }, michael@0: michael@0: /** michael@0: * Renames a property. michael@0: * michael@0: * @param {TextProperty} aProperty michael@0: * The property to rename. michael@0: * @param {string} aName michael@0: * The new property name (such as "background" or "border-top"). michael@0: */ michael@0: setPropertyName: function(aProperty, aName) { michael@0: if (aName === aProperty.name) { michael@0: return; michael@0: } michael@0: let modifications = this.style.startModifyingProperties(); michael@0: modifications.removeProperty(aProperty.name); michael@0: aProperty.name = aName; michael@0: this.applyProperties(modifications, aName); michael@0: }, michael@0: michael@0: /** michael@0: * Sets the value and priority of a property, then reapply all properties. michael@0: * michael@0: * @param {TextProperty} aProperty michael@0: * The property to manipulate. michael@0: * @param {string} aValue michael@0: * The property's value (not including priority). michael@0: * @param {string} aPriority michael@0: * The property's priority (either "important" or an empty string). michael@0: */ michael@0: setPropertyValue: function(aProperty, aValue, aPriority) { michael@0: if (aValue === aProperty.value && aPriority === aProperty.priority) { michael@0: return; michael@0: } michael@0: michael@0: aProperty.value = aValue; michael@0: aProperty.priority = aPriority; michael@0: this.applyProperties(null, aProperty.name); michael@0: }, michael@0: michael@0: /** michael@0: * Just sets the value and priority of a property, in order to preview its michael@0: * effect on the content document. michael@0: * michael@0: * @param {TextProperty} aProperty michael@0: * The property which value will be previewed michael@0: * @param {String} aValue michael@0: * The value to be used for the preview michael@0: * @param {String} aPriority michael@0: * The property's priority (either "important" or an empty string). michael@0: */ michael@0: previewPropertyValue: function(aProperty, aValue, aPriority) { michael@0: let modifications = this.style.startModifyingProperties(); michael@0: modifications.setProperty(aProperty.name, aValue, aPriority); michael@0: modifications.apply(); michael@0: }, michael@0: michael@0: /** michael@0: * Disables or enables given TextProperty. michael@0: * michael@0: * @param {TextProperty} aProperty michael@0: * The property to enable/disable michael@0: * @param {Boolean} aValue michael@0: */ michael@0: setPropertyEnabled: function(aProperty, aValue) { michael@0: aProperty.enabled = !!aValue; michael@0: let modifications = this.style.startModifyingProperties(); michael@0: if (!aProperty.enabled) { michael@0: modifications.removeProperty(aProperty.name); michael@0: } michael@0: this.applyProperties(modifications); michael@0: }, michael@0: michael@0: /** michael@0: * Remove a given TextProperty from the rule and update the rule michael@0: * accordingly. michael@0: * michael@0: * @param {TextProperty} aProperty michael@0: * The property to be removed michael@0: */ michael@0: removeProperty: function(aProperty) { michael@0: this.textProps = this.textProps.filter(function(prop) prop != aProperty); michael@0: let modifications = this.style.startModifyingProperties(); michael@0: modifications.removeProperty(aProperty.name); michael@0: // Need to re-apply properties in case removing this TextProperty michael@0: // exposes another one. michael@0: this.applyProperties(modifications); michael@0: }, michael@0: michael@0: /** michael@0: * Get the list of TextProperties from the style. Needs michael@0: * to parse the style's cssText. michael@0: */ michael@0: _getTextProperties: function() { michael@0: let textProps = []; michael@0: let store = this.elementStyle.store; michael@0: let props = parseDeclarations(this.style.cssText); michael@0: for (let prop of props) { michael@0: let name = prop.name; michael@0: if (this.inherited && !domUtils.isInheritedProperty(name)) { michael@0: continue; michael@0: } michael@0: let value = store.userProperties.getProperty(this.style, name, prop.value); michael@0: let textProp = new TextProperty(this, name, value, prop.priority); michael@0: textProps.push(textProp); michael@0: } michael@0: michael@0: return textProps; michael@0: }, michael@0: michael@0: /** michael@0: * Return the list of disabled properties from the store for this rule. michael@0: */ michael@0: _getDisabledProperties: function() { michael@0: let store = this.elementStyle.store; michael@0: michael@0: // Include properties from the disabled property store, if any. michael@0: let disabledProps = store.disabled.get(this.style); michael@0: if (!disabledProps) { michael@0: return []; michael@0: } michael@0: michael@0: let textProps = []; michael@0: michael@0: for each (let prop in disabledProps) { michael@0: let value = store.userProperties.getProperty(this.style, prop.name, prop.value); michael@0: let textProp = new TextProperty(this, prop.name, value, prop.priority); michael@0: textProp.enabled = false; michael@0: textProps.push(textProp); michael@0: } michael@0: michael@0: return textProps; michael@0: }, michael@0: michael@0: /** michael@0: * Reread the current state of the rules and rebuild text michael@0: * properties as needed. michael@0: */ michael@0: refresh: function(aOptions) { michael@0: this.matchedSelectors = aOptions.matchedSelectors || []; michael@0: let newTextProps = this._getTextProperties(); michael@0: michael@0: // Update current properties for each property present on the style. michael@0: // This will mark any touched properties with _visited so we michael@0: // can detect properties that weren't touched (because they were michael@0: // removed from the style). michael@0: // Also keep track of properties that didn't exist in the current set michael@0: // of properties. michael@0: let brandNewProps = []; michael@0: for (let newProp of newTextProps) { michael@0: if (!this._updateTextProperty(newProp)) { michael@0: brandNewProps.push(newProp); michael@0: } michael@0: } michael@0: michael@0: // Refresh editors and disabled state for all the properties that michael@0: // were updated. michael@0: for (let prop of this.textProps) { michael@0: // Properties that weren't touched during the update michael@0: // process must no longer exist on the node. Mark them disabled. michael@0: if (!prop._visited) { michael@0: prop.enabled = false; michael@0: prop.updateEditor(); michael@0: } else { michael@0: delete prop._visited; michael@0: } michael@0: } michael@0: michael@0: // Add brand new properties. michael@0: this.textProps = this.textProps.concat(brandNewProps); michael@0: michael@0: // Refresh the editor if one already exists. michael@0: if (this.editor) { michael@0: this.editor.populate(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Update the current TextProperties that match a given property michael@0: * from the cssText. Will choose one existing TextProperty to update michael@0: * with the new property's value, and will disable all others. michael@0: * michael@0: * When choosing the best match to reuse, properties will be chosen michael@0: * by assigning a rank and choosing the highest-ranked property: michael@0: * Name, value, and priority match, enabled. (6) michael@0: * Name, value, and priority match, disabled. (5) michael@0: * Name and value match, enabled. (4) michael@0: * Name and value match, disabled. (3) michael@0: * Name matches, enabled. (2) michael@0: * Name matches, disabled. (1) michael@0: * michael@0: * If no existing properties match the property, nothing happens. michael@0: * michael@0: * @param {TextProperty} aNewProp michael@0: * The current version of the property, as parsed from the michael@0: * cssText in Rule._getTextProperties(). michael@0: * michael@0: * @return {bool} true if a property was updated, false if no properties michael@0: * were updated. michael@0: */ michael@0: _updateTextProperty: function(aNewProp) { michael@0: let match = { rank: 0, prop: null }; michael@0: michael@0: for each (let prop in this.textProps) { michael@0: if (prop.name != aNewProp.name) michael@0: continue; michael@0: michael@0: // Mark this property visited. michael@0: prop._visited = true; michael@0: michael@0: // Start at rank 1 for matching name. michael@0: let rank = 1; michael@0: michael@0: // Value and Priority matches add 2 to the rank. michael@0: // Being enabled adds 1. This ranks better matches higher, michael@0: // with priority breaking ties. michael@0: if (prop.value === aNewProp.value) { michael@0: rank += 2; michael@0: if (prop.priority === aNewProp.priority) { michael@0: rank += 2; michael@0: } michael@0: } michael@0: michael@0: if (prop.enabled) { michael@0: rank += 1; michael@0: } michael@0: michael@0: if (rank > match.rank) { michael@0: if (match.prop) { michael@0: // We outrank a previous match, disable it. michael@0: match.prop.enabled = false; michael@0: match.prop.updateEditor(); michael@0: } michael@0: match.rank = rank; michael@0: match.prop = prop; michael@0: } else if (rank) { michael@0: // A previous match outranks us, disable ourself. michael@0: prop.enabled = false; michael@0: prop.updateEditor(); michael@0: } michael@0: } michael@0: michael@0: // If we found a match, update its value with the new text property michael@0: // value. michael@0: if (match.prop) { michael@0: match.prop.set(aNewProp); michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * Jump between editable properties in the UI. Will begin editing the next michael@0: * name, if possible. If this is the last element in the set, then begin michael@0: * editing the previous value. If this is the *only* element in the set, michael@0: * then settle for focusing the new property editor. michael@0: * michael@0: * @param {TextProperty} aTextProperty michael@0: * The text property that will be left to focus on a sibling. michael@0: * michael@0: */ michael@0: editClosestTextProperty: function(aTextProperty) { michael@0: let index = this.textProps.indexOf(aTextProperty); michael@0: let previous = false; michael@0: michael@0: // If this is the last element, move to the previous instead of next michael@0: if (index === this.textProps.length - 1) { michael@0: index = index - 1; michael@0: previous = true; michael@0: } michael@0: else { michael@0: index = index + 1; michael@0: } michael@0: michael@0: let nextProp = this.textProps[index]; michael@0: michael@0: // If possible, begin editing the next name or previous value. michael@0: // Otherwise, settle for focusing the new property element. michael@0: if (nextProp) { michael@0: if (previous) { michael@0: nextProp.editor.valueSpan.click(); michael@0: } else { michael@0: nextProp.editor.nameSpan.click(); michael@0: } michael@0: } else { michael@0: aTextProperty.rule.editor.closeBrace.focus(); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * A single property in a rule's cssText. michael@0: * michael@0: * @param {Rule} aRule michael@0: * The rule this TextProperty came from. michael@0: * @param {string} aName michael@0: * The text property name (such as "background" or "border-top"). michael@0: * @param {string} aValue michael@0: * The property's value (not including priority). michael@0: * @param {string} aPriority michael@0: * The property's priority (either "important" or an empty string). michael@0: * michael@0: */ michael@0: function TextProperty(aRule, aName, aValue, aPriority) { michael@0: this.rule = aRule; michael@0: this.name = aName; michael@0: this.value = aValue; michael@0: this.priority = aPriority; michael@0: this.enabled = true; michael@0: this.updateComputed(); michael@0: } michael@0: michael@0: TextProperty.prototype = { michael@0: /** michael@0: * Update the editor associated with this text property, michael@0: * if any. michael@0: */ michael@0: updateEditor: function() { michael@0: if (this.editor) { michael@0: this.editor.update(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Update the list of computed properties for this text property. michael@0: */ michael@0: updateComputed: function() { michael@0: if (!this.name) { michael@0: return; michael@0: } michael@0: michael@0: // This is a bit funky. To get the list of computed properties michael@0: // for this text property, we'll set the property on a dummy element michael@0: // and see what the computed style looks like. michael@0: let dummyElement = this.rule.elementStyle.dummyElement; michael@0: let dummyStyle = dummyElement.style; michael@0: dummyStyle.cssText = ""; michael@0: dummyStyle.setProperty(this.name, this.value, this.priority); michael@0: michael@0: this.computed = []; michael@0: for (let i = 0, n = dummyStyle.length; i < n; i++) { michael@0: let prop = dummyStyle.item(i); michael@0: this.computed.push({ michael@0: textProp: this, michael@0: name: prop, michael@0: value: dummyStyle.getPropertyValue(prop), michael@0: priority: dummyStyle.getPropertyPriority(prop), michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Set all the values from another TextProperty instance into michael@0: * this TextProperty instance. michael@0: * michael@0: * @param {TextProperty} aOther michael@0: * The other TextProperty instance. michael@0: */ michael@0: set: function(aOther) { michael@0: let changed = false; michael@0: for (let item of ["name", "value", "priority", "enabled"]) { michael@0: if (this[item] != aOther[item]) { michael@0: this[item] = aOther[item]; michael@0: changed = true; michael@0: } michael@0: } michael@0: michael@0: if (changed) { michael@0: this.updateEditor(); michael@0: } michael@0: }, michael@0: michael@0: setValue: function(aValue, aPriority) { michael@0: this.rule.setPropertyValue(this, aValue, aPriority); michael@0: this.updateEditor(); michael@0: }, michael@0: michael@0: setName: function(aName) { michael@0: this.rule.setPropertyName(this, aName); michael@0: this.updateEditor(); michael@0: }, michael@0: michael@0: setEnabled: function(aValue) { michael@0: this.rule.setPropertyEnabled(this, aValue); michael@0: this.updateEditor(); michael@0: }, michael@0: michael@0: remove: function() { michael@0: this.rule.removeProperty(this); michael@0: } michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * View hierarchy mostly follows the model hierarchy. michael@0: * michael@0: * CssRuleView: michael@0: * Owns an ElementStyle and creates a list of RuleEditors for its michael@0: * Rules. michael@0: * RuleEditor: michael@0: * Owns a Rule object and creates a list of TextPropertyEditors michael@0: * for its TextProperties. michael@0: * Manages creation of new text properties. michael@0: * TextPropertyEditor: michael@0: * Owns a TextProperty object. michael@0: * Manages changes to the TextProperty. michael@0: * Can be expanded to display computed properties. michael@0: * Can mark a property disabled or enabled. michael@0: */ michael@0: michael@0: /** michael@0: * CssRuleView is a view of the style rules and declarations that michael@0: * apply to a given element. After construction, the 'element' michael@0: * property will be available with the user interface. michael@0: * michael@0: * @param {Inspector} aInspector michael@0: * @param {Document} aDoc michael@0: * The document that will contain the rule view. michael@0: * @param {object} aStore michael@0: * The CSS rule view can use this object to store metadata michael@0: * that might outlast the rule view, particularly the current michael@0: * set of disabled properties. michael@0: * @param {PageStyleFront} aPageStyle michael@0: * The PageStyleFront for communicating with the remote server. michael@0: * @constructor michael@0: */ michael@0: function CssRuleView(aInspector, aDoc, aStore, aPageStyle) { michael@0: this.inspector = aInspector; michael@0: this.doc = aDoc; michael@0: this.store = aStore || {}; michael@0: this.pageStyle = aPageStyle; michael@0: this.element = this.doc.createElementNS(HTML_NS, "div"); michael@0: this.element.className = "ruleview devtools-monospace"; michael@0: this.element.flex = 1; michael@0: michael@0: this._outputParser = new OutputParser(); michael@0: michael@0: this._buildContextMenu = this._buildContextMenu.bind(this); michael@0: this._contextMenuUpdate = this._contextMenuUpdate.bind(this); michael@0: this._onSelectAll = this._onSelectAll.bind(this); michael@0: this._onCopy = this._onCopy.bind(this); michael@0: this._onToggleOrigSources = this._onToggleOrigSources.bind(this); michael@0: michael@0: this.element.addEventListener("copy", this._onCopy); michael@0: michael@0: this._handlePrefChange = this._handlePrefChange.bind(this); michael@0: gDevTools.on("pref-changed", this._handlePrefChange); michael@0: michael@0: this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this); michael@0: this._prefObserver = new PrefObserver("devtools."); michael@0: this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged); michael@0: michael@0: let options = { michael@0: autoSelect: true, michael@0: theme: "auto" michael@0: }; michael@0: this.popup = new AutocompletePopup(aDoc.defaultView.parent.document, options); michael@0: michael@0: // Create a tooltip for previewing things in the rule view (images for now) michael@0: this.previewTooltip = new Tooltip(this.inspector.panelDoc); michael@0: this.previewTooltip.startTogglingOnHover(this.element, michael@0: this._onTooltipTargetHover.bind(this)); michael@0: michael@0: // Also create a more complex tooltip for editing colors with the spectrum michael@0: // color picker michael@0: this.colorPicker = new SwatchColorPickerTooltip(this.inspector.panelDoc); michael@0: michael@0: this._buildContextMenu(); michael@0: this._showEmpty(); michael@0: } michael@0: michael@0: exports.CssRuleView = CssRuleView; michael@0: michael@0: CssRuleView.prototype = { michael@0: // The element that we're inspecting. michael@0: _viewedElement: null, michael@0: michael@0: /** michael@0: * Build the context menu. michael@0: */ michael@0: _buildContextMenu: function() { michael@0: let doc = this.doc.defaultView.parent.document; michael@0: michael@0: this._contextmenu = doc.createElementNS(XUL_NS, "menupopup"); michael@0: this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate); michael@0: this._contextmenu.id = "rule-view-context-menu"; michael@0: michael@0: this.menuitemSelectAll = createMenuItem(this._contextmenu, { michael@0: label: "ruleView.contextmenu.selectAll", michael@0: accesskey: "ruleView.contextmenu.selectAll.accessKey", michael@0: command: this._onSelectAll michael@0: }); michael@0: this.menuitemCopy = createMenuItem(this._contextmenu, { michael@0: label: "ruleView.contextmenu.copy", michael@0: accesskey: "ruleView.contextmenu.copy.accessKey", michael@0: command: this._onCopy michael@0: }); michael@0: this.menuitemSources= createMenuItem(this._contextmenu, { michael@0: label: "ruleView.contextmenu.showOrigSources", michael@0: accesskey: "ruleView.contextmenu.showOrigSources.accessKey", michael@0: command: this._onToggleOrigSources michael@0: }); michael@0: michael@0: let popupset = doc.documentElement.querySelector("popupset"); michael@0: if (!popupset) { michael@0: popupset = doc.createElementNS(XUL_NS, "popupset"); michael@0: doc.documentElement.appendChild(popupset); michael@0: } michael@0: michael@0: popupset.appendChild(this._contextmenu); michael@0: }, michael@0: michael@0: /** michael@0: * Which type of hover-tooltip should be shown for the given element? michael@0: * This depends on the element: does it contain an image URL, a CSS transform, michael@0: * a font-family, ... michael@0: * @param {DOMNode} el The element to test michael@0: * @return {String} The type of hover-tooltip michael@0: */ michael@0: _getHoverTooltipTypeForTarget: function(el) { michael@0: let prop = el.textProperty; michael@0: michael@0: // Test for css transform michael@0: if (prop && prop.name === "transform") { michael@0: return "transform"; michael@0: } michael@0: michael@0: // Test for image michael@0: let isUrl = el.classList.contains("theme-link") && michael@0: el.parentNode.classList.contains("ruleview-propertyvalue"); michael@0: if (this.inspector.hasUrlToImageDataResolver && isUrl) { michael@0: return "image"; michael@0: } michael@0: michael@0: // Test for font-family michael@0: let propertyRoot = el.parentNode; michael@0: let propertyNameNode = propertyRoot.querySelector(".ruleview-propertyname"); michael@0: if (!propertyNameNode) { michael@0: propertyRoot = propertyRoot.parentNode; michael@0: propertyNameNode = propertyRoot.querySelector(".ruleview-propertyname"); michael@0: } michael@0: let propertyName; michael@0: if (propertyNameNode) { michael@0: propertyName = propertyNameNode.textContent; michael@0: } michael@0: if (propertyName === "font-family" && el.classList.contains("ruleview-propertyvalue")) { michael@0: return "font"; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Executed by the tooltip when the pointer hovers over an element of the view. michael@0: * Used to decide whether the tooltip should be shown or not and to actually michael@0: * put content in it. michael@0: * Checks if the hovered target is a css value we support tooltips for. michael@0: * @param {DOMNode} target michael@0: * @return {Boolean|Promise} Either a boolean or a promise, used by the michael@0: * Tooltip class to wait for the content to be put in the tooltip and finally michael@0: * decide whether or not the tooltip should be shown. michael@0: */ michael@0: _onTooltipTargetHover: function(target) { michael@0: let tooltipType = this._getHoverTooltipTypeForTarget(target); michael@0: if (!tooltipType) { michael@0: return false; michael@0: } michael@0: michael@0: if (this.colorPicker.tooltip.isShown()) { michael@0: this.colorPicker.revert(); michael@0: this.colorPicker.hide(); michael@0: } michael@0: michael@0: if (tooltipType === "transform") { michael@0: return this.previewTooltip.setCssTransformContent(target.textProperty.value, michael@0: this.pageStyle, this._viewedElement); michael@0: } michael@0: if (tooltipType === "image") { michael@0: let prop = target.parentNode.textProperty; michael@0: let dim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize"); michael@0: let uri = CssLogic.getBackgroundImageUriFromProperty(prop.value, prop.rule.domRule.href); michael@0: return this.previewTooltip.setRelativeImageContent(uri, this.inspector.inspector, dim); michael@0: } michael@0: if (tooltipType === "font") { michael@0: this.previewTooltip.setFontFamilyContent(target.textContent); michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * Update the context menu. This means enabling or disabling menuitems as michael@0: * appropriate. michael@0: */ michael@0: _contextMenuUpdate: function() { michael@0: let win = this.doc.defaultView; michael@0: michael@0: // Copy selection. michael@0: let selection = win.getSelection(); michael@0: let copy; michael@0: michael@0: if (selection.toString()) { michael@0: // Panel text selected michael@0: copy = true; michael@0: } else if (selection.anchorNode) { michael@0: // input type="text" michael@0: let { selectionStart, selectionEnd } = this.doc.popupNode; michael@0: michael@0: if (isFinite(selectionStart) && isFinite(selectionEnd) && michael@0: selectionStart !== selectionEnd) { michael@0: copy = true; michael@0: } michael@0: } else { michael@0: // No text selected, disable copy. michael@0: copy = false; michael@0: } michael@0: michael@0: this.menuitemCopy.disabled = !copy; michael@0: michael@0: let label = "ruleView.contextmenu.showOrigSources"; michael@0: if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { michael@0: label = "ruleView.contextmenu.showCSSSources"; michael@0: } michael@0: this.menuitemSources.setAttribute("label", michael@0: _strings.GetStringFromName(label)); michael@0: michael@0: let accessKey = label + ".accessKey"; michael@0: this.menuitemSources.setAttribute("accesskey", michael@0: _strings.GetStringFromName(accessKey)); michael@0: }, michael@0: michael@0: /** michael@0: * Select all text. michael@0: */ michael@0: _onSelectAll: function() { michael@0: let win = this.doc.defaultView; michael@0: let selection = win.getSelection(); michael@0: michael@0: selection.selectAllChildren(this.doc.documentElement); michael@0: }, michael@0: michael@0: /** michael@0: * Copy selected text from the rule view. michael@0: * michael@0: * @param {Event} event michael@0: * The event object. michael@0: */ michael@0: _onCopy: function(event) { michael@0: try { michael@0: let target = event.target; michael@0: let text; michael@0: michael@0: if (event.target.nodeName === "menuitem") { michael@0: target = this.doc.popupNode; michael@0: } michael@0: michael@0: if (target.nodeName == "input") { michael@0: let start = Math.min(target.selectionStart, target.selectionEnd); michael@0: let end = Math.max(target.selectionStart, target.selectionEnd); michael@0: let count = end - start; michael@0: text = target.value.substr(start, count); michael@0: } else { michael@0: let win = this.doc.defaultView; michael@0: let selection = win.getSelection(); michael@0: michael@0: text = selection.toString(); michael@0: michael@0: // Remove any double newlines. michael@0: text = text.replace(/(\r?\n)\r?\n/g, "$1"); michael@0: michael@0: // Remove "inline" michael@0: let inline = _strings.GetStringFromName("rule.sourceInline"); michael@0: let rx = new RegExp("^" + inline + "\\r?\\n?", "g"); michael@0: text = text.replace(rx, ""); michael@0: } michael@0: michael@0: clipboardHelper.copyString(text, this.doc); michael@0: event.preventDefault(); michael@0: } catch(e) { michael@0: console.error(e); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Toggle the original sources pref. michael@0: */ michael@0: _onToggleOrigSources: function() { michael@0: let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); michael@0: Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled); michael@0: }, michael@0: michael@0: setPageStyle: function(aPageStyle) { michael@0: this.pageStyle = aPageStyle; michael@0: }, michael@0: michael@0: /** michael@0: * Return {bool} true if the rule view currently has an input editor visible. michael@0: */ michael@0: get isEditing() { michael@0: return this.element.querySelectorAll(".styleinspector-propertyeditor").length > 0 michael@0: || this.colorPicker.tooltip.isShown(); michael@0: }, michael@0: michael@0: _handlePrefChange: function(event, data) { michael@0: if (data.pref == "devtools.defaultColorUnit") { michael@0: let element = this._viewedElement; michael@0: this._viewedElement = null; michael@0: this.highlight(element); michael@0: } michael@0: }, michael@0: michael@0: _onSourcePrefChanged: function() { michael@0: if (this.menuitemSources) { michael@0: let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); michael@0: this.menuitemSources.setAttribute("checked", isEnabled); michael@0: } michael@0: michael@0: // update text of source links michael@0: for (let rule of this._elementStyle.rules) { michael@0: if (rule.editor) { michael@0: rule.editor.updateSourceLink(); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: destroy: function() { michael@0: this.clear(); michael@0: michael@0: gDummyPromise = null; michael@0: gDevTools.off("pref-changed", this._handlePrefChange); michael@0: michael@0: this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged); michael@0: this._prefObserver.destroy(); michael@0: michael@0: this.element.removeEventListener("copy", this._onCopy); michael@0: delete this._onCopy; michael@0: michael@0: delete this._outputParser; michael@0: michael@0: // Remove context menu michael@0: if (this._contextmenu) { michael@0: // Destroy the Select All menuitem. michael@0: this.menuitemSelectAll.removeEventListener("command", this._onSelectAll); michael@0: this.menuitemSelectAll = null; michael@0: michael@0: // Destroy the Copy menuitem. michael@0: this.menuitemCopy.removeEventListener("command", this._onCopy); michael@0: this.menuitemCopy = null; michael@0: michael@0: this.menuitemSources.removeEventListener("command", this._onToggleOrigSources); michael@0: this.menuitemSources = null; michael@0: michael@0: // Destroy the context menu. michael@0: this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate); michael@0: this._contextmenu.parentNode.removeChild(this._contextmenu); michael@0: this._contextmenu = null; michael@0: } michael@0: michael@0: // We manage the popupNode ourselves so we also need to destroy it. michael@0: this.doc.popupNode = null; michael@0: michael@0: this.previewTooltip.stopTogglingOnHover(this.element); michael@0: this.previewTooltip.destroy(); michael@0: this.colorPicker.destroy(); michael@0: michael@0: if (this.element.parentNode) { michael@0: this.element.parentNode.removeChild(this.element); michael@0: } michael@0: michael@0: if (this.elementStyle) { michael@0: this.elementStyle.destroy(); michael@0: } michael@0: michael@0: this.popup.destroy(); michael@0: }, michael@0: michael@0: /** michael@0: * Update the highlighted element. michael@0: * michael@0: * @param {NodeActor} aElement michael@0: * The node whose style rules we'll inspect. michael@0: */ michael@0: highlight: function(aElement) { michael@0: if (this._viewedElement === aElement) { michael@0: return promise.resolve(undefined); michael@0: } michael@0: michael@0: this.clear(); michael@0: michael@0: if (this._elementStyle) { michael@0: delete this._elementStyle; michael@0: } michael@0: michael@0: this._viewedElement = aElement; michael@0: if (!this._viewedElement) { michael@0: this._showEmpty(); michael@0: return promise.resolve(undefined); michael@0: } michael@0: michael@0: this._elementStyle = new ElementStyle(aElement, this.store, this.pageStyle); michael@0: return this._elementStyle.init().then(() => { michael@0: return this._populate(); michael@0: }).then(() => { michael@0: // A new node may already be selected, in which this._elementStyle will michael@0: // be null. michael@0: if (this._elementStyle) { michael@0: this._elementStyle.onChanged = () => { michael@0: this._changed(); michael@0: }; michael@0: } michael@0: }).then(null, console.error); michael@0: }, michael@0: michael@0: /** michael@0: * Update the rules for the currently highlighted element. michael@0: */ michael@0: nodeChanged: function() { michael@0: // Ignore refreshes during editing or when no element is selected. michael@0: if (this.isEditing || !this._elementStyle) { michael@0: return; michael@0: } michael@0: michael@0: this._clearRules(); michael@0: michael@0: // Repopulate the element style. michael@0: this._populate(); michael@0: }, michael@0: michael@0: _populate: function() { michael@0: let elementStyle = this._elementStyle; michael@0: return this._elementStyle.populate().then(() => { michael@0: if (this._elementStyle != elementStyle) { michael@0: return; michael@0: } michael@0: this._createEditors(); michael@0: michael@0: // Notify anyone that cares that we refreshed. michael@0: var evt = this.doc.createEvent("Events"); michael@0: evt.initEvent("CssRuleViewRefreshed", true, false); michael@0: this.element.dispatchEvent(evt); michael@0: return undefined; michael@0: }).then(null, promiseWarn); michael@0: }, michael@0: michael@0: /** michael@0: * Show the user that the rule view has no node selected. michael@0: */ michael@0: _showEmpty: function() { michael@0: if (this.doc.getElementById("noResults") > 0) { michael@0: return; michael@0: } michael@0: michael@0: createChild(this.element, "div", { michael@0: id: "noResults", michael@0: textContent: CssLogic.l10n("rule.empty") michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Clear the rules. michael@0: */ michael@0: _clearRules: function() { michael@0: while (this.element.hasChildNodes()) { michael@0: this.element.removeChild(this.element.lastChild); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Clear the rule view. michael@0: */ michael@0: clear: function() { michael@0: this._clearRules(); michael@0: this._viewedElement = null; michael@0: this._elementStyle = null; michael@0: michael@0: this.previewTooltip.hide(); michael@0: this.colorPicker.hide(); michael@0: }, michael@0: michael@0: /** michael@0: * Called when the user has made changes to the ElementStyle. michael@0: * Emits an event that clients can listen to. michael@0: */ michael@0: _changed: function() { michael@0: var evt = this.doc.createEvent("Events"); michael@0: evt.initEvent("CssRuleViewChanged", true, false); michael@0: this.element.dispatchEvent(evt); michael@0: }, michael@0: michael@0: /** michael@0: * Text for header that shows above rules for this element michael@0: */ michael@0: get selectedElementLabel() { michael@0: if (this._selectedElementLabel) { michael@0: return this._selectedElementLabel; michael@0: } michael@0: this._selectedElementLabel = CssLogic.l10n("rule.selectedElement"); michael@0: return this._selectedElementLabel; michael@0: }, michael@0: michael@0: /** michael@0: * Text for header that shows above rules for pseudo elements michael@0: */ michael@0: get pseudoElementLabel() { michael@0: if (this._pseudoElementLabel) { michael@0: return this._pseudoElementLabel; michael@0: } michael@0: this._pseudoElementLabel = CssLogic.l10n("rule.pseudoElement"); michael@0: return this._pseudoElementLabel; michael@0: }, michael@0: michael@0: togglePseudoElementVisibility: function(value) { michael@0: this._showPseudoElements = !!value; michael@0: let isOpen = this.showPseudoElements; michael@0: michael@0: Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements", michael@0: isOpen); michael@0: michael@0: this.element.classList.toggle("show-pseudo-elements", isOpen); michael@0: michael@0: if (this.pseudoElementTwisty) { michael@0: if (isOpen) { michael@0: this.pseudoElementTwisty.setAttribute("open", "true"); michael@0: } michael@0: else { michael@0: this.pseudoElementTwisty.removeAttribute("open"); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: get showPseudoElements() { michael@0: if (this._showPseudoElements === undefined) { michael@0: this._showPseudoElements = michael@0: Services.prefs.getBoolPref("devtools.inspector.show_pseudo_elements"); michael@0: } michael@0: return this._showPseudoElements; michael@0: }, michael@0: michael@0: _getRuleViewHeaderClassName: function(isPseudo) { michael@0: let baseClassName = "theme-gutter ruleview-header"; michael@0: return isPseudo ? baseClassName + " ruleview-expandable-header" : baseClassName; michael@0: }, michael@0: michael@0: /** michael@0: * Creates editor UI for each of the rules in _elementStyle. michael@0: */ michael@0: _createEditors: function() { michael@0: // Run through the current list of rules, attaching michael@0: // their editors in order. Create editors if needed. michael@0: let lastInheritedSource = ""; michael@0: let seenPseudoElement = false; michael@0: let seenNormalElement = false; michael@0: michael@0: for (let rule of this._elementStyle.rules) { michael@0: if (rule.domRule.system) { michael@0: continue; michael@0: } michael@0: michael@0: // Only print header for this element if there are pseudo elements michael@0: if (seenPseudoElement && !seenNormalElement && !rule.pseudoElement) { michael@0: seenNormalElement = true; michael@0: let div = this.doc.createElementNS(HTML_NS, "div"); michael@0: div.className = this._getRuleViewHeaderClassName(); michael@0: div.textContent = this.selectedElementLabel; michael@0: this.element.appendChild(div); michael@0: } michael@0: michael@0: let inheritedSource = rule.inheritedSource; michael@0: if (inheritedSource != lastInheritedSource) { michael@0: let div = this.doc.createElementNS(HTML_NS, "div"); michael@0: div.className = this._getRuleViewHeaderClassName(); michael@0: div.textContent = inheritedSource; michael@0: lastInheritedSource = inheritedSource; michael@0: this.element.appendChild(div); michael@0: } michael@0: michael@0: if (!seenPseudoElement && rule.pseudoElement) { michael@0: seenPseudoElement = true; michael@0: michael@0: let div = this.doc.createElementNS(HTML_NS, "div"); michael@0: div.className = this._getRuleViewHeaderClassName(true); michael@0: div.textContent = this.pseudoElementLabel; michael@0: div.addEventListener("dblclick", () => { michael@0: this.togglePseudoElementVisibility(!this.showPseudoElements); michael@0: }, false); michael@0: michael@0: let twisty = this.pseudoElementTwisty = michael@0: this.doc.createElementNS(HTML_NS, "span"); michael@0: twisty.className = "ruleview-expander theme-twisty"; michael@0: twisty.addEventListener("click", () => { michael@0: this.togglePseudoElementVisibility(!this.showPseudoElements); michael@0: }, false); michael@0: michael@0: div.insertBefore(twisty, div.firstChild); michael@0: this.element.appendChild(div); michael@0: } michael@0: michael@0: if (!rule.editor) { michael@0: rule.editor = new RuleEditor(this, rule); michael@0: } michael@0: michael@0: this.element.appendChild(rule.editor.element); michael@0: } michael@0: michael@0: this.togglePseudoElementVisibility(this.showPseudoElements); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Create a RuleEditor. michael@0: * michael@0: * @param {CssRuleView} aRuleView michael@0: * The CssRuleView containg the document holding this rule editor. michael@0: * @param {Rule} aRule michael@0: * The Rule object we're editing. michael@0: * @constructor michael@0: */ michael@0: function RuleEditor(aRuleView, aRule) { michael@0: this.ruleView = aRuleView; michael@0: this.doc = this.ruleView.doc; michael@0: this.rule = aRule; michael@0: michael@0: this._onNewProperty = this._onNewProperty.bind(this); michael@0: this._newPropertyDestroy = this._newPropertyDestroy.bind(this); michael@0: michael@0: this._create(); michael@0: } michael@0: michael@0: RuleEditor.prototype = { michael@0: _create: function() { michael@0: this.element = this.doc.createElementNS(HTML_NS, "div"); michael@0: this.element.className = "ruleview-rule theme-separator"; michael@0: this.element._ruleEditor = this; michael@0: if (this.rule.pseudoElement) { michael@0: this.element.classList.add("ruleview-rule-pseudo-element"); michael@0: } michael@0: michael@0: // Give a relative position for the inplace editor's measurement michael@0: // span to be placed absolutely against. michael@0: this.element.style.position = "relative"; michael@0: michael@0: // Add the source link. michael@0: let source = createChild(this.element, "div", { michael@0: class: "ruleview-rule-source theme-link" michael@0: }); michael@0: source.addEventListener("click", function() { michael@0: let rule = this.rule.domRule; michael@0: let evt = this.doc.createEvent("CustomEvent"); michael@0: evt.initCustomEvent("CssRuleViewCSSLinkClicked", true, false, { michael@0: rule: rule, michael@0: }); michael@0: this.element.dispatchEvent(evt); michael@0: }.bind(this)); michael@0: let sourceLabel = this.doc.createElementNS(XUL_NS, "label"); michael@0: sourceLabel.setAttribute("crop", "center"); michael@0: sourceLabel.classList.add("source-link-label"); michael@0: source.appendChild(sourceLabel); michael@0: michael@0: this.updateSourceLink(); michael@0: michael@0: let code = createChild(this.element, "div", { michael@0: class: "ruleview-code" michael@0: }); michael@0: michael@0: let header = createChild(code, "div", {}); michael@0: michael@0: this.selectorText = createChild(header, "span", { michael@0: class: "ruleview-selector theme-fg-color3" michael@0: }); michael@0: michael@0: this.openBrace = createChild(header, "span", { michael@0: class: "ruleview-ruleopen", michael@0: textContent: " {" michael@0: }); michael@0: michael@0: code.addEventListener("click", function() { michael@0: let selection = this.doc.defaultView.getSelection(); michael@0: if (selection.isCollapsed) { michael@0: this.newProperty(); michael@0: } michael@0: }.bind(this), false); michael@0: michael@0: this.element.addEventListener("mousedown", function() { michael@0: this.doc.defaultView.focus(); michael@0: }.bind(this), false); michael@0: michael@0: this.element.addEventListener("contextmenu", event => { michael@0: try { michael@0: // In the sidebar we do not have this.doc.popupNode so we need to save michael@0: // the node ourselves. michael@0: this.doc.popupNode = event.explicitOriginalTarget; michael@0: let win = this.doc.defaultView; michael@0: win.focus(); michael@0: michael@0: this.ruleView._contextmenu.openPopupAtScreen( michael@0: event.screenX, event.screenY, true); michael@0: michael@0: } catch(e) { michael@0: console.error(e); michael@0: } michael@0: }, false); michael@0: michael@0: this.propertyList = createChild(code, "ul", { michael@0: class: "ruleview-propertylist" michael@0: }); michael@0: michael@0: this.populate(); michael@0: michael@0: this.closeBrace = createChild(code, "div", { michael@0: class: "ruleview-ruleclose", michael@0: tabindex: "0", michael@0: textContent: "}" michael@0: }); michael@0: michael@0: // Create a property editor when the close brace is clicked. michael@0: editableItem({ element: this.closeBrace }, (aElement) => { michael@0: this.newProperty(); michael@0: }); michael@0: }, michael@0: michael@0: updateSourceLink: function RuleEditor_updateSourceLink() michael@0: { michael@0: let sourceLabel = this.element.querySelector(".source-link-label"); michael@0: sourceLabel.setAttribute("value", this.rule.title); michael@0: michael@0: let sourceHref = (this.rule.sheet && this.rule.sheet.href) ? michael@0: this.rule.sheet.href : this.rule.title; michael@0: michael@0: sourceLabel.setAttribute("tooltiptext", sourceHref); michael@0: michael@0: let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); michael@0: if (showOrig && this.rule.domRule.type != ELEMENT_STYLE) { michael@0: this.rule.getOriginalSourceStrings().then((strings) => { michael@0: sourceLabel.setAttribute("value", strings.short); michael@0: sourceLabel.setAttribute("tooltiptext", strings.full); michael@0: }) michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Update the rule editor with the contents of the rule. michael@0: */ michael@0: populate: function() { michael@0: // Clear out existing viewers. michael@0: while (this.selectorText.hasChildNodes()) { michael@0: this.selectorText.removeChild(this.selectorText.lastChild); michael@0: } michael@0: michael@0: // If selector text comes from a css rule, highlight selectors that michael@0: // actually match. For custom selector text (such as for the 'element' michael@0: // style, just show the text directly. michael@0: if (this.rule.domRule.type === ELEMENT_STYLE) { michael@0: this.selectorText.textContent = this.rule.selectorText; michael@0: } else { michael@0: this.rule.domRule.selectors.forEach((selector, i) => { michael@0: if (i != 0) { michael@0: createChild(this.selectorText, "span", { michael@0: class: "ruleview-selector-separator", michael@0: textContent: ", " michael@0: }); michael@0: } michael@0: let cls; michael@0: if (this.rule.matchedSelectors.indexOf(selector) > -1) { michael@0: cls = "ruleview-selector-matched"; michael@0: } else { michael@0: cls = "ruleview-selector-unmatched"; michael@0: } michael@0: createChild(this.selectorText, "span", { michael@0: class: cls, michael@0: textContent: selector michael@0: }); michael@0: }); michael@0: } michael@0: michael@0: for (let prop of this.rule.textProps) { michael@0: if (!prop.editor) { michael@0: let editor = new TextPropertyEditor(this, prop); michael@0: this.propertyList.appendChild(editor.element); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Programatically add a new property to the rule. michael@0: * michael@0: * @param {string} aName michael@0: * Property name. michael@0: * @param {string} aValue michael@0: * Property value. michael@0: * @param {string} aPriority michael@0: * Property priority. michael@0: * @param {TextProperty} aSiblingProp michael@0: * Optional, property next to which the new property will be added. michael@0: * @return {TextProperty} michael@0: * The new property michael@0: */ michael@0: addProperty: function(aName, aValue, aPriority, aSiblingProp) { michael@0: let prop = this.rule.createProperty(aName, aValue, aPriority, aSiblingProp); michael@0: let index = this.rule.textProps.indexOf(prop); michael@0: let editor = new TextPropertyEditor(this, prop); michael@0: michael@0: // Insert this node before the DOM node that is currently at its new index michael@0: // in the property list. There is currently one less node in the DOM than michael@0: // in the property list, so this causes it to appear after aSiblingProp. michael@0: // If there is no node at its index, as is the case where this is the last michael@0: // node being inserted, then this behaves as appendChild. michael@0: this.propertyList.insertBefore(editor.element, michael@0: this.propertyList.children[index]); michael@0: michael@0: return prop; michael@0: }, michael@0: michael@0: /** michael@0: * Programatically add a list of new properties to the rule. Focus the UI michael@0: * to the proper location after adding (either focus the value on the michael@0: * last property if it is empty, or create a new property and focus it). michael@0: * michael@0: * @param {Array} aProperties michael@0: * Array of properties, which are objects with this signature: michael@0: * { michael@0: * name: {string}, michael@0: * value: {string}, michael@0: * priority: {string} michael@0: * } michael@0: * @param {TextProperty} aSiblingProp michael@0: * Optional, the property next to which all new props should be added. michael@0: */ michael@0: addProperties: function(aProperties, aSiblingProp) { michael@0: if (!aProperties || !aProperties.length) { michael@0: return; michael@0: } michael@0: michael@0: let lastProp = aSiblingProp; michael@0: for (let p of aProperties) { michael@0: lastProp = this.addProperty(p.name, p.value, p.priority, lastProp); michael@0: } michael@0: michael@0: // Either focus on the last value if incomplete, or start a new one. michael@0: if (lastProp && lastProp.value.trim() === "") { michael@0: lastProp.editor.valueSpan.click(); michael@0: } else { michael@0: this.newProperty(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Create a text input for a property name. If a non-empty property michael@0: * name is given, we'll create a real TextProperty and add it to the michael@0: * rule. michael@0: */ michael@0: newProperty: function() { michael@0: // If we're already creating a new property, ignore this. michael@0: if (!this.closeBrace.hasAttribute("tabindex")) { michael@0: return; michael@0: } michael@0: michael@0: // While we're editing a new property, it doesn't make sense to michael@0: // start a second new property editor, so disable focusing the michael@0: // close brace for now. michael@0: this.closeBrace.removeAttribute("tabindex"); michael@0: michael@0: this.newPropItem = createChild(this.propertyList, "li", { michael@0: class: "ruleview-property ruleview-newproperty", michael@0: }); michael@0: michael@0: this.newPropSpan = createChild(this.newPropItem, "span", { michael@0: class: "ruleview-propertyname", michael@0: tabindex: "0" michael@0: }); michael@0: michael@0: this.multipleAddedProperties = null; michael@0: michael@0: this.editor = new InplaceEditor({ michael@0: element: this.newPropSpan, michael@0: done: this._onNewProperty, michael@0: destroy: this._newPropertyDestroy, michael@0: advanceChars: ":", michael@0: contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY, michael@0: popup: this.ruleView.popup michael@0: }); michael@0: michael@0: // Auto-close the input if multiple rules get pasted into new property. michael@0: this.editor.input.addEventListener("paste", michael@0: blurOnMultipleProperties, false); michael@0: }, michael@0: michael@0: /** michael@0: * Called when the new property input has been dismissed. michael@0: * michael@0: * @param {string} aValue michael@0: * The value in the editor. michael@0: * @param {bool} aCommit michael@0: * True if the value should be committed. michael@0: */ michael@0: _onNewProperty: function(aValue, aCommit) { michael@0: if (!aValue || !aCommit) { michael@0: return; michael@0: } michael@0: michael@0: // parseDeclarations allows for name-less declarations, but in the present michael@0: // case, we're creating a new declaration, it doesn't make sense to accept michael@0: // these entries michael@0: this.multipleAddedProperties = parseDeclarations(aValue).filter(d => d.name); michael@0: michael@0: // Blur the editor field now and deal with adding declarations later when michael@0: // the field gets destroyed (see _newPropertyDestroy) michael@0: this.editor.input.blur(); michael@0: }, michael@0: michael@0: /** michael@0: * Called when the new property editor is destroyed. michael@0: * This is where the properties (type TextProperty) are actually being michael@0: * added, since we want to wait until after the inplace editor `destroy` michael@0: * event has been fired to keep consistent UI state. michael@0: */ michael@0: _newPropertyDestroy: function() { michael@0: // We're done, make the close brace focusable again. michael@0: this.closeBrace.setAttribute("tabindex", "0"); michael@0: michael@0: this.propertyList.removeChild(this.newPropItem); michael@0: delete this.newPropItem; michael@0: delete this.newPropSpan; michael@0: michael@0: // If properties were added, we want to focus the proper element. michael@0: // If the last new property has no value, focus the value on it. michael@0: // Otherwise, start a new property and focus that field. michael@0: if (this.multipleAddedProperties && this.multipleAddedProperties.length) { michael@0: this.addProperties(this.multipleAddedProperties); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Create a TextPropertyEditor. michael@0: * michael@0: * @param {RuleEditor} aRuleEditor michael@0: * The rule editor that owns this TextPropertyEditor. michael@0: * @param {TextProperty} aProperty michael@0: * The text property to edit. michael@0: * @constructor michael@0: */ michael@0: function TextPropertyEditor(aRuleEditor, aProperty) { michael@0: this.ruleEditor = aRuleEditor; michael@0: this.doc = this.ruleEditor.doc; michael@0: this.popup = this.ruleEditor.ruleView.popup; michael@0: this.prop = aProperty; michael@0: this.prop.editor = this; michael@0: this.browserWindow = this.doc.defaultView.top; michael@0: this.removeOnRevert = this.prop.value === ""; michael@0: michael@0: this._onEnableClicked = this._onEnableClicked.bind(this); michael@0: this._onExpandClicked = this._onExpandClicked.bind(this); michael@0: this._onStartEditing = this._onStartEditing.bind(this); michael@0: this._onNameDone = this._onNameDone.bind(this); michael@0: this._onValueDone = this._onValueDone.bind(this); michael@0: this._onValidate = throttle(this._previewValue, 10, this); michael@0: this.update = this.update.bind(this); michael@0: michael@0: this._create(); michael@0: this.update(); michael@0: } michael@0: michael@0: TextPropertyEditor.prototype = { michael@0: /** michael@0: * Boolean indicating if the name or value is being currently edited. michael@0: */ michael@0: get editing() { michael@0: return !!(this.nameSpan.inplaceEditor || this.valueSpan.inplaceEditor || michael@0: this.ruleEditor.ruleView.colorPicker.tooltip.isShown() || michael@0: this.ruleEditor.ruleView.colorPicker.eyedropperOpen) || michael@0: this.popup.isOpen; michael@0: }, michael@0: michael@0: /** michael@0: * Create the property editor's DOM. michael@0: */ michael@0: _create: function() { michael@0: this.element = this.doc.createElementNS(HTML_NS, "li"); michael@0: this.element.classList.add("ruleview-property"); michael@0: michael@0: // The enable checkbox will disable or enable the rule. michael@0: this.enable = createChild(this.element, "div", { michael@0: class: "ruleview-enableproperty theme-checkbox", michael@0: tabindex: "-1" michael@0: }); michael@0: this.enable.addEventListener("click", this._onEnableClicked, true); michael@0: michael@0: // Click to expand the computed properties of the text property. michael@0: this.expander = createChild(this.element, "span", { michael@0: class: "ruleview-expander theme-twisty" michael@0: }); michael@0: this.expander.addEventListener("click", this._onExpandClicked, true); michael@0: michael@0: this.nameContainer = createChild(this.element, "span", { michael@0: class: "ruleview-namecontainer" michael@0: }); michael@0: this.nameContainer.addEventListener("click", (aEvent) => { michael@0: // Clicks within the name shouldn't propagate any further. michael@0: aEvent.stopPropagation(); michael@0: if (aEvent.target === propertyContainer) { michael@0: this.nameSpan.click(); michael@0: } michael@0: }, false); michael@0: michael@0: // Property name, editable when focused. Property name michael@0: // is committed when the editor is unfocused. michael@0: this.nameSpan = createChild(this.nameContainer, "span", { michael@0: class: "ruleview-propertyname theme-fg-color5", michael@0: tabindex: "0", michael@0: }); michael@0: michael@0: editableField({ michael@0: start: this._onStartEditing, michael@0: element: this.nameSpan, michael@0: done: this._onNameDone, michael@0: destroy: this.update, michael@0: advanceChars: ':', michael@0: contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY, michael@0: popup: this.popup michael@0: }); michael@0: michael@0: // Auto blur name field on multiple CSS rules get pasted in. michael@0: this.nameContainer.addEventListener("paste", michael@0: blurOnMultipleProperties, false); michael@0: michael@0: appendText(this.nameContainer, ": "); michael@0: michael@0: // Create a span that will hold the property and semicolon. michael@0: // Use this span to create a slightly larger click target michael@0: // for the value. michael@0: let propertyContainer = createChild(this.element, "span", { michael@0: class: "ruleview-propertycontainer" michael@0: }); michael@0: michael@0: propertyContainer.addEventListener("click", (aEvent) => { michael@0: // Clicks within the value shouldn't propagate any further. michael@0: aEvent.stopPropagation(); michael@0: michael@0: if (aEvent.target === propertyContainer) { michael@0: this.valueSpan.click(); michael@0: } michael@0: }, false); michael@0: michael@0: // Property value, editable when focused. Changes to the michael@0: // property value are applied as they are typed, and reverted michael@0: // if the user presses escape. michael@0: this.valueSpan = createChild(propertyContainer, "span", { michael@0: class: "ruleview-propertyvalue theme-fg-color1", michael@0: tabindex: "0", michael@0: }); michael@0: michael@0: this.valueSpan.addEventListener("click", (event) => { michael@0: let target = event.target; michael@0: michael@0: if (target.nodeName === "a") { michael@0: event.stopPropagation(); michael@0: event.preventDefault(); michael@0: this.browserWindow.openUILinkIn(target.href, "tab"); michael@0: } michael@0: }, false); michael@0: michael@0: // Storing the TextProperty on the valuespan for easy access michael@0: // (for instance by the tooltip) michael@0: this.valueSpan.textProperty = this.prop; michael@0: michael@0: // Save the initial value as the last committed value, michael@0: // for restoring after pressing escape. michael@0: this.committed = { name: this.prop.name, michael@0: value: this.prop.value, michael@0: priority: this.prop.priority }; michael@0: michael@0: appendText(propertyContainer, ";"); michael@0: michael@0: this.warning = createChild(this.element, "div", { michael@0: class: "ruleview-warning", michael@0: hidden: "", michael@0: title: CssLogic.l10n("rule.warning.title"), michael@0: }); michael@0: michael@0: // Holds the viewers for the computed properties. michael@0: // will be populated in |_updateComputed|. michael@0: this.computed = createChild(this.element, "ul", { michael@0: class: "ruleview-computedlist", michael@0: }); michael@0: michael@0: editableField({ michael@0: start: this._onStartEditing, michael@0: element: this.valueSpan, michael@0: done: this._onValueDone, michael@0: destroy: this.update, michael@0: validate: this._onValidate, michael@0: advanceChars: ';', michael@0: contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE, michael@0: property: this.prop, michael@0: popup: this.popup michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Get the path from which to resolve requests for this michael@0: * rule's stylesheet. michael@0: * @return {string} the stylesheet's href. michael@0: */ michael@0: get sheetHref() { michael@0: let domRule = this.prop.rule.domRule; michael@0: if (domRule) { michael@0: return domRule.href || domRule.nodeHref; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Get the URI from which to resolve relative requests for michael@0: * this rule's stylesheet. michael@0: * @return {nsIURI} A URI based on the the stylesheet's href. michael@0: */ michael@0: get sheetURI() { michael@0: if (this._sheetURI === undefined) { michael@0: if (this.sheetHref) { michael@0: this._sheetURI = IOService.newURI(this.sheetHref, null, null); michael@0: } else { michael@0: this._sheetURI = null; michael@0: } michael@0: } michael@0: michael@0: return this._sheetURI; michael@0: }, michael@0: michael@0: /** michael@0: * Resolve a URI based on the rule stylesheet michael@0: * @param {string} relativePath the path to resolve michael@0: * @return {string} the resolved path. michael@0: */ michael@0: resolveURI: function(relativePath) { michael@0: if (this.sheetURI) { michael@0: relativePath = this.sheetURI.resolve(relativePath); michael@0: } michael@0: return relativePath; michael@0: }, michael@0: michael@0: /** michael@0: * Check the property value to find an external resource (if any). michael@0: * @return {string} the URI in the property value, or null if there is no match. michael@0: */ michael@0: getResourceURI: function() { michael@0: let val = this.prop.value; michael@0: let uriMatch = CSS_RESOURCE_RE.exec(val); michael@0: let uri = null; michael@0: michael@0: if (uriMatch && uriMatch[1]) { michael@0: uri = uriMatch[1]; michael@0: } michael@0: michael@0: return uri; michael@0: }, michael@0: michael@0: /** michael@0: * Populate the span based on changes to the TextProperty. michael@0: */ michael@0: update: function() { michael@0: if (this.prop.enabled) { michael@0: this.enable.style.removeProperty("visibility"); michael@0: this.enable.setAttribute("checked", ""); michael@0: } else { michael@0: this.enable.style.visibility = "visible"; michael@0: this.enable.removeAttribute("checked"); michael@0: } michael@0: michael@0: this.warning.hidden = this.editing || this.isValid(); michael@0: michael@0: if ((this.prop.overridden || !this.prop.enabled) && !this.editing) { michael@0: this.element.classList.add("ruleview-overridden"); michael@0: } else { michael@0: this.element.classList.remove("ruleview-overridden"); michael@0: } michael@0: michael@0: let name = this.prop.name; michael@0: this.nameSpan.textContent = name; michael@0: michael@0: // Combine the property's value and priority into one string for michael@0: // the value. michael@0: let val = this.prop.value; michael@0: if (this.prop.priority) { michael@0: val += " !" + this.prop.priority; michael@0: } michael@0: michael@0: let store = this.prop.rule.elementStyle.store; michael@0: let propDirty = store.userProperties.contains(this.prop.rule.style, name); michael@0: michael@0: if (propDirty) { michael@0: this.element.setAttribute("dirty", ""); michael@0: } else { michael@0: this.element.removeAttribute("dirty"); michael@0: } michael@0: michael@0: let swatchClass = "ruleview-colorswatch"; michael@0: let outputParser = this.ruleEditor.ruleView._outputParser; michael@0: let frag = outputParser.parseCssProperty(name, val, { michael@0: colorSwatchClass: swatchClass, michael@0: colorClass: "ruleview-color", michael@0: defaultColorType: !propDirty, michael@0: urlClass: "theme-link", michael@0: baseURI: this.sheetURI michael@0: }); michael@0: this.valueSpan.innerHTML = ""; michael@0: this.valueSpan.appendChild(frag); michael@0: michael@0: // Attach the color picker tooltip to the color swatches michael@0: this._swatchSpans = this.valueSpan.querySelectorAll("." + swatchClass); michael@0: for (let span of this._swatchSpans) { michael@0: // Capture the original declaration value to be able to revert later michael@0: let originalValue = this.valueSpan.textContent; michael@0: // Adding this swatch to the list of swatches our colorpicker knows about michael@0: this.ruleEditor.ruleView.colorPicker.addSwatch(span, { michael@0: onPreview: () => this._previewValue(this.valueSpan.textContent), michael@0: onCommit: () => this._applyNewValue(this.valueSpan.textContent), michael@0: onRevert: () => this._applyNewValue(originalValue) michael@0: }); michael@0: } michael@0: michael@0: // Populate the computed styles. michael@0: this._updateComputed(); michael@0: }, michael@0: michael@0: _onStartEditing: function() { michael@0: this.element.classList.remove("ruleview-overridden"); michael@0: this._previewValue(this.prop.value); michael@0: }, michael@0: michael@0: /** michael@0: * Populate the list of computed styles. michael@0: */ michael@0: _updateComputed: function () { michael@0: // Clear out existing viewers. michael@0: while (this.computed.hasChildNodes()) { michael@0: this.computed.removeChild(this.computed.lastChild); michael@0: } michael@0: michael@0: let showExpander = false; michael@0: for each (let computed in this.prop.computed) { michael@0: // Don't bother to duplicate information already michael@0: // shown in the text property. michael@0: if (computed.name === this.prop.name) { michael@0: continue; michael@0: } michael@0: michael@0: showExpander = true; michael@0: michael@0: let li = createChild(this.computed, "li", { michael@0: class: "ruleview-computed" michael@0: }); michael@0: michael@0: if (computed.overridden) { michael@0: li.classList.add("ruleview-overridden"); michael@0: } michael@0: michael@0: createChild(li, "span", { michael@0: class: "ruleview-propertyname theme-fg-color5", michael@0: textContent: computed.name michael@0: }); michael@0: appendText(li, ": "); michael@0: michael@0: let outputParser = this.ruleEditor.ruleView._outputParser; michael@0: let frag = outputParser.parseCssProperty( michael@0: computed.name, computed.value, { michael@0: colorSwatchClass: "ruleview-colorswatch", michael@0: urlClass: "theme-link", michael@0: baseURI: this.sheetURI michael@0: } michael@0: ); michael@0: michael@0: createChild(li, "span", { michael@0: class: "ruleview-propertyvalue theme-fg-color1", michael@0: child: frag michael@0: }); michael@0: michael@0: appendText(li, ";"); michael@0: } michael@0: michael@0: // Show or hide the expander as needed. michael@0: if (showExpander) { michael@0: this.expander.style.visibility = "visible"; michael@0: } else { michael@0: this.expander.style.visibility = "hidden"; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handles clicks on the disabled property. michael@0: */ michael@0: _onEnableClicked: function(aEvent) { michael@0: let checked = this.enable.hasAttribute("checked"); michael@0: if (checked) { michael@0: this.enable.removeAttribute("checked"); michael@0: } else { michael@0: this.enable.setAttribute("checked", ""); michael@0: } michael@0: this.prop.setEnabled(!checked); michael@0: aEvent.stopPropagation(); michael@0: }, michael@0: michael@0: /** michael@0: * Handles clicks on the computed property expander. michael@0: */ michael@0: _onExpandClicked: function(aEvent) { michael@0: this.computed.classList.toggle("styleinspector-open"); michael@0: if (this.computed.classList.contains("styleinspector-open")) { michael@0: this.expander.setAttribute("open", "true"); michael@0: } else { michael@0: this.expander.removeAttribute("open"); michael@0: } michael@0: aEvent.stopPropagation(); michael@0: }, michael@0: michael@0: /** michael@0: * Called when the property name's inplace editor is closed. michael@0: * Ignores the change if the user pressed escape, otherwise michael@0: * commits it. michael@0: * michael@0: * @param {string} aValue michael@0: * The value contained in the editor. michael@0: * @param {boolean} aCommit michael@0: * True if the change should be applied. michael@0: */ michael@0: _onNameDone: function(aValue, aCommit) { michael@0: if (aCommit) { michael@0: // Unlike the value editor, if a name is empty the entire property michael@0: // should always be removed. michael@0: if (aValue.trim() === "") { michael@0: this.remove(); michael@0: } else { michael@0: // Adding multiple rules inside of name field overwrites the current michael@0: // property with the first, then adds any more onto the property list. michael@0: let properties = parseDeclarations(aValue); michael@0: michael@0: if (properties.length) { michael@0: this.prop.setName(properties[0].name); michael@0: if (properties.length > 1) { michael@0: this.prop.setValue(properties[0].value, properties[0].priority); michael@0: this.ruleEditor.addProperties(properties.slice(1), this.prop); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Remove property from style and the editors from DOM. michael@0: * Begin editing next available property. michael@0: */ michael@0: remove: function() { michael@0: if (this._swatchSpans && this._swatchSpans.length) { michael@0: for (let span of this._swatchSpans) { michael@0: this.ruleEditor.ruleView.colorPicker.removeSwatch(span); michael@0: } michael@0: } michael@0: michael@0: this.element.parentNode.removeChild(this.element); michael@0: this.ruleEditor.rule.editClosestTextProperty(this.prop); michael@0: this.valueSpan.textProperty = null; michael@0: this.prop.remove(); michael@0: }, michael@0: michael@0: /** michael@0: * Called when a value editor closes. If the user pressed escape, michael@0: * revert to the value this property had before editing. michael@0: * michael@0: * @param {string} aValue michael@0: * The value contained in the editor. michael@0: * @param {bool} aCommit michael@0: * True if the change should be applied. michael@0: */ michael@0: _onValueDone: function(aValue, aCommit) { michael@0: if (!aCommit) { michael@0: // A new property should be removed when escape is pressed. michael@0: if (this.removeOnRevert) { michael@0: this.remove(); michael@0: } else { michael@0: this.prop.setValue(this.committed.value, this.committed.priority); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: let {propertiesToAdd,firstValue} = this._getValueAndExtraProperties(aValue); michael@0: michael@0: // First, set this property value (common case, only modified a property) michael@0: let val = parseSingleValue(firstValue); michael@0: this.prop.setValue(val.value, val.priority); michael@0: this.removeOnRevert = false; michael@0: this.committed.value = this.prop.value; michael@0: this.committed.priority = this.prop.priority; michael@0: michael@0: // If needed, add any new properties after this.prop. michael@0: this.ruleEditor.addProperties(propertiesToAdd, this.prop); michael@0: michael@0: // If the name or value is not actively being edited, and the value is michael@0: // empty, then remove the whole property. michael@0: // A timeout is used here to accurately check the state, since the inplace michael@0: // editor `done` and `destroy` events fire before the next editor michael@0: // is focused. michael@0: if (val.value.trim() === "") { michael@0: setTimeout(() => { michael@0: if (!this.editing) { michael@0: this.remove(); michael@0: } michael@0: }, 0); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Parse a value string and break it into pieces, starting with the michael@0: * first value, and into an array of additional properties (if any). michael@0: * michael@0: * Example: Calling with "red; width: 100px" would return michael@0: * { firstValue: "red", propertiesToAdd: [{ name: "width", value: "100px" }] } michael@0: * michael@0: * @param {string} aValue michael@0: * The string to parse michael@0: * @return {object} An object with the following properties: michael@0: * firstValue: A string containing a simple value, like michael@0: * "red" or "100px!important" michael@0: * propertiesToAdd: An array with additional properties, following the michael@0: * parseDeclarations format of {name,value,priority} michael@0: */ michael@0: _getValueAndExtraProperties: function(aValue) { michael@0: // The inplace editor will prevent manual typing of multiple properties, michael@0: // but we need to deal with the case during a paste event. michael@0: // Adding multiple properties inside of value editor sets value with the michael@0: // first, then adds any more onto the property list (below this property). michael@0: let firstValue = aValue; michael@0: let propertiesToAdd = []; michael@0: michael@0: let properties = parseDeclarations(aValue); michael@0: michael@0: // Check to see if the input string can be parsed as multiple properties michael@0: if (properties.length) { michael@0: // Get the first property value (if any), and any remaining properties (if any) michael@0: if (!properties[0].name && properties[0].value) { michael@0: firstValue = properties[0].value; michael@0: propertiesToAdd = properties.slice(1); michael@0: } michael@0: // In some cases, the value could be a property:value pair itself. michael@0: // Join them as one value string and append potentially following properties michael@0: else if (properties[0].name && properties[0].value) { michael@0: firstValue = properties[0].name + ": " + properties[0].value; michael@0: propertiesToAdd = properties.slice(1); michael@0: } michael@0: } michael@0: michael@0: return { michael@0: propertiesToAdd: propertiesToAdd, michael@0: firstValue: firstValue michael@0: }; michael@0: }, michael@0: michael@0: _applyNewValue: function(aValue) { michael@0: let val = parseSingleValue(aValue); michael@0: michael@0: this.prop.setValue(val.value, val.priority); michael@0: this.removeOnRevert = false; michael@0: this.committed.value = this.prop.value; michael@0: this.committed.priority = this.prop.priority; michael@0: }, michael@0: michael@0: /** michael@0: * Live preview this property, without committing changes. michael@0: * @param {string} aValue The value to set the current property to. michael@0: */ michael@0: _previewValue: function(aValue) { michael@0: // Since function call is throttled, we need to make sure we are still editing michael@0: if (!this.editing) { michael@0: return; michael@0: } michael@0: michael@0: let val = parseSingleValue(aValue); michael@0: this.ruleEditor.rule.previewPropertyValue(this.prop, val.value, val.priority); michael@0: }, michael@0: michael@0: /** michael@0: * Validate this property. Does it make sense for this value to be assigned michael@0: * to this property name? This does not apply the property value michael@0: * michael@0: * @param {string} [aValue] michael@0: * The property value used for validation. michael@0: * Defaults to the current value for this.prop michael@0: * michael@0: * @return {bool} true if the property value is valid, false otherwise. michael@0: */ michael@0: isValid: function(aValue) { michael@0: let name = this.prop.name; michael@0: let value = typeof aValue == "undefined" ? this.prop.value : aValue; michael@0: let val = parseSingleValue(value); michael@0: michael@0: let style = this.doc.createElementNS(HTML_NS, "div").style; michael@0: let prefs = Services.prefs; michael@0: michael@0: // We toggle output of errors whilst the user is typing a property value. michael@0: let prefVal = prefs.getBoolPref("layout.css.report_errors"); michael@0: prefs.setBoolPref("layout.css.report_errors", false); michael@0: michael@0: let validValue = false; michael@0: try { michael@0: style.setProperty(name, val.value, val.priority); michael@0: validValue = style.getPropertyValue(name) !== "" || val.value === ""; michael@0: } finally { michael@0: prefs.setBoolPref("layout.css.report_errors", prefVal); michael@0: } michael@0: return validValue; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Store of CSSStyleDeclarations mapped to properties that have been changed by michael@0: * the user. michael@0: */ michael@0: function UserProperties() { michael@0: this.map = new Map(); michael@0: } michael@0: michael@0: UserProperties.prototype = { michael@0: /** michael@0: * Get a named property for a given CSSStyleDeclaration. michael@0: * michael@0: * @param {CSSStyleDeclaration} aStyle michael@0: * The CSSStyleDeclaration against which the property is mapped. michael@0: * @param {string} aName michael@0: * The name of the property to get. michael@0: * @param {string} aDefault michael@0: * The value to return if the property is has been changed outside of michael@0: * the rule view. michael@0: * @return {string} michael@0: * The property value if it has previously been set by the user, null michael@0: * otherwise. michael@0: */ michael@0: getProperty: function(aStyle, aName, aDefault) { michael@0: let key = this.getKey(aStyle); michael@0: let entry = this.map.get(key, null); michael@0: michael@0: if (entry && aName in entry) { michael@0: let item = entry[aName]; michael@0: if (item != aDefault) { michael@0: delete entry[aName]; michael@0: return aDefault; michael@0: } michael@0: return item; michael@0: } michael@0: return aDefault; michael@0: }, michael@0: michael@0: /** michael@0: * Set a named property for a given CSSStyleDeclaration. michael@0: * michael@0: * @param {CSSStyleDeclaration} aStyle michael@0: * The CSSStyleDeclaration against which the property is to be mapped. michael@0: * @param {String} aName michael@0: * The name of the property to set. michael@0: * @param {String} aUserValue michael@0: * The value of the property to set. michael@0: */ michael@0: setProperty: function(aStyle, aName, aUserValue) { michael@0: let key = this.getKey(aStyle); michael@0: let entry = this.map.get(key, null); michael@0: if (entry) { michael@0: entry[aName] = aUserValue; michael@0: } else { michael@0: let props = {}; michael@0: props[aName] = aUserValue; michael@0: this.map.set(key, props); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Check whether a named property for a given CSSStyleDeclaration is stored. michael@0: * michael@0: * @param {CSSStyleDeclaration} aStyle michael@0: * The CSSStyleDeclaration against which the property would be mapped. michael@0: * @param {String} aName michael@0: * The name of the property to check. michael@0: */ michael@0: contains: function(aStyle, aName) { michael@0: let key = this.getKey(aStyle); michael@0: let entry = this.map.get(key, null); michael@0: return !!entry && aName in entry; michael@0: }, michael@0: michael@0: getKey: function(aStyle) { michael@0: return aStyle.href + ":" + aStyle.line; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Helper functions michael@0: */ michael@0: michael@0: /** michael@0: * Create a child element with a set of attributes. michael@0: * michael@0: * @param {Element} aParent michael@0: * The parent node. michael@0: * @param {string} aTag michael@0: * The tag name. michael@0: * @param {object} aAttributes michael@0: * A set of attributes to set on the node. michael@0: */ michael@0: function createChild(aParent, aTag, aAttributes) { michael@0: let elt = aParent.ownerDocument.createElementNS(HTML_NS, aTag); michael@0: for (let attr in aAttributes) { michael@0: if (aAttributes.hasOwnProperty(attr)) { michael@0: if (attr === "textContent") { michael@0: elt.textContent = aAttributes[attr]; michael@0: } else if(attr === "child") { michael@0: elt.appendChild(aAttributes[attr]); michael@0: } else { michael@0: elt.setAttribute(attr, aAttributes[attr]); michael@0: } michael@0: } michael@0: } michael@0: aParent.appendChild(elt); michael@0: return elt; michael@0: } michael@0: michael@0: function createMenuItem(aMenu, aAttributes) { michael@0: let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem"); michael@0: michael@0: item.setAttribute("label", _strings.GetStringFromName(aAttributes.label)); michael@0: item.setAttribute("accesskey", _strings.GetStringFromName(aAttributes.accesskey)); michael@0: item.addEventListener("command", aAttributes.command); michael@0: michael@0: aMenu.appendChild(item); michael@0: michael@0: return item; michael@0: } michael@0: michael@0: function setTimeout() { michael@0: let window = Services.appShell.hiddenDOMWindow; michael@0: return window.setTimeout.apply(window, arguments); michael@0: } michael@0: michael@0: function clearTimeout() { michael@0: let window = Services.appShell.hiddenDOMWindow; michael@0: return window.clearTimeout.apply(window, arguments); michael@0: } michael@0: michael@0: function throttle(func, wait, scope) { michael@0: var timer = null; michael@0: return function() { michael@0: if(timer) { michael@0: clearTimeout(timer); michael@0: } michael@0: var args = arguments; michael@0: timer = setTimeout(function() { michael@0: timer = null; michael@0: func.apply(scope, args); michael@0: }, wait); michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * Event handler that causes a blur on the target if the input has michael@0: * multiple CSS properties as the value. michael@0: */ michael@0: function blurOnMultipleProperties(e) { michael@0: setTimeout(() => { michael@0: let props = parseDeclarations(e.target.value); michael@0: if (props.length > 1) { michael@0: e.target.blur(); michael@0: } michael@0: }, 0); michael@0: } michael@0: michael@0: /** michael@0: * Append a text node to an element. michael@0: */ michael@0: function appendText(aParent, aText) { michael@0: aParent.appendChild(aParent.ownerDocument.createTextNode(aText)); michael@0: } michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() { michael@0: return Cc["@mozilla.org/widget/clipboardhelper;1"]. michael@0: getService(Ci.nsIClipboardHelper); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "_strings", function() { michael@0: return Services.strings.createBundle( michael@0: "chrome://global/locale/devtools/styleinspector.properties"); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "domUtils", function() { michael@0: return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); michael@0: }); michael@0: michael@0: loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup);