1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/styleinspector/rule-view.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,2722 @@ 1.4 +/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +"use strict"; 1.11 + 1.12 +const {Cc, Ci, Cu} = require("chrome"); 1.13 +const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); 1.14 +const {CssLogic} = require("devtools/styleinspector/css-logic"); 1.15 +const {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor"); 1.16 +const {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles"); 1.17 +const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); 1.18 +const {Tooltip, SwatchColorPickerTooltip} = require("devtools/shared/widgets/Tooltip"); 1.19 +const {OutputParser} = require("devtools/output-parser"); 1.20 +const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils"); 1.21 +const {parseSingleValue, parseDeclarations} = require("devtools/styleinspector/css-parsing-utils"); 1.22 + 1.23 +Cu.import("resource://gre/modules/Services.jsm"); 1.24 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.25 + 1.26 +const HTML_NS = "http://www.w3.org/1999/xhtml"; 1.27 +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; 1.28 + 1.29 +/** 1.30 + * These regular expressions are adapted from firebug's css.js, and are 1.31 + * used to parse CSSStyleDeclaration's cssText attribute. 1.32 + */ 1.33 + 1.34 +// Used to split on css line separators 1.35 +const CSS_LINE_RE = /(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g; 1.36 + 1.37 +// Used to parse a single property line. 1.38 +const CSS_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*(?:! (important))?;?$/; 1.39 + 1.40 +// Used to parse an external resource from a property value 1.41 +const CSS_RESOURCE_RE = /url\([\'\"]?(.*?)[\'\"]?\)/; 1.42 + 1.43 +const IOService = Cc["@mozilla.org/network/io-service;1"] 1.44 + .getService(Ci.nsIIOService); 1.45 + 1.46 +function promiseWarn(err) { 1.47 + console.error(err); 1.48 + return promise.reject(err); 1.49 +} 1.50 + 1.51 +/** 1.52 + * To figure out how shorthand properties are interpreted by the 1.53 + * engine, we will set properties on a dummy element and observe 1.54 + * how their .style attribute reflects them as computed values. 1.55 + * This function creates the document in which those dummy elements 1.56 + * will be created. 1.57 + */ 1.58 +var gDummyPromise; 1.59 +function createDummyDocument() { 1.60 + if (gDummyPromise) { 1.61 + return gDummyPromise; 1.62 + } 1.63 + const { getDocShell, create: makeFrame } = require("sdk/frame/utils"); 1.64 + 1.65 + let frame = makeFrame(Services.appShell.hiddenDOMWindow.document, { 1.66 + nodeName: "iframe", 1.67 + namespaceURI: "http://www.w3.org/1999/xhtml", 1.68 + allowJavascript: false, 1.69 + allowPlugins: false, 1.70 + allowAuth: false 1.71 + }); 1.72 + let docShell = getDocShell(frame); 1.73 + let eventTarget = docShell.chromeEventHandler; 1.74 + docShell.createAboutBlankContentViewer(Cc["@mozilla.org/nullprincipal;1"].createInstance(Ci.nsIPrincipal)); 1.75 + let window = docShell.contentViewer.DOMDocument.defaultView; 1.76 + window.location = "data:text/html,<html></html>"; 1.77 + let deferred = promise.defer(); 1.78 + eventTarget.addEventListener("DOMContentLoaded", function handler(event) { 1.79 + eventTarget.removeEventListener("DOMContentLoaded", handler, false); 1.80 + deferred.resolve(window.document); 1.81 + frame.remove(); 1.82 + }, false); 1.83 + gDummyPromise = deferred.promise; 1.84 + return gDummyPromise; 1.85 +} 1.86 + 1.87 +/** 1.88 + * Our model looks like this: 1.89 + * 1.90 + * ElementStyle: 1.91 + * Responsible for keeping track of which properties are overridden. 1.92 + * Maintains a list of Rule objects that apply to the element. 1.93 + * Rule: 1.94 + * Manages a single style declaration or rule. 1.95 + * Responsible for applying changes to the properties in a rule. 1.96 + * Maintains a list of TextProperty objects. 1.97 + * TextProperty: 1.98 + * Manages a single property from the cssText attribute of the 1.99 + * relevant declaration. 1.100 + * Maintains a list of computed properties that come from this 1.101 + * property declaration. 1.102 + * Changes to the TextProperty are sent to its related Rule for 1.103 + * application. 1.104 + */ 1.105 + 1.106 +/** 1.107 + * ElementStyle maintains a list of Rule objects for a given element. 1.108 + * 1.109 + * @param {Element} aElement 1.110 + * The element whose style we are viewing. 1.111 + * @param {object} aStore 1.112 + * The ElementStyle can use this object to store metadata 1.113 + * that might outlast the rule view, particularly the current 1.114 + * set of disabled properties. 1.115 + * @param {PageStyleFront} aPageStyle 1.116 + * Front for the page style actor that will be providing 1.117 + * the style information. 1.118 + * 1.119 + * @constructor 1.120 + */ 1.121 +function ElementStyle(aElement, aStore, aPageStyle) { 1.122 + this.element = aElement; 1.123 + this.store = aStore || {}; 1.124 + this.pageStyle = aPageStyle; 1.125 + 1.126 + // We don't want to overwrite this.store.userProperties so we only create it 1.127 + // if it doesn't already exist. 1.128 + if (!("userProperties" in this.store)) { 1.129 + this.store.userProperties = new UserProperties(); 1.130 + } 1.131 + 1.132 + if (!("disabled" in this.store)) { 1.133 + this.store.disabled = new WeakMap(); 1.134 + } 1.135 +} 1.136 + 1.137 +// We're exporting _ElementStyle for unit tests. 1.138 +exports._ElementStyle = ElementStyle; 1.139 + 1.140 +ElementStyle.prototype = { 1.141 + // The element we're looking at. 1.142 + element: null, 1.143 + 1.144 + // Empty, unconnected element of the same type as this node, used 1.145 + // to figure out how shorthand properties will be parsed. 1.146 + dummyElement: null, 1.147 + 1.148 + init: function() 1.149 + { 1.150 + // To figure out how shorthand properties are interpreted by the 1.151 + // engine, we will set properties on a dummy element and observe 1.152 + // how their .style attribute reflects them as computed values. 1.153 + return this.dummyElementPromise = createDummyDocument().then(document => { 1.154 + this.dummyElement = document.createElementNS(this.element.namespaceURI, 1.155 + this.element.tagName); 1.156 + document.documentElement.appendChild(this.dummyElement); 1.157 + return this.dummyElement; 1.158 + }).then(null, promiseWarn); 1.159 + }, 1.160 + 1.161 + destroy: function() { 1.162 + this.dummyElement = null; 1.163 + this.dummyElementPromise.then(dummyElement => { 1.164 + if (dummyElement.parentNode) { 1.165 + dummyElement.parentNode.removeChild(dummyElement); 1.166 + } 1.167 + this.dummyElementPromise = null; 1.168 + }); 1.169 + }, 1.170 + 1.171 + /** 1.172 + * Called by the Rule object when it has been changed through the 1.173 + * setProperty* methods. 1.174 + */ 1.175 + _changed: function() { 1.176 + if (this.onChanged) { 1.177 + this.onChanged(); 1.178 + } 1.179 + }, 1.180 + 1.181 + /** 1.182 + * Refresh the list of rules to be displayed for the active element. 1.183 + * Upon completion, this.rules[] will hold a list of Rule objects. 1.184 + * 1.185 + * Returns a promise that will be resolved when the elementStyle is 1.186 + * ready. 1.187 + */ 1.188 + populate: function() { 1.189 + let populated = this.pageStyle.getApplied(this.element, { 1.190 + inherited: true, 1.191 + matchedSelectors: true 1.192 + }).then(entries => { 1.193 + // Make sure the dummy element has been created before continuing... 1.194 + return this.dummyElementPromise.then(() => { 1.195 + if (this.populated != populated) { 1.196 + // Don't care anymore. 1.197 + return promise.reject("unused"); 1.198 + } 1.199 + 1.200 + // Store the current list of rules (if any) during the population 1.201 + // process. They will be reused if possible. 1.202 + this._refreshRules = this.rules; 1.203 + 1.204 + this.rules = []; 1.205 + 1.206 + for (let entry of entries) { 1.207 + this._maybeAddRule(entry); 1.208 + } 1.209 + 1.210 + // Mark overridden computed styles. 1.211 + this.markOverriddenAll(); 1.212 + 1.213 + this._sortRulesForPseudoElement(); 1.214 + 1.215 + // We're done with the previous list of rules. 1.216 + delete this._refreshRules; 1.217 + 1.218 + return null; 1.219 + }); 1.220 + }).then(null, promiseWarn); 1.221 + this.populated = populated; 1.222 + return this.populated; 1.223 + }, 1.224 + 1.225 + /** 1.226 + * Put pseudo elements in front of others. 1.227 + */ 1.228 + _sortRulesForPseudoElement: function() { 1.229 + this.rules = this.rules.sort((a, b) => { 1.230 + return (a.pseudoElement || "z") > (b.pseudoElement || "z"); 1.231 + }); 1.232 + }, 1.233 + 1.234 + /** 1.235 + * Add a rule if it's one we care about. Filters out duplicates and 1.236 + * inherited styles with no inherited properties. 1.237 + * 1.238 + * @param {object} aOptions 1.239 + * Options for creating the Rule, see the Rule constructor. 1.240 + * 1.241 + * @return {bool} true if we added the rule. 1.242 + */ 1.243 + _maybeAddRule: function(aOptions) { 1.244 + // If we've already included this domRule (for example, when a 1.245 + // common selector is inherited), ignore it. 1.246 + if (aOptions.rule && 1.247 + this.rules.some(function(rule) rule.domRule === aOptions.rule)) { 1.248 + return false; 1.249 + } 1.250 + 1.251 + if (aOptions.system) { 1.252 + return false; 1.253 + } 1.254 + 1.255 + let rule = null; 1.256 + 1.257 + // If we're refreshing and the rule previously existed, reuse the 1.258 + // Rule object. 1.259 + if (this._refreshRules) { 1.260 + for (let r of this._refreshRules) { 1.261 + if (r.matches(aOptions)) { 1.262 + rule = r; 1.263 + rule.refresh(aOptions); 1.264 + break; 1.265 + } 1.266 + } 1.267 + } 1.268 + 1.269 + // If this is a new rule, create its Rule object. 1.270 + if (!rule) { 1.271 + rule = new Rule(this, aOptions); 1.272 + } 1.273 + 1.274 + // Ignore inherited rules with no properties. 1.275 + if (aOptions.inherited && rule.textProps.length == 0) { 1.276 + return false; 1.277 + } 1.278 + 1.279 + this.rules.push(rule); 1.280 + return true; 1.281 + }, 1.282 + 1.283 + /** 1.284 + * Calls markOverridden with all supported pseudo elements 1.285 + */ 1.286 + markOverriddenAll: function() { 1.287 + this.markOverridden(); 1.288 + for (let pseudo of PSEUDO_ELEMENTS) { 1.289 + this.markOverridden(pseudo); 1.290 + } 1.291 + }, 1.292 + 1.293 + /** 1.294 + * Mark the properties listed in this.rules for a given pseudo element 1.295 + * with an overridden flag if an earlier property overrides it. 1.296 + * @param {string} pseudo 1.297 + * Which pseudo element to flag as overridden. 1.298 + * Empty string or undefined will default to no pseudo element. 1.299 + */ 1.300 + markOverridden: function(pseudo="") { 1.301 + // Gather all the text properties applied by these rules, ordered 1.302 + // from more- to less-specific. 1.303 + let textProps = []; 1.304 + for (let rule of this.rules) { 1.305 + if (rule.pseudoElement == pseudo) { 1.306 + textProps = textProps.concat(rule.textProps.slice(0).reverse()); 1.307 + } 1.308 + } 1.309 + 1.310 + // Gather all the computed properties applied by those text 1.311 + // properties. 1.312 + let computedProps = []; 1.313 + for (let textProp of textProps) { 1.314 + computedProps = computedProps.concat(textProp.computed); 1.315 + } 1.316 + 1.317 + // Walk over the computed properties. As we see a property name 1.318 + // for the first time, mark that property's name as taken by this 1.319 + // property. 1.320 + // 1.321 + // If we come across a property whose name is already taken, check 1.322 + // its priority against the property that was found first: 1.323 + // 1.324 + // If the new property is a higher priority, mark the old 1.325 + // property overridden and mark the property name as taken by 1.326 + // the new property. 1.327 + // 1.328 + // If the new property is a lower or equal priority, mark it as 1.329 + // overridden. 1.330 + // 1.331 + // _overriddenDirty will be set on each prop, indicating whether its 1.332 + // dirty status changed during this pass. 1.333 + let taken = {}; 1.334 + for (let computedProp of computedProps) { 1.335 + let earlier = taken[computedProp.name]; 1.336 + let overridden; 1.337 + if (earlier && 1.338 + computedProp.priority === "important" && 1.339 + earlier.priority !== "important") { 1.340 + // New property is higher priority. Mark the earlier property 1.341 + // overridden (which will reverse its dirty state). 1.342 + earlier._overriddenDirty = !earlier._overriddenDirty; 1.343 + earlier.overridden = true; 1.344 + overridden = false; 1.345 + } else { 1.346 + overridden = !!earlier; 1.347 + } 1.348 + 1.349 + computedProp._overriddenDirty = (!!computedProp.overridden != overridden); 1.350 + computedProp.overridden = overridden; 1.351 + if (!computedProp.overridden && computedProp.textProp.enabled) { 1.352 + taken[computedProp.name] = computedProp; 1.353 + } 1.354 + } 1.355 + 1.356 + // For each TextProperty, mark it overridden if all of its 1.357 + // computed properties are marked overridden. Update the text 1.358 + // property's associated editor, if any. This will clear the 1.359 + // _overriddenDirty state on all computed properties. 1.360 + for (let textProp of textProps) { 1.361 + // _updatePropertyOverridden will return true if the 1.362 + // overridden state has changed for the text property. 1.363 + if (this._updatePropertyOverridden(textProp)) { 1.364 + textProp.updateEditor(); 1.365 + } 1.366 + } 1.367 + }, 1.368 + 1.369 + /** 1.370 + * Mark a given TextProperty as overridden or not depending on the 1.371 + * state of its computed properties. Clears the _overriddenDirty state 1.372 + * on all computed properties. 1.373 + * 1.374 + * @param {TextProperty} aProp 1.375 + * The text property to update. 1.376 + * 1.377 + * @return {bool} true if the TextProperty's overridden state (or any of its 1.378 + * computed properties overridden state) changed. 1.379 + */ 1.380 + _updatePropertyOverridden: function(aProp) { 1.381 + let overridden = true; 1.382 + let dirty = false; 1.383 + for each (let computedProp in aProp.computed) { 1.384 + if (!computedProp.overridden) { 1.385 + overridden = false; 1.386 + } 1.387 + dirty = computedProp._overriddenDirty || dirty; 1.388 + delete computedProp._overriddenDirty; 1.389 + } 1.390 + 1.391 + dirty = (!!aProp.overridden != overridden) || dirty; 1.392 + aProp.overridden = overridden; 1.393 + return dirty; 1.394 + } 1.395 +}; 1.396 + 1.397 +/** 1.398 + * A single style rule or declaration. 1.399 + * 1.400 + * @param {ElementStyle} aElementStyle 1.401 + * The ElementStyle to which this rule belongs. 1.402 + * @param {object} aOptions 1.403 + * The information used to construct this rule. Properties include: 1.404 + * rule: A StyleRuleActor 1.405 + * inherited: An element this rule was inherited from. If omitted, 1.406 + * the rule applies directly to the current element. 1.407 + * @constructor 1.408 + */ 1.409 +function Rule(aElementStyle, aOptions) { 1.410 + this.elementStyle = aElementStyle; 1.411 + this.domRule = aOptions.rule || null; 1.412 + this.style = aOptions.rule; 1.413 + this.matchedSelectors = aOptions.matchedSelectors || []; 1.414 + this.pseudoElement = aOptions.pseudoElement || ""; 1.415 + 1.416 + this.inherited = aOptions.inherited || null; 1.417 + this._modificationDepth = 0; 1.418 + 1.419 + if (this.domRule) { 1.420 + let parentRule = this.domRule.parentRule; 1.421 + if (parentRule && parentRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE) { 1.422 + this.mediaText = parentRule.mediaText; 1.423 + } 1.424 + } 1.425 + 1.426 + // Populate the text properties with the style's current cssText 1.427 + // value, and add in any disabled properties from the store. 1.428 + this.textProps = this._getTextProperties(); 1.429 + this.textProps = this.textProps.concat(this._getDisabledProperties()); 1.430 +} 1.431 + 1.432 +Rule.prototype = { 1.433 + mediaText: "", 1.434 + 1.435 + get title() { 1.436 + if (this._title) { 1.437 + return this._title; 1.438 + } 1.439 + this._title = CssLogic.shortSource(this.sheet); 1.440 + if (this.domRule.type !== ELEMENT_STYLE) { 1.441 + this._title += ":" + this.ruleLine; 1.442 + } 1.443 + 1.444 + this._title = this._title + (this.mediaText ? " @media " + this.mediaText : ""); 1.445 + return this._title; 1.446 + }, 1.447 + 1.448 + get inheritedSource() { 1.449 + if (this._inheritedSource) { 1.450 + return this._inheritedSource; 1.451 + } 1.452 + this._inheritedSource = ""; 1.453 + if (this.inherited) { 1.454 + let eltText = this.inherited.tagName.toLowerCase(); 1.455 + if (this.inherited.id) { 1.456 + eltText += "#" + this.inherited.id; 1.457 + } 1.458 + this._inheritedSource = 1.459 + CssLogic._strings.formatStringFromName("rule.inheritedFrom", [eltText], 1); 1.460 + } 1.461 + return this._inheritedSource; 1.462 + }, 1.463 + 1.464 + get selectorText() { 1.465 + return this.domRule.selectors ? this.domRule.selectors.join(", ") : CssLogic.l10n("rule.sourceElement"); 1.466 + }, 1.467 + 1.468 + /** 1.469 + * The rule's stylesheet. 1.470 + */ 1.471 + get sheet() { 1.472 + return this.domRule ? this.domRule.parentStyleSheet : null; 1.473 + }, 1.474 + 1.475 + /** 1.476 + * The rule's line within a stylesheet 1.477 + */ 1.478 + get ruleLine() { 1.479 + return this.domRule ? this.domRule.line : null; 1.480 + }, 1.481 + 1.482 + /** 1.483 + * The rule's column within a stylesheet 1.484 + */ 1.485 + get ruleColumn() { 1.486 + return this.domRule ? this.domRule.column : null; 1.487 + }, 1.488 + 1.489 + /** 1.490 + * Get display name for this rule based on the original source 1.491 + * for this rule's style sheet. 1.492 + * 1.493 + * @return {Promise} 1.494 + * Promise which resolves with location as an object containing 1.495 + * both the full and short version of the source string. 1.496 + */ 1.497 + getOriginalSourceStrings: function() { 1.498 + if (this._originalSourceStrings) { 1.499 + return promise.resolve(this._originalSourceStrings); 1.500 + } 1.501 + return this.domRule.getOriginalLocation().then(({href, line}) => { 1.502 + let sourceStrings = { 1.503 + full: href + ":" + line, 1.504 + short: CssLogic.shortSource({href: href}) + ":" + line 1.505 + }; 1.506 + 1.507 + this._originalSourceStrings = sourceStrings; 1.508 + return sourceStrings; 1.509 + }); 1.510 + }, 1.511 + 1.512 + /** 1.513 + * Returns true if the rule matches the creation options 1.514 + * specified. 1.515 + * 1.516 + * @param {object} aOptions 1.517 + * Creation options. See the Rule constructor for documentation. 1.518 + */ 1.519 + matches: function(aOptions) { 1.520 + return this.style === aOptions.rule; 1.521 + }, 1.522 + 1.523 + /** 1.524 + * Create a new TextProperty to include in the rule. 1.525 + * 1.526 + * @param {string} aName 1.527 + * The text property name (such as "background" or "border-top"). 1.528 + * @param {string} aValue 1.529 + * The property's value (not including priority). 1.530 + * @param {string} aPriority 1.531 + * The property's priority (either "important" or an empty string). 1.532 + * @param {TextProperty} aSiblingProp 1.533 + * Optional, property next to which the new property will be added. 1.534 + */ 1.535 + createProperty: function(aName, aValue, aPriority, aSiblingProp) { 1.536 + let prop = new TextProperty(this, aName, aValue, aPriority); 1.537 + 1.538 + if (aSiblingProp) { 1.539 + let ind = this.textProps.indexOf(aSiblingProp); 1.540 + this.textProps.splice(ind + 1, 0, prop); 1.541 + } 1.542 + else { 1.543 + this.textProps.push(prop); 1.544 + } 1.545 + 1.546 + this.applyProperties(); 1.547 + return prop; 1.548 + }, 1.549 + 1.550 + /** 1.551 + * Reapply all the properties in this rule, and update their 1.552 + * computed styles. Store disabled properties in the element 1.553 + * style's store. Will re-mark overridden properties. 1.554 + * 1.555 + * @param {string} [aName] 1.556 + * A text property name (such as "background" or "border-top") used 1.557 + * when calling from setPropertyValue & setPropertyName to signify 1.558 + * that the property should be saved in store.userProperties. 1.559 + */ 1.560 + applyProperties: function(aModifications, aName) { 1.561 + this.elementStyle.markOverriddenAll(); 1.562 + 1.563 + if (!aModifications) { 1.564 + aModifications = this.style.startModifyingProperties(); 1.565 + } 1.566 + let disabledProps = []; 1.567 + let store = this.elementStyle.store; 1.568 + 1.569 + for (let prop of this.textProps) { 1.570 + if (!prop.enabled) { 1.571 + disabledProps.push({ 1.572 + name: prop.name, 1.573 + value: prop.value, 1.574 + priority: prop.priority 1.575 + }); 1.576 + continue; 1.577 + } 1.578 + if (prop.value.trim() === "") { 1.579 + continue; 1.580 + } 1.581 + 1.582 + aModifications.setProperty(prop.name, prop.value, prop.priority); 1.583 + 1.584 + prop.updateComputed(); 1.585 + } 1.586 + 1.587 + // Store disabled properties in the disabled store. 1.588 + let disabled = this.elementStyle.store.disabled; 1.589 + if (disabledProps.length > 0) { 1.590 + disabled.set(this.style, disabledProps); 1.591 + } else { 1.592 + disabled.delete(this.style); 1.593 + } 1.594 + 1.595 + let promise = aModifications.apply().then(() => { 1.596 + let cssProps = {}; 1.597 + for (let cssProp of parseDeclarations(this.style.cssText)) { 1.598 + cssProps[cssProp.name] = cssProp; 1.599 + } 1.600 + 1.601 + for (let textProp of this.textProps) { 1.602 + if (!textProp.enabled) { 1.603 + continue; 1.604 + } 1.605 + let cssProp = cssProps[textProp.name]; 1.606 + 1.607 + if (!cssProp) { 1.608 + cssProp = { 1.609 + name: textProp.name, 1.610 + value: "", 1.611 + priority: "" 1.612 + }; 1.613 + } 1.614 + 1.615 + if (aName && textProp.name == aName) { 1.616 + store.userProperties.setProperty( 1.617 + this.style, 1.618 + textProp.name, 1.619 + textProp.value); 1.620 + } 1.621 + textProp.priority = cssProp.priority; 1.622 + } 1.623 + 1.624 + this.elementStyle.markOverriddenAll(); 1.625 + 1.626 + if (promise === this._applyingModifications) { 1.627 + this._applyingModifications = null; 1.628 + } 1.629 + 1.630 + this.elementStyle._changed(); 1.631 + }).then(null, promiseWarn); 1.632 + 1.633 + this._applyingModifications = promise; 1.634 + return promise; 1.635 + }, 1.636 + 1.637 + /** 1.638 + * Renames a property. 1.639 + * 1.640 + * @param {TextProperty} aProperty 1.641 + * The property to rename. 1.642 + * @param {string} aName 1.643 + * The new property name (such as "background" or "border-top"). 1.644 + */ 1.645 + setPropertyName: function(aProperty, aName) { 1.646 + if (aName === aProperty.name) { 1.647 + return; 1.648 + } 1.649 + let modifications = this.style.startModifyingProperties(); 1.650 + modifications.removeProperty(aProperty.name); 1.651 + aProperty.name = aName; 1.652 + this.applyProperties(modifications, aName); 1.653 + }, 1.654 + 1.655 + /** 1.656 + * Sets the value and priority of a property, then reapply all properties. 1.657 + * 1.658 + * @param {TextProperty} aProperty 1.659 + * The property to manipulate. 1.660 + * @param {string} aValue 1.661 + * The property's value (not including priority). 1.662 + * @param {string} aPriority 1.663 + * The property's priority (either "important" or an empty string). 1.664 + */ 1.665 + setPropertyValue: function(aProperty, aValue, aPriority) { 1.666 + if (aValue === aProperty.value && aPriority === aProperty.priority) { 1.667 + return; 1.668 + } 1.669 + 1.670 + aProperty.value = aValue; 1.671 + aProperty.priority = aPriority; 1.672 + this.applyProperties(null, aProperty.name); 1.673 + }, 1.674 + 1.675 + /** 1.676 + * Just sets the value and priority of a property, in order to preview its 1.677 + * effect on the content document. 1.678 + * 1.679 + * @param {TextProperty} aProperty 1.680 + * The property which value will be previewed 1.681 + * @param {String} aValue 1.682 + * The value to be used for the preview 1.683 + * @param {String} aPriority 1.684 + * The property's priority (either "important" or an empty string). 1.685 + */ 1.686 + previewPropertyValue: function(aProperty, aValue, aPriority) { 1.687 + let modifications = this.style.startModifyingProperties(); 1.688 + modifications.setProperty(aProperty.name, aValue, aPriority); 1.689 + modifications.apply(); 1.690 + }, 1.691 + 1.692 + /** 1.693 + * Disables or enables given TextProperty. 1.694 + * 1.695 + * @param {TextProperty} aProperty 1.696 + * The property to enable/disable 1.697 + * @param {Boolean} aValue 1.698 + */ 1.699 + setPropertyEnabled: function(aProperty, aValue) { 1.700 + aProperty.enabled = !!aValue; 1.701 + let modifications = this.style.startModifyingProperties(); 1.702 + if (!aProperty.enabled) { 1.703 + modifications.removeProperty(aProperty.name); 1.704 + } 1.705 + this.applyProperties(modifications); 1.706 + }, 1.707 + 1.708 + /** 1.709 + * Remove a given TextProperty from the rule and update the rule 1.710 + * accordingly. 1.711 + * 1.712 + * @param {TextProperty} aProperty 1.713 + * The property to be removed 1.714 + */ 1.715 + removeProperty: function(aProperty) { 1.716 + this.textProps = this.textProps.filter(function(prop) prop != aProperty); 1.717 + let modifications = this.style.startModifyingProperties(); 1.718 + modifications.removeProperty(aProperty.name); 1.719 + // Need to re-apply properties in case removing this TextProperty 1.720 + // exposes another one. 1.721 + this.applyProperties(modifications); 1.722 + }, 1.723 + 1.724 + /** 1.725 + * Get the list of TextProperties from the style. Needs 1.726 + * to parse the style's cssText. 1.727 + */ 1.728 + _getTextProperties: function() { 1.729 + let textProps = []; 1.730 + let store = this.elementStyle.store; 1.731 + let props = parseDeclarations(this.style.cssText); 1.732 + for (let prop of props) { 1.733 + let name = prop.name; 1.734 + if (this.inherited && !domUtils.isInheritedProperty(name)) { 1.735 + continue; 1.736 + } 1.737 + let value = store.userProperties.getProperty(this.style, name, prop.value); 1.738 + let textProp = new TextProperty(this, name, value, prop.priority); 1.739 + textProps.push(textProp); 1.740 + } 1.741 + 1.742 + return textProps; 1.743 + }, 1.744 + 1.745 + /** 1.746 + * Return the list of disabled properties from the store for this rule. 1.747 + */ 1.748 + _getDisabledProperties: function() { 1.749 + let store = this.elementStyle.store; 1.750 + 1.751 + // Include properties from the disabled property store, if any. 1.752 + let disabledProps = store.disabled.get(this.style); 1.753 + if (!disabledProps) { 1.754 + return []; 1.755 + } 1.756 + 1.757 + let textProps = []; 1.758 + 1.759 + for each (let prop in disabledProps) { 1.760 + let value = store.userProperties.getProperty(this.style, prop.name, prop.value); 1.761 + let textProp = new TextProperty(this, prop.name, value, prop.priority); 1.762 + textProp.enabled = false; 1.763 + textProps.push(textProp); 1.764 + } 1.765 + 1.766 + return textProps; 1.767 + }, 1.768 + 1.769 + /** 1.770 + * Reread the current state of the rules and rebuild text 1.771 + * properties as needed. 1.772 + */ 1.773 + refresh: function(aOptions) { 1.774 + this.matchedSelectors = aOptions.matchedSelectors || []; 1.775 + let newTextProps = this._getTextProperties(); 1.776 + 1.777 + // Update current properties for each property present on the style. 1.778 + // This will mark any touched properties with _visited so we 1.779 + // can detect properties that weren't touched (because they were 1.780 + // removed from the style). 1.781 + // Also keep track of properties that didn't exist in the current set 1.782 + // of properties. 1.783 + let brandNewProps = []; 1.784 + for (let newProp of newTextProps) { 1.785 + if (!this._updateTextProperty(newProp)) { 1.786 + brandNewProps.push(newProp); 1.787 + } 1.788 + } 1.789 + 1.790 + // Refresh editors and disabled state for all the properties that 1.791 + // were updated. 1.792 + for (let prop of this.textProps) { 1.793 + // Properties that weren't touched during the update 1.794 + // process must no longer exist on the node. Mark them disabled. 1.795 + if (!prop._visited) { 1.796 + prop.enabled = false; 1.797 + prop.updateEditor(); 1.798 + } else { 1.799 + delete prop._visited; 1.800 + } 1.801 + } 1.802 + 1.803 + // Add brand new properties. 1.804 + this.textProps = this.textProps.concat(brandNewProps); 1.805 + 1.806 + // Refresh the editor if one already exists. 1.807 + if (this.editor) { 1.808 + this.editor.populate(); 1.809 + } 1.810 + }, 1.811 + 1.812 + /** 1.813 + * Update the current TextProperties that match a given property 1.814 + * from the cssText. Will choose one existing TextProperty to update 1.815 + * with the new property's value, and will disable all others. 1.816 + * 1.817 + * When choosing the best match to reuse, properties will be chosen 1.818 + * by assigning a rank and choosing the highest-ranked property: 1.819 + * Name, value, and priority match, enabled. (6) 1.820 + * Name, value, and priority match, disabled. (5) 1.821 + * Name and value match, enabled. (4) 1.822 + * Name and value match, disabled. (3) 1.823 + * Name matches, enabled. (2) 1.824 + * Name matches, disabled. (1) 1.825 + * 1.826 + * If no existing properties match the property, nothing happens. 1.827 + * 1.828 + * @param {TextProperty} aNewProp 1.829 + * The current version of the property, as parsed from the 1.830 + * cssText in Rule._getTextProperties(). 1.831 + * 1.832 + * @return {bool} true if a property was updated, false if no properties 1.833 + * were updated. 1.834 + */ 1.835 + _updateTextProperty: function(aNewProp) { 1.836 + let match = { rank: 0, prop: null }; 1.837 + 1.838 + for each (let prop in this.textProps) { 1.839 + if (prop.name != aNewProp.name) 1.840 + continue; 1.841 + 1.842 + // Mark this property visited. 1.843 + prop._visited = true; 1.844 + 1.845 + // Start at rank 1 for matching name. 1.846 + let rank = 1; 1.847 + 1.848 + // Value and Priority matches add 2 to the rank. 1.849 + // Being enabled adds 1. This ranks better matches higher, 1.850 + // with priority breaking ties. 1.851 + if (prop.value === aNewProp.value) { 1.852 + rank += 2; 1.853 + if (prop.priority === aNewProp.priority) { 1.854 + rank += 2; 1.855 + } 1.856 + } 1.857 + 1.858 + if (prop.enabled) { 1.859 + rank += 1; 1.860 + } 1.861 + 1.862 + if (rank > match.rank) { 1.863 + if (match.prop) { 1.864 + // We outrank a previous match, disable it. 1.865 + match.prop.enabled = false; 1.866 + match.prop.updateEditor(); 1.867 + } 1.868 + match.rank = rank; 1.869 + match.prop = prop; 1.870 + } else if (rank) { 1.871 + // A previous match outranks us, disable ourself. 1.872 + prop.enabled = false; 1.873 + prop.updateEditor(); 1.874 + } 1.875 + } 1.876 + 1.877 + // If we found a match, update its value with the new text property 1.878 + // value. 1.879 + if (match.prop) { 1.880 + match.prop.set(aNewProp); 1.881 + return true; 1.882 + } 1.883 + 1.884 + return false; 1.885 + }, 1.886 + 1.887 + /** 1.888 + * Jump between editable properties in the UI. Will begin editing the next 1.889 + * name, if possible. If this is the last element in the set, then begin 1.890 + * editing the previous value. If this is the *only* element in the set, 1.891 + * then settle for focusing the new property editor. 1.892 + * 1.893 + * @param {TextProperty} aTextProperty 1.894 + * The text property that will be left to focus on a sibling. 1.895 + * 1.896 + */ 1.897 + editClosestTextProperty: function(aTextProperty) { 1.898 + let index = this.textProps.indexOf(aTextProperty); 1.899 + let previous = false; 1.900 + 1.901 + // If this is the last element, move to the previous instead of next 1.902 + if (index === this.textProps.length - 1) { 1.903 + index = index - 1; 1.904 + previous = true; 1.905 + } 1.906 + else { 1.907 + index = index + 1; 1.908 + } 1.909 + 1.910 + let nextProp = this.textProps[index]; 1.911 + 1.912 + // If possible, begin editing the next name or previous value. 1.913 + // Otherwise, settle for focusing the new property element. 1.914 + if (nextProp) { 1.915 + if (previous) { 1.916 + nextProp.editor.valueSpan.click(); 1.917 + } else { 1.918 + nextProp.editor.nameSpan.click(); 1.919 + } 1.920 + } else { 1.921 + aTextProperty.rule.editor.closeBrace.focus(); 1.922 + } 1.923 + } 1.924 +}; 1.925 + 1.926 +/** 1.927 + * A single property in a rule's cssText. 1.928 + * 1.929 + * @param {Rule} aRule 1.930 + * The rule this TextProperty came from. 1.931 + * @param {string} aName 1.932 + * The text property name (such as "background" or "border-top"). 1.933 + * @param {string} aValue 1.934 + * The property's value (not including priority). 1.935 + * @param {string} aPriority 1.936 + * The property's priority (either "important" or an empty string). 1.937 + * 1.938 + */ 1.939 +function TextProperty(aRule, aName, aValue, aPriority) { 1.940 + this.rule = aRule; 1.941 + this.name = aName; 1.942 + this.value = aValue; 1.943 + this.priority = aPriority; 1.944 + this.enabled = true; 1.945 + this.updateComputed(); 1.946 +} 1.947 + 1.948 +TextProperty.prototype = { 1.949 + /** 1.950 + * Update the editor associated with this text property, 1.951 + * if any. 1.952 + */ 1.953 + updateEditor: function() { 1.954 + if (this.editor) { 1.955 + this.editor.update(); 1.956 + } 1.957 + }, 1.958 + 1.959 + /** 1.960 + * Update the list of computed properties for this text property. 1.961 + */ 1.962 + updateComputed: function() { 1.963 + if (!this.name) { 1.964 + return; 1.965 + } 1.966 + 1.967 + // This is a bit funky. To get the list of computed properties 1.968 + // for this text property, we'll set the property on a dummy element 1.969 + // and see what the computed style looks like. 1.970 + let dummyElement = this.rule.elementStyle.dummyElement; 1.971 + let dummyStyle = dummyElement.style; 1.972 + dummyStyle.cssText = ""; 1.973 + dummyStyle.setProperty(this.name, this.value, this.priority); 1.974 + 1.975 + this.computed = []; 1.976 + for (let i = 0, n = dummyStyle.length; i < n; i++) { 1.977 + let prop = dummyStyle.item(i); 1.978 + this.computed.push({ 1.979 + textProp: this, 1.980 + name: prop, 1.981 + value: dummyStyle.getPropertyValue(prop), 1.982 + priority: dummyStyle.getPropertyPriority(prop), 1.983 + }); 1.984 + } 1.985 + }, 1.986 + 1.987 + /** 1.988 + * Set all the values from another TextProperty instance into 1.989 + * this TextProperty instance. 1.990 + * 1.991 + * @param {TextProperty} aOther 1.992 + * The other TextProperty instance. 1.993 + */ 1.994 + set: function(aOther) { 1.995 + let changed = false; 1.996 + for (let item of ["name", "value", "priority", "enabled"]) { 1.997 + if (this[item] != aOther[item]) { 1.998 + this[item] = aOther[item]; 1.999 + changed = true; 1.1000 + } 1.1001 + } 1.1002 + 1.1003 + if (changed) { 1.1004 + this.updateEditor(); 1.1005 + } 1.1006 + }, 1.1007 + 1.1008 + setValue: function(aValue, aPriority) { 1.1009 + this.rule.setPropertyValue(this, aValue, aPriority); 1.1010 + this.updateEditor(); 1.1011 + }, 1.1012 + 1.1013 + setName: function(aName) { 1.1014 + this.rule.setPropertyName(this, aName); 1.1015 + this.updateEditor(); 1.1016 + }, 1.1017 + 1.1018 + setEnabled: function(aValue) { 1.1019 + this.rule.setPropertyEnabled(this, aValue); 1.1020 + this.updateEditor(); 1.1021 + }, 1.1022 + 1.1023 + remove: function() { 1.1024 + this.rule.removeProperty(this); 1.1025 + } 1.1026 +}; 1.1027 + 1.1028 + 1.1029 +/** 1.1030 + * View hierarchy mostly follows the model hierarchy. 1.1031 + * 1.1032 + * CssRuleView: 1.1033 + * Owns an ElementStyle and creates a list of RuleEditors for its 1.1034 + * Rules. 1.1035 + * RuleEditor: 1.1036 + * Owns a Rule object and creates a list of TextPropertyEditors 1.1037 + * for its TextProperties. 1.1038 + * Manages creation of new text properties. 1.1039 + * TextPropertyEditor: 1.1040 + * Owns a TextProperty object. 1.1041 + * Manages changes to the TextProperty. 1.1042 + * Can be expanded to display computed properties. 1.1043 + * Can mark a property disabled or enabled. 1.1044 + */ 1.1045 + 1.1046 +/** 1.1047 + * CssRuleView is a view of the style rules and declarations that 1.1048 + * apply to a given element. After construction, the 'element' 1.1049 + * property will be available with the user interface. 1.1050 + * 1.1051 + * @param {Inspector} aInspector 1.1052 + * @param {Document} aDoc 1.1053 + * The document that will contain the rule view. 1.1054 + * @param {object} aStore 1.1055 + * The CSS rule view can use this object to store metadata 1.1056 + * that might outlast the rule view, particularly the current 1.1057 + * set of disabled properties. 1.1058 + * @param {PageStyleFront} aPageStyle 1.1059 + * The PageStyleFront for communicating with the remote server. 1.1060 + * @constructor 1.1061 + */ 1.1062 +function CssRuleView(aInspector, aDoc, aStore, aPageStyle) { 1.1063 + this.inspector = aInspector; 1.1064 + this.doc = aDoc; 1.1065 + this.store = aStore || {}; 1.1066 + this.pageStyle = aPageStyle; 1.1067 + this.element = this.doc.createElementNS(HTML_NS, "div"); 1.1068 + this.element.className = "ruleview devtools-monospace"; 1.1069 + this.element.flex = 1; 1.1070 + 1.1071 + this._outputParser = new OutputParser(); 1.1072 + 1.1073 + this._buildContextMenu = this._buildContextMenu.bind(this); 1.1074 + this._contextMenuUpdate = this._contextMenuUpdate.bind(this); 1.1075 + this._onSelectAll = this._onSelectAll.bind(this); 1.1076 + this._onCopy = this._onCopy.bind(this); 1.1077 + this._onToggleOrigSources = this._onToggleOrigSources.bind(this); 1.1078 + 1.1079 + this.element.addEventListener("copy", this._onCopy); 1.1080 + 1.1081 + this._handlePrefChange = this._handlePrefChange.bind(this); 1.1082 + gDevTools.on("pref-changed", this._handlePrefChange); 1.1083 + 1.1084 + this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this); 1.1085 + this._prefObserver = new PrefObserver("devtools."); 1.1086 + this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged); 1.1087 + 1.1088 + let options = { 1.1089 + autoSelect: true, 1.1090 + theme: "auto" 1.1091 + }; 1.1092 + this.popup = new AutocompletePopup(aDoc.defaultView.parent.document, options); 1.1093 + 1.1094 + // Create a tooltip for previewing things in the rule view (images for now) 1.1095 + this.previewTooltip = new Tooltip(this.inspector.panelDoc); 1.1096 + this.previewTooltip.startTogglingOnHover(this.element, 1.1097 + this._onTooltipTargetHover.bind(this)); 1.1098 + 1.1099 + // Also create a more complex tooltip for editing colors with the spectrum 1.1100 + // color picker 1.1101 + this.colorPicker = new SwatchColorPickerTooltip(this.inspector.panelDoc); 1.1102 + 1.1103 + this._buildContextMenu(); 1.1104 + this._showEmpty(); 1.1105 +} 1.1106 + 1.1107 +exports.CssRuleView = CssRuleView; 1.1108 + 1.1109 +CssRuleView.prototype = { 1.1110 + // The element that we're inspecting. 1.1111 + _viewedElement: null, 1.1112 + 1.1113 + /** 1.1114 + * Build the context menu. 1.1115 + */ 1.1116 + _buildContextMenu: function() { 1.1117 + let doc = this.doc.defaultView.parent.document; 1.1118 + 1.1119 + this._contextmenu = doc.createElementNS(XUL_NS, "menupopup"); 1.1120 + this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate); 1.1121 + this._contextmenu.id = "rule-view-context-menu"; 1.1122 + 1.1123 + this.menuitemSelectAll = createMenuItem(this._contextmenu, { 1.1124 + label: "ruleView.contextmenu.selectAll", 1.1125 + accesskey: "ruleView.contextmenu.selectAll.accessKey", 1.1126 + command: this._onSelectAll 1.1127 + }); 1.1128 + this.menuitemCopy = createMenuItem(this._contextmenu, { 1.1129 + label: "ruleView.contextmenu.copy", 1.1130 + accesskey: "ruleView.contextmenu.copy.accessKey", 1.1131 + command: this._onCopy 1.1132 + }); 1.1133 + this.menuitemSources= createMenuItem(this._contextmenu, { 1.1134 + label: "ruleView.contextmenu.showOrigSources", 1.1135 + accesskey: "ruleView.contextmenu.showOrigSources.accessKey", 1.1136 + command: this._onToggleOrigSources 1.1137 + }); 1.1138 + 1.1139 + let popupset = doc.documentElement.querySelector("popupset"); 1.1140 + if (!popupset) { 1.1141 + popupset = doc.createElementNS(XUL_NS, "popupset"); 1.1142 + doc.documentElement.appendChild(popupset); 1.1143 + } 1.1144 + 1.1145 + popupset.appendChild(this._contextmenu); 1.1146 + }, 1.1147 + 1.1148 + /** 1.1149 + * Which type of hover-tooltip should be shown for the given element? 1.1150 + * This depends on the element: does it contain an image URL, a CSS transform, 1.1151 + * a font-family, ... 1.1152 + * @param {DOMNode} el The element to test 1.1153 + * @return {String} The type of hover-tooltip 1.1154 + */ 1.1155 + _getHoverTooltipTypeForTarget: function(el) { 1.1156 + let prop = el.textProperty; 1.1157 + 1.1158 + // Test for css transform 1.1159 + if (prop && prop.name === "transform") { 1.1160 + return "transform"; 1.1161 + } 1.1162 + 1.1163 + // Test for image 1.1164 + let isUrl = el.classList.contains("theme-link") && 1.1165 + el.parentNode.classList.contains("ruleview-propertyvalue"); 1.1166 + if (this.inspector.hasUrlToImageDataResolver && isUrl) { 1.1167 + return "image"; 1.1168 + } 1.1169 + 1.1170 + // Test for font-family 1.1171 + let propertyRoot = el.parentNode; 1.1172 + let propertyNameNode = propertyRoot.querySelector(".ruleview-propertyname"); 1.1173 + if (!propertyNameNode) { 1.1174 + propertyRoot = propertyRoot.parentNode; 1.1175 + propertyNameNode = propertyRoot.querySelector(".ruleview-propertyname"); 1.1176 + } 1.1177 + let propertyName; 1.1178 + if (propertyNameNode) { 1.1179 + propertyName = propertyNameNode.textContent; 1.1180 + } 1.1181 + if (propertyName === "font-family" && el.classList.contains("ruleview-propertyvalue")) { 1.1182 + return "font"; 1.1183 + } 1.1184 + }, 1.1185 + 1.1186 + /** 1.1187 + * Executed by the tooltip when the pointer hovers over an element of the view. 1.1188 + * Used to decide whether the tooltip should be shown or not and to actually 1.1189 + * put content in it. 1.1190 + * Checks if the hovered target is a css value we support tooltips for. 1.1191 + * @param {DOMNode} target 1.1192 + * @return {Boolean|Promise} Either a boolean or a promise, used by the 1.1193 + * Tooltip class to wait for the content to be put in the tooltip and finally 1.1194 + * decide whether or not the tooltip should be shown. 1.1195 + */ 1.1196 + _onTooltipTargetHover: function(target) { 1.1197 + let tooltipType = this._getHoverTooltipTypeForTarget(target); 1.1198 + if (!tooltipType) { 1.1199 + return false; 1.1200 + } 1.1201 + 1.1202 + if (this.colorPicker.tooltip.isShown()) { 1.1203 + this.colorPicker.revert(); 1.1204 + this.colorPicker.hide(); 1.1205 + } 1.1206 + 1.1207 + if (tooltipType === "transform") { 1.1208 + return this.previewTooltip.setCssTransformContent(target.textProperty.value, 1.1209 + this.pageStyle, this._viewedElement); 1.1210 + } 1.1211 + if (tooltipType === "image") { 1.1212 + let prop = target.parentNode.textProperty; 1.1213 + let dim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize"); 1.1214 + let uri = CssLogic.getBackgroundImageUriFromProperty(prop.value, prop.rule.domRule.href); 1.1215 + return this.previewTooltip.setRelativeImageContent(uri, this.inspector.inspector, dim); 1.1216 + } 1.1217 + if (tooltipType === "font") { 1.1218 + this.previewTooltip.setFontFamilyContent(target.textContent); 1.1219 + return true; 1.1220 + } 1.1221 + 1.1222 + return false; 1.1223 + }, 1.1224 + 1.1225 + /** 1.1226 + * Update the context menu. This means enabling or disabling menuitems as 1.1227 + * appropriate. 1.1228 + */ 1.1229 + _contextMenuUpdate: function() { 1.1230 + let win = this.doc.defaultView; 1.1231 + 1.1232 + // Copy selection. 1.1233 + let selection = win.getSelection(); 1.1234 + let copy; 1.1235 + 1.1236 + if (selection.toString()) { 1.1237 + // Panel text selected 1.1238 + copy = true; 1.1239 + } else if (selection.anchorNode) { 1.1240 + // input type="text" 1.1241 + let { selectionStart, selectionEnd } = this.doc.popupNode; 1.1242 + 1.1243 + if (isFinite(selectionStart) && isFinite(selectionEnd) && 1.1244 + selectionStart !== selectionEnd) { 1.1245 + copy = true; 1.1246 + } 1.1247 + } else { 1.1248 + // No text selected, disable copy. 1.1249 + copy = false; 1.1250 + } 1.1251 + 1.1252 + this.menuitemCopy.disabled = !copy; 1.1253 + 1.1254 + let label = "ruleView.contextmenu.showOrigSources"; 1.1255 + if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { 1.1256 + label = "ruleView.contextmenu.showCSSSources"; 1.1257 + } 1.1258 + this.menuitemSources.setAttribute("label", 1.1259 + _strings.GetStringFromName(label)); 1.1260 + 1.1261 + let accessKey = label + ".accessKey"; 1.1262 + this.menuitemSources.setAttribute("accesskey", 1.1263 + _strings.GetStringFromName(accessKey)); 1.1264 + }, 1.1265 + 1.1266 + /** 1.1267 + * Select all text. 1.1268 + */ 1.1269 + _onSelectAll: function() { 1.1270 + let win = this.doc.defaultView; 1.1271 + let selection = win.getSelection(); 1.1272 + 1.1273 + selection.selectAllChildren(this.doc.documentElement); 1.1274 + }, 1.1275 + 1.1276 + /** 1.1277 + * Copy selected text from the rule view. 1.1278 + * 1.1279 + * @param {Event} event 1.1280 + * The event object. 1.1281 + */ 1.1282 + _onCopy: function(event) { 1.1283 + try { 1.1284 + let target = event.target; 1.1285 + let text; 1.1286 + 1.1287 + if (event.target.nodeName === "menuitem") { 1.1288 + target = this.doc.popupNode; 1.1289 + } 1.1290 + 1.1291 + if (target.nodeName == "input") { 1.1292 + let start = Math.min(target.selectionStart, target.selectionEnd); 1.1293 + let end = Math.max(target.selectionStart, target.selectionEnd); 1.1294 + let count = end - start; 1.1295 + text = target.value.substr(start, count); 1.1296 + } else { 1.1297 + let win = this.doc.defaultView; 1.1298 + let selection = win.getSelection(); 1.1299 + 1.1300 + text = selection.toString(); 1.1301 + 1.1302 + // Remove any double newlines. 1.1303 + text = text.replace(/(\r?\n)\r?\n/g, "$1"); 1.1304 + 1.1305 + // Remove "inline" 1.1306 + let inline = _strings.GetStringFromName("rule.sourceInline"); 1.1307 + let rx = new RegExp("^" + inline + "\\r?\\n?", "g"); 1.1308 + text = text.replace(rx, ""); 1.1309 + } 1.1310 + 1.1311 + clipboardHelper.copyString(text, this.doc); 1.1312 + event.preventDefault(); 1.1313 + } catch(e) { 1.1314 + console.error(e); 1.1315 + } 1.1316 + }, 1.1317 + 1.1318 + /** 1.1319 + * Toggle the original sources pref. 1.1320 + */ 1.1321 + _onToggleOrigSources: function() { 1.1322 + let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); 1.1323 + Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled); 1.1324 + }, 1.1325 + 1.1326 + setPageStyle: function(aPageStyle) { 1.1327 + this.pageStyle = aPageStyle; 1.1328 + }, 1.1329 + 1.1330 + /** 1.1331 + * Return {bool} true if the rule view currently has an input editor visible. 1.1332 + */ 1.1333 + get isEditing() { 1.1334 + return this.element.querySelectorAll(".styleinspector-propertyeditor").length > 0 1.1335 + || this.colorPicker.tooltip.isShown(); 1.1336 + }, 1.1337 + 1.1338 + _handlePrefChange: function(event, data) { 1.1339 + if (data.pref == "devtools.defaultColorUnit") { 1.1340 + let element = this._viewedElement; 1.1341 + this._viewedElement = null; 1.1342 + this.highlight(element); 1.1343 + } 1.1344 + }, 1.1345 + 1.1346 + _onSourcePrefChanged: function() { 1.1347 + if (this.menuitemSources) { 1.1348 + let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); 1.1349 + this.menuitemSources.setAttribute("checked", isEnabled); 1.1350 + } 1.1351 + 1.1352 + // update text of source links 1.1353 + for (let rule of this._elementStyle.rules) { 1.1354 + if (rule.editor) { 1.1355 + rule.editor.updateSourceLink(); 1.1356 + } 1.1357 + } 1.1358 + }, 1.1359 + 1.1360 + destroy: function() { 1.1361 + this.clear(); 1.1362 + 1.1363 + gDummyPromise = null; 1.1364 + gDevTools.off("pref-changed", this._handlePrefChange); 1.1365 + 1.1366 + this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged); 1.1367 + this._prefObserver.destroy(); 1.1368 + 1.1369 + this.element.removeEventListener("copy", this._onCopy); 1.1370 + delete this._onCopy; 1.1371 + 1.1372 + delete this._outputParser; 1.1373 + 1.1374 + // Remove context menu 1.1375 + if (this._contextmenu) { 1.1376 + // Destroy the Select All menuitem. 1.1377 + this.menuitemSelectAll.removeEventListener("command", this._onSelectAll); 1.1378 + this.menuitemSelectAll = null; 1.1379 + 1.1380 + // Destroy the Copy menuitem. 1.1381 + this.menuitemCopy.removeEventListener("command", this._onCopy); 1.1382 + this.menuitemCopy = null; 1.1383 + 1.1384 + this.menuitemSources.removeEventListener("command", this._onToggleOrigSources); 1.1385 + this.menuitemSources = null; 1.1386 + 1.1387 + // Destroy the context menu. 1.1388 + this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate); 1.1389 + this._contextmenu.parentNode.removeChild(this._contextmenu); 1.1390 + this._contextmenu = null; 1.1391 + } 1.1392 + 1.1393 + // We manage the popupNode ourselves so we also need to destroy it. 1.1394 + this.doc.popupNode = null; 1.1395 + 1.1396 + this.previewTooltip.stopTogglingOnHover(this.element); 1.1397 + this.previewTooltip.destroy(); 1.1398 + this.colorPicker.destroy(); 1.1399 + 1.1400 + if (this.element.parentNode) { 1.1401 + this.element.parentNode.removeChild(this.element); 1.1402 + } 1.1403 + 1.1404 + if (this.elementStyle) { 1.1405 + this.elementStyle.destroy(); 1.1406 + } 1.1407 + 1.1408 + this.popup.destroy(); 1.1409 + }, 1.1410 + 1.1411 + /** 1.1412 + * Update the highlighted element. 1.1413 + * 1.1414 + * @param {NodeActor} aElement 1.1415 + * The node whose style rules we'll inspect. 1.1416 + */ 1.1417 + highlight: function(aElement) { 1.1418 + if (this._viewedElement === aElement) { 1.1419 + return promise.resolve(undefined); 1.1420 + } 1.1421 + 1.1422 + this.clear(); 1.1423 + 1.1424 + if (this._elementStyle) { 1.1425 + delete this._elementStyle; 1.1426 + } 1.1427 + 1.1428 + this._viewedElement = aElement; 1.1429 + if (!this._viewedElement) { 1.1430 + this._showEmpty(); 1.1431 + return promise.resolve(undefined); 1.1432 + } 1.1433 + 1.1434 + this._elementStyle = new ElementStyle(aElement, this.store, this.pageStyle); 1.1435 + return this._elementStyle.init().then(() => { 1.1436 + return this._populate(); 1.1437 + }).then(() => { 1.1438 + // A new node may already be selected, in which this._elementStyle will 1.1439 + // be null. 1.1440 + if (this._elementStyle) { 1.1441 + this._elementStyle.onChanged = () => { 1.1442 + this._changed(); 1.1443 + }; 1.1444 + } 1.1445 + }).then(null, console.error); 1.1446 + }, 1.1447 + 1.1448 + /** 1.1449 + * Update the rules for the currently highlighted element. 1.1450 + */ 1.1451 + nodeChanged: function() { 1.1452 + // Ignore refreshes during editing or when no element is selected. 1.1453 + if (this.isEditing || !this._elementStyle) { 1.1454 + return; 1.1455 + } 1.1456 + 1.1457 + this._clearRules(); 1.1458 + 1.1459 + // Repopulate the element style. 1.1460 + this._populate(); 1.1461 + }, 1.1462 + 1.1463 + _populate: function() { 1.1464 + let elementStyle = this._elementStyle; 1.1465 + return this._elementStyle.populate().then(() => { 1.1466 + if (this._elementStyle != elementStyle) { 1.1467 + return; 1.1468 + } 1.1469 + this._createEditors(); 1.1470 + 1.1471 + // Notify anyone that cares that we refreshed. 1.1472 + var evt = this.doc.createEvent("Events"); 1.1473 + evt.initEvent("CssRuleViewRefreshed", true, false); 1.1474 + this.element.dispatchEvent(evt); 1.1475 + return undefined; 1.1476 + }).then(null, promiseWarn); 1.1477 + }, 1.1478 + 1.1479 + /** 1.1480 + * Show the user that the rule view has no node selected. 1.1481 + */ 1.1482 + _showEmpty: function() { 1.1483 + if (this.doc.getElementById("noResults") > 0) { 1.1484 + return; 1.1485 + } 1.1486 + 1.1487 + createChild(this.element, "div", { 1.1488 + id: "noResults", 1.1489 + textContent: CssLogic.l10n("rule.empty") 1.1490 + }); 1.1491 + }, 1.1492 + 1.1493 + /** 1.1494 + * Clear the rules. 1.1495 + */ 1.1496 + _clearRules: function() { 1.1497 + while (this.element.hasChildNodes()) { 1.1498 + this.element.removeChild(this.element.lastChild); 1.1499 + } 1.1500 + }, 1.1501 + 1.1502 + /** 1.1503 + * Clear the rule view. 1.1504 + */ 1.1505 + clear: function() { 1.1506 + this._clearRules(); 1.1507 + this._viewedElement = null; 1.1508 + this._elementStyle = null; 1.1509 + 1.1510 + this.previewTooltip.hide(); 1.1511 + this.colorPicker.hide(); 1.1512 + }, 1.1513 + 1.1514 + /** 1.1515 + * Called when the user has made changes to the ElementStyle. 1.1516 + * Emits an event that clients can listen to. 1.1517 + */ 1.1518 + _changed: function() { 1.1519 + var evt = this.doc.createEvent("Events"); 1.1520 + evt.initEvent("CssRuleViewChanged", true, false); 1.1521 + this.element.dispatchEvent(evt); 1.1522 + }, 1.1523 + 1.1524 + /** 1.1525 + * Text for header that shows above rules for this element 1.1526 + */ 1.1527 + get selectedElementLabel() { 1.1528 + if (this._selectedElementLabel) { 1.1529 + return this._selectedElementLabel; 1.1530 + } 1.1531 + this._selectedElementLabel = CssLogic.l10n("rule.selectedElement"); 1.1532 + return this._selectedElementLabel; 1.1533 + }, 1.1534 + 1.1535 + /** 1.1536 + * Text for header that shows above rules for pseudo elements 1.1537 + */ 1.1538 + get pseudoElementLabel() { 1.1539 + if (this._pseudoElementLabel) { 1.1540 + return this._pseudoElementLabel; 1.1541 + } 1.1542 + this._pseudoElementLabel = CssLogic.l10n("rule.pseudoElement"); 1.1543 + return this._pseudoElementLabel; 1.1544 + }, 1.1545 + 1.1546 + togglePseudoElementVisibility: function(value) { 1.1547 + this._showPseudoElements = !!value; 1.1548 + let isOpen = this.showPseudoElements; 1.1549 + 1.1550 + Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements", 1.1551 + isOpen); 1.1552 + 1.1553 + this.element.classList.toggle("show-pseudo-elements", isOpen); 1.1554 + 1.1555 + if (this.pseudoElementTwisty) { 1.1556 + if (isOpen) { 1.1557 + this.pseudoElementTwisty.setAttribute("open", "true"); 1.1558 + } 1.1559 + else { 1.1560 + this.pseudoElementTwisty.removeAttribute("open"); 1.1561 + } 1.1562 + } 1.1563 + }, 1.1564 + 1.1565 + get showPseudoElements() { 1.1566 + if (this._showPseudoElements === undefined) { 1.1567 + this._showPseudoElements = 1.1568 + Services.prefs.getBoolPref("devtools.inspector.show_pseudo_elements"); 1.1569 + } 1.1570 + return this._showPseudoElements; 1.1571 + }, 1.1572 + 1.1573 + _getRuleViewHeaderClassName: function(isPseudo) { 1.1574 + let baseClassName = "theme-gutter ruleview-header"; 1.1575 + return isPseudo ? baseClassName + " ruleview-expandable-header" : baseClassName; 1.1576 + }, 1.1577 + 1.1578 + /** 1.1579 + * Creates editor UI for each of the rules in _elementStyle. 1.1580 + */ 1.1581 + _createEditors: function() { 1.1582 + // Run through the current list of rules, attaching 1.1583 + // their editors in order. Create editors if needed. 1.1584 + let lastInheritedSource = ""; 1.1585 + let seenPseudoElement = false; 1.1586 + let seenNormalElement = false; 1.1587 + 1.1588 + for (let rule of this._elementStyle.rules) { 1.1589 + if (rule.domRule.system) { 1.1590 + continue; 1.1591 + } 1.1592 + 1.1593 + // Only print header for this element if there are pseudo elements 1.1594 + if (seenPseudoElement && !seenNormalElement && !rule.pseudoElement) { 1.1595 + seenNormalElement = true; 1.1596 + let div = this.doc.createElementNS(HTML_NS, "div"); 1.1597 + div.className = this._getRuleViewHeaderClassName(); 1.1598 + div.textContent = this.selectedElementLabel; 1.1599 + this.element.appendChild(div); 1.1600 + } 1.1601 + 1.1602 + let inheritedSource = rule.inheritedSource; 1.1603 + if (inheritedSource != lastInheritedSource) { 1.1604 + let div = this.doc.createElementNS(HTML_NS, "div"); 1.1605 + div.className = this._getRuleViewHeaderClassName(); 1.1606 + div.textContent = inheritedSource; 1.1607 + lastInheritedSource = inheritedSource; 1.1608 + this.element.appendChild(div); 1.1609 + } 1.1610 + 1.1611 + if (!seenPseudoElement && rule.pseudoElement) { 1.1612 + seenPseudoElement = true; 1.1613 + 1.1614 + let div = this.doc.createElementNS(HTML_NS, "div"); 1.1615 + div.className = this._getRuleViewHeaderClassName(true); 1.1616 + div.textContent = this.pseudoElementLabel; 1.1617 + div.addEventListener("dblclick", () => { 1.1618 + this.togglePseudoElementVisibility(!this.showPseudoElements); 1.1619 + }, false); 1.1620 + 1.1621 + let twisty = this.pseudoElementTwisty = 1.1622 + this.doc.createElementNS(HTML_NS, "span"); 1.1623 + twisty.className = "ruleview-expander theme-twisty"; 1.1624 + twisty.addEventListener("click", () => { 1.1625 + this.togglePseudoElementVisibility(!this.showPseudoElements); 1.1626 + }, false); 1.1627 + 1.1628 + div.insertBefore(twisty, div.firstChild); 1.1629 + this.element.appendChild(div); 1.1630 + } 1.1631 + 1.1632 + if (!rule.editor) { 1.1633 + rule.editor = new RuleEditor(this, rule); 1.1634 + } 1.1635 + 1.1636 + this.element.appendChild(rule.editor.element); 1.1637 + } 1.1638 + 1.1639 + this.togglePseudoElementVisibility(this.showPseudoElements); 1.1640 + } 1.1641 +}; 1.1642 + 1.1643 +/** 1.1644 + * Create a RuleEditor. 1.1645 + * 1.1646 + * @param {CssRuleView} aRuleView 1.1647 + * The CssRuleView containg the document holding this rule editor. 1.1648 + * @param {Rule} aRule 1.1649 + * The Rule object we're editing. 1.1650 + * @constructor 1.1651 + */ 1.1652 +function RuleEditor(aRuleView, aRule) { 1.1653 + this.ruleView = aRuleView; 1.1654 + this.doc = this.ruleView.doc; 1.1655 + this.rule = aRule; 1.1656 + 1.1657 + this._onNewProperty = this._onNewProperty.bind(this); 1.1658 + this._newPropertyDestroy = this._newPropertyDestroy.bind(this); 1.1659 + 1.1660 + this._create(); 1.1661 +} 1.1662 + 1.1663 +RuleEditor.prototype = { 1.1664 + _create: function() { 1.1665 + this.element = this.doc.createElementNS(HTML_NS, "div"); 1.1666 + this.element.className = "ruleview-rule theme-separator"; 1.1667 + this.element._ruleEditor = this; 1.1668 + if (this.rule.pseudoElement) { 1.1669 + this.element.classList.add("ruleview-rule-pseudo-element"); 1.1670 + } 1.1671 + 1.1672 + // Give a relative position for the inplace editor's measurement 1.1673 + // span to be placed absolutely against. 1.1674 + this.element.style.position = "relative"; 1.1675 + 1.1676 + // Add the source link. 1.1677 + let source = createChild(this.element, "div", { 1.1678 + class: "ruleview-rule-source theme-link" 1.1679 + }); 1.1680 + source.addEventListener("click", function() { 1.1681 + let rule = this.rule.domRule; 1.1682 + let evt = this.doc.createEvent("CustomEvent"); 1.1683 + evt.initCustomEvent("CssRuleViewCSSLinkClicked", true, false, { 1.1684 + rule: rule, 1.1685 + }); 1.1686 + this.element.dispatchEvent(evt); 1.1687 + }.bind(this)); 1.1688 + let sourceLabel = this.doc.createElementNS(XUL_NS, "label"); 1.1689 + sourceLabel.setAttribute("crop", "center"); 1.1690 + sourceLabel.classList.add("source-link-label"); 1.1691 + source.appendChild(sourceLabel); 1.1692 + 1.1693 + this.updateSourceLink(); 1.1694 + 1.1695 + let code = createChild(this.element, "div", { 1.1696 + class: "ruleview-code" 1.1697 + }); 1.1698 + 1.1699 + let header = createChild(code, "div", {}); 1.1700 + 1.1701 + this.selectorText = createChild(header, "span", { 1.1702 + class: "ruleview-selector theme-fg-color3" 1.1703 + }); 1.1704 + 1.1705 + this.openBrace = createChild(header, "span", { 1.1706 + class: "ruleview-ruleopen", 1.1707 + textContent: " {" 1.1708 + }); 1.1709 + 1.1710 + code.addEventListener("click", function() { 1.1711 + let selection = this.doc.defaultView.getSelection(); 1.1712 + if (selection.isCollapsed) { 1.1713 + this.newProperty(); 1.1714 + } 1.1715 + }.bind(this), false); 1.1716 + 1.1717 + this.element.addEventListener("mousedown", function() { 1.1718 + this.doc.defaultView.focus(); 1.1719 + }.bind(this), false); 1.1720 + 1.1721 + this.element.addEventListener("contextmenu", event => { 1.1722 + try { 1.1723 + // In the sidebar we do not have this.doc.popupNode so we need to save 1.1724 + // the node ourselves. 1.1725 + this.doc.popupNode = event.explicitOriginalTarget; 1.1726 + let win = this.doc.defaultView; 1.1727 + win.focus(); 1.1728 + 1.1729 + this.ruleView._contextmenu.openPopupAtScreen( 1.1730 + event.screenX, event.screenY, true); 1.1731 + 1.1732 + } catch(e) { 1.1733 + console.error(e); 1.1734 + } 1.1735 + }, false); 1.1736 + 1.1737 + this.propertyList = createChild(code, "ul", { 1.1738 + class: "ruleview-propertylist" 1.1739 + }); 1.1740 + 1.1741 + this.populate(); 1.1742 + 1.1743 + this.closeBrace = createChild(code, "div", { 1.1744 + class: "ruleview-ruleclose", 1.1745 + tabindex: "0", 1.1746 + textContent: "}" 1.1747 + }); 1.1748 + 1.1749 + // Create a property editor when the close brace is clicked. 1.1750 + editableItem({ element: this.closeBrace }, (aElement) => { 1.1751 + this.newProperty(); 1.1752 + }); 1.1753 + }, 1.1754 + 1.1755 + updateSourceLink: function RuleEditor_updateSourceLink() 1.1756 + { 1.1757 + let sourceLabel = this.element.querySelector(".source-link-label"); 1.1758 + sourceLabel.setAttribute("value", this.rule.title); 1.1759 + 1.1760 + let sourceHref = (this.rule.sheet && this.rule.sheet.href) ? 1.1761 + this.rule.sheet.href : this.rule.title; 1.1762 + 1.1763 + sourceLabel.setAttribute("tooltiptext", sourceHref); 1.1764 + 1.1765 + let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); 1.1766 + if (showOrig && this.rule.domRule.type != ELEMENT_STYLE) { 1.1767 + this.rule.getOriginalSourceStrings().then((strings) => { 1.1768 + sourceLabel.setAttribute("value", strings.short); 1.1769 + sourceLabel.setAttribute("tooltiptext", strings.full); 1.1770 + }) 1.1771 + } 1.1772 + }, 1.1773 + 1.1774 + /** 1.1775 + * Update the rule editor with the contents of the rule. 1.1776 + */ 1.1777 + populate: function() { 1.1778 + // Clear out existing viewers. 1.1779 + while (this.selectorText.hasChildNodes()) { 1.1780 + this.selectorText.removeChild(this.selectorText.lastChild); 1.1781 + } 1.1782 + 1.1783 + // If selector text comes from a css rule, highlight selectors that 1.1784 + // actually match. For custom selector text (such as for the 'element' 1.1785 + // style, just show the text directly. 1.1786 + if (this.rule.domRule.type === ELEMENT_STYLE) { 1.1787 + this.selectorText.textContent = this.rule.selectorText; 1.1788 + } else { 1.1789 + this.rule.domRule.selectors.forEach((selector, i) => { 1.1790 + if (i != 0) { 1.1791 + createChild(this.selectorText, "span", { 1.1792 + class: "ruleview-selector-separator", 1.1793 + textContent: ", " 1.1794 + }); 1.1795 + } 1.1796 + let cls; 1.1797 + if (this.rule.matchedSelectors.indexOf(selector) > -1) { 1.1798 + cls = "ruleview-selector-matched"; 1.1799 + } else { 1.1800 + cls = "ruleview-selector-unmatched"; 1.1801 + } 1.1802 + createChild(this.selectorText, "span", { 1.1803 + class: cls, 1.1804 + textContent: selector 1.1805 + }); 1.1806 + }); 1.1807 + } 1.1808 + 1.1809 + for (let prop of this.rule.textProps) { 1.1810 + if (!prop.editor) { 1.1811 + let editor = new TextPropertyEditor(this, prop); 1.1812 + this.propertyList.appendChild(editor.element); 1.1813 + } 1.1814 + } 1.1815 + }, 1.1816 + 1.1817 + /** 1.1818 + * Programatically add a new property to the rule. 1.1819 + * 1.1820 + * @param {string} aName 1.1821 + * Property name. 1.1822 + * @param {string} aValue 1.1823 + * Property value. 1.1824 + * @param {string} aPriority 1.1825 + * Property priority. 1.1826 + * @param {TextProperty} aSiblingProp 1.1827 + * Optional, property next to which the new property will be added. 1.1828 + * @return {TextProperty} 1.1829 + * The new property 1.1830 + */ 1.1831 + addProperty: function(aName, aValue, aPriority, aSiblingProp) { 1.1832 + let prop = this.rule.createProperty(aName, aValue, aPriority, aSiblingProp); 1.1833 + let index = this.rule.textProps.indexOf(prop); 1.1834 + let editor = new TextPropertyEditor(this, prop); 1.1835 + 1.1836 + // Insert this node before the DOM node that is currently at its new index 1.1837 + // in the property list. There is currently one less node in the DOM than 1.1838 + // in the property list, so this causes it to appear after aSiblingProp. 1.1839 + // If there is no node at its index, as is the case where this is the last 1.1840 + // node being inserted, then this behaves as appendChild. 1.1841 + this.propertyList.insertBefore(editor.element, 1.1842 + this.propertyList.children[index]); 1.1843 + 1.1844 + return prop; 1.1845 + }, 1.1846 + 1.1847 + /** 1.1848 + * Programatically add a list of new properties to the rule. Focus the UI 1.1849 + * to the proper location after adding (either focus the value on the 1.1850 + * last property if it is empty, or create a new property and focus it). 1.1851 + * 1.1852 + * @param {Array} aProperties 1.1853 + * Array of properties, which are objects with this signature: 1.1854 + * { 1.1855 + * name: {string}, 1.1856 + * value: {string}, 1.1857 + * priority: {string} 1.1858 + * } 1.1859 + * @param {TextProperty} aSiblingProp 1.1860 + * Optional, the property next to which all new props should be added. 1.1861 + */ 1.1862 + addProperties: function(aProperties, aSiblingProp) { 1.1863 + if (!aProperties || !aProperties.length) { 1.1864 + return; 1.1865 + } 1.1866 + 1.1867 + let lastProp = aSiblingProp; 1.1868 + for (let p of aProperties) { 1.1869 + lastProp = this.addProperty(p.name, p.value, p.priority, lastProp); 1.1870 + } 1.1871 + 1.1872 + // Either focus on the last value if incomplete, or start a new one. 1.1873 + if (lastProp && lastProp.value.trim() === "") { 1.1874 + lastProp.editor.valueSpan.click(); 1.1875 + } else { 1.1876 + this.newProperty(); 1.1877 + } 1.1878 + }, 1.1879 + 1.1880 + /** 1.1881 + * Create a text input for a property name. If a non-empty property 1.1882 + * name is given, we'll create a real TextProperty and add it to the 1.1883 + * rule. 1.1884 + */ 1.1885 + newProperty: function() { 1.1886 + // If we're already creating a new property, ignore this. 1.1887 + if (!this.closeBrace.hasAttribute("tabindex")) { 1.1888 + return; 1.1889 + } 1.1890 + 1.1891 + // While we're editing a new property, it doesn't make sense to 1.1892 + // start a second new property editor, so disable focusing the 1.1893 + // close brace for now. 1.1894 + this.closeBrace.removeAttribute("tabindex"); 1.1895 + 1.1896 + this.newPropItem = createChild(this.propertyList, "li", { 1.1897 + class: "ruleview-property ruleview-newproperty", 1.1898 + }); 1.1899 + 1.1900 + this.newPropSpan = createChild(this.newPropItem, "span", { 1.1901 + class: "ruleview-propertyname", 1.1902 + tabindex: "0" 1.1903 + }); 1.1904 + 1.1905 + this.multipleAddedProperties = null; 1.1906 + 1.1907 + this.editor = new InplaceEditor({ 1.1908 + element: this.newPropSpan, 1.1909 + done: this._onNewProperty, 1.1910 + destroy: this._newPropertyDestroy, 1.1911 + advanceChars: ":", 1.1912 + contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY, 1.1913 + popup: this.ruleView.popup 1.1914 + }); 1.1915 + 1.1916 + // Auto-close the input if multiple rules get pasted into new property. 1.1917 + this.editor.input.addEventListener("paste", 1.1918 + blurOnMultipleProperties, false); 1.1919 + }, 1.1920 + 1.1921 + /** 1.1922 + * Called when the new property input has been dismissed. 1.1923 + * 1.1924 + * @param {string} aValue 1.1925 + * The value in the editor. 1.1926 + * @param {bool} aCommit 1.1927 + * True if the value should be committed. 1.1928 + */ 1.1929 + _onNewProperty: function(aValue, aCommit) { 1.1930 + if (!aValue || !aCommit) { 1.1931 + return; 1.1932 + } 1.1933 + 1.1934 + // parseDeclarations allows for name-less declarations, but in the present 1.1935 + // case, we're creating a new declaration, it doesn't make sense to accept 1.1936 + // these entries 1.1937 + this.multipleAddedProperties = parseDeclarations(aValue).filter(d => d.name); 1.1938 + 1.1939 + // Blur the editor field now and deal with adding declarations later when 1.1940 + // the field gets destroyed (see _newPropertyDestroy) 1.1941 + this.editor.input.blur(); 1.1942 + }, 1.1943 + 1.1944 + /** 1.1945 + * Called when the new property editor is destroyed. 1.1946 + * This is where the properties (type TextProperty) are actually being 1.1947 + * added, since we want to wait until after the inplace editor `destroy` 1.1948 + * event has been fired to keep consistent UI state. 1.1949 + */ 1.1950 + _newPropertyDestroy: function() { 1.1951 + // We're done, make the close brace focusable again. 1.1952 + this.closeBrace.setAttribute("tabindex", "0"); 1.1953 + 1.1954 + this.propertyList.removeChild(this.newPropItem); 1.1955 + delete this.newPropItem; 1.1956 + delete this.newPropSpan; 1.1957 + 1.1958 + // If properties were added, we want to focus the proper element. 1.1959 + // If the last new property has no value, focus the value on it. 1.1960 + // Otherwise, start a new property and focus that field. 1.1961 + if (this.multipleAddedProperties && this.multipleAddedProperties.length) { 1.1962 + this.addProperties(this.multipleAddedProperties); 1.1963 + } 1.1964 + } 1.1965 +}; 1.1966 + 1.1967 +/** 1.1968 + * Create a TextPropertyEditor. 1.1969 + * 1.1970 + * @param {RuleEditor} aRuleEditor 1.1971 + * The rule editor that owns this TextPropertyEditor. 1.1972 + * @param {TextProperty} aProperty 1.1973 + * The text property to edit. 1.1974 + * @constructor 1.1975 + */ 1.1976 +function TextPropertyEditor(aRuleEditor, aProperty) { 1.1977 + this.ruleEditor = aRuleEditor; 1.1978 + this.doc = this.ruleEditor.doc; 1.1979 + this.popup = this.ruleEditor.ruleView.popup; 1.1980 + this.prop = aProperty; 1.1981 + this.prop.editor = this; 1.1982 + this.browserWindow = this.doc.defaultView.top; 1.1983 + this.removeOnRevert = this.prop.value === ""; 1.1984 + 1.1985 + this._onEnableClicked = this._onEnableClicked.bind(this); 1.1986 + this._onExpandClicked = this._onExpandClicked.bind(this); 1.1987 + this._onStartEditing = this._onStartEditing.bind(this); 1.1988 + this._onNameDone = this._onNameDone.bind(this); 1.1989 + this._onValueDone = this._onValueDone.bind(this); 1.1990 + this._onValidate = throttle(this._previewValue, 10, this); 1.1991 + this.update = this.update.bind(this); 1.1992 + 1.1993 + this._create(); 1.1994 + this.update(); 1.1995 +} 1.1996 + 1.1997 +TextPropertyEditor.prototype = { 1.1998 + /** 1.1999 + * Boolean indicating if the name or value is being currently edited. 1.2000 + */ 1.2001 + get editing() { 1.2002 + return !!(this.nameSpan.inplaceEditor || this.valueSpan.inplaceEditor || 1.2003 + this.ruleEditor.ruleView.colorPicker.tooltip.isShown() || 1.2004 + this.ruleEditor.ruleView.colorPicker.eyedropperOpen) || 1.2005 + this.popup.isOpen; 1.2006 + }, 1.2007 + 1.2008 + /** 1.2009 + * Create the property editor's DOM. 1.2010 + */ 1.2011 + _create: function() { 1.2012 + this.element = this.doc.createElementNS(HTML_NS, "li"); 1.2013 + this.element.classList.add("ruleview-property"); 1.2014 + 1.2015 + // The enable checkbox will disable or enable the rule. 1.2016 + this.enable = createChild(this.element, "div", { 1.2017 + class: "ruleview-enableproperty theme-checkbox", 1.2018 + tabindex: "-1" 1.2019 + }); 1.2020 + this.enable.addEventListener("click", this._onEnableClicked, true); 1.2021 + 1.2022 + // Click to expand the computed properties of the text property. 1.2023 + this.expander = createChild(this.element, "span", { 1.2024 + class: "ruleview-expander theme-twisty" 1.2025 + }); 1.2026 + this.expander.addEventListener("click", this._onExpandClicked, true); 1.2027 + 1.2028 + this.nameContainer = createChild(this.element, "span", { 1.2029 + class: "ruleview-namecontainer" 1.2030 + }); 1.2031 + this.nameContainer.addEventListener("click", (aEvent) => { 1.2032 + // Clicks within the name shouldn't propagate any further. 1.2033 + aEvent.stopPropagation(); 1.2034 + if (aEvent.target === propertyContainer) { 1.2035 + this.nameSpan.click(); 1.2036 + } 1.2037 + }, false); 1.2038 + 1.2039 + // Property name, editable when focused. Property name 1.2040 + // is committed when the editor is unfocused. 1.2041 + this.nameSpan = createChild(this.nameContainer, "span", { 1.2042 + class: "ruleview-propertyname theme-fg-color5", 1.2043 + tabindex: "0", 1.2044 + }); 1.2045 + 1.2046 + editableField({ 1.2047 + start: this._onStartEditing, 1.2048 + element: this.nameSpan, 1.2049 + done: this._onNameDone, 1.2050 + destroy: this.update, 1.2051 + advanceChars: ':', 1.2052 + contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY, 1.2053 + popup: this.popup 1.2054 + }); 1.2055 + 1.2056 + // Auto blur name field on multiple CSS rules get pasted in. 1.2057 + this.nameContainer.addEventListener("paste", 1.2058 + blurOnMultipleProperties, false); 1.2059 + 1.2060 + appendText(this.nameContainer, ": "); 1.2061 + 1.2062 + // Create a span that will hold the property and semicolon. 1.2063 + // Use this span to create a slightly larger click target 1.2064 + // for the value. 1.2065 + let propertyContainer = createChild(this.element, "span", { 1.2066 + class: "ruleview-propertycontainer" 1.2067 + }); 1.2068 + 1.2069 + propertyContainer.addEventListener("click", (aEvent) => { 1.2070 + // Clicks within the value shouldn't propagate any further. 1.2071 + aEvent.stopPropagation(); 1.2072 + 1.2073 + if (aEvent.target === propertyContainer) { 1.2074 + this.valueSpan.click(); 1.2075 + } 1.2076 + }, false); 1.2077 + 1.2078 + // Property value, editable when focused. Changes to the 1.2079 + // property value are applied as they are typed, and reverted 1.2080 + // if the user presses escape. 1.2081 + this.valueSpan = createChild(propertyContainer, "span", { 1.2082 + class: "ruleview-propertyvalue theme-fg-color1", 1.2083 + tabindex: "0", 1.2084 + }); 1.2085 + 1.2086 + this.valueSpan.addEventListener("click", (event) => { 1.2087 + let target = event.target; 1.2088 + 1.2089 + if (target.nodeName === "a") { 1.2090 + event.stopPropagation(); 1.2091 + event.preventDefault(); 1.2092 + this.browserWindow.openUILinkIn(target.href, "tab"); 1.2093 + } 1.2094 + }, false); 1.2095 + 1.2096 + // Storing the TextProperty on the valuespan for easy access 1.2097 + // (for instance by the tooltip) 1.2098 + this.valueSpan.textProperty = this.prop; 1.2099 + 1.2100 + // Save the initial value as the last committed value, 1.2101 + // for restoring after pressing escape. 1.2102 + this.committed = { name: this.prop.name, 1.2103 + value: this.prop.value, 1.2104 + priority: this.prop.priority }; 1.2105 + 1.2106 + appendText(propertyContainer, ";"); 1.2107 + 1.2108 + this.warning = createChild(this.element, "div", { 1.2109 + class: "ruleview-warning", 1.2110 + hidden: "", 1.2111 + title: CssLogic.l10n("rule.warning.title"), 1.2112 + }); 1.2113 + 1.2114 + // Holds the viewers for the computed properties. 1.2115 + // will be populated in |_updateComputed|. 1.2116 + this.computed = createChild(this.element, "ul", { 1.2117 + class: "ruleview-computedlist", 1.2118 + }); 1.2119 + 1.2120 + editableField({ 1.2121 + start: this._onStartEditing, 1.2122 + element: this.valueSpan, 1.2123 + done: this._onValueDone, 1.2124 + destroy: this.update, 1.2125 + validate: this._onValidate, 1.2126 + advanceChars: ';', 1.2127 + contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE, 1.2128 + property: this.prop, 1.2129 + popup: this.popup 1.2130 + }); 1.2131 + }, 1.2132 + 1.2133 + /** 1.2134 + * Get the path from which to resolve requests for this 1.2135 + * rule's stylesheet. 1.2136 + * @return {string} the stylesheet's href. 1.2137 + */ 1.2138 + get sheetHref() { 1.2139 + let domRule = this.prop.rule.domRule; 1.2140 + if (domRule) { 1.2141 + return domRule.href || domRule.nodeHref; 1.2142 + } 1.2143 + }, 1.2144 + 1.2145 + /** 1.2146 + * Get the URI from which to resolve relative requests for 1.2147 + * this rule's stylesheet. 1.2148 + * @return {nsIURI} A URI based on the the stylesheet's href. 1.2149 + */ 1.2150 + get sheetURI() { 1.2151 + if (this._sheetURI === undefined) { 1.2152 + if (this.sheetHref) { 1.2153 + this._sheetURI = IOService.newURI(this.sheetHref, null, null); 1.2154 + } else { 1.2155 + this._sheetURI = null; 1.2156 + } 1.2157 + } 1.2158 + 1.2159 + return this._sheetURI; 1.2160 + }, 1.2161 + 1.2162 + /** 1.2163 + * Resolve a URI based on the rule stylesheet 1.2164 + * @param {string} relativePath the path to resolve 1.2165 + * @return {string} the resolved path. 1.2166 + */ 1.2167 + resolveURI: function(relativePath) { 1.2168 + if (this.sheetURI) { 1.2169 + relativePath = this.sheetURI.resolve(relativePath); 1.2170 + } 1.2171 + return relativePath; 1.2172 + }, 1.2173 + 1.2174 + /** 1.2175 + * Check the property value to find an external resource (if any). 1.2176 + * @return {string} the URI in the property value, or null if there is no match. 1.2177 + */ 1.2178 + getResourceURI: function() { 1.2179 + let val = this.prop.value; 1.2180 + let uriMatch = CSS_RESOURCE_RE.exec(val); 1.2181 + let uri = null; 1.2182 + 1.2183 + if (uriMatch && uriMatch[1]) { 1.2184 + uri = uriMatch[1]; 1.2185 + } 1.2186 + 1.2187 + return uri; 1.2188 + }, 1.2189 + 1.2190 + /** 1.2191 + * Populate the span based on changes to the TextProperty. 1.2192 + */ 1.2193 + update: function() { 1.2194 + if (this.prop.enabled) { 1.2195 + this.enable.style.removeProperty("visibility"); 1.2196 + this.enable.setAttribute("checked", ""); 1.2197 + } else { 1.2198 + this.enable.style.visibility = "visible"; 1.2199 + this.enable.removeAttribute("checked"); 1.2200 + } 1.2201 + 1.2202 + this.warning.hidden = this.editing || this.isValid(); 1.2203 + 1.2204 + if ((this.prop.overridden || !this.prop.enabled) && !this.editing) { 1.2205 + this.element.classList.add("ruleview-overridden"); 1.2206 + } else { 1.2207 + this.element.classList.remove("ruleview-overridden"); 1.2208 + } 1.2209 + 1.2210 + let name = this.prop.name; 1.2211 + this.nameSpan.textContent = name; 1.2212 + 1.2213 + // Combine the property's value and priority into one string for 1.2214 + // the value. 1.2215 + let val = this.prop.value; 1.2216 + if (this.prop.priority) { 1.2217 + val += " !" + this.prop.priority; 1.2218 + } 1.2219 + 1.2220 + let store = this.prop.rule.elementStyle.store; 1.2221 + let propDirty = store.userProperties.contains(this.prop.rule.style, name); 1.2222 + 1.2223 + if (propDirty) { 1.2224 + this.element.setAttribute("dirty", ""); 1.2225 + } else { 1.2226 + this.element.removeAttribute("dirty"); 1.2227 + } 1.2228 + 1.2229 + let swatchClass = "ruleview-colorswatch"; 1.2230 + let outputParser = this.ruleEditor.ruleView._outputParser; 1.2231 + let frag = outputParser.parseCssProperty(name, val, { 1.2232 + colorSwatchClass: swatchClass, 1.2233 + colorClass: "ruleview-color", 1.2234 + defaultColorType: !propDirty, 1.2235 + urlClass: "theme-link", 1.2236 + baseURI: this.sheetURI 1.2237 + }); 1.2238 + this.valueSpan.innerHTML = ""; 1.2239 + this.valueSpan.appendChild(frag); 1.2240 + 1.2241 + // Attach the color picker tooltip to the color swatches 1.2242 + this._swatchSpans = this.valueSpan.querySelectorAll("." + swatchClass); 1.2243 + for (let span of this._swatchSpans) { 1.2244 + // Capture the original declaration value to be able to revert later 1.2245 + let originalValue = this.valueSpan.textContent; 1.2246 + // Adding this swatch to the list of swatches our colorpicker knows about 1.2247 + this.ruleEditor.ruleView.colorPicker.addSwatch(span, { 1.2248 + onPreview: () => this._previewValue(this.valueSpan.textContent), 1.2249 + onCommit: () => this._applyNewValue(this.valueSpan.textContent), 1.2250 + onRevert: () => this._applyNewValue(originalValue) 1.2251 + }); 1.2252 + } 1.2253 + 1.2254 + // Populate the computed styles. 1.2255 + this._updateComputed(); 1.2256 + }, 1.2257 + 1.2258 + _onStartEditing: function() { 1.2259 + this.element.classList.remove("ruleview-overridden"); 1.2260 + this._previewValue(this.prop.value); 1.2261 + }, 1.2262 + 1.2263 + /** 1.2264 + * Populate the list of computed styles. 1.2265 + */ 1.2266 + _updateComputed: function () { 1.2267 + // Clear out existing viewers. 1.2268 + while (this.computed.hasChildNodes()) { 1.2269 + this.computed.removeChild(this.computed.lastChild); 1.2270 + } 1.2271 + 1.2272 + let showExpander = false; 1.2273 + for each (let computed in this.prop.computed) { 1.2274 + // Don't bother to duplicate information already 1.2275 + // shown in the text property. 1.2276 + if (computed.name === this.prop.name) { 1.2277 + continue; 1.2278 + } 1.2279 + 1.2280 + showExpander = true; 1.2281 + 1.2282 + let li = createChild(this.computed, "li", { 1.2283 + class: "ruleview-computed" 1.2284 + }); 1.2285 + 1.2286 + if (computed.overridden) { 1.2287 + li.classList.add("ruleview-overridden"); 1.2288 + } 1.2289 + 1.2290 + createChild(li, "span", { 1.2291 + class: "ruleview-propertyname theme-fg-color5", 1.2292 + textContent: computed.name 1.2293 + }); 1.2294 + appendText(li, ": "); 1.2295 + 1.2296 + let outputParser = this.ruleEditor.ruleView._outputParser; 1.2297 + let frag = outputParser.parseCssProperty( 1.2298 + computed.name, computed.value, { 1.2299 + colorSwatchClass: "ruleview-colorswatch", 1.2300 + urlClass: "theme-link", 1.2301 + baseURI: this.sheetURI 1.2302 + } 1.2303 + ); 1.2304 + 1.2305 + createChild(li, "span", { 1.2306 + class: "ruleview-propertyvalue theme-fg-color1", 1.2307 + child: frag 1.2308 + }); 1.2309 + 1.2310 + appendText(li, ";"); 1.2311 + } 1.2312 + 1.2313 + // Show or hide the expander as needed. 1.2314 + if (showExpander) { 1.2315 + this.expander.style.visibility = "visible"; 1.2316 + } else { 1.2317 + this.expander.style.visibility = "hidden"; 1.2318 + } 1.2319 + }, 1.2320 + 1.2321 + /** 1.2322 + * Handles clicks on the disabled property. 1.2323 + */ 1.2324 + _onEnableClicked: function(aEvent) { 1.2325 + let checked = this.enable.hasAttribute("checked"); 1.2326 + if (checked) { 1.2327 + this.enable.removeAttribute("checked"); 1.2328 + } else { 1.2329 + this.enable.setAttribute("checked", ""); 1.2330 + } 1.2331 + this.prop.setEnabled(!checked); 1.2332 + aEvent.stopPropagation(); 1.2333 + }, 1.2334 + 1.2335 + /** 1.2336 + * Handles clicks on the computed property expander. 1.2337 + */ 1.2338 + _onExpandClicked: function(aEvent) { 1.2339 + this.computed.classList.toggle("styleinspector-open"); 1.2340 + if (this.computed.classList.contains("styleinspector-open")) { 1.2341 + this.expander.setAttribute("open", "true"); 1.2342 + } else { 1.2343 + this.expander.removeAttribute("open"); 1.2344 + } 1.2345 + aEvent.stopPropagation(); 1.2346 + }, 1.2347 + 1.2348 + /** 1.2349 + * Called when the property name's inplace editor is closed. 1.2350 + * Ignores the change if the user pressed escape, otherwise 1.2351 + * commits it. 1.2352 + * 1.2353 + * @param {string} aValue 1.2354 + * The value contained in the editor. 1.2355 + * @param {boolean} aCommit 1.2356 + * True if the change should be applied. 1.2357 + */ 1.2358 + _onNameDone: function(aValue, aCommit) { 1.2359 + if (aCommit) { 1.2360 + // Unlike the value editor, if a name is empty the entire property 1.2361 + // should always be removed. 1.2362 + if (aValue.trim() === "") { 1.2363 + this.remove(); 1.2364 + } else { 1.2365 + // Adding multiple rules inside of name field overwrites the current 1.2366 + // property with the first, then adds any more onto the property list. 1.2367 + let properties = parseDeclarations(aValue); 1.2368 + 1.2369 + if (properties.length) { 1.2370 + this.prop.setName(properties[0].name); 1.2371 + if (properties.length > 1) { 1.2372 + this.prop.setValue(properties[0].value, properties[0].priority); 1.2373 + this.ruleEditor.addProperties(properties.slice(1), this.prop); 1.2374 + } 1.2375 + } 1.2376 + } 1.2377 + } 1.2378 + }, 1.2379 + 1.2380 + /** 1.2381 + * Remove property from style and the editors from DOM. 1.2382 + * Begin editing next available property. 1.2383 + */ 1.2384 + remove: function() { 1.2385 + if (this._swatchSpans && this._swatchSpans.length) { 1.2386 + for (let span of this._swatchSpans) { 1.2387 + this.ruleEditor.ruleView.colorPicker.removeSwatch(span); 1.2388 + } 1.2389 + } 1.2390 + 1.2391 + this.element.parentNode.removeChild(this.element); 1.2392 + this.ruleEditor.rule.editClosestTextProperty(this.prop); 1.2393 + this.valueSpan.textProperty = null; 1.2394 + this.prop.remove(); 1.2395 + }, 1.2396 + 1.2397 + /** 1.2398 + * Called when a value editor closes. If the user pressed escape, 1.2399 + * revert to the value this property had before editing. 1.2400 + * 1.2401 + * @param {string} aValue 1.2402 + * The value contained in the editor. 1.2403 + * @param {bool} aCommit 1.2404 + * True if the change should be applied. 1.2405 + */ 1.2406 + _onValueDone: function(aValue, aCommit) { 1.2407 + if (!aCommit) { 1.2408 + // A new property should be removed when escape is pressed. 1.2409 + if (this.removeOnRevert) { 1.2410 + this.remove(); 1.2411 + } else { 1.2412 + this.prop.setValue(this.committed.value, this.committed.priority); 1.2413 + } 1.2414 + return; 1.2415 + } 1.2416 + 1.2417 + let {propertiesToAdd,firstValue} = this._getValueAndExtraProperties(aValue); 1.2418 + 1.2419 + // First, set this property value (common case, only modified a property) 1.2420 + let val = parseSingleValue(firstValue); 1.2421 + this.prop.setValue(val.value, val.priority); 1.2422 + this.removeOnRevert = false; 1.2423 + this.committed.value = this.prop.value; 1.2424 + this.committed.priority = this.prop.priority; 1.2425 + 1.2426 + // If needed, add any new properties after this.prop. 1.2427 + this.ruleEditor.addProperties(propertiesToAdd, this.prop); 1.2428 + 1.2429 + // If the name or value is not actively being edited, and the value is 1.2430 + // empty, then remove the whole property. 1.2431 + // A timeout is used here to accurately check the state, since the inplace 1.2432 + // editor `done` and `destroy` events fire before the next editor 1.2433 + // is focused. 1.2434 + if (val.value.trim() === "") { 1.2435 + setTimeout(() => { 1.2436 + if (!this.editing) { 1.2437 + this.remove(); 1.2438 + } 1.2439 + }, 0); 1.2440 + } 1.2441 + }, 1.2442 + 1.2443 + /** 1.2444 + * Parse a value string and break it into pieces, starting with the 1.2445 + * first value, and into an array of additional properties (if any). 1.2446 + * 1.2447 + * Example: Calling with "red; width: 100px" would return 1.2448 + * { firstValue: "red", propertiesToAdd: [{ name: "width", value: "100px" }] } 1.2449 + * 1.2450 + * @param {string} aValue 1.2451 + * The string to parse 1.2452 + * @return {object} An object with the following properties: 1.2453 + * firstValue: A string containing a simple value, like 1.2454 + * "red" or "100px!important" 1.2455 + * propertiesToAdd: An array with additional properties, following the 1.2456 + * parseDeclarations format of {name,value,priority} 1.2457 + */ 1.2458 + _getValueAndExtraProperties: function(aValue) { 1.2459 + // The inplace editor will prevent manual typing of multiple properties, 1.2460 + // but we need to deal with the case during a paste event. 1.2461 + // Adding multiple properties inside of value editor sets value with the 1.2462 + // first, then adds any more onto the property list (below this property). 1.2463 + let firstValue = aValue; 1.2464 + let propertiesToAdd = []; 1.2465 + 1.2466 + let properties = parseDeclarations(aValue); 1.2467 + 1.2468 + // Check to see if the input string can be parsed as multiple properties 1.2469 + if (properties.length) { 1.2470 + // Get the first property value (if any), and any remaining properties (if any) 1.2471 + if (!properties[0].name && properties[0].value) { 1.2472 + firstValue = properties[0].value; 1.2473 + propertiesToAdd = properties.slice(1); 1.2474 + } 1.2475 + // In some cases, the value could be a property:value pair itself. 1.2476 + // Join them as one value string and append potentially following properties 1.2477 + else if (properties[0].name && properties[0].value) { 1.2478 + firstValue = properties[0].name + ": " + properties[0].value; 1.2479 + propertiesToAdd = properties.slice(1); 1.2480 + } 1.2481 + } 1.2482 + 1.2483 + return { 1.2484 + propertiesToAdd: propertiesToAdd, 1.2485 + firstValue: firstValue 1.2486 + }; 1.2487 + }, 1.2488 + 1.2489 + _applyNewValue: function(aValue) { 1.2490 + let val = parseSingleValue(aValue); 1.2491 + 1.2492 + this.prop.setValue(val.value, val.priority); 1.2493 + this.removeOnRevert = false; 1.2494 + this.committed.value = this.prop.value; 1.2495 + this.committed.priority = this.prop.priority; 1.2496 + }, 1.2497 + 1.2498 + /** 1.2499 + * Live preview this property, without committing changes. 1.2500 + * @param {string} aValue The value to set the current property to. 1.2501 + */ 1.2502 + _previewValue: function(aValue) { 1.2503 + // Since function call is throttled, we need to make sure we are still editing 1.2504 + if (!this.editing) { 1.2505 + return; 1.2506 + } 1.2507 + 1.2508 + let val = parseSingleValue(aValue); 1.2509 + this.ruleEditor.rule.previewPropertyValue(this.prop, val.value, val.priority); 1.2510 + }, 1.2511 + 1.2512 + /** 1.2513 + * Validate this property. Does it make sense for this value to be assigned 1.2514 + * to this property name? This does not apply the property value 1.2515 + * 1.2516 + * @param {string} [aValue] 1.2517 + * The property value used for validation. 1.2518 + * Defaults to the current value for this.prop 1.2519 + * 1.2520 + * @return {bool} true if the property value is valid, false otherwise. 1.2521 + */ 1.2522 + isValid: function(aValue) { 1.2523 + let name = this.prop.name; 1.2524 + let value = typeof aValue == "undefined" ? this.prop.value : aValue; 1.2525 + let val = parseSingleValue(value); 1.2526 + 1.2527 + let style = this.doc.createElementNS(HTML_NS, "div").style; 1.2528 + let prefs = Services.prefs; 1.2529 + 1.2530 + // We toggle output of errors whilst the user is typing a property value. 1.2531 + let prefVal = prefs.getBoolPref("layout.css.report_errors"); 1.2532 + prefs.setBoolPref("layout.css.report_errors", false); 1.2533 + 1.2534 + let validValue = false; 1.2535 + try { 1.2536 + style.setProperty(name, val.value, val.priority); 1.2537 + validValue = style.getPropertyValue(name) !== "" || val.value === ""; 1.2538 + } finally { 1.2539 + prefs.setBoolPref("layout.css.report_errors", prefVal); 1.2540 + } 1.2541 + return validValue; 1.2542 + } 1.2543 +}; 1.2544 + 1.2545 +/** 1.2546 + * Store of CSSStyleDeclarations mapped to properties that have been changed by 1.2547 + * the user. 1.2548 + */ 1.2549 +function UserProperties() { 1.2550 + this.map = new Map(); 1.2551 +} 1.2552 + 1.2553 +UserProperties.prototype = { 1.2554 + /** 1.2555 + * Get a named property for a given CSSStyleDeclaration. 1.2556 + * 1.2557 + * @param {CSSStyleDeclaration} aStyle 1.2558 + * The CSSStyleDeclaration against which the property is mapped. 1.2559 + * @param {string} aName 1.2560 + * The name of the property to get. 1.2561 + * @param {string} aDefault 1.2562 + * The value to return if the property is has been changed outside of 1.2563 + * the rule view. 1.2564 + * @return {string} 1.2565 + * The property value if it has previously been set by the user, null 1.2566 + * otherwise. 1.2567 + */ 1.2568 + getProperty: function(aStyle, aName, aDefault) { 1.2569 + let key = this.getKey(aStyle); 1.2570 + let entry = this.map.get(key, null); 1.2571 + 1.2572 + if (entry && aName in entry) { 1.2573 + let item = entry[aName]; 1.2574 + if (item != aDefault) { 1.2575 + delete entry[aName]; 1.2576 + return aDefault; 1.2577 + } 1.2578 + return item; 1.2579 + } 1.2580 + return aDefault; 1.2581 + }, 1.2582 + 1.2583 + /** 1.2584 + * Set a named property for a given CSSStyleDeclaration. 1.2585 + * 1.2586 + * @param {CSSStyleDeclaration} aStyle 1.2587 + * The CSSStyleDeclaration against which the property is to be mapped. 1.2588 + * @param {String} aName 1.2589 + * The name of the property to set. 1.2590 + * @param {String} aUserValue 1.2591 + * The value of the property to set. 1.2592 + */ 1.2593 + setProperty: function(aStyle, aName, aUserValue) { 1.2594 + let key = this.getKey(aStyle); 1.2595 + let entry = this.map.get(key, null); 1.2596 + if (entry) { 1.2597 + entry[aName] = aUserValue; 1.2598 + } else { 1.2599 + let props = {}; 1.2600 + props[aName] = aUserValue; 1.2601 + this.map.set(key, props); 1.2602 + } 1.2603 + }, 1.2604 + 1.2605 + /** 1.2606 + * Check whether a named property for a given CSSStyleDeclaration is stored. 1.2607 + * 1.2608 + * @param {CSSStyleDeclaration} aStyle 1.2609 + * The CSSStyleDeclaration against which the property would be mapped. 1.2610 + * @param {String} aName 1.2611 + * The name of the property to check. 1.2612 + */ 1.2613 + contains: function(aStyle, aName) { 1.2614 + let key = this.getKey(aStyle); 1.2615 + let entry = this.map.get(key, null); 1.2616 + return !!entry && aName in entry; 1.2617 + }, 1.2618 + 1.2619 + getKey: function(aStyle) { 1.2620 + return aStyle.href + ":" + aStyle.line; 1.2621 + } 1.2622 +}; 1.2623 + 1.2624 +/** 1.2625 + * Helper functions 1.2626 + */ 1.2627 + 1.2628 +/** 1.2629 + * Create a child element with a set of attributes. 1.2630 + * 1.2631 + * @param {Element} aParent 1.2632 + * The parent node. 1.2633 + * @param {string} aTag 1.2634 + * The tag name. 1.2635 + * @param {object} aAttributes 1.2636 + * A set of attributes to set on the node. 1.2637 + */ 1.2638 +function createChild(aParent, aTag, aAttributes) { 1.2639 + let elt = aParent.ownerDocument.createElementNS(HTML_NS, aTag); 1.2640 + for (let attr in aAttributes) { 1.2641 + if (aAttributes.hasOwnProperty(attr)) { 1.2642 + if (attr === "textContent") { 1.2643 + elt.textContent = aAttributes[attr]; 1.2644 + } else if(attr === "child") { 1.2645 + elt.appendChild(aAttributes[attr]); 1.2646 + } else { 1.2647 + elt.setAttribute(attr, aAttributes[attr]); 1.2648 + } 1.2649 + } 1.2650 + } 1.2651 + aParent.appendChild(elt); 1.2652 + return elt; 1.2653 +} 1.2654 + 1.2655 +function createMenuItem(aMenu, aAttributes) { 1.2656 + let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem"); 1.2657 + 1.2658 + item.setAttribute("label", _strings.GetStringFromName(aAttributes.label)); 1.2659 + item.setAttribute("accesskey", _strings.GetStringFromName(aAttributes.accesskey)); 1.2660 + item.addEventListener("command", aAttributes.command); 1.2661 + 1.2662 + aMenu.appendChild(item); 1.2663 + 1.2664 + return item; 1.2665 +} 1.2666 + 1.2667 +function setTimeout() { 1.2668 + let window = Services.appShell.hiddenDOMWindow; 1.2669 + return window.setTimeout.apply(window, arguments); 1.2670 +} 1.2671 + 1.2672 +function clearTimeout() { 1.2673 + let window = Services.appShell.hiddenDOMWindow; 1.2674 + return window.clearTimeout.apply(window, arguments); 1.2675 +} 1.2676 + 1.2677 +function throttle(func, wait, scope) { 1.2678 + var timer = null; 1.2679 + return function() { 1.2680 + if(timer) { 1.2681 + clearTimeout(timer); 1.2682 + } 1.2683 + var args = arguments; 1.2684 + timer = setTimeout(function() { 1.2685 + timer = null; 1.2686 + func.apply(scope, args); 1.2687 + }, wait); 1.2688 + }; 1.2689 +} 1.2690 + 1.2691 +/** 1.2692 + * Event handler that causes a blur on the target if the input has 1.2693 + * multiple CSS properties as the value. 1.2694 + */ 1.2695 +function blurOnMultipleProperties(e) { 1.2696 + setTimeout(() => { 1.2697 + let props = parseDeclarations(e.target.value); 1.2698 + if (props.length > 1) { 1.2699 + e.target.blur(); 1.2700 + } 1.2701 + }, 0); 1.2702 +} 1.2703 + 1.2704 +/** 1.2705 + * Append a text node to an element. 1.2706 + */ 1.2707 +function appendText(aParent, aText) { 1.2708 + aParent.appendChild(aParent.ownerDocument.createTextNode(aText)); 1.2709 +} 1.2710 + 1.2711 +XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() { 1.2712 + return Cc["@mozilla.org/widget/clipboardhelper;1"]. 1.2713 + getService(Ci.nsIClipboardHelper); 1.2714 +}); 1.2715 + 1.2716 +XPCOMUtils.defineLazyGetter(this, "_strings", function() { 1.2717 + return Services.strings.createBundle( 1.2718 + "chrome://global/locale/devtools/styleinspector.properties"); 1.2719 +}); 1.2720 + 1.2721 +XPCOMUtils.defineLazyGetter(this, "domUtils", function() { 1.2722 + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); 1.2723 +}); 1.2724 + 1.2725 +loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup);