diff -r 000000000000 -r 6474c204b198 toolkit/devtools/server/actors/stylesheets.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolkit/devtools/server/actors/stylesheets.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,1055 @@ +/* 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"; + +let { components, Cc, Ci, Cu } = require("chrome"); +let Services = require("Services"); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/devtools/SourceMap.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); + +const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); +const events = require("sdk/event/core"); +const protocol = require("devtools/server/protocol"); +const {Arg, Option, method, RetVal, types} = protocol; +const {LongStringActor, ShortLongString} = require("devtools/server/actors/string"); + +loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic); + +let TRANSITION_CLASS = "moz-styleeditor-transitioning"; +let TRANSITION_DURATION_MS = 500; +let TRANSITION_BUFFER_MS = 1000; +let TRANSITION_RULE = "\ +:root.moz-styleeditor-transitioning, :root.moz-styleeditor-transitioning * {\ +transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \ +transition-delay: 0ms !important;\ +transition-timing-function: ease-out !important;\ +transition-property: all !important;\ +}"; + +let LOAD_ERROR = "error-load"; + +exports.register = function(handle) { + handle.addTabActor(StyleSheetsActor, "styleSheetsActor"); + handle.addGlobalActor(StyleSheetsActor, "styleSheetsActor"); +}; + +exports.unregister = function(handle) { + handle.removeTabActor(StyleSheetsActor); + handle.removeGlobalActor(StyleSheetsActor); +}; + +types.addActorType("stylesheet"); +types.addActorType("originalsource"); + +/** + * Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the + * stylesheets of a document. + */ +let StyleSheetsActor = protocol.ActorClass({ + typeName: "stylesheets", + + /** + * The window we work with, taken from the parent actor. + */ + get window() this.parentActor.window, + + /** + * The current content document of the window we work with. + */ + get document() this.window.document, + + form: function() + { + return { actor: this.actorID }; + }, + + initialize: function (conn, tabActor) { + protocol.Actor.prototype.initialize.call(this, null); + + this.parentActor = tabActor; + + // keep a map of sheets-to-actors so we don't create two actors for one sheet + this._sheets = new Map(); + }, + + /** + * Destroy the current StyleSheetsActor instance. + */ + destroy: function() + { + this._sheets.clear(); + }, + + /** + * Protocol method for getting a list of StyleSheetActors representing + * all the style sheets in this document. + */ + getStyleSheets: method(function() { + let deferred = promise.defer(); + + let window = this.window; + var domReady = () => { + window.removeEventListener("DOMContentLoaded", domReady, true); + this._addAllStyleSheets().then(deferred.resolve, Cu.reportError); + }; + + if (window.document.readyState === "loading") { + window.addEventListener("DOMContentLoaded", domReady, true); + } else { + domReady(); + } + + return deferred.promise; + }, { + request: {}, + response: { styleSheets: RetVal("array:stylesheet") } + }), + + /** + * Add all the stylesheets in this document and its subframes. + * Assumes the document is loaded. + * + * @return {Promise} + * Promise that resolves with an array of StyleSheetActors + */ + _addAllStyleSheets: function() { + return Task.spawn(function() { + let documents = [this.document]; + let actors = []; + + for (let doc of documents) { + let sheets = yield this._addStyleSheets(doc.styleSheets); + actors = actors.concat(sheets); + + // Recursively handle style sheets of the documents in iframes. + for (let iframe of doc.getElementsByTagName("iframe")) { + if (iframe.contentDocument) { + // Sometimes, iframes don't have any document, like the + // one that are over deeply nested (bug 285395) + documents.push(iframe.contentDocument); + } + } + } + throw new Task.Result(actors); + }.bind(this)); + }, + + /** + * Add all the stylesheets to the map and create an actor for each one + * if not already created. + * + * @param {[DOMStyleSheet]} styleSheets + * Stylesheets to add + * + * @return {Promise} + * Promise that resolves to an array of StyleSheetActors + */ + _addStyleSheets: function(styleSheets) + { + return Task.spawn(function() { + let actors = []; + for (let i = 0; i < styleSheets.length; i++) { + let actor = this._createStyleSheetActor(styleSheets[i]); + actors.push(actor); + + // Get all sheets, including imported ones + let imports = yield this._getImported(actor); + actors = actors.concat(imports); + } + throw new Task.Result(actors); + }.bind(this)); + }, + + /** + * Get all the stylesheets @imported from a stylesheet. + * + * @param {DOMStyleSheet} styleSheet + * Style sheet to search + * @return {Promise} + * A promise that resolves with an array of StyleSheetActors + */ + _getImported: function(styleSheet) { + return Task.spawn(function() { + let rules = yield styleSheet.getCSSRules(); + let imported = []; + + for (let i = 0; i < rules.length; i++) { + let rule = rules[i]; + if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) { + // Associated styleSheet may be null if it has already been seen due + // to duplicate @imports for the same URL. + if (!rule.styleSheet) { + continue; + } + let actor = this._createStyleSheetActor(rule.styleSheet); + imported.push(actor); + + // recurse imports in this stylesheet as well + let children = yield this._getImported(actor); + imported = imported.concat(children); + } + else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) { + // @import rules must precede all others except @charset + break; + } + } + + throw new Task.Result(imported); + }.bind(this)); + }, + + /** + * Create a new actor for a style sheet, if it hasn't already been created. + * + * @param {DOMStyleSheet} styleSheet + * The style sheet to create an actor for. + * @return {StyleSheetActor} + * The actor for this style sheet + */ + _createStyleSheetActor: function(styleSheet) + { + if (this._sheets.has(styleSheet)) { + return this._sheets.get(styleSheet); + } + let actor = new StyleSheetActor(styleSheet, this); + + this.manage(actor); + this._sheets.set(styleSheet, actor); + + return actor; + }, + + /** + * Clear all the current stylesheet actors in map. + */ + _clearStyleSheetActors: function() { + for (let actor in this._sheets) { + this.unmanage(this._sheets[actor]); + } + this._sheets.clear(); + }, + + /** + * Create a new style sheet in the document with the given text. + * Return an actor for it. + * + * @param {object} request + * Debugging protocol request object, with 'text property' + * @return {object} + * Object with 'styelSheet' property for form on new actor. + */ + addStyleSheet: method(function(text) { + let parent = this.document.documentElement; + let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style"); + style.setAttribute("type", "text/css"); + + if (text) { + style.appendChild(this.document.createTextNode(text)); + } + parent.appendChild(style); + + let actor = this._createStyleSheetActor(style.sheet); + return actor; + }, { + request: { text: Arg(0, "string") }, + response: { styleSheet: RetVal("stylesheet") } + }) +}); + +/** + * The corresponding Front object for the StyleSheetsActor. + */ +let StyleSheetsFront = protocol.FrontClass(StyleSheetsActor, { + initialize: function(client, tabForm) { + protocol.Front.prototype.initialize.call(this, client); + this.actorID = tabForm.styleSheetsActor; + + client.addActorPool(this); + this.manage(this); + } +}); + +/** + * A StyleSheetActor represents a stylesheet on the server. + */ +let StyleSheetActor = protocol.ActorClass({ + typeName: "stylesheet", + + events: { + "property-change" : { + type: "propertyChange", + property: Arg(0, "string"), + value: Arg(1, "json") + }, + "style-applied" : { + type: "styleApplied" + } + }, + + /* List of original sources that generated this stylesheet */ + _originalSources: null, + + toString: function() { + return "[StyleSheetActor " + this.actorID + "]"; + }, + + /** + * Window of target + */ + get window() this._window || this.parentActor.window, + + /** + * Document of target. + */ + get document() this.window.document, + + /** + * URL of underlying stylesheet. + */ + get href() this.rawSheet.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; + }, + + 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; + + this._transitionRefCount = 0; + }, + + /** + * 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); + } + + let ownerNode = this.rawSheet.ownerNode; + if (!ownerNode) { + return promise.resolve([]); + } + + if (this._cssRules) { + return this._cssRules; + } + + let deferred = promise.defer(); + + let onSheetLoaded = function(event) { + ownerNode.removeEventListener("load", onSheetLoaded, false); + + deferred.resolve(this.rawSheet.cssRules); + }.bind(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; + let ownerNode = this.rawSheet.ownerNode; + if (ownerNode) { + if (ownerNode instanceof Ci.nsIDOMHTMLDocument) { + docHref = ownerNode.location.href; + } + else if (ownerNode.ownerDocument && ownerNode.ownerDocument.location) { + docHref = 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: method(function() { + this.rawSheet.disabled = !this.rawSheet.disabled; + this._notifyPropertyChanged("disabled"); + + return this.rawSheet.disabled; + }, { + response: { disabled: RetVal("boolean")} + }), + + /** + * 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: method(function() { + return this._getText().then((text) => { + return new LongStringActor(this.conn, text || ""); + }); + }, { + response: { + text: RetVal("longstring") + } + }), + + /** + * 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 (this.text) { + return promise.resolve(this.text); + } + + if (!this.href) { + // this is an inline