1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/devtools/server/actors/styles.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,860 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const {Cc, Ci, Cu} = require("chrome"); 1.11 +const Services = require("Services"); 1.12 +const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); 1.13 +const protocol = require("devtools/server/protocol"); 1.14 +const {Arg, Option, method, RetVal, types} = protocol; 1.15 +const events = require("sdk/event/core"); 1.16 +const object = require("sdk/util/object"); 1.17 +const { Class } = require("sdk/core/heritage"); 1.18 +const { StyleSheetActor } = require("devtools/server/actors/stylesheets"); 1.19 + 1.20 +loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic); 1.21 +loader.lazyGetter(this, "DOMUtils", () => Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)); 1.22 + 1.23 +// The PageStyle actor flattens the DOM CSS objects a little bit, merging 1.24 +// Rules and their Styles into one actor. For elements (which have a style 1.25 +// but no associated rule) we fake a rule with the following style id. 1.26 +const ELEMENT_STYLE = 100; 1.27 +exports.ELEMENT_STYLE = ELEMENT_STYLE; 1.28 + 1.29 +const PSEUDO_ELEMENTS = [":first-line", ":first-letter", ":before", ":after", ":-moz-selection"]; 1.30 +exports.PSEUDO_ELEMENTS = PSEUDO_ELEMENTS; 1.31 + 1.32 +// Predeclare the domnode actor type for use in requests. 1.33 +types.addActorType("domnode"); 1.34 + 1.35 +/** 1.36 + * DOM Nodes returned by the style actor will be owned by the DOM walker 1.37 + * for the connection. 1.38 + */ 1.39 +types.addLifetime("walker", "walker"); 1.40 + 1.41 +/** 1.42 + * When asking for the styles applied to a node, we return a list of 1.43 + * appliedstyle json objects that lists the rules that apply to the node 1.44 + * and which element they were inherited from (if any). 1.45 + */ 1.46 +types.addDictType("appliedstyle", { 1.47 + rule: "domstylerule#actorid", 1.48 + inherited: "nullable:domnode#actorid" 1.49 +}); 1.50 + 1.51 +types.addDictType("matchedselector", { 1.52 + rule: "domstylerule#actorid", 1.53 + selector: "string", 1.54 + value: "string", 1.55 + status: "number" 1.56 +}); 1.57 + 1.58 +/** 1.59 + * The PageStyle actor lets the client look at the styles on a page, as 1.60 + * they are applied to a given node. 1.61 + */ 1.62 +var PageStyleActor = protocol.ActorClass({ 1.63 + typeName: "pagestyle", 1.64 + 1.65 + /** 1.66 + * Create a PageStyleActor. 1.67 + * 1.68 + * @param inspector 1.69 + * The InspectorActor that owns this PageStyleActor. 1.70 + * 1.71 + * @constructor 1.72 + */ 1.73 + initialize: function(inspector) { 1.74 + protocol.Actor.prototype.initialize.call(this, null); 1.75 + this.inspector = inspector; 1.76 + if (!this.inspector.walker) { 1.77 + throw Error("The inspector's WalkerActor must be created before " + 1.78 + "creating a PageStyleActor."); 1.79 + } 1.80 + this.walker = inspector.walker; 1.81 + this.cssLogic = new CssLogic; 1.82 + 1.83 + // Stores the association of DOM objects -> actors 1.84 + this.refMap = new Map; 1.85 + }, 1.86 + 1.87 + get conn() this.inspector.conn, 1.88 + 1.89 + /** 1.90 + * Return or create a StyleRuleActor for the given item. 1.91 + * @param item Either a CSSStyleRule or a DOM element. 1.92 + */ 1.93 + _styleRef: function(item) { 1.94 + if (this.refMap.has(item)) { 1.95 + return this.refMap.get(item); 1.96 + } 1.97 + let actor = StyleRuleActor(this, item); 1.98 + this.manage(actor); 1.99 + this.refMap.set(item, actor); 1.100 + 1.101 + return actor; 1.102 + }, 1.103 + 1.104 + /** 1.105 + * Return or create a StyleSheetActor for the given 1.106 + * nsIDOMCSSStyleSheet 1.107 + */ 1.108 + _sheetRef: function(sheet) { 1.109 + if (this.refMap.has(sheet)) { 1.110 + return this.refMap.get(sheet); 1.111 + } 1.112 + let actor = new StyleSheetActor(sheet, this, this.walker.rootWin); 1.113 + this.manage(actor); 1.114 + this.refMap.set(sheet, actor); 1.115 + 1.116 + return actor; 1.117 + }, 1.118 + 1.119 + /** 1.120 + * Get the computed style for a node. 1.121 + * 1.122 + * @param NodeActor node 1.123 + * @param object options 1.124 + * `filter`: A string filter that affects the "matched" handling. 1.125 + * 'user': Include properties from user style sheets. 1.126 + * 'ua': Include properties from user and user-agent sheets. 1.127 + * Default value is 'ua' 1.128 + * `markMatched`: true if you want the 'matched' property to be added 1.129 + * when a computed property has been modified by a style included 1.130 + * by `filter`. 1.131 + * `onlyMatched`: true if unmatched properties shouldn't be included. 1.132 + * 1.133 + * @returns a JSON blob with the following form: 1.134 + * { 1.135 + * "property-name": { 1.136 + * value: "property-value", 1.137 + * priority: "!important" <optional> 1.138 + * matched: <true if there are matched selectors for this value> 1.139 + * }, 1.140 + * ... 1.141 + * } 1.142 + */ 1.143 + getComputed: method(function(node, options) { 1.144 + let ret = Object.create(null); 1.145 + 1.146 + this.cssLogic.sourceFilter = options.filter || CssLogic.FILTER.UA; 1.147 + this.cssLogic.highlight(node.rawNode); 1.148 + let computed = this.cssLogic._computedStyle || []; 1.149 + 1.150 + Array.prototype.forEach.call(computed, name => { 1.151 + let matched = undefined; 1.152 + ret[name] = { 1.153 + value: computed.getPropertyValue(name), 1.154 + priority: computed.getPropertyPriority(name) || undefined 1.155 + }; 1.156 + }); 1.157 + 1.158 + if (options.markMatched || options.onlyMatched) { 1.159 + let matched = this.cssLogic.hasMatchedSelectors(Object.keys(ret)); 1.160 + for (let key in ret) { 1.161 + if (matched[key]) { 1.162 + ret[key].matched = options.markMatched ? true : undefined 1.163 + } else if (options.onlyMatched) { 1.164 + delete ret[key]; 1.165 + } 1.166 + } 1.167 + } 1.168 + 1.169 + return ret; 1.170 + }, { 1.171 + request: { 1.172 + node: Arg(0, "domnode"), 1.173 + markMatched: Option(1, "boolean"), 1.174 + onlyMatched: Option(1, "boolean"), 1.175 + filter: Option(1, "string"), 1.176 + }, 1.177 + response: { 1.178 + computed: RetVal("json") 1.179 + } 1.180 + }), 1.181 + 1.182 + /** 1.183 + * Get a list of selectors that match a given property for a node. 1.184 + * 1.185 + * @param NodeActor node 1.186 + * @param string property 1.187 + * @param object options 1.188 + * `filter`: A string filter that affects the "matched" handling. 1.189 + * 'user': Include properties from user style sheets. 1.190 + * 'ua': Include properties from user and user-agent sheets. 1.191 + * Default value is 'ua' 1.192 + * 1.193 + * @returns a JSON object with the following form: 1.194 + * { 1.195 + * // An ordered list of rules that apply 1.196 + * matched: [{ 1.197 + * rule: <rule actorid>, 1.198 + * sourceText: <string>, // The source of the selector, relative 1.199 + * // to the node in question. 1.200 + * selector: <string>, // the selector ID that matched 1.201 + * value: <string>, // the value of the property 1.202 + * status: <int>, 1.203 + * // The status of the match - high numbers are better placed 1.204 + * // to provide styling information: 1.205 + * // 3: Best match, was used. 1.206 + * // 2: Matched, but was overridden. 1.207 + * // 1: Rule from a parent matched. 1.208 + * // 0: Unmatched (never returned in this API) 1.209 + * }, ...], 1.210 + * 1.211 + * // The full form of any domrule referenced. 1.212 + * rules: [ <domrule>, ... ], // The full form of any domrule referenced 1.213 + * 1.214 + * // The full form of any sheets referenced. 1.215 + * sheets: [ <domsheet>, ... ] 1.216 + * } 1.217 + */ 1.218 + getMatchedSelectors: method(function(node, property, options) { 1.219 + this.cssLogic.sourceFilter = options.filter || CssLogic.FILTER.UA; 1.220 + this.cssLogic.highlight(node.rawNode); 1.221 + 1.222 + let walker = node.parent(); 1.223 + 1.224 + let rules = new Set; 1.225 + let sheets = new Set; 1.226 + 1.227 + let matched = []; 1.228 + let propInfo = this.cssLogic.getPropertyInfo(property); 1.229 + for (let selectorInfo of propInfo.matchedSelectors) { 1.230 + let cssRule = selectorInfo.selector.cssRule; 1.231 + let domRule = cssRule.sourceElement || cssRule.domRule; 1.232 + 1.233 + let rule = this._styleRef(domRule); 1.234 + rules.add(rule); 1.235 + 1.236 + matched.push({ 1.237 + rule: rule, 1.238 + sourceText: this.getSelectorSource(selectorInfo, node.rawNode), 1.239 + selector: selectorInfo.selector.text, 1.240 + name: selectorInfo.property, 1.241 + value: selectorInfo.value, 1.242 + status: selectorInfo.status 1.243 + }); 1.244 + } 1.245 + 1.246 + this.expandSets(rules, sheets); 1.247 + 1.248 + return { 1.249 + matched: matched, 1.250 + rules: [...rules], 1.251 + sheets: [...sheets], 1.252 + }; 1.253 + }, { 1.254 + request: { 1.255 + node: Arg(0, "domnode"), 1.256 + property: Arg(1, "string"), 1.257 + filter: Option(2, "string") 1.258 + }, 1.259 + response: RetVal(types.addDictType("matchedselectorresponse", { 1.260 + rules: "array:domstylerule", 1.261 + sheets: "array:stylesheet", 1.262 + matched: "array:matchedselector" 1.263 + })) 1.264 + }), 1.265 + 1.266 + // Get a selector source for a CssSelectorInfo relative to a given 1.267 + // node. 1.268 + getSelectorSource: function(selectorInfo, relativeTo) { 1.269 + let result = selectorInfo.selector.text; 1.270 + if (selectorInfo.elementStyle) { 1.271 + let source = selectorInfo.sourceElement; 1.272 + if (source === relativeTo) { 1.273 + result = "this"; 1.274 + } else { 1.275 + result = CssLogic.getShortName(source); 1.276 + } 1.277 + result += ".style" 1.278 + } 1.279 + return result; 1.280 + }, 1.281 + 1.282 + /** 1.283 + * Get the set of styles that apply to a given node. 1.284 + * @param NodeActor node 1.285 + * @param string property 1.286 + * @param object options 1.287 + * `filter`: A string filter that affects the "matched" handling. 1.288 + * 'user': Include properties from user style sheets. 1.289 + * 'ua': Include properties from user and user-agent sheets. 1.290 + * Default value is 'ua' 1.291 + * `inherited`: Include styles inherited from parent nodes. 1.292 + * `matchedSeletors`: Include an array of specific selectors that 1.293 + * caused this rule to match its node. 1.294 + */ 1.295 + getApplied: method(function(node, options) { 1.296 + let entries = []; 1.297 + 1.298 + this.addElementRules(node.rawNode, undefined, options, entries); 1.299 + 1.300 + if (options.inherited) { 1.301 + let parent = this.walker.parentNode(node); 1.302 + while (parent && parent.rawNode.nodeType != Ci.nsIDOMNode.DOCUMENT_NODE) { 1.303 + this.addElementRules(parent.rawNode, parent, options, entries); 1.304 + parent = this.walker.parentNode(parent); 1.305 + } 1.306 + } 1.307 + 1.308 + if (options.matchedSelectors) { 1.309 + for (let entry of entries) { 1.310 + if (entry.rule.type === ELEMENT_STYLE) { 1.311 + continue; 1.312 + } 1.313 + 1.314 + let domRule = entry.rule.rawRule; 1.315 + let selectors = CssLogic.getSelectors(domRule); 1.316 + let element = entry.inherited ? entry.inherited.rawNode : node.rawNode; 1.317 + entry.matchedSelectors = []; 1.318 + for (let i = 0; i < selectors.length; i++) { 1.319 + if (DOMUtils.selectorMatchesElement(element, domRule, i)) { 1.320 + entry.matchedSelectors.push(selectors[i]); 1.321 + } 1.322 + } 1.323 + 1.324 + } 1.325 + } 1.326 + 1.327 + let rules = new Set; 1.328 + let sheets = new Set; 1.329 + entries.forEach(entry => rules.add(entry.rule)); 1.330 + this.expandSets(rules, sheets); 1.331 + 1.332 + return { 1.333 + entries: entries, 1.334 + rules: [...rules], 1.335 + sheets: [...sheets] 1.336 + } 1.337 + }, { 1.338 + request: { 1.339 + node: Arg(0, "domnode"), 1.340 + inherited: Option(1, "boolean"), 1.341 + matchedSelectors: Option(1, "boolean"), 1.342 + filter: Option(1, "string") 1.343 + }, 1.344 + response: RetVal(types.addDictType("appliedStylesReturn", { 1.345 + entries: "array:appliedstyle", 1.346 + rules: "array:domstylerule", 1.347 + sheets: "array:stylesheet" 1.348 + })) 1.349 + }), 1.350 + 1.351 + _hasInheritedProps: function(style) { 1.352 + return Array.prototype.some.call(style, prop => { 1.353 + return DOMUtils.isInheritedProperty(prop); 1.354 + }); 1.355 + }, 1.356 + 1.357 + /** 1.358 + * Helper function for getApplied, adds all the rules from a given 1.359 + * element. 1.360 + */ 1.361 + addElementRules: function(element, inherited, options, rules) 1.362 + { 1.363 + if (!element.style) { 1.364 + return; 1.365 + } 1.366 + 1.367 + let elementStyle = this._styleRef(element); 1.368 + 1.369 + if (!inherited || this._hasInheritedProps(element.style)) { 1.370 + rules.push({ 1.371 + rule: elementStyle, 1.372 + inherited: inherited, 1.373 + }); 1.374 + } 1.375 + 1.376 + let pseudoElements = inherited ? [null] : [null, ...PSEUDO_ELEMENTS]; 1.377 + for (let pseudo of pseudoElements) { 1.378 + 1.379 + // Get the styles that apply to the element. 1.380 + let domRules = DOMUtils.getCSSStyleRules(element, pseudo); 1.381 + 1.382 + if (!domRules) { 1.383 + continue; 1.384 + } 1.385 + 1.386 + // getCSSStyleRules returns ordered from least-specific to 1.387 + // most-specific. 1.388 + for (let i = domRules.Count() - 1; i >= 0; i--) { 1.389 + let domRule = domRules.GetElementAt(i); 1.390 + 1.391 + let isSystem = !CssLogic.isContentStylesheet(domRule.parentStyleSheet); 1.392 + 1.393 + if (isSystem && options.filter != CssLogic.FILTER.UA) { 1.394 + continue; 1.395 + } 1.396 + 1.397 + if (inherited) { 1.398 + // Don't include inherited rules if none of its properties 1.399 + // are inheritable. 1.400 + let hasInherited = Array.prototype.some.call(domRule.style, prop => { 1.401 + return DOMUtils.isInheritedProperty(prop); 1.402 + }); 1.403 + if (!hasInherited) { 1.404 + continue; 1.405 + } 1.406 + } 1.407 + 1.408 + let ruleActor = this._styleRef(domRule); 1.409 + rules.push({ 1.410 + rule: ruleActor, 1.411 + inherited: inherited, 1.412 + pseudoElement: pseudo 1.413 + }); 1.414 + } 1.415 + 1.416 + } 1.417 + }, 1.418 + 1.419 + /** 1.420 + * Expand Sets of rules and sheets to include all parent rules and sheets. 1.421 + */ 1.422 + expandSets: function(ruleSet, sheetSet) { 1.423 + // Sets include new items in their iteration 1.424 + for (let rule of ruleSet) { 1.425 + if (rule.rawRule.parentRule) { 1.426 + let parent = this._styleRef(rule.rawRule.parentRule); 1.427 + if (!ruleSet.has(parent)) { 1.428 + ruleSet.add(parent); 1.429 + } 1.430 + } 1.431 + if (rule.rawRule.parentStyleSheet) { 1.432 + let parent = this._sheetRef(rule.rawRule.parentStyleSheet); 1.433 + if (!sheetSet.has(parent)) { 1.434 + sheetSet.add(parent); 1.435 + } 1.436 + } 1.437 + } 1.438 + 1.439 + for (let sheet of sheetSet) { 1.440 + if (sheet.rawSheet.parentStyleSheet) { 1.441 + let parent = this._sheetRef(sheet.rawSheet.parentStyleSheet); 1.442 + if (!sheetSet.has(parent)) { 1.443 + sheetSet.add(parent); 1.444 + } 1.445 + } 1.446 + } 1.447 + }, 1.448 + 1.449 + getLayout: method(function(node, options) { 1.450 + this.cssLogic.highlight(node.rawNode); 1.451 + 1.452 + let layout = {}; 1.453 + 1.454 + // First, we update the first part of the layout view, with 1.455 + // the size of the element. 1.456 + 1.457 + let clientRect = node.rawNode.getBoundingClientRect(); 1.458 + layout.width = Math.round(clientRect.width); 1.459 + layout.height = Math.round(clientRect.height); 1.460 + 1.461 + // We compute and update the values of margins & co. 1.462 + let style = node.rawNode.ownerDocument.defaultView.getComputedStyle(node.rawNode); 1.463 + for (let prop of [ 1.464 + "position", 1.465 + "margin-top", 1.466 + "margin-right", 1.467 + "margin-bottom", 1.468 + "margin-left", 1.469 + "padding-top", 1.470 + "padding-right", 1.471 + "padding-bottom", 1.472 + "padding-left", 1.473 + "border-top-width", 1.474 + "border-right-width", 1.475 + "border-bottom-width", 1.476 + "border-left-width" 1.477 + ]) { 1.478 + layout[prop] = style.getPropertyValue(prop); 1.479 + } 1.480 + 1.481 + if (options.autoMargins) { 1.482 + layout.autoMargins = this.processMargins(this.cssLogic); 1.483 + } 1.484 + 1.485 + for (let i in this.map) { 1.486 + let property = this.map[i].property; 1.487 + this.map[i].value = parseInt(style.getPropertyValue(property)); 1.488 + } 1.489 + 1.490 + 1.491 + if (options.margins) { 1.492 + layout.margins = this.processMargins(cssLogic); 1.493 + } 1.494 + 1.495 + return layout; 1.496 + }, { 1.497 + request: { 1.498 + node: Arg(0, "domnode"), 1.499 + autoMargins: Option(1, "boolean") 1.500 + }, 1.501 + response: RetVal("json") 1.502 + }), 1.503 + 1.504 + /** 1.505 + * Find 'auto' margin properties. 1.506 + */ 1.507 + processMargins: function(cssLogic) { 1.508 + let margins = {}; 1.509 + 1.510 + for (let prop of ["top", "bottom", "left", "right"]) { 1.511 + let info = cssLogic.getPropertyInfo("margin-" + prop); 1.512 + let selectors = info.matchedSelectors; 1.513 + if (selectors && selectors.length > 0 && selectors[0].value == "auto") { 1.514 + margins[prop] = "auto"; 1.515 + } 1.516 + } 1.517 + 1.518 + return margins; 1.519 + }, 1.520 + 1.521 +}); 1.522 +exports.PageStyleActor = PageStyleActor; 1.523 + 1.524 +/** 1.525 + * Front object for the PageStyleActor 1.526 + */ 1.527 +var PageStyleFront = protocol.FrontClass(PageStyleActor, { 1.528 + initialize: function(conn, form, ctx, detail) { 1.529 + protocol.Front.prototype.initialize.call(this, conn, form, ctx, detail); 1.530 + this.inspector = this.parent(); 1.531 + }, 1.532 + 1.533 + destroy: function() { 1.534 + protocol.Front.prototype.destroy.call(this); 1.535 + }, 1.536 + 1.537 + get walker() { 1.538 + return this.inspector.walker; 1.539 + }, 1.540 + 1.541 + getMatchedSelectors: protocol.custom(function(node, property, options) { 1.542 + return this._getMatchedSelectors(node, property, options).then(ret => { 1.543 + return ret.matched; 1.544 + }); 1.545 + }, { 1.546 + impl: "_getMatchedSelectors" 1.547 + }), 1.548 + 1.549 + getApplied: protocol.custom(function(node, options={}) { 1.550 + return this._getApplied(node, options).then(ret => { 1.551 + return ret.entries; 1.552 + }); 1.553 + }, { 1.554 + impl: "_getApplied" 1.555 + }) 1.556 +}); 1.557 + 1.558 +// Predeclare the domstylerule actor type 1.559 +types.addActorType("domstylerule"); 1.560 + 1.561 +/** 1.562 + * An actor that represents a CSS style object on the protocol. 1.563 + * 1.564 + * We slightly flatten the CSSOM for this actor, it represents 1.565 + * both the CSSRule and CSSStyle objects in one actor. For nodes 1.566 + * (which have a CSSStyle but no CSSRule) we create a StyleRuleActor 1.567 + * with a special rule type (100). 1.568 + */ 1.569 +var StyleRuleActor = protocol.ActorClass({ 1.570 + typeName: "domstylerule", 1.571 + initialize: function(pageStyle, item) { 1.572 + protocol.Actor.prototype.initialize.call(this, null); 1.573 + this.pageStyle = pageStyle; 1.574 + this.rawStyle = item.style; 1.575 + 1.576 + if (item instanceof (Ci.nsIDOMCSSRule)) { 1.577 + this.type = item.type; 1.578 + this.rawRule = item; 1.579 + if (this.rawRule instanceof Ci.nsIDOMCSSStyleRule && this.rawRule.parentStyleSheet) { 1.580 + this.line = DOMUtils.getRuleLine(this.rawRule); 1.581 + this.column = DOMUtils.getRuleColumn(this.rawRule); 1.582 + } 1.583 + } else { 1.584 + // Fake a rule 1.585 + this.type = ELEMENT_STYLE; 1.586 + this.rawNode = item; 1.587 + this.rawRule = { 1.588 + style: item.style, 1.589 + toString: function() "[element rule " + this.style + "]" 1.590 + } 1.591 + } 1.592 + }, 1.593 + 1.594 + get conn() this.pageStyle.conn, 1.595 + 1.596 + // Objects returned by this actor are owned by the PageStyleActor 1.597 + // to which this rule belongs. 1.598 + get marshallPool() this.pageStyle, 1.599 + 1.600 + toString: function() "[StyleRuleActor for " + this.rawRule + "]", 1.601 + 1.602 + form: function(detail) { 1.603 + if (detail === "actorid") { 1.604 + return this.actorID; 1.605 + } 1.606 + 1.607 + let form = { 1.608 + actor: this.actorID, 1.609 + type: this.type, 1.610 + line: this.line || undefined, 1.611 + column: this.column 1.612 + }; 1.613 + 1.614 + if (this.rawRule.parentRule) { 1.615 + form.parentRule = this.pageStyle._styleRef(this.rawRule.parentRule).actorID; 1.616 + } 1.617 + if (this.rawRule.parentStyleSheet) { 1.618 + form.parentStyleSheet = this.pageStyle._sheetRef(this.rawRule.parentStyleSheet).actorID; 1.619 + } 1.620 + 1.621 + switch (this.type) { 1.622 + case Ci.nsIDOMCSSRule.STYLE_RULE: 1.623 + form.selectors = CssLogic.getSelectors(this.rawRule); 1.624 + form.cssText = this.rawStyle.cssText || ""; 1.625 + break; 1.626 + case ELEMENT_STYLE: 1.627 + // Elements don't have a parent stylesheet, and therefore 1.628 + // don't have an associated URI. Provide a URI for 1.629 + // those. 1.630 + form.href = this.rawNode.ownerDocument.location.href; 1.631 + form.cssText = this.rawStyle.cssText || ""; 1.632 + break; 1.633 + case Ci.nsIDOMCSSRule.CHARSET_RULE: 1.634 + form.encoding = this.rawRule.encoding; 1.635 + break; 1.636 + case Ci.nsIDOMCSSRule.IMPORT_RULE: 1.637 + form.href = this.rawRule.href; 1.638 + break; 1.639 + case Ci.nsIDOMCSSRule.MEDIA_RULE: 1.640 + form.media = []; 1.641 + for (let i = 0, n = this.rawRule.media.length; i < n; i++) { 1.642 + form.media.push(this.rawRule.media.item(i)); 1.643 + } 1.644 + break; 1.645 + } 1.646 + 1.647 + return form; 1.648 + }, 1.649 + 1.650 + /** 1.651 + * Modify a rule's properties. Passed an array of modifications: 1.652 + * { 1.653 + * type: "set", 1.654 + * name: <string>, 1.655 + * value: <string>, 1.656 + * priority: <optional string> 1.657 + * } 1.658 + * or 1.659 + * { 1.660 + * type: "remove", 1.661 + * name: <string>, 1.662 + * } 1.663 + * 1.664 + * @returns the rule with updated properties 1.665 + */ 1.666 + modifyProperties: method(function(modifications) { 1.667 + let validProps = new Map(); 1.668 + 1.669 + // Use a fresh element for each call to this function to prevent side effects 1.670 + // that pop up based on property values that were already set on the element. 1.671 + 1.672 + let document; 1.673 + if (this.rawNode) { 1.674 + document = this.rawNode.ownerDocument; 1.675 + } else { 1.676 + let parentStyleSheet = this.rawRule.parentStyleSheet; 1.677 + while (parentStyleSheet.ownerRule && 1.678 + parentStyleSheet.ownerRule instanceof Ci.nsIDOMCSSImportRule) { 1.679 + parentStyleSheet = parentStyleSheet.ownerRule.parentStyleSheet; 1.680 + } 1.681 + 1.682 + if (parentStyleSheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) { 1.683 + document = parentStyleSheet.ownerNode; 1.684 + } else { 1.685 + document = parentStyleSheet.ownerNode.ownerDocument; 1.686 + } 1.687 + } 1.688 + 1.689 + let tempElement = document.createElement("div"); 1.690 + 1.691 + for (let mod of modifications) { 1.692 + if (mod.type === "set") { 1.693 + tempElement.style.setProperty(mod.name, mod.value, mod.priority || ""); 1.694 + this.rawStyle.setProperty(mod.name, 1.695 + tempElement.style.getPropertyValue(mod.name), mod.priority || ""); 1.696 + } else if (mod.type === "remove") { 1.697 + this.rawStyle.removeProperty(mod.name); 1.698 + } 1.699 + } 1.700 + 1.701 + return this; 1.702 + }, { 1.703 + request: { modifications: Arg(0, "array:json") }, 1.704 + response: { rule: RetVal("domstylerule") } 1.705 + }) 1.706 +}); 1.707 + 1.708 +/** 1.709 + * Front for the StyleRule actor. 1.710 + */ 1.711 +var StyleRuleFront = protocol.FrontClass(StyleRuleActor, { 1.712 + initialize: function(client, form, ctx, detail) { 1.713 + protocol.Front.prototype.initialize.call(this, client, form, ctx, detail); 1.714 + }, 1.715 + 1.716 + destroy: function() { 1.717 + protocol.Front.prototype.destroy.call(this); 1.718 + }, 1.719 + 1.720 + form: function(form, detail) { 1.721 + if (detail === "actorid") { 1.722 + this.actorID = form; 1.723 + return; 1.724 + } 1.725 + this.actorID = form.actor; 1.726 + this._form = form; 1.727 + if (this._mediaText) { 1.728 + this._mediaText = null; 1.729 + } 1.730 + }, 1.731 + 1.732 + /** 1.733 + * Return a new RuleModificationList for this node. 1.734 + */ 1.735 + startModifyingProperties: function() { 1.736 + return new RuleModificationList(this); 1.737 + }, 1.738 + 1.739 + get type() this._form.type, 1.740 + get line() this._form.line || -1, 1.741 + get column() this._form.column || -1, 1.742 + get cssText() { 1.743 + return this._form.cssText; 1.744 + }, 1.745 + get selectors() { 1.746 + return this._form.selectors; 1.747 + }, 1.748 + get media() { 1.749 + return this._form.media; 1.750 + }, 1.751 + get mediaText() { 1.752 + if (!this._form.media) { 1.753 + return null; 1.754 + } 1.755 + if (this._mediaText) { 1.756 + return this._mediaText; 1.757 + } 1.758 + this._mediaText = this.media.join(", "); 1.759 + return this._mediaText; 1.760 + }, 1.761 + 1.762 + get parentRule() { 1.763 + return this.conn.getActor(this._form.parentRule); 1.764 + }, 1.765 + 1.766 + get parentStyleSheet() { 1.767 + return this.conn.getActor(this._form.parentStyleSheet); 1.768 + }, 1.769 + 1.770 + get element() { 1.771 + return this.conn.getActor(this._form.element); 1.772 + }, 1.773 + 1.774 + get href() { 1.775 + if (this._form.href) { 1.776 + return this._form.href; 1.777 + } 1.778 + let sheet = this.parentStyleSheet; 1.779 + return sheet.href; 1.780 + }, 1.781 + 1.782 + get nodeHref() { 1.783 + let sheet = this.parentStyleSheet; 1.784 + return sheet ? sheet.nodeHref : ""; 1.785 + }, 1.786 + 1.787 + get location() 1.788 + { 1.789 + return { 1.790 + href: this.href, 1.791 + line: this.line, 1.792 + column: this.column 1.793 + }; 1.794 + }, 1.795 + 1.796 + getOriginalLocation: function() 1.797 + { 1.798 + if (this._originalLocation) { 1.799 + return promise.resolve(this._originalLocation); 1.800 + } 1.801 + 1.802 + let parentSheet = this.parentStyleSheet; 1.803 + if (!parentSheet) { 1.804 + return promise.resolve(this.location); 1.805 + } 1.806 + return parentSheet.getOriginalLocation(this.line, this.column) 1.807 + .then(({ source, line, column }) => { 1.808 + let location = { 1.809 + href: source, 1.810 + line: line, 1.811 + column: column 1.812 + } 1.813 + if (!source) { 1.814 + location.href = this.href; 1.815 + } 1.816 + this._originalLocation = location; 1.817 + return location; 1.818 + }) 1.819 + }, 1.820 + 1.821 + // Only used for testing, please keep it that way. 1.822 + _rawStyle: function() { 1.823 + if (!this.conn._transport._serverConnection) { 1.824 + console.warn("Tried to use rawNode on a remote connection."); 1.825 + return null; 1.826 + } 1.827 + let actor = this.conn._transport._serverConnection.getActor(this.actorID); 1.828 + if (!actor) { 1.829 + return null; 1.830 + } 1.831 + return actor.rawStyle; 1.832 + } 1.833 +}); 1.834 + 1.835 +/** 1.836 + * Convenience API for building a list of attribute modifications 1.837 + * for the `modifyAttributes` request. 1.838 + */ 1.839 +var RuleModificationList = Class({ 1.840 + initialize: function(rule) { 1.841 + this.rule = rule; 1.842 + this.modifications = []; 1.843 + }, 1.844 + 1.845 + apply: function() { 1.846 + return this.rule.modifyProperties(this.modifications); 1.847 + }, 1.848 + setProperty: function(name, value, priority) { 1.849 + this.modifications.push({ 1.850 + type: "set", 1.851 + name: name, 1.852 + value: value, 1.853 + priority: priority 1.854 + }); 1.855 + }, 1.856 + removeProperty: function(name) { 1.857 + this.modifications.push({ 1.858 + type: "remove", 1.859 + name: name 1.860 + }); 1.861 + } 1.862 +}); 1.863 +