/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const {Cc, Ci} = require("chrome"); const Services = require("Services"); const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm"); const promise = require("promise"); const {Task} = require("devtools/shared/task"); const events = require("sdk/event/core"); const protocol = require("devtools/shared/protocol"); const {LongStringActor} = require("devtools/server/actors/string"); const {fetch} = require("devtools/shared/DevToolsUtils"); const {originalSourceSpec, mediaRuleSpec, styleSheetSpec, styleSheetsSpec} = require("devtools/shared/specs/stylesheets"); const {SourceMapConsumer} = require("source-map"); const { installHelperSheet, addPseudoClassLock, removePseudoClassLock } = require("devtools/server/actors/highlighters/utils/markup"); loader.lazyGetter(this, "CssLogic", () => require("devtools/shared/inspector/css-logic")); XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () { return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); }); var TRANSITION_PSEUDO_CLASS = ":-moz-styleeditor-transitioning"; var TRANSITION_DURATION_MS = 500; var TRANSITION_BUFFER_MS = 1000; var TRANSITION_RULE_SELECTOR = `:root${TRANSITION_PSEUDO_CLASS}, :root${TRANSITION_PSEUDO_CLASS} *`; var TRANSITION_RULE = `${TRANSITION_RULE_SELECTOR} { transition-duration: ${TRANSITION_DURATION_MS}ms !important; transition-delay: 0ms !important; transition-timing-function: ease-out !important; transition-property: all !important; }`; var LOAD_ERROR = "error-load"; // The possible kinds of style-applied events. // UPDATE_PRESERVING_RULES means that the update is guaranteed to // preserve the number and order of rules on the style sheet. // UPDATE_GENERAL covers any other kind of change to the style sheet. const UPDATE_PRESERVING_RULES = 0; exports.UPDATE_PRESERVING_RULES = UPDATE_PRESERVING_RULES; const UPDATE_GENERAL = 1; exports.UPDATE_GENERAL = UPDATE_GENERAL; // If the user edits a style sheet, we stash a copy of the edited text // here, keyed by the style sheet. This way, if the tools are closed // and then reopened, the edited text will be available. A weak map // is used so that navigation by the user will eventually cause the // edited text to be collected. let modifiedStyleSheets = new WeakMap(); /** * Actor representing an original source of a style sheet that was specified * in a source map. */ var OriginalSourceActor = protocol.ActorClassWithSpec(originalSourceSpec, { initialize: function (aUrl, aSourceMap, aParentActor) { protocol.Actor.prototype.initialize.call(this, null); this.url = aUrl; this.sourceMap = aSourceMap; this.parentActor = aParentActor; this.conn = this.parentActor.conn; this.text = null; }, form: function () { return { actor: this.actorID, // actorID is set when it's added to a pool url: this.url, relatedStyleSheet: this.parentActor.form() }; }, _getText: function () { if (this.text) { return promise.resolve(this.text); } let content = this.sourceMap.sourceContentFor(this.url); if (content) { this.text = content; return promise.resolve(content); } let options = { policy: Ci.nsIContentPolicy.TYPE_INTERNAL_STYLESHEET, window: this.window }; return fetch(this.url, options).then(({content}) => { this.text = content; return content; }); }, /** * Protocol method to get the text of this source. */ getText: function () { return this._getText().then((text) => { return new LongStringActor(this.conn, text || ""); }); } }); /** * A MediaRuleActor lives on the server and provides access to properties * of a DOM @media rule and emits events when it changes. */ var MediaRuleActor = protocol.ActorClassWithSpec(mediaRuleSpec, { get window() { return this.parentActor.window; }, get document() { return this.window.document; }, get matches() { return this.mql ? this.mql.matches : null; }, initialize: function (aMediaRule, aParentActor) { protocol.Actor.prototype.initialize.call(this, null); this.rawRule = aMediaRule; this.parentActor = aParentActor; this.conn = this.parentActor.conn; this._matchesChange = this._matchesChange.bind(this); this.line = DOMUtils.getRuleLine(aMediaRule); this.column = DOMUtils.getRuleColumn(aMediaRule); try { this.mql = this.window.matchMedia(aMediaRule.media.mediaText); } catch (e) { } if (this.mql) { this.mql.addListener(this._matchesChange); } }, destroy: function () { if (this.mql) { this.mql.removeListener(this._matchesChange); } protocol.Actor.prototype.destroy.call(this); }, form: function (detail) { if (detail === "actorid") { return this.actorID; } let form = { actor: this.actorID, // actorID is set when this is added to a pool mediaText: this.rawRule.media.mediaText, conditionText: this.rawRule.conditionText, matches: this.matches, line: this.line, column: this.column, parentStyleSheet: this.parentActor.actorID }; return form; }, _matchesChange: function () { events.emit(this, "matches-change", this.matches); } }); /** * A StyleSheetActor represents a stylesheet on the server. */ var StyleSheetActor = protocol.ActorClassWithSpec(styleSheetSpec, { /* List of original sources that generated this stylesheet */ _originalSources: null, toString: function () { return "[StyleSheetActor " + this.actorID + "]"; }, /** * Window of target */ get window() { return this._window || this.parentActor.window; }, /** * Document of target. */ get document() { return this.window.document; }, get ownerNode() { return this.rawSheet.ownerNode; }, /** * URL of underlying stylesheet. */ get href() { return this.rawSheet.href; }, /** * Returns the stylesheet href or the document href if the sheet is inline. */ get safeHref() { let href = this.href; if (!href) { if (this.ownerNode instanceof Ci.nsIDOMHTMLDocument) { href = this.ownerNode.location.href; } else if (this.ownerNode.ownerDocument && this.ownerNode.ownerDocument.location) { href = this.ownerNode.ownerDocument.location.href; } } return href; }, /** * Retrieve the index (order) of stylesheet in the document. * * @return number */ get styleSheetIndex() { if (this._styleSheetIndex == -1) { for (let i = 0; i < this.document.styleSheets.length; i++) { if (this.document.styleSheets[i] == this.rawSheet) { this._styleSheetIndex = i; break; } } } return this._styleSheetIndex; }, destroy: function () { if (this._transitionTimeout && this.window) { this.window.clearTimeout(this._transitionTimeout); removePseudoClassLock( this.document.documentElement, TRANSITION_PSEUDO_CLASS); } }, /** * Since StyleSheetActor doesn't have a protocol.js parent actor that take * care of its lifetime, implementing disconnect is required to cleanup. */ disconnect: function () { this.destroy(); }, initialize: function (aStyleSheet, aParentActor, aWindow) { protocol.Actor.prototype.initialize.call(this, null); this.rawSheet = aStyleSheet; this.parentActor = aParentActor; this.conn = this.parentActor.conn; this._window = aWindow; // text and index are unknown until source load this.text = null; this._styleSheetIndex = -1; }, /** * Test whether all the rules in this sheet have associated source. * @return {Boolean} true if all the rules have source; false if * some rule was created via CSSOM. */ allRulesHaveSource: function () { let rules; try { rules = this.rawSheet.cssRules; } catch (e) { // sheet isn't loaded yet return true; } for (let i = 0; i < rules.length; i++) { let rule = rules[i]; if (DOMUtils.getRelativeRuleLine(rule) === 0) { return false; } } return true; }, /** * Get the raw stylesheet's cssRules once the sheet has been loaded. * * @return {Promise} * Promise that resolves with a CSSRuleList */ getCSSRules: function () { let rules; try { rules = this.rawSheet.cssRules; } catch (e) { // sheet isn't loaded yet } if (rules) { return promise.resolve(rules); } if (!this.ownerNode) { return promise.resolve([]); } if (this._cssRules) { return this._cssRules; } let deferred = promise.defer(); let onSheetLoaded = (event) => { this.ownerNode.removeEventListener("load", onSheetLoaded, false); deferred.resolve(this.rawSheet.cssRules); }; this.ownerNode.addEventListener("load", onSheetLoaded, false); // cache so we don't add many listeners if this is called multiple times. this._cssRules = deferred.promise; return this._cssRules; }, /** * Get the current state of the actor * * @return {object} * With properties of the underlying stylesheet, plus 'text', * 'styleSheetIndex' and 'parentActor' if it's @imported */ form: function (detail) { if (detail === "actorid") { return this.actorID; } let docHref; if (this.ownerNode) { if (this.ownerNode instanceof Ci.nsIDOMHTMLDocument) { docHref = this.ownerNode.location.href; } else if (this.ownerNode.ownerDocument && this.ownerNode.ownerDocument.location) { docHref = this.ownerNode.ownerDocument.location.href; } } let form = { actor: this.actorID, // actorID is set when this actor is added to a pool href: this.href, nodeHref: docHref, disabled: this.rawSheet.disabled, title: this.rawSheet.title, system: !CssLogic.isContentStylesheet(this.rawSheet), styleSheetIndex: this.styleSheetIndex }; try { form.ruleCount = this.rawSheet.cssRules.length; } catch (e) { // stylesheet had an @import rule that wasn't loaded yet this.getCSSRules().then(() => { this._notifyPropertyChanged("ruleCount"); }); } return form; }, /** * Toggle the disabled property of the style sheet * * @return {object} * 'disabled' - the disabled state after toggling. */ toggleDisabled: function () { this.rawSheet.disabled = !this.rawSheet.disabled; this._notifyPropertyChanged("disabled"); return this.rawSheet.disabled; }, /** * Send an event notifying that a property of the stylesheet * has changed. * * @param {string} property * Name of the changed property */ _notifyPropertyChanged: function (property) { events.emit(this, "property-change", property, this.form()[property]); }, /** * Protocol method to get the text of this stylesheet. */ getText: function () { return this._getText().then((text) => { return new LongStringActor(this.conn, text || ""); }); }, /** * Fetch the text for this stylesheet from the cache or network. Return * cached text if it's already been fetched. * * @return {Promise} * Promise that resolves with a string text of the stylesheet. */ _getText: function () { if (typeof this.text === "string") { return promise.resolve(this.text); } let cssText = modifiedStyleSheets.get(this.rawSheet); if (cssText !== undefined) { this.text = cssText; return promise.resolve(cssText); } if (!this.href) { // this is an inline