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