Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* 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/. */
5 "use strict";
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");
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));
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;
26 const PSEUDO_ELEMENTS = [":first-line", ":first-letter", ":before", ":after", ":-moz-selection"];
27 exports.PSEUDO_ELEMENTS = PSEUDO_ELEMENTS;
29 // Predeclare the domnode actor type for use in requests.
30 types.addActorType("domnode");
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");
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 });
48 types.addDictType("matchedselector", {
49 rule: "domstylerule#actorid",
50 selector: "string",
51 value: "string",
52 status: "number"
53 });
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",
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;
80 // Stores the association of DOM objects -> actors
81 this.refMap = new Map;
82 },
84 get conn() this.inspector.conn,
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);
98 return actor;
99 },
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);
113 return actor;
114 },
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);
143 this.cssLogic.sourceFilter = options.filter || CssLogic.FILTER.UA;
144 this.cssLogic.highlight(node.rawNode);
145 let computed = this.cssLogic._computedStyle || [];
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 });
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 }
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 }),
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);
219 let walker = node.parent();
221 let rules = new Set;
222 let sheets = new Set;
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;
230 let rule = this._styleRef(domRule);
231 rules.add(rule);
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 }
243 this.expandSets(rules, sheets);
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 }),
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 },
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 = [];
295 this.addElementRules(node.rawNode, undefined, options, entries);
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 }
305 if (options.matchedSelectors) {
306 for (let entry of entries) {
307 if (entry.rule.type === ELEMENT_STYLE) {
308 continue;
309 }
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 }
321 }
322 }
324 let rules = new Set;
325 let sheets = new Set;
326 entries.forEach(entry => rules.add(entry.rule));
327 this.expandSets(rules, sheets);
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 }),
348 _hasInheritedProps: function(style) {
349 return Array.prototype.some.call(style, prop => {
350 return DOMUtils.isInheritedProperty(prop);
351 });
352 },
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 }
364 let elementStyle = this._styleRef(element);
366 if (!inherited || this._hasInheritedProps(element.style)) {
367 rules.push({
368 rule: elementStyle,
369 inherited: inherited,
370 });
371 }
373 let pseudoElements = inherited ? [null] : [null, ...PSEUDO_ELEMENTS];
374 for (let pseudo of pseudoElements) {
376 // Get the styles that apply to the element.
377 let domRules = DOMUtils.getCSSStyleRules(element, pseudo);
379 if (!domRules) {
380 continue;
381 }
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);
388 let isSystem = !CssLogic.isContentStylesheet(domRule.parentStyleSheet);
390 if (isSystem && options.filter != CssLogic.FILTER.UA) {
391 continue;
392 }
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 }
405 let ruleActor = this._styleRef(domRule);
406 rules.push({
407 rule: ruleActor,
408 inherited: inherited,
409 pseudoElement: pseudo
410 });
411 }
413 }
414 },
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 }
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 },
446 getLayout: method(function(node, options) {
447 this.cssLogic.highlight(node.rawNode);
449 let layout = {};
451 // First, we update the first part of the layout view, with
452 // the size of the element.
454 let clientRect = node.rawNode.getBoundingClientRect();
455 layout.width = Math.round(clientRect.width);
456 layout.height = Math.round(clientRect.height);
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 }
478 if (options.autoMargins) {
479 layout.autoMargins = this.processMargins(this.cssLogic);
480 }
482 for (let i in this.map) {
483 let property = this.map[i].property;
484 this.map[i].value = parseInt(style.getPropertyValue(property));
485 }
488 if (options.margins) {
489 layout.margins = this.processMargins(cssLogic);
490 }
492 return layout;
493 }, {
494 request: {
495 node: Arg(0, "domnode"),
496 autoMargins: Option(1, "boolean")
497 },
498 response: RetVal("json")
499 }),
501 /**
502 * Find 'auto' margin properties.
503 */
504 processMargins: function(cssLogic) {
505 let margins = {};
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 }
515 return margins;
516 },
518 });
519 exports.PageStyleActor = PageStyleActor;
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 },
530 destroy: function() {
531 protocol.Front.prototype.destroy.call(this);
532 },
534 get walker() {
535 return this.inspector.walker;
536 },
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 }),
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 });
555 // Predeclare the domstylerule actor type
556 types.addActorType("domstylerule");
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;
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 },
591 get conn() this.pageStyle.conn,
593 // Objects returned by this actor are owned by the PageStyleActor
594 // to which this rule belongs.
595 get marshallPool() this.pageStyle,
597 toString: function() "[StyleRuleActor for " + this.rawRule + "]",
599 form: function(detail) {
600 if (detail === "actorid") {
601 return this.actorID;
602 }
604 let form = {
605 actor: this.actorID,
606 type: this.type,
607 line: this.line || undefined,
608 column: this.column
609 };
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 }
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 }
644 return form;
645 },
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();
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.
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 }
679 if (parentStyleSheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) {
680 document = parentStyleSheet.ownerNode;
681 } else {
682 document = parentStyleSheet.ownerNode.ownerDocument;
683 }
684 }
686 let tempElement = document.createElement("div");
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 }
698 return this;
699 }, {
700 request: { modifications: Arg(0, "array:json") },
701 response: { rule: RetVal("domstylerule") }
702 })
703 });
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 },
713 destroy: function() {
714 protocol.Front.prototype.destroy.call(this);
715 },
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 },
729 /**
730 * Return a new RuleModificationList for this node.
731 */
732 startModifyingProperties: function() {
733 return new RuleModificationList(this);
734 },
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 },
759 get parentRule() {
760 return this.conn.getActor(this._form.parentRule);
761 },
763 get parentStyleSheet() {
764 return this.conn.getActor(this._form.parentStyleSheet);
765 },
767 get element() {
768 return this.conn.getActor(this._form.element);
769 },
771 get href() {
772 if (this._form.href) {
773 return this._form.href;
774 }
775 let sheet = this.parentStyleSheet;
776 return sheet.href;
777 },
779 get nodeHref() {
780 let sheet = this.parentStyleSheet;
781 return sheet ? sheet.nodeHref : "";
782 },
784 get location()
785 {
786 return {
787 href: this.href,
788 line: this.line,
789 column: this.column
790 };
791 },
793 getOriginalLocation: function()
794 {
795 if (this._originalLocation) {
796 return promise.resolve(this._originalLocation);
797 }
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 },
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 });
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 },
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 });