|
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/. */ |
|
6 |
|
7 "use strict"; |
|
8 |
|
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"); |
|
19 |
|
20 Cu.import("resource://gre/modules/Services.jsm"); |
|
21 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
22 |
|
23 const HTML_NS = "http://www.w3.org/1999/xhtml"; |
|
24 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; |
|
25 |
|
26 /** |
|
27 * These regular expressions are adapted from firebug's css.js, and are |
|
28 * used to parse CSSStyleDeclaration's cssText attribute. |
|
29 */ |
|
30 |
|
31 // Used to split on css line separators |
|
32 const CSS_LINE_RE = /(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g; |
|
33 |
|
34 // Used to parse a single property line. |
|
35 const CSS_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*(?:! (important))?;?$/; |
|
36 |
|
37 // Used to parse an external resource from a property value |
|
38 const CSS_RESOURCE_RE = /url\([\'\"]?(.*?)[\'\"]?\)/; |
|
39 |
|
40 const IOService = Cc["@mozilla.org/network/io-service;1"] |
|
41 .getService(Ci.nsIIOService); |
|
42 |
|
43 function promiseWarn(err) { |
|
44 console.error(err); |
|
45 return promise.reject(err); |
|
46 } |
|
47 |
|
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"); |
|
61 |
|
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 } |
|
83 |
|
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 */ |
|
102 |
|
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; |
|
122 |
|
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 } |
|
128 |
|
129 if (!("disabled" in this.store)) { |
|
130 this.store.disabled = new WeakMap(); |
|
131 } |
|
132 } |
|
133 |
|
134 // We're exporting _ElementStyle for unit tests. |
|
135 exports._ElementStyle = ElementStyle; |
|
136 |
|
137 ElementStyle.prototype = { |
|
138 // The element we're looking at. |
|
139 element: null, |
|
140 |
|
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, |
|
144 |
|
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 }, |
|
157 |
|
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 }, |
|
167 |
|
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 }, |
|
177 |
|
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 } |
|
196 |
|
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; |
|
200 |
|
201 this.rules = []; |
|
202 |
|
203 for (let entry of entries) { |
|
204 this._maybeAddRule(entry); |
|
205 } |
|
206 |
|
207 // Mark overridden computed styles. |
|
208 this.markOverriddenAll(); |
|
209 |
|
210 this._sortRulesForPseudoElement(); |
|
211 |
|
212 // We're done with the previous list of rules. |
|
213 delete this._refreshRules; |
|
214 |
|
215 return null; |
|
216 }); |
|
217 }).then(null, promiseWarn); |
|
218 this.populated = populated; |
|
219 return this.populated; |
|
220 }, |
|
221 |
|
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 }, |
|
230 |
|
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 } |
|
247 |
|
248 if (aOptions.system) { |
|
249 return false; |
|
250 } |
|
251 |
|
252 let rule = null; |
|
253 |
|
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 } |
|
265 |
|
266 // If this is a new rule, create its Rule object. |
|
267 if (!rule) { |
|
268 rule = new Rule(this, aOptions); |
|
269 } |
|
270 |
|
271 // Ignore inherited rules with no properties. |
|
272 if (aOptions.inherited && rule.textProps.length == 0) { |
|
273 return false; |
|
274 } |
|
275 |
|
276 this.rules.push(rule); |
|
277 return true; |
|
278 }, |
|
279 |
|
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 }, |
|
289 |
|
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 } |
|
306 |
|
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 } |
|
313 |
|
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 } |
|
345 |
|
346 computedProp._overriddenDirty = (!!computedProp.overridden != overridden); |
|
347 computedProp.overridden = overridden; |
|
348 if (!computedProp.overridden && computedProp.textProp.enabled) { |
|
349 taken[computedProp.name] = computedProp; |
|
350 } |
|
351 } |
|
352 |
|
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 }, |
|
365 |
|
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 } |
|
387 |
|
388 dirty = (!!aProp.overridden != overridden) || dirty; |
|
389 aProp.overridden = overridden; |
|
390 return dirty; |
|
391 } |
|
392 }; |
|
393 |
|
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 || ""; |
|
412 |
|
413 this.inherited = aOptions.inherited || null; |
|
414 this._modificationDepth = 0; |
|
415 |
|
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 } |
|
422 |
|
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 } |
|
428 |
|
429 Rule.prototype = { |
|
430 mediaText: "", |
|
431 |
|
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 } |
|
440 |
|
441 this._title = this._title + (this.mediaText ? " @media " + this.mediaText : ""); |
|
442 return this._title; |
|
443 }, |
|
444 |
|
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 }, |
|
460 |
|
461 get selectorText() { |
|
462 return this.domRule.selectors ? this.domRule.selectors.join(", ") : CssLogic.l10n("rule.sourceElement"); |
|
463 }, |
|
464 |
|
465 /** |
|
466 * The rule's stylesheet. |
|
467 */ |
|
468 get sheet() { |
|
469 return this.domRule ? this.domRule.parentStyleSheet : null; |
|
470 }, |
|
471 |
|
472 /** |
|
473 * The rule's line within a stylesheet |
|
474 */ |
|
475 get ruleLine() { |
|
476 return this.domRule ? this.domRule.line : null; |
|
477 }, |
|
478 |
|
479 /** |
|
480 * The rule's column within a stylesheet |
|
481 */ |
|
482 get ruleColumn() { |
|
483 return this.domRule ? this.domRule.column : null; |
|
484 }, |
|
485 |
|
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 }; |
|
503 |
|
504 this._originalSourceStrings = sourceStrings; |
|
505 return sourceStrings; |
|
506 }); |
|
507 }, |
|
508 |
|
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 }, |
|
519 |
|
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); |
|
534 |
|
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 } |
|
542 |
|
543 this.applyProperties(); |
|
544 return prop; |
|
545 }, |
|
546 |
|
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(); |
|
559 |
|
560 if (!aModifications) { |
|
561 aModifications = this.style.startModifyingProperties(); |
|
562 } |
|
563 let disabledProps = []; |
|
564 let store = this.elementStyle.store; |
|
565 |
|
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 } |
|
578 |
|
579 aModifications.setProperty(prop.name, prop.value, prop.priority); |
|
580 |
|
581 prop.updateComputed(); |
|
582 } |
|
583 |
|
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 } |
|
591 |
|
592 let promise = aModifications.apply().then(() => { |
|
593 let cssProps = {}; |
|
594 for (let cssProp of parseDeclarations(this.style.cssText)) { |
|
595 cssProps[cssProp.name] = cssProp; |
|
596 } |
|
597 |
|
598 for (let textProp of this.textProps) { |
|
599 if (!textProp.enabled) { |
|
600 continue; |
|
601 } |
|
602 let cssProp = cssProps[textProp.name]; |
|
603 |
|
604 if (!cssProp) { |
|
605 cssProp = { |
|
606 name: textProp.name, |
|
607 value: "", |
|
608 priority: "" |
|
609 }; |
|
610 } |
|
611 |
|
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 } |
|
620 |
|
621 this.elementStyle.markOverriddenAll(); |
|
622 |
|
623 if (promise === this._applyingModifications) { |
|
624 this._applyingModifications = null; |
|
625 } |
|
626 |
|
627 this.elementStyle._changed(); |
|
628 }).then(null, promiseWarn); |
|
629 |
|
630 this._applyingModifications = promise; |
|
631 return promise; |
|
632 }, |
|
633 |
|
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 }, |
|
651 |
|
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 } |
|
666 |
|
667 aProperty.value = aValue; |
|
668 aProperty.priority = aPriority; |
|
669 this.applyProperties(null, aProperty.name); |
|
670 }, |
|
671 |
|
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 }, |
|
688 |
|
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 }, |
|
704 |
|
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 }, |
|
720 |
|
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 } |
|
738 |
|
739 return textProps; |
|
740 }, |
|
741 |
|
742 /** |
|
743 * Return the list of disabled properties from the store for this rule. |
|
744 */ |
|
745 _getDisabledProperties: function() { |
|
746 let store = this.elementStyle.store; |
|
747 |
|
748 // Include properties from the disabled property store, if any. |
|
749 let disabledProps = store.disabled.get(this.style); |
|
750 if (!disabledProps) { |
|
751 return []; |
|
752 } |
|
753 |
|
754 let textProps = []; |
|
755 |
|
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 } |
|
762 |
|
763 return textProps; |
|
764 }, |
|
765 |
|
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(); |
|
773 |
|
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 } |
|
786 |
|
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 } |
|
799 |
|
800 // Add brand new properties. |
|
801 this.textProps = this.textProps.concat(brandNewProps); |
|
802 |
|
803 // Refresh the editor if one already exists. |
|
804 if (this.editor) { |
|
805 this.editor.populate(); |
|
806 } |
|
807 }, |
|
808 |
|
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 }; |
|
834 |
|
835 for each (let prop in this.textProps) { |
|
836 if (prop.name != aNewProp.name) |
|
837 continue; |
|
838 |
|
839 // Mark this property visited. |
|
840 prop._visited = true; |
|
841 |
|
842 // Start at rank 1 for matching name. |
|
843 let rank = 1; |
|
844 |
|
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 } |
|
854 |
|
855 if (prop.enabled) { |
|
856 rank += 1; |
|
857 } |
|
858 |
|
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 } |
|
873 |
|
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 } |
|
880 |
|
881 return false; |
|
882 }, |
|
883 |
|
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; |
|
897 |
|
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 } |
|
906 |
|
907 let nextProp = this.textProps[index]; |
|
908 |
|
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 }; |
|
922 |
|
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 } |
|
944 |
|
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 }, |
|
955 |
|
956 /** |
|
957 * Update the list of computed properties for this text property. |
|
958 */ |
|
959 updateComputed: function() { |
|
960 if (!this.name) { |
|
961 return; |
|
962 } |
|
963 |
|
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); |
|
971 |
|
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 }, |
|
983 |
|
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 } |
|
999 |
|
1000 if (changed) { |
|
1001 this.updateEditor(); |
|
1002 } |
|
1003 }, |
|
1004 |
|
1005 setValue: function(aValue, aPriority) { |
|
1006 this.rule.setPropertyValue(this, aValue, aPriority); |
|
1007 this.updateEditor(); |
|
1008 }, |
|
1009 |
|
1010 setName: function(aName) { |
|
1011 this.rule.setPropertyName(this, aName); |
|
1012 this.updateEditor(); |
|
1013 }, |
|
1014 |
|
1015 setEnabled: function(aValue) { |
|
1016 this.rule.setPropertyEnabled(this, aValue); |
|
1017 this.updateEditor(); |
|
1018 }, |
|
1019 |
|
1020 remove: function() { |
|
1021 this.rule.removeProperty(this); |
|
1022 } |
|
1023 }; |
|
1024 |
|
1025 |
|
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 */ |
|
1042 |
|
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; |
|
1067 |
|
1068 this._outputParser = new OutputParser(); |
|
1069 |
|
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); |
|
1075 |
|
1076 this.element.addEventListener("copy", this._onCopy); |
|
1077 |
|
1078 this._handlePrefChange = this._handlePrefChange.bind(this); |
|
1079 gDevTools.on("pref-changed", this._handlePrefChange); |
|
1080 |
|
1081 this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this); |
|
1082 this._prefObserver = new PrefObserver("devtools."); |
|
1083 this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged); |
|
1084 |
|
1085 let options = { |
|
1086 autoSelect: true, |
|
1087 theme: "auto" |
|
1088 }; |
|
1089 this.popup = new AutocompletePopup(aDoc.defaultView.parent.document, options); |
|
1090 |
|
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)); |
|
1095 |
|
1096 // Also create a more complex tooltip for editing colors with the spectrum |
|
1097 // color picker |
|
1098 this.colorPicker = new SwatchColorPickerTooltip(this.inspector.panelDoc); |
|
1099 |
|
1100 this._buildContextMenu(); |
|
1101 this._showEmpty(); |
|
1102 } |
|
1103 |
|
1104 exports.CssRuleView = CssRuleView; |
|
1105 |
|
1106 CssRuleView.prototype = { |
|
1107 // The element that we're inspecting. |
|
1108 _viewedElement: null, |
|
1109 |
|
1110 /** |
|
1111 * Build the context menu. |
|
1112 */ |
|
1113 _buildContextMenu: function() { |
|
1114 let doc = this.doc.defaultView.parent.document; |
|
1115 |
|
1116 this._contextmenu = doc.createElementNS(XUL_NS, "menupopup"); |
|
1117 this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate); |
|
1118 this._contextmenu.id = "rule-view-context-menu"; |
|
1119 |
|
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 }); |
|
1135 |
|
1136 let popupset = doc.documentElement.querySelector("popupset"); |
|
1137 if (!popupset) { |
|
1138 popupset = doc.createElementNS(XUL_NS, "popupset"); |
|
1139 doc.documentElement.appendChild(popupset); |
|
1140 } |
|
1141 |
|
1142 popupset.appendChild(this._contextmenu); |
|
1143 }, |
|
1144 |
|
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; |
|
1154 |
|
1155 // Test for css transform |
|
1156 if (prop && prop.name === "transform") { |
|
1157 return "transform"; |
|
1158 } |
|
1159 |
|
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 } |
|
1166 |
|
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 }, |
|
1182 |
|
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 } |
|
1198 |
|
1199 if (this.colorPicker.tooltip.isShown()) { |
|
1200 this.colorPicker.revert(); |
|
1201 this.colorPicker.hide(); |
|
1202 } |
|
1203 |
|
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 } |
|
1218 |
|
1219 return false; |
|
1220 }, |
|
1221 |
|
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; |
|
1228 |
|
1229 // Copy selection. |
|
1230 let selection = win.getSelection(); |
|
1231 let copy; |
|
1232 |
|
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; |
|
1239 |
|
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 } |
|
1248 |
|
1249 this.menuitemCopy.disabled = !copy; |
|
1250 |
|
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)); |
|
1257 |
|
1258 let accessKey = label + ".accessKey"; |
|
1259 this.menuitemSources.setAttribute("accesskey", |
|
1260 _strings.GetStringFromName(accessKey)); |
|
1261 }, |
|
1262 |
|
1263 /** |
|
1264 * Select all text. |
|
1265 */ |
|
1266 _onSelectAll: function() { |
|
1267 let win = this.doc.defaultView; |
|
1268 let selection = win.getSelection(); |
|
1269 |
|
1270 selection.selectAllChildren(this.doc.documentElement); |
|
1271 }, |
|
1272 |
|
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; |
|
1283 |
|
1284 if (event.target.nodeName === "menuitem") { |
|
1285 target = this.doc.popupNode; |
|
1286 } |
|
1287 |
|
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(); |
|
1296 |
|
1297 text = selection.toString(); |
|
1298 |
|
1299 // Remove any double newlines. |
|
1300 text = text.replace(/(\r?\n)\r?\n/g, "$1"); |
|
1301 |
|
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 } |
|
1307 |
|
1308 clipboardHelper.copyString(text, this.doc); |
|
1309 event.preventDefault(); |
|
1310 } catch(e) { |
|
1311 console.error(e); |
|
1312 } |
|
1313 }, |
|
1314 |
|
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 }, |
|
1322 |
|
1323 setPageStyle: function(aPageStyle) { |
|
1324 this.pageStyle = aPageStyle; |
|
1325 }, |
|
1326 |
|
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 }, |
|
1334 |
|
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 }, |
|
1342 |
|
1343 _onSourcePrefChanged: function() { |
|
1344 if (this.menuitemSources) { |
|
1345 let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); |
|
1346 this.menuitemSources.setAttribute("checked", isEnabled); |
|
1347 } |
|
1348 |
|
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 }, |
|
1356 |
|
1357 destroy: function() { |
|
1358 this.clear(); |
|
1359 |
|
1360 gDummyPromise = null; |
|
1361 gDevTools.off("pref-changed", this._handlePrefChange); |
|
1362 |
|
1363 this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged); |
|
1364 this._prefObserver.destroy(); |
|
1365 |
|
1366 this.element.removeEventListener("copy", this._onCopy); |
|
1367 delete this._onCopy; |
|
1368 |
|
1369 delete this._outputParser; |
|
1370 |
|
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; |
|
1376 |
|
1377 // Destroy the Copy menuitem. |
|
1378 this.menuitemCopy.removeEventListener("command", this._onCopy); |
|
1379 this.menuitemCopy = null; |
|
1380 |
|
1381 this.menuitemSources.removeEventListener("command", this._onToggleOrigSources); |
|
1382 this.menuitemSources = null; |
|
1383 |
|
1384 // Destroy the context menu. |
|
1385 this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate); |
|
1386 this._contextmenu.parentNode.removeChild(this._contextmenu); |
|
1387 this._contextmenu = null; |
|
1388 } |
|
1389 |
|
1390 // We manage the popupNode ourselves so we also need to destroy it. |
|
1391 this.doc.popupNode = null; |
|
1392 |
|
1393 this.previewTooltip.stopTogglingOnHover(this.element); |
|
1394 this.previewTooltip.destroy(); |
|
1395 this.colorPicker.destroy(); |
|
1396 |
|
1397 if (this.element.parentNode) { |
|
1398 this.element.parentNode.removeChild(this.element); |
|
1399 } |
|
1400 |
|
1401 if (this.elementStyle) { |
|
1402 this.elementStyle.destroy(); |
|
1403 } |
|
1404 |
|
1405 this.popup.destroy(); |
|
1406 }, |
|
1407 |
|
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 } |
|
1418 |
|
1419 this.clear(); |
|
1420 |
|
1421 if (this._elementStyle) { |
|
1422 delete this._elementStyle; |
|
1423 } |
|
1424 |
|
1425 this._viewedElement = aElement; |
|
1426 if (!this._viewedElement) { |
|
1427 this._showEmpty(); |
|
1428 return promise.resolve(undefined); |
|
1429 } |
|
1430 |
|
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 }, |
|
1444 |
|
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 } |
|
1453 |
|
1454 this._clearRules(); |
|
1455 |
|
1456 // Repopulate the element style. |
|
1457 this._populate(); |
|
1458 }, |
|
1459 |
|
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(); |
|
1467 |
|
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 }, |
|
1475 |
|
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 } |
|
1483 |
|
1484 createChild(this.element, "div", { |
|
1485 id: "noResults", |
|
1486 textContent: CssLogic.l10n("rule.empty") |
|
1487 }); |
|
1488 }, |
|
1489 |
|
1490 /** |
|
1491 * Clear the rules. |
|
1492 */ |
|
1493 _clearRules: function() { |
|
1494 while (this.element.hasChildNodes()) { |
|
1495 this.element.removeChild(this.element.lastChild); |
|
1496 } |
|
1497 }, |
|
1498 |
|
1499 /** |
|
1500 * Clear the rule view. |
|
1501 */ |
|
1502 clear: function() { |
|
1503 this._clearRules(); |
|
1504 this._viewedElement = null; |
|
1505 this._elementStyle = null; |
|
1506 |
|
1507 this.previewTooltip.hide(); |
|
1508 this.colorPicker.hide(); |
|
1509 }, |
|
1510 |
|
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 }, |
|
1520 |
|
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 }, |
|
1531 |
|
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 }, |
|
1542 |
|
1543 togglePseudoElementVisibility: function(value) { |
|
1544 this._showPseudoElements = !!value; |
|
1545 let isOpen = this.showPseudoElements; |
|
1546 |
|
1547 Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements", |
|
1548 isOpen); |
|
1549 |
|
1550 this.element.classList.toggle("show-pseudo-elements", isOpen); |
|
1551 |
|
1552 if (this.pseudoElementTwisty) { |
|
1553 if (isOpen) { |
|
1554 this.pseudoElementTwisty.setAttribute("open", "true"); |
|
1555 } |
|
1556 else { |
|
1557 this.pseudoElementTwisty.removeAttribute("open"); |
|
1558 } |
|
1559 } |
|
1560 }, |
|
1561 |
|
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 }, |
|
1569 |
|
1570 _getRuleViewHeaderClassName: function(isPseudo) { |
|
1571 let baseClassName = "theme-gutter ruleview-header"; |
|
1572 return isPseudo ? baseClassName + " ruleview-expandable-header" : baseClassName; |
|
1573 }, |
|
1574 |
|
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; |
|
1584 |
|
1585 for (let rule of this._elementStyle.rules) { |
|
1586 if (rule.domRule.system) { |
|
1587 continue; |
|
1588 } |
|
1589 |
|
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 } |
|
1598 |
|
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 } |
|
1607 |
|
1608 if (!seenPseudoElement && rule.pseudoElement) { |
|
1609 seenPseudoElement = true; |
|
1610 |
|
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); |
|
1617 |
|
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); |
|
1624 |
|
1625 div.insertBefore(twisty, div.firstChild); |
|
1626 this.element.appendChild(div); |
|
1627 } |
|
1628 |
|
1629 if (!rule.editor) { |
|
1630 rule.editor = new RuleEditor(this, rule); |
|
1631 } |
|
1632 |
|
1633 this.element.appendChild(rule.editor.element); |
|
1634 } |
|
1635 |
|
1636 this.togglePseudoElementVisibility(this.showPseudoElements); |
|
1637 } |
|
1638 }; |
|
1639 |
|
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; |
|
1653 |
|
1654 this._onNewProperty = this._onNewProperty.bind(this); |
|
1655 this._newPropertyDestroy = this._newPropertyDestroy.bind(this); |
|
1656 |
|
1657 this._create(); |
|
1658 } |
|
1659 |
|
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 } |
|
1668 |
|
1669 // Give a relative position for the inplace editor's measurement |
|
1670 // span to be placed absolutely against. |
|
1671 this.element.style.position = "relative"; |
|
1672 |
|
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); |
|
1689 |
|
1690 this.updateSourceLink(); |
|
1691 |
|
1692 let code = createChild(this.element, "div", { |
|
1693 class: "ruleview-code" |
|
1694 }); |
|
1695 |
|
1696 let header = createChild(code, "div", {}); |
|
1697 |
|
1698 this.selectorText = createChild(header, "span", { |
|
1699 class: "ruleview-selector theme-fg-color3" |
|
1700 }); |
|
1701 |
|
1702 this.openBrace = createChild(header, "span", { |
|
1703 class: "ruleview-ruleopen", |
|
1704 textContent: " {" |
|
1705 }); |
|
1706 |
|
1707 code.addEventListener("click", function() { |
|
1708 let selection = this.doc.defaultView.getSelection(); |
|
1709 if (selection.isCollapsed) { |
|
1710 this.newProperty(); |
|
1711 } |
|
1712 }.bind(this), false); |
|
1713 |
|
1714 this.element.addEventListener("mousedown", function() { |
|
1715 this.doc.defaultView.focus(); |
|
1716 }.bind(this), false); |
|
1717 |
|
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(); |
|
1725 |
|
1726 this.ruleView._contextmenu.openPopupAtScreen( |
|
1727 event.screenX, event.screenY, true); |
|
1728 |
|
1729 } catch(e) { |
|
1730 console.error(e); |
|
1731 } |
|
1732 }, false); |
|
1733 |
|
1734 this.propertyList = createChild(code, "ul", { |
|
1735 class: "ruleview-propertylist" |
|
1736 }); |
|
1737 |
|
1738 this.populate(); |
|
1739 |
|
1740 this.closeBrace = createChild(code, "div", { |
|
1741 class: "ruleview-ruleclose", |
|
1742 tabindex: "0", |
|
1743 textContent: "}" |
|
1744 }); |
|
1745 |
|
1746 // Create a property editor when the close brace is clicked. |
|
1747 editableItem({ element: this.closeBrace }, (aElement) => { |
|
1748 this.newProperty(); |
|
1749 }); |
|
1750 }, |
|
1751 |
|
1752 updateSourceLink: function RuleEditor_updateSourceLink() |
|
1753 { |
|
1754 let sourceLabel = this.element.querySelector(".source-link-label"); |
|
1755 sourceLabel.setAttribute("value", this.rule.title); |
|
1756 |
|
1757 let sourceHref = (this.rule.sheet && this.rule.sheet.href) ? |
|
1758 this.rule.sheet.href : this.rule.title; |
|
1759 |
|
1760 sourceLabel.setAttribute("tooltiptext", sourceHref); |
|
1761 |
|
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 }, |
|
1770 |
|
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 } |
|
1779 |
|
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 } |
|
1805 |
|
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 }, |
|
1813 |
|
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); |
|
1832 |
|
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]); |
|
1840 |
|
1841 return prop; |
|
1842 }, |
|
1843 |
|
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 } |
|
1863 |
|
1864 let lastProp = aSiblingProp; |
|
1865 for (let p of aProperties) { |
|
1866 lastProp = this.addProperty(p.name, p.value, p.priority, lastProp); |
|
1867 } |
|
1868 |
|
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 }, |
|
1876 |
|
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 } |
|
1887 |
|
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"); |
|
1892 |
|
1893 this.newPropItem = createChild(this.propertyList, "li", { |
|
1894 class: "ruleview-property ruleview-newproperty", |
|
1895 }); |
|
1896 |
|
1897 this.newPropSpan = createChild(this.newPropItem, "span", { |
|
1898 class: "ruleview-propertyname", |
|
1899 tabindex: "0" |
|
1900 }); |
|
1901 |
|
1902 this.multipleAddedProperties = null; |
|
1903 |
|
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 }); |
|
1912 |
|
1913 // Auto-close the input if multiple rules get pasted into new property. |
|
1914 this.editor.input.addEventListener("paste", |
|
1915 blurOnMultipleProperties, false); |
|
1916 }, |
|
1917 |
|
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 } |
|
1930 |
|
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); |
|
1935 |
|
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 }, |
|
1940 |
|
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"); |
|
1950 |
|
1951 this.propertyList.removeChild(this.newPropItem); |
|
1952 delete this.newPropItem; |
|
1953 delete this.newPropSpan; |
|
1954 |
|
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 }; |
|
1963 |
|
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 === ""; |
|
1981 |
|
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); |
|
1989 |
|
1990 this._create(); |
|
1991 this.update(); |
|
1992 } |
|
1993 |
|
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 }, |
|
2004 |
|
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"); |
|
2011 |
|
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); |
|
2018 |
|
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); |
|
2024 |
|
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); |
|
2035 |
|
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 }); |
|
2042 |
|
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 }); |
|
2052 |
|
2053 // Auto blur name field on multiple CSS rules get pasted in. |
|
2054 this.nameContainer.addEventListener("paste", |
|
2055 blurOnMultipleProperties, false); |
|
2056 |
|
2057 appendText(this.nameContainer, ": "); |
|
2058 |
|
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 }); |
|
2065 |
|
2066 propertyContainer.addEventListener("click", (aEvent) => { |
|
2067 // Clicks within the value shouldn't propagate any further. |
|
2068 aEvent.stopPropagation(); |
|
2069 |
|
2070 if (aEvent.target === propertyContainer) { |
|
2071 this.valueSpan.click(); |
|
2072 } |
|
2073 }, false); |
|
2074 |
|
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 }); |
|
2082 |
|
2083 this.valueSpan.addEventListener("click", (event) => { |
|
2084 let target = event.target; |
|
2085 |
|
2086 if (target.nodeName === "a") { |
|
2087 event.stopPropagation(); |
|
2088 event.preventDefault(); |
|
2089 this.browserWindow.openUILinkIn(target.href, "tab"); |
|
2090 } |
|
2091 }, false); |
|
2092 |
|
2093 // Storing the TextProperty on the valuespan for easy access |
|
2094 // (for instance by the tooltip) |
|
2095 this.valueSpan.textProperty = this.prop; |
|
2096 |
|
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 }; |
|
2102 |
|
2103 appendText(propertyContainer, ";"); |
|
2104 |
|
2105 this.warning = createChild(this.element, "div", { |
|
2106 class: "ruleview-warning", |
|
2107 hidden: "", |
|
2108 title: CssLogic.l10n("rule.warning.title"), |
|
2109 }); |
|
2110 |
|
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 }); |
|
2116 |
|
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 }, |
|
2129 |
|
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 }, |
|
2141 |
|
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 } |
|
2155 |
|
2156 return this._sheetURI; |
|
2157 }, |
|
2158 |
|
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 }, |
|
2170 |
|
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; |
|
2179 |
|
2180 if (uriMatch && uriMatch[1]) { |
|
2181 uri = uriMatch[1]; |
|
2182 } |
|
2183 |
|
2184 return uri; |
|
2185 }, |
|
2186 |
|
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 } |
|
2198 |
|
2199 this.warning.hidden = this.editing || this.isValid(); |
|
2200 |
|
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 } |
|
2206 |
|
2207 let name = this.prop.name; |
|
2208 this.nameSpan.textContent = name; |
|
2209 |
|
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 } |
|
2216 |
|
2217 let store = this.prop.rule.elementStyle.store; |
|
2218 let propDirty = store.userProperties.contains(this.prop.rule.style, name); |
|
2219 |
|
2220 if (propDirty) { |
|
2221 this.element.setAttribute("dirty", ""); |
|
2222 } else { |
|
2223 this.element.removeAttribute("dirty"); |
|
2224 } |
|
2225 |
|
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); |
|
2237 |
|
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 } |
|
2250 |
|
2251 // Populate the computed styles. |
|
2252 this._updateComputed(); |
|
2253 }, |
|
2254 |
|
2255 _onStartEditing: function() { |
|
2256 this.element.classList.remove("ruleview-overridden"); |
|
2257 this._previewValue(this.prop.value); |
|
2258 }, |
|
2259 |
|
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 } |
|
2268 |
|
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 } |
|
2276 |
|
2277 showExpander = true; |
|
2278 |
|
2279 let li = createChild(this.computed, "li", { |
|
2280 class: "ruleview-computed" |
|
2281 }); |
|
2282 |
|
2283 if (computed.overridden) { |
|
2284 li.classList.add("ruleview-overridden"); |
|
2285 } |
|
2286 |
|
2287 createChild(li, "span", { |
|
2288 class: "ruleview-propertyname theme-fg-color5", |
|
2289 textContent: computed.name |
|
2290 }); |
|
2291 appendText(li, ": "); |
|
2292 |
|
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 ); |
|
2301 |
|
2302 createChild(li, "span", { |
|
2303 class: "ruleview-propertyvalue theme-fg-color1", |
|
2304 child: frag |
|
2305 }); |
|
2306 |
|
2307 appendText(li, ";"); |
|
2308 } |
|
2309 |
|
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 }, |
|
2317 |
|
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 }, |
|
2331 |
|
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 }, |
|
2344 |
|
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); |
|
2365 |
|
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 }, |
|
2376 |
|
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 } |
|
2387 |
|
2388 this.element.parentNode.removeChild(this.element); |
|
2389 this.ruleEditor.rule.editClosestTextProperty(this.prop); |
|
2390 this.valueSpan.textProperty = null; |
|
2391 this.prop.remove(); |
|
2392 }, |
|
2393 |
|
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 } |
|
2413 |
|
2414 let {propertiesToAdd,firstValue} = this._getValueAndExtraProperties(aValue); |
|
2415 |
|
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; |
|
2422 |
|
2423 // If needed, add any new properties after this.prop. |
|
2424 this.ruleEditor.addProperties(propertiesToAdd, this.prop); |
|
2425 |
|
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 }, |
|
2439 |
|
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 = []; |
|
2462 |
|
2463 let properties = parseDeclarations(aValue); |
|
2464 |
|
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 } |
|
2479 |
|
2480 return { |
|
2481 propertiesToAdd: propertiesToAdd, |
|
2482 firstValue: firstValue |
|
2483 }; |
|
2484 }, |
|
2485 |
|
2486 _applyNewValue: function(aValue) { |
|
2487 let val = parseSingleValue(aValue); |
|
2488 |
|
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 }, |
|
2494 |
|
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 } |
|
2504 |
|
2505 let val = parseSingleValue(aValue); |
|
2506 this.ruleEditor.rule.previewPropertyValue(this.prop, val.value, val.priority); |
|
2507 }, |
|
2508 |
|
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); |
|
2523 |
|
2524 let style = this.doc.createElementNS(HTML_NS, "div").style; |
|
2525 let prefs = Services.prefs; |
|
2526 |
|
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); |
|
2530 |
|
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 }; |
|
2541 |
|
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 } |
|
2549 |
|
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); |
|
2568 |
|
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 }, |
|
2579 |
|
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 }, |
|
2601 |
|
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 }, |
|
2615 |
|
2616 getKey: function(aStyle) { |
|
2617 return aStyle.href + ":" + aStyle.line; |
|
2618 } |
|
2619 }; |
|
2620 |
|
2621 /** |
|
2622 * Helper functions |
|
2623 */ |
|
2624 |
|
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 } |
|
2651 |
|
2652 function createMenuItem(aMenu, aAttributes) { |
|
2653 let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem"); |
|
2654 |
|
2655 item.setAttribute("label", _strings.GetStringFromName(aAttributes.label)); |
|
2656 item.setAttribute("accesskey", _strings.GetStringFromName(aAttributes.accesskey)); |
|
2657 item.addEventListener("command", aAttributes.command); |
|
2658 |
|
2659 aMenu.appendChild(item); |
|
2660 |
|
2661 return item; |
|
2662 } |
|
2663 |
|
2664 function setTimeout() { |
|
2665 let window = Services.appShell.hiddenDOMWindow; |
|
2666 return window.setTimeout.apply(window, arguments); |
|
2667 } |
|
2668 |
|
2669 function clearTimeout() { |
|
2670 let window = Services.appShell.hiddenDOMWindow; |
|
2671 return window.clearTimeout.apply(window, arguments); |
|
2672 } |
|
2673 |
|
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 } |
|
2687 |
|
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 } |
|
2700 |
|
2701 /** |
|
2702 * Append a text node to an element. |
|
2703 */ |
|
2704 function appendText(aParent, aText) { |
|
2705 aParent.appendChild(aParent.ownerDocument.createTextNode(aText)); |
|
2706 } |
|
2707 |
|
2708 XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() { |
|
2709 return Cc["@mozilla.org/widget/clipboardhelper;1"]. |
|
2710 getService(Ci.nsIClipboardHelper); |
|
2711 }); |
|
2712 |
|
2713 XPCOMUtils.defineLazyGetter(this, "_strings", function() { |
|
2714 return Services.strings.createBundle( |
|
2715 "chrome://global/locale/devtools/styleinspector.properties"); |
|
2716 }); |
|
2717 |
|
2718 XPCOMUtils.defineLazyGetter(this, "domUtils", function() { |
|
2719 return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); |
|
2720 }); |
|
2721 |
|
2722 loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup); |