|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 const {Cc, Ci, Cu} = require("chrome"); |
|
8 const Services = require("Services"); |
|
9 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); |
|
10 const protocol = require("devtools/server/protocol"); |
|
11 const {Arg, Option, method, RetVal, types} = protocol; |
|
12 const events = require("sdk/event/core"); |
|
13 const object = require("sdk/util/object"); |
|
14 const { Class } = require("sdk/core/heritage"); |
|
15 const { StyleSheetActor } = require("devtools/server/actors/stylesheets"); |
|
16 |
|
17 loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic); |
|
18 loader.lazyGetter(this, "DOMUtils", () => Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)); |
|
19 |
|
20 // The PageStyle actor flattens the DOM CSS objects a little bit, merging |
|
21 // Rules and their Styles into one actor. For elements (which have a style |
|
22 // but no associated rule) we fake a rule with the following style id. |
|
23 const ELEMENT_STYLE = 100; |
|
24 exports.ELEMENT_STYLE = ELEMENT_STYLE; |
|
25 |
|
26 const PSEUDO_ELEMENTS = [":first-line", ":first-letter", ":before", ":after", ":-moz-selection"]; |
|
27 exports.PSEUDO_ELEMENTS = PSEUDO_ELEMENTS; |
|
28 |
|
29 // Predeclare the domnode actor type for use in requests. |
|
30 types.addActorType("domnode"); |
|
31 |
|
32 /** |
|
33 * DOM Nodes returned by the style actor will be owned by the DOM walker |
|
34 * for the connection. |
|
35 */ |
|
36 types.addLifetime("walker", "walker"); |
|
37 |
|
38 /** |
|
39 * When asking for the styles applied to a node, we return a list of |
|
40 * appliedstyle json objects that lists the rules that apply to the node |
|
41 * and which element they were inherited from (if any). |
|
42 */ |
|
43 types.addDictType("appliedstyle", { |
|
44 rule: "domstylerule#actorid", |
|
45 inherited: "nullable:domnode#actorid" |
|
46 }); |
|
47 |
|
48 types.addDictType("matchedselector", { |
|
49 rule: "domstylerule#actorid", |
|
50 selector: "string", |
|
51 value: "string", |
|
52 status: "number" |
|
53 }); |
|
54 |
|
55 /** |
|
56 * The PageStyle actor lets the client look at the styles on a page, as |
|
57 * they are applied to a given node. |
|
58 */ |
|
59 var PageStyleActor = protocol.ActorClass({ |
|
60 typeName: "pagestyle", |
|
61 |
|
62 /** |
|
63 * Create a PageStyleActor. |
|
64 * |
|
65 * @param inspector |
|
66 * The InspectorActor that owns this PageStyleActor. |
|
67 * |
|
68 * @constructor |
|
69 */ |
|
70 initialize: function(inspector) { |
|
71 protocol.Actor.prototype.initialize.call(this, null); |
|
72 this.inspector = inspector; |
|
73 if (!this.inspector.walker) { |
|
74 throw Error("The inspector's WalkerActor must be created before " + |
|
75 "creating a PageStyleActor."); |
|
76 } |
|
77 this.walker = inspector.walker; |
|
78 this.cssLogic = new CssLogic; |
|
79 |
|
80 // Stores the association of DOM objects -> actors |
|
81 this.refMap = new Map; |
|
82 }, |
|
83 |
|
84 get conn() this.inspector.conn, |
|
85 |
|
86 /** |
|
87 * Return or create a StyleRuleActor for the given item. |
|
88 * @param item Either a CSSStyleRule or a DOM element. |
|
89 */ |
|
90 _styleRef: function(item) { |
|
91 if (this.refMap.has(item)) { |
|
92 return this.refMap.get(item); |
|
93 } |
|
94 let actor = StyleRuleActor(this, item); |
|
95 this.manage(actor); |
|
96 this.refMap.set(item, actor); |
|
97 |
|
98 return actor; |
|
99 }, |
|
100 |
|
101 /** |
|
102 * Return or create a StyleSheetActor for the given |
|
103 * nsIDOMCSSStyleSheet |
|
104 */ |
|
105 _sheetRef: function(sheet) { |
|
106 if (this.refMap.has(sheet)) { |
|
107 return this.refMap.get(sheet); |
|
108 } |
|
109 let actor = new StyleSheetActor(sheet, this, this.walker.rootWin); |
|
110 this.manage(actor); |
|
111 this.refMap.set(sheet, actor); |
|
112 |
|
113 return actor; |
|
114 }, |
|
115 |
|
116 /** |
|
117 * Get the computed style for a node. |
|
118 * |
|
119 * @param NodeActor node |
|
120 * @param object options |
|
121 * `filter`: A string filter that affects the "matched" handling. |
|
122 * 'user': Include properties from user style sheets. |
|
123 * 'ua': Include properties from user and user-agent sheets. |
|
124 * Default value is 'ua' |
|
125 * `markMatched`: true if you want the 'matched' property to be added |
|
126 * when a computed property has been modified by a style included |
|
127 * by `filter`. |
|
128 * `onlyMatched`: true if unmatched properties shouldn't be included. |
|
129 * |
|
130 * @returns a JSON blob with the following form: |
|
131 * { |
|
132 * "property-name": { |
|
133 * value: "property-value", |
|
134 * priority: "!important" <optional> |
|
135 * matched: <true if there are matched selectors for this value> |
|
136 * }, |
|
137 * ... |
|
138 * } |
|
139 */ |
|
140 getComputed: method(function(node, options) { |
|
141 let ret = Object.create(null); |
|
142 |
|
143 this.cssLogic.sourceFilter = options.filter || CssLogic.FILTER.UA; |
|
144 this.cssLogic.highlight(node.rawNode); |
|
145 let computed = this.cssLogic._computedStyle || []; |
|
146 |
|
147 Array.prototype.forEach.call(computed, name => { |
|
148 let matched = undefined; |
|
149 ret[name] = { |
|
150 value: computed.getPropertyValue(name), |
|
151 priority: computed.getPropertyPriority(name) || undefined |
|
152 }; |
|
153 }); |
|
154 |
|
155 if (options.markMatched || options.onlyMatched) { |
|
156 let matched = this.cssLogic.hasMatchedSelectors(Object.keys(ret)); |
|
157 for (let key in ret) { |
|
158 if (matched[key]) { |
|
159 ret[key].matched = options.markMatched ? true : undefined |
|
160 } else if (options.onlyMatched) { |
|
161 delete ret[key]; |
|
162 } |
|
163 } |
|
164 } |
|
165 |
|
166 return ret; |
|
167 }, { |
|
168 request: { |
|
169 node: Arg(0, "domnode"), |
|
170 markMatched: Option(1, "boolean"), |
|
171 onlyMatched: Option(1, "boolean"), |
|
172 filter: Option(1, "string"), |
|
173 }, |
|
174 response: { |
|
175 computed: RetVal("json") |
|
176 } |
|
177 }), |
|
178 |
|
179 /** |
|
180 * Get a list of selectors that match a given property for a node. |
|
181 * |
|
182 * @param NodeActor node |
|
183 * @param string property |
|
184 * @param object options |
|
185 * `filter`: A string filter that affects the "matched" handling. |
|
186 * 'user': Include properties from user style sheets. |
|
187 * 'ua': Include properties from user and user-agent sheets. |
|
188 * Default value is 'ua' |
|
189 * |
|
190 * @returns a JSON object with the following form: |
|
191 * { |
|
192 * // An ordered list of rules that apply |
|
193 * matched: [{ |
|
194 * rule: <rule actorid>, |
|
195 * sourceText: <string>, // The source of the selector, relative |
|
196 * // to the node in question. |
|
197 * selector: <string>, // the selector ID that matched |
|
198 * value: <string>, // the value of the property |
|
199 * status: <int>, |
|
200 * // The status of the match - high numbers are better placed |
|
201 * // to provide styling information: |
|
202 * // 3: Best match, was used. |
|
203 * // 2: Matched, but was overridden. |
|
204 * // 1: Rule from a parent matched. |
|
205 * // 0: Unmatched (never returned in this API) |
|
206 * }, ...], |
|
207 * |
|
208 * // The full form of any domrule referenced. |
|
209 * rules: [ <domrule>, ... ], // The full form of any domrule referenced |
|
210 * |
|
211 * // The full form of any sheets referenced. |
|
212 * sheets: [ <domsheet>, ... ] |
|
213 * } |
|
214 */ |
|
215 getMatchedSelectors: method(function(node, property, options) { |
|
216 this.cssLogic.sourceFilter = options.filter || CssLogic.FILTER.UA; |
|
217 this.cssLogic.highlight(node.rawNode); |
|
218 |
|
219 let walker = node.parent(); |
|
220 |
|
221 let rules = new Set; |
|
222 let sheets = new Set; |
|
223 |
|
224 let matched = []; |
|
225 let propInfo = this.cssLogic.getPropertyInfo(property); |
|
226 for (let selectorInfo of propInfo.matchedSelectors) { |
|
227 let cssRule = selectorInfo.selector.cssRule; |
|
228 let domRule = cssRule.sourceElement || cssRule.domRule; |
|
229 |
|
230 let rule = this._styleRef(domRule); |
|
231 rules.add(rule); |
|
232 |
|
233 matched.push({ |
|
234 rule: rule, |
|
235 sourceText: this.getSelectorSource(selectorInfo, node.rawNode), |
|
236 selector: selectorInfo.selector.text, |
|
237 name: selectorInfo.property, |
|
238 value: selectorInfo.value, |
|
239 status: selectorInfo.status |
|
240 }); |
|
241 } |
|
242 |
|
243 this.expandSets(rules, sheets); |
|
244 |
|
245 return { |
|
246 matched: matched, |
|
247 rules: [...rules], |
|
248 sheets: [...sheets], |
|
249 }; |
|
250 }, { |
|
251 request: { |
|
252 node: Arg(0, "domnode"), |
|
253 property: Arg(1, "string"), |
|
254 filter: Option(2, "string") |
|
255 }, |
|
256 response: RetVal(types.addDictType("matchedselectorresponse", { |
|
257 rules: "array:domstylerule", |
|
258 sheets: "array:stylesheet", |
|
259 matched: "array:matchedselector" |
|
260 })) |
|
261 }), |
|
262 |
|
263 // Get a selector source for a CssSelectorInfo relative to a given |
|
264 // node. |
|
265 getSelectorSource: function(selectorInfo, relativeTo) { |
|
266 let result = selectorInfo.selector.text; |
|
267 if (selectorInfo.elementStyle) { |
|
268 let source = selectorInfo.sourceElement; |
|
269 if (source === relativeTo) { |
|
270 result = "this"; |
|
271 } else { |
|
272 result = CssLogic.getShortName(source); |
|
273 } |
|
274 result += ".style" |
|
275 } |
|
276 return result; |
|
277 }, |
|
278 |
|
279 /** |
|
280 * Get the set of styles that apply to a given node. |
|
281 * @param NodeActor node |
|
282 * @param string property |
|
283 * @param object options |
|
284 * `filter`: A string filter that affects the "matched" handling. |
|
285 * 'user': Include properties from user style sheets. |
|
286 * 'ua': Include properties from user and user-agent sheets. |
|
287 * Default value is 'ua' |
|
288 * `inherited`: Include styles inherited from parent nodes. |
|
289 * `matchedSeletors`: Include an array of specific selectors that |
|
290 * caused this rule to match its node. |
|
291 */ |
|
292 getApplied: method(function(node, options) { |
|
293 let entries = []; |
|
294 |
|
295 this.addElementRules(node.rawNode, undefined, options, entries); |
|
296 |
|
297 if (options.inherited) { |
|
298 let parent = this.walker.parentNode(node); |
|
299 while (parent && parent.rawNode.nodeType != Ci.nsIDOMNode.DOCUMENT_NODE) { |
|
300 this.addElementRules(parent.rawNode, parent, options, entries); |
|
301 parent = this.walker.parentNode(parent); |
|
302 } |
|
303 } |
|
304 |
|
305 if (options.matchedSelectors) { |
|
306 for (let entry of entries) { |
|
307 if (entry.rule.type === ELEMENT_STYLE) { |
|
308 continue; |
|
309 } |
|
310 |
|
311 let domRule = entry.rule.rawRule; |
|
312 let selectors = CssLogic.getSelectors(domRule); |
|
313 let element = entry.inherited ? entry.inherited.rawNode : node.rawNode; |
|
314 entry.matchedSelectors = []; |
|
315 for (let i = 0; i < selectors.length; i++) { |
|
316 if (DOMUtils.selectorMatchesElement(element, domRule, i)) { |
|
317 entry.matchedSelectors.push(selectors[i]); |
|
318 } |
|
319 } |
|
320 |
|
321 } |
|
322 } |
|
323 |
|
324 let rules = new Set; |
|
325 let sheets = new Set; |
|
326 entries.forEach(entry => rules.add(entry.rule)); |
|
327 this.expandSets(rules, sheets); |
|
328 |
|
329 return { |
|
330 entries: entries, |
|
331 rules: [...rules], |
|
332 sheets: [...sheets] |
|
333 } |
|
334 }, { |
|
335 request: { |
|
336 node: Arg(0, "domnode"), |
|
337 inherited: Option(1, "boolean"), |
|
338 matchedSelectors: Option(1, "boolean"), |
|
339 filter: Option(1, "string") |
|
340 }, |
|
341 response: RetVal(types.addDictType("appliedStylesReturn", { |
|
342 entries: "array:appliedstyle", |
|
343 rules: "array:domstylerule", |
|
344 sheets: "array:stylesheet" |
|
345 })) |
|
346 }), |
|
347 |
|
348 _hasInheritedProps: function(style) { |
|
349 return Array.prototype.some.call(style, prop => { |
|
350 return DOMUtils.isInheritedProperty(prop); |
|
351 }); |
|
352 }, |
|
353 |
|
354 /** |
|
355 * Helper function for getApplied, adds all the rules from a given |
|
356 * element. |
|
357 */ |
|
358 addElementRules: function(element, inherited, options, rules) |
|
359 { |
|
360 if (!element.style) { |
|
361 return; |
|
362 } |
|
363 |
|
364 let elementStyle = this._styleRef(element); |
|
365 |
|
366 if (!inherited || this._hasInheritedProps(element.style)) { |
|
367 rules.push({ |
|
368 rule: elementStyle, |
|
369 inherited: inherited, |
|
370 }); |
|
371 } |
|
372 |
|
373 let pseudoElements = inherited ? [null] : [null, ...PSEUDO_ELEMENTS]; |
|
374 for (let pseudo of pseudoElements) { |
|
375 |
|
376 // Get the styles that apply to the element. |
|
377 let domRules = DOMUtils.getCSSStyleRules(element, pseudo); |
|
378 |
|
379 if (!domRules) { |
|
380 continue; |
|
381 } |
|
382 |
|
383 // getCSSStyleRules returns ordered from least-specific to |
|
384 // most-specific. |
|
385 for (let i = domRules.Count() - 1; i >= 0; i--) { |
|
386 let domRule = domRules.GetElementAt(i); |
|
387 |
|
388 let isSystem = !CssLogic.isContentStylesheet(domRule.parentStyleSheet); |
|
389 |
|
390 if (isSystem && options.filter != CssLogic.FILTER.UA) { |
|
391 continue; |
|
392 } |
|
393 |
|
394 if (inherited) { |
|
395 // Don't include inherited rules if none of its properties |
|
396 // are inheritable. |
|
397 let hasInherited = Array.prototype.some.call(domRule.style, prop => { |
|
398 return DOMUtils.isInheritedProperty(prop); |
|
399 }); |
|
400 if (!hasInherited) { |
|
401 continue; |
|
402 } |
|
403 } |
|
404 |
|
405 let ruleActor = this._styleRef(domRule); |
|
406 rules.push({ |
|
407 rule: ruleActor, |
|
408 inherited: inherited, |
|
409 pseudoElement: pseudo |
|
410 }); |
|
411 } |
|
412 |
|
413 } |
|
414 }, |
|
415 |
|
416 /** |
|
417 * Expand Sets of rules and sheets to include all parent rules and sheets. |
|
418 */ |
|
419 expandSets: function(ruleSet, sheetSet) { |
|
420 // Sets include new items in their iteration |
|
421 for (let rule of ruleSet) { |
|
422 if (rule.rawRule.parentRule) { |
|
423 let parent = this._styleRef(rule.rawRule.parentRule); |
|
424 if (!ruleSet.has(parent)) { |
|
425 ruleSet.add(parent); |
|
426 } |
|
427 } |
|
428 if (rule.rawRule.parentStyleSheet) { |
|
429 let parent = this._sheetRef(rule.rawRule.parentStyleSheet); |
|
430 if (!sheetSet.has(parent)) { |
|
431 sheetSet.add(parent); |
|
432 } |
|
433 } |
|
434 } |
|
435 |
|
436 for (let sheet of sheetSet) { |
|
437 if (sheet.rawSheet.parentStyleSheet) { |
|
438 let parent = this._sheetRef(sheet.rawSheet.parentStyleSheet); |
|
439 if (!sheetSet.has(parent)) { |
|
440 sheetSet.add(parent); |
|
441 } |
|
442 } |
|
443 } |
|
444 }, |
|
445 |
|
446 getLayout: method(function(node, options) { |
|
447 this.cssLogic.highlight(node.rawNode); |
|
448 |
|
449 let layout = {}; |
|
450 |
|
451 // First, we update the first part of the layout view, with |
|
452 // the size of the element. |
|
453 |
|
454 let clientRect = node.rawNode.getBoundingClientRect(); |
|
455 layout.width = Math.round(clientRect.width); |
|
456 layout.height = Math.round(clientRect.height); |
|
457 |
|
458 // We compute and update the values of margins & co. |
|
459 let style = node.rawNode.ownerDocument.defaultView.getComputedStyle(node.rawNode); |
|
460 for (let prop of [ |
|
461 "position", |
|
462 "margin-top", |
|
463 "margin-right", |
|
464 "margin-bottom", |
|
465 "margin-left", |
|
466 "padding-top", |
|
467 "padding-right", |
|
468 "padding-bottom", |
|
469 "padding-left", |
|
470 "border-top-width", |
|
471 "border-right-width", |
|
472 "border-bottom-width", |
|
473 "border-left-width" |
|
474 ]) { |
|
475 layout[prop] = style.getPropertyValue(prop); |
|
476 } |
|
477 |
|
478 if (options.autoMargins) { |
|
479 layout.autoMargins = this.processMargins(this.cssLogic); |
|
480 } |
|
481 |
|
482 for (let i in this.map) { |
|
483 let property = this.map[i].property; |
|
484 this.map[i].value = parseInt(style.getPropertyValue(property)); |
|
485 } |
|
486 |
|
487 |
|
488 if (options.margins) { |
|
489 layout.margins = this.processMargins(cssLogic); |
|
490 } |
|
491 |
|
492 return layout; |
|
493 }, { |
|
494 request: { |
|
495 node: Arg(0, "domnode"), |
|
496 autoMargins: Option(1, "boolean") |
|
497 }, |
|
498 response: RetVal("json") |
|
499 }), |
|
500 |
|
501 /** |
|
502 * Find 'auto' margin properties. |
|
503 */ |
|
504 processMargins: function(cssLogic) { |
|
505 let margins = {}; |
|
506 |
|
507 for (let prop of ["top", "bottom", "left", "right"]) { |
|
508 let info = cssLogic.getPropertyInfo("margin-" + prop); |
|
509 let selectors = info.matchedSelectors; |
|
510 if (selectors && selectors.length > 0 && selectors[0].value == "auto") { |
|
511 margins[prop] = "auto"; |
|
512 } |
|
513 } |
|
514 |
|
515 return margins; |
|
516 }, |
|
517 |
|
518 }); |
|
519 exports.PageStyleActor = PageStyleActor; |
|
520 |
|
521 /** |
|
522 * Front object for the PageStyleActor |
|
523 */ |
|
524 var PageStyleFront = protocol.FrontClass(PageStyleActor, { |
|
525 initialize: function(conn, form, ctx, detail) { |
|
526 protocol.Front.prototype.initialize.call(this, conn, form, ctx, detail); |
|
527 this.inspector = this.parent(); |
|
528 }, |
|
529 |
|
530 destroy: function() { |
|
531 protocol.Front.prototype.destroy.call(this); |
|
532 }, |
|
533 |
|
534 get walker() { |
|
535 return this.inspector.walker; |
|
536 }, |
|
537 |
|
538 getMatchedSelectors: protocol.custom(function(node, property, options) { |
|
539 return this._getMatchedSelectors(node, property, options).then(ret => { |
|
540 return ret.matched; |
|
541 }); |
|
542 }, { |
|
543 impl: "_getMatchedSelectors" |
|
544 }), |
|
545 |
|
546 getApplied: protocol.custom(function(node, options={}) { |
|
547 return this._getApplied(node, options).then(ret => { |
|
548 return ret.entries; |
|
549 }); |
|
550 }, { |
|
551 impl: "_getApplied" |
|
552 }) |
|
553 }); |
|
554 |
|
555 // Predeclare the domstylerule actor type |
|
556 types.addActorType("domstylerule"); |
|
557 |
|
558 /** |
|
559 * An actor that represents a CSS style object on the protocol. |
|
560 * |
|
561 * We slightly flatten the CSSOM for this actor, it represents |
|
562 * both the CSSRule and CSSStyle objects in one actor. For nodes |
|
563 * (which have a CSSStyle but no CSSRule) we create a StyleRuleActor |
|
564 * with a special rule type (100). |
|
565 */ |
|
566 var StyleRuleActor = protocol.ActorClass({ |
|
567 typeName: "domstylerule", |
|
568 initialize: function(pageStyle, item) { |
|
569 protocol.Actor.prototype.initialize.call(this, null); |
|
570 this.pageStyle = pageStyle; |
|
571 this.rawStyle = item.style; |
|
572 |
|
573 if (item instanceof (Ci.nsIDOMCSSRule)) { |
|
574 this.type = item.type; |
|
575 this.rawRule = item; |
|
576 if (this.rawRule instanceof Ci.nsIDOMCSSStyleRule && this.rawRule.parentStyleSheet) { |
|
577 this.line = DOMUtils.getRuleLine(this.rawRule); |
|
578 this.column = DOMUtils.getRuleColumn(this.rawRule); |
|
579 } |
|
580 } else { |
|
581 // Fake a rule |
|
582 this.type = ELEMENT_STYLE; |
|
583 this.rawNode = item; |
|
584 this.rawRule = { |
|
585 style: item.style, |
|
586 toString: function() "[element rule " + this.style + "]" |
|
587 } |
|
588 } |
|
589 }, |
|
590 |
|
591 get conn() this.pageStyle.conn, |
|
592 |
|
593 // Objects returned by this actor are owned by the PageStyleActor |
|
594 // to which this rule belongs. |
|
595 get marshallPool() this.pageStyle, |
|
596 |
|
597 toString: function() "[StyleRuleActor for " + this.rawRule + "]", |
|
598 |
|
599 form: function(detail) { |
|
600 if (detail === "actorid") { |
|
601 return this.actorID; |
|
602 } |
|
603 |
|
604 let form = { |
|
605 actor: this.actorID, |
|
606 type: this.type, |
|
607 line: this.line || undefined, |
|
608 column: this.column |
|
609 }; |
|
610 |
|
611 if (this.rawRule.parentRule) { |
|
612 form.parentRule = this.pageStyle._styleRef(this.rawRule.parentRule).actorID; |
|
613 } |
|
614 if (this.rawRule.parentStyleSheet) { |
|
615 form.parentStyleSheet = this.pageStyle._sheetRef(this.rawRule.parentStyleSheet).actorID; |
|
616 } |
|
617 |
|
618 switch (this.type) { |
|
619 case Ci.nsIDOMCSSRule.STYLE_RULE: |
|
620 form.selectors = CssLogic.getSelectors(this.rawRule); |
|
621 form.cssText = this.rawStyle.cssText || ""; |
|
622 break; |
|
623 case ELEMENT_STYLE: |
|
624 // Elements don't have a parent stylesheet, and therefore |
|
625 // don't have an associated URI. Provide a URI for |
|
626 // those. |
|
627 form.href = this.rawNode.ownerDocument.location.href; |
|
628 form.cssText = this.rawStyle.cssText || ""; |
|
629 break; |
|
630 case Ci.nsIDOMCSSRule.CHARSET_RULE: |
|
631 form.encoding = this.rawRule.encoding; |
|
632 break; |
|
633 case Ci.nsIDOMCSSRule.IMPORT_RULE: |
|
634 form.href = this.rawRule.href; |
|
635 break; |
|
636 case Ci.nsIDOMCSSRule.MEDIA_RULE: |
|
637 form.media = []; |
|
638 for (let i = 0, n = this.rawRule.media.length; i < n; i++) { |
|
639 form.media.push(this.rawRule.media.item(i)); |
|
640 } |
|
641 break; |
|
642 } |
|
643 |
|
644 return form; |
|
645 }, |
|
646 |
|
647 /** |
|
648 * Modify a rule's properties. Passed an array of modifications: |
|
649 * { |
|
650 * type: "set", |
|
651 * name: <string>, |
|
652 * value: <string>, |
|
653 * priority: <optional string> |
|
654 * } |
|
655 * or |
|
656 * { |
|
657 * type: "remove", |
|
658 * name: <string>, |
|
659 * } |
|
660 * |
|
661 * @returns the rule with updated properties |
|
662 */ |
|
663 modifyProperties: method(function(modifications) { |
|
664 let validProps = new Map(); |
|
665 |
|
666 // Use a fresh element for each call to this function to prevent side effects |
|
667 // that pop up based on property values that were already set on the element. |
|
668 |
|
669 let document; |
|
670 if (this.rawNode) { |
|
671 document = this.rawNode.ownerDocument; |
|
672 } else { |
|
673 let parentStyleSheet = this.rawRule.parentStyleSheet; |
|
674 while (parentStyleSheet.ownerRule && |
|
675 parentStyleSheet.ownerRule instanceof Ci.nsIDOMCSSImportRule) { |
|
676 parentStyleSheet = parentStyleSheet.ownerRule.parentStyleSheet; |
|
677 } |
|
678 |
|
679 if (parentStyleSheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) { |
|
680 document = parentStyleSheet.ownerNode; |
|
681 } else { |
|
682 document = parentStyleSheet.ownerNode.ownerDocument; |
|
683 } |
|
684 } |
|
685 |
|
686 let tempElement = document.createElement("div"); |
|
687 |
|
688 for (let mod of modifications) { |
|
689 if (mod.type === "set") { |
|
690 tempElement.style.setProperty(mod.name, mod.value, mod.priority || ""); |
|
691 this.rawStyle.setProperty(mod.name, |
|
692 tempElement.style.getPropertyValue(mod.name), mod.priority || ""); |
|
693 } else if (mod.type === "remove") { |
|
694 this.rawStyle.removeProperty(mod.name); |
|
695 } |
|
696 } |
|
697 |
|
698 return this; |
|
699 }, { |
|
700 request: { modifications: Arg(0, "array:json") }, |
|
701 response: { rule: RetVal("domstylerule") } |
|
702 }) |
|
703 }); |
|
704 |
|
705 /** |
|
706 * Front for the StyleRule actor. |
|
707 */ |
|
708 var StyleRuleFront = protocol.FrontClass(StyleRuleActor, { |
|
709 initialize: function(client, form, ctx, detail) { |
|
710 protocol.Front.prototype.initialize.call(this, client, form, ctx, detail); |
|
711 }, |
|
712 |
|
713 destroy: function() { |
|
714 protocol.Front.prototype.destroy.call(this); |
|
715 }, |
|
716 |
|
717 form: function(form, detail) { |
|
718 if (detail === "actorid") { |
|
719 this.actorID = form; |
|
720 return; |
|
721 } |
|
722 this.actorID = form.actor; |
|
723 this._form = form; |
|
724 if (this._mediaText) { |
|
725 this._mediaText = null; |
|
726 } |
|
727 }, |
|
728 |
|
729 /** |
|
730 * Return a new RuleModificationList for this node. |
|
731 */ |
|
732 startModifyingProperties: function() { |
|
733 return new RuleModificationList(this); |
|
734 }, |
|
735 |
|
736 get type() this._form.type, |
|
737 get line() this._form.line || -1, |
|
738 get column() this._form.column || -1, |
|
739 get cssText() { |
|
740 return this._form.cssText; |
|
741 }, |
|
742 get selectors() { |
|
743 return this._form.selectors; |
|
744 }, |
|
745 get media() { |
|
746 return this._form.media; |
|
747 }, |
|
748 get mediaText() { |
|
749 if (!this._form.media) { |
|
750 return null; |
|
751 } |
|
752 if (this._mediaText) { |
|
753 return this._mediaText; |
|
754 } |
|
755 this._mediaText = this.media.join(", "); |
|
756 return this._mediaText; |
|
757 }, |
|
758 |
|
759 get parentRule() { |
|
760 return this.conn.getActor(this._form.parentRule); |
|
761 }, |
|
762 |
|
763 get parentStyleSheet() { |
|
764 return this.conn.getActor(this._form.parentStyleSheet); |
|
765 }, |
|
766 |
|
767 get element() { |
|
768 return this.conn.getActor(this._form.element); |
|
769 }, |
|
770 |
|
771 get href() { |
|
772 if (this._form.href) { |
|
773 return this._form.href; |
|
774 } |
|
775 let sheet = this.parentStyleSheet; |
|
776 return sheet.href; |
|
777 }, |
|
778 |
|
779 get nodeHref() { |
|
780 let sheet = this.parentStyleSheet; |
|
781 return sheet ? sheet.nodeHref : ""; |
|
782 }, |
|
783 |
|
784 get location() |
|
785 { |
|
786 return { |
|
787 href: this.href, |
|
788 line: this.line, |
|
789 column: this.column |
|
790 }; |
|
791 }, |
|
792 |
|
793 getOriginalLocation: function() |
|
794 { |
|
795 if (this._originalLocation) { |
|
796 return promise.resolve(this._originalLocation); |
|
797 } |
|
798 |
|
799 let parentSheet = this.parentStyleSheet; |
|
800 if (!parentSheet) { |
|
801 return promise.resolve(this.location); |
|
802 } |
|
803 return parentSheet.getOriginalLocation(this.line, this.column) |
|
804 .then(({ source, line, column }) => { |
|
805 let location = { |
|
806 href: source, |
|
807 line: line, |
|
808 column: column |
|
809 } |
|
810 if (!source) { |
|
811 location.href = this.href; |
|
812 } |
|
813 this._originalLocation = location; |
|
814 return location; |
|
815 }) |
|
816 }, |
|
817 |
|
818 // Only used for testing, please keep it that way. |
|
819 _rawStyle: function() { |
|
820 if (!this.conn._transport._serverConnection) { |
|
821 console.warn("Tried to use rawNode on a remote connection."); |
|
822 return null; |
|
823 } |
|
824 let actor = this.conn._transport._serverConnection.getActor(this.actorID); |
|
825 if (!actor) { |
|
826 return null; |
|
827 } |
|
828 return actor.rawStyle; |
|
829 } |
|
830 }); |
|
831 |
|
832 /** |
|
833 * Convenience API for building a list of attribute modifications |
|
834 * for the `modifyAttributes` request. |
|
835 */ |
|
836 var RuleModificationList = Class({ |
|
837 initialize: function(rule) { |
|
838 this.rule = rule; |
|
839 this.modifications = []; |
|
840 }, |
|
841 |
|
842 apply: function() { |
|
843 return this.rule.modifyProperties(this.modifications); |
|
844 }, |
|
845 setProperty: function(name, value, priority) { |
|
846 this.modifications.push({ |
|
847 type: "set", |
|
848 name: name, |
|
849 value: value, |
|
850 priority: priority |
|
851 }); |
|
852 }, |
|
853 removeProperty: function(name) { |
|
854 this.modifications.push({ |
|
855 type: "remove", |
|
856 name: name |
|
857 }); |
|
858 } |
|
859 }); |
|
860 |