michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: let { components, Cc, Ci, Cu } = require("chrome"); michael@0: let Services = require("Services"); michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/NetUtil.jsm"); michael@0: Cu.import("resource://gre/modules/FileUtils.jsm"); michael@0: Cu.import("resource://gre/modules/devtools/SourceMap.jsm"); michael@0: Cu.import("resource://gre/modules/Task.jsm"); michael@0: michael@0: const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); michael@0: const events = require("sdk/event/core"); michael@0: const protocol = require("devtools/server/protocol"); michael@0: const {Arg, Option, method, RetVal, types} = protocol; michael@0: const {LongStringActor, ShortLongString} = require("devtools/server/actors/string"); michael@0: michael@0: loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic); michael@0: michael@0: let TRANSITION_CLASS = "moz-styleeditor-transitioning"; michael@0: let TRANSITION_DURATION_MS = 500; michael@0: let TRANSITION_BUFFER_MS = 1000; michael@0: let TRANSITION_RULE = "\ michael@0: :root.moz-styleeditor-transitioning, :root.moz-styleeditor-transitioning * {\ michael@0: transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \ michael@0: transition-delay: 0ms !important;\ michael@0: transition-timing-function: ease-out !important;\ michael@0: transition-property: all !important;\ michael@0: }"; michael@0: michael@0: let LOAD_ERROR = "error-load"; michael@0: michael@0: exports.register = function(handle) { michael@0: handle.addTabActor(StyleSheetsActor, "styleSheetsActor"); michael@0: handle.addGlobalActor(StyleSheetsActor, "styleSheetsActor"); michael@0: }; michael@0: michael@0: exports.unregister = function(handle) { michael@0: handle.removeTabActor(StyleSheetsActor); michael@0: handle.removeGlobalActor(StyleSheetsActor); michael@0: }; michael@0: michael@0: types.addActorType("stylesheet"); michael@0: types.addActorType("originalsource"); michael@0: michael@0: /** michael@0: * Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the michael@0: * stylesheets of a document. michael@0: */ michael@0: let StyleSheetsActor = protocol.ActorClass({ michael@0: typeName: "stylesheets", michael@0: michael@0: /** michael@0: * The window we work with, taken from the parent actor. michael@0: */ michael@0: get window() this.parentActor.window, michael@0: michael@0: /** michael@0: * The current content document of the window we work with. michael@0: */ michael@0: get document() this.window.document, michael@0: michael@0: form: function() michael@0: { michael@0: return { actor: this.actorID }; michael@0: }, michael@0: michael@0: initialize: function (conn, tabActor) { michael@0: protocol.Actor.prototype.initialize.call(this, null); michael@0: michael@0: this.parentActor = tabActor; michael@0: michael@0: // keep a map of sheets-to-actors so we don't create two actors for one sheet michael@0: this._sheets = new Map(); michael@0: }, michael@0: michael@0: /** michael@0: * Destroy the current StyleSheetsActor instance. michael@0: */ michael@0: destroy: function() michael@0: { michael@0: this._sheets.clear(); michael@0: }, michael@0: michael@0: /** michael@0: * Protocol method for getting a list of StyleSheetActors representing michael@0: * all the style sheets in this document. michael@0: */ michael@0: getStyleSheets: method(function() { michael@0: let deferred = promise.defer(); michael@0: michael@0: let window = this.window; michael@0: var domReady = () => { michael@0: window.removeEventListener("DOMContentLoaded", domReady, true); michael@0: this._addAllStyleSheets().then(deferred.resolve, Cu.reportError); michael@0: }; michael@0: michael@0: if (window.document.readyState === "loading") { michael@0: window.addEventListener("DOMContentLoaded", domReady, true); michael@0: } else { michael@0: domReady(); michael@0: } michael@0: michael@0: return deferred.promise; michael@0: }, { michael@0: request: {}, michael@0: response: { styleSheets: RetVal("array:stylesheet") } michael@0: }), michael@0: michael@0: /** michael@0: * Add all the stylesheets in this document and its subframes. michael@0: * Assumes the document is loaded. michael@0: * michael@0: * @return {Promise} michael@0: * Promise that resolves with an array of StyleSheetActors michael@0: */ michael@0: _addAllStyleSheets: function() { michael@0: return Task.spawn(function() { michael@0: let documents = [this.document]; michael@0: let actors = []; michael@0: michael@0: for (let doc of documents) { michael@0: let sheets = yield this._addStyleSheets(doc.styleSheets); michael@0: actors = actors.concat(sheets); michael@0: michael@0: // Recursively handle style sheets of the documents in iframes. michael@0: for (let iframe of doc.getElementsByTagName("iframe")) { michael@0: if (iframe.contentDocument) { michael@0: // Sometimes, iframes don't have any document, like the michael@0: // one that are over deeply nested (bug 285395) michael@0: documents.push(iframe.contentDocument); michael@0: } michael@0: } michael@0: } michael@0: throw new Task.Result(actors); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Add all the stylesheets to the map and create an actor for each one michael@0: * if not already created. michael@0: * michael@0: * @param {[DOMStyleSheet]} styleSheets michael@0: * Stylesheets to add michael@0: * michael@0: * @return {Promise} michael@0: * Promise that resolves to an array of StyleSheetActors michael@0: */ michael@0: _addStyleSheets: function(styleSheets) michael@0: { michael@0: return Task.spawn(function() { michael@0: let actors = []; michael@0: for (let i = 0; i < styleSheets.length; i++) { michael@0: let actor = this._createStyleSheetActor(styleSheets[i]); michael@0: actors.push(actor); michael@0: michael@0: // Get all sheets, including imported ones michael@0: let imports = yield this._getImported(actor); michael@0: actors = actors.concat(imports); michael@0: } michael@0: throw new Task.Result(actors); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Get all the stylesheets @imported from a stylesheet. michael@0: * michael@0: * @param {DOMStyleSheet} styleSheet michael@0: * Style sheet to search michael@0: * @return {Promise} michael@0: * A promise that resolves with an array of StyleSheetActors michael@0: */ michael@0: _getImported: function(styleSheet) { michael@0: return Task.spawn(function() { michael@0: let rules = yield styleSheet.getCSSRules(); michael@0: let imported = []; michael@0: michael@0: for (let i = 0; i < rules.length; i++) { michael@0: let rule = rules[i]; michael@0: if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) { michael@0: // Associated styleSheet may be null if it has already been seen due michael@0: // to duplicate @imports for the same URL. michael@0: if (!rule.styleSheet) { michael@0: continue; michael@0: } michael@0: let actor = this._createStyleSheetActor(rule.styleSheet); michael@0: imported.push(actor); michael@0: michael@0: // recurse imports in this stylesheet as well michael@0: let children = yield this._getImported(actor); michael@0: imported = imported.concat(children); michael@0: } michael@0: else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) { michael@0: // @import rules must precede all others except @charset michael@0: break; michael@0: } michael@0: } michael@0: michael@0: throw new Task.Result(imported); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Create a new actor for a style sheet, if it hasn't already been created. michael@0: * michael@0: * @param {DOMStyleSheet} styleSheet michael@0: * The style sheet to create an actor for. michael@0: * @return {StyleSheetActor} michael@0: * The actor for this style sheet michael@0: */ michael@0: _createStyleSheetActor: function(styleSheet) michael@0: { michael@0: if (this._sheets.has(styleSheet)) { michael@0: return this._sheets.get(styleSheet); michael@0: } michael@0: let actor = new StyleSheetActor(styleSheet, this); michael@0: michael@0: this.manage(actor); michael@0: this._sheets.set(styleSheet, actor); michael@0: michael@0: return actor; michael@0: }, michael@0: michael@0: /** michael@0: * Clear all the current stylesheet actors in map. michael@0: */ michael@0: _clearStyleSheetActors: function() { michael@0: for (let actor in this._sheets) { michael@0: this.unmanage(this._sheets[actor]); michael@0: } michael@0: this._sheets.clear(); michael@0: }, michael@0: michael@0: /** michael@0: * Create a new style sheet in the document with the given text. michael@0: * Return an actor for it. michael@0: * michael@0: * @param {object} request michael@0: * Debugging protocol request object, with 'text property' michael@0: * @return {object} michael@0: * Object with 'styelSheet' property for form on new actor. michael@0: */ michael@0: addStyleSheet: method(function(text) { michael@0: let parent = this.document.documentElement; michael@0: let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style"); michael@0: style.setAttribute("type", "text/css"); michael@0: michael@0: if (text) { michael@0: style.appendChild(this.document.createTextNode(text)); michael@0: } michael@0: parent.appendChild(style); michael@0: michael@0: let actor = this._createStyleSheetActor(style.sheet); michael@0: return actor; michael@0: }, { michael@0: request: { text: Arg(0, "string") }, michael@0: response: { styleSheet: RetVal("stylesheet") } michael@0: }) michael@0: }); michael@0: michael@0: /** michael@0: * The corresponding Front object for the StyleSheetsActor. michael@0: */ michael@0: let StyleSheetsFront = protocol.FrontClass(StyleSheetsActor, { michael@0: initialize: function(client, tabForm) { michael@0: protocol.Front.prototype.initialize.call(this, client); michael@0: this.actorID = tabForm.styleSheetsActor; michael@0: michael@0: client.addActorPool(this); michael@0: this.manage(this); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * A StyleSheetActor represents a stylesheet on the server. michael@0: */ michael@0: let StyleSheetActor = protocol.ActorClass({ michael@0: typeName: "stylesheet", michael@0: michael@0: events: { michael@0: "property-change" : { michael@0: type: "propertyChange", michael@0: property: Arg(0, "string"), michael@0: value: Arg(1, "json") michael@0: }, michael@0: "style-applied" : { michael@0: type: "styleApplied" michael@0: } michael@0: }, michael@0: michael@0: /* List of original sources that generated this stylesheet */ michael@0: _originalSources: null, michael@0: michael@0: toString: function() { michael@0: return "[StyleSheetActor " + this.actorID + "]"; michael@0: }, michael@0: michael@0: /** michael@0: * Window of target michael@0: */ michael@0: get window() this._window || this.parentActor.window, michael@0: michael@0: /** michael@0: * Document of target. michael@0: */ michael@0: get document() this.window.document, michael@0: michael@0: /** michael@0: * URL of underlying stylesheet. michael@0: */ michael@0: get href() this.rawSheet.href, michael@0: michael@0: /** michael@0: * Retrieve the index (order) of stylesheet in the document. michael@0: * michael@0: * @return number michael@0: */ michael@0: get styleSheetIndex() michael@0: { michael@0: if (this._styleSheetIndex == -1) { michael@0: for (let i = 0; i < this.document.styleSheets.length; i++) { michael@0: if (this.document.styleSheets[i] == this.rawSheet) { michael@0: this._styleSheetIndex = i; michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: return this._styleSheetIndex; michael@0: }, michael@0: michael@0: initialize: function(aStyleSheet, aParentActor, aWindow) { michael@0: protocol.Actor.prototype.initialize.call(this, null); michael@0: michael@0: this.rawSheet = aStyleSheet; michael@0: this.parentActor = aParentActor; michael@0: this.conn = this.parentActor.conn; michael@0: michael@0: this._window = aWindow; michael@0: michael@0: // text and index are unknown until source load michael@0: this.text = null; michael@0: this._styleSheetIndex = -1; michael@0: michael@0: this._transitionRefCount = 0; michael@0: }, michael@0: michael@0: /** michael@0: * Get the raw stylesheet's cssRules once the sheet has been loaded. michael@0: * michael@0: * @return {Promise} michael@0: * Promise that resolves with a CSSRuleList michael@0: */ michael@0: getCSSRules: function() { michael@0: let rules; michael@0: try { michael@0: rules = this.rawSheet.cssRules; michael@0: } michael@0: catch (e) { michael@0: // sheet isn't loaded yet michael@0: } michael@0: michael@0: if (rules) { michael@0: return promise.resolve(rules); michael@0: } michael@0: michael@0: let ownerNode = this.rawSheet.ownerNode; michael@0: if (!ownerNode) { michael@0: return promise.resolve([]); michael@0: } michael@0: michael@0: if (this._cssRules) { michael@0: return this._cssRules; michael@0: } michael@0: michael@0: let deferred = promise.defer(); michael@0: michael@0: let onSheetLoaded = function(event) { michael@0: ownerNode.removeEventListener("load", onSheetLoaded, false); michael@0: michael@0: deferred.resolve(this.rawSheet.cssRules); michael@0: }.bind(this); michael@0: michael@0: ownerNode.addEventListener("load", onSheetLoaded, false); michael@0: michael@0: // cache so we don't add many listeners if this is called multiple times. michael@0: this._cssRules = deferred.promise; michael@0: michael@0: return this._cssRules; michael@0: }, michael@0: michael@0: /** michael@0: * Get the current state of the actor michael@0: * michael@0: * @return {object} michael@0: * With properties of the underlying stylesheet, plus 'text', michael@0: * 'styleSheetIndex' and 'parentActor' if it's @imported michael@0: */ michael@0: form: function(detail) { michael@0: if (detail === "actorid") { michael@0: return this.actorID; michael@0: } michael@0: michael@0: let docHref; michael@0: let ownerNode = this.rawSheet.ownerNode; michael@0: if (ownerNode) { michael@0: if (ownerNode instanceof Ci.nsIDOMHTMLDocument) { michael@0: docHref = ownerNode.location.href; michael@0: } michael@0: else if (ownerNode.ownerDocument && ownerNode.ownerDocument.location) { michael@0: docHref = ownerNode.ownerDocument.location.href; michael@0: } michael@0: } michael@0: michael@0: let form = { michael@0: actor: this.actorID, // actorID is set when this actor is added to a pool michael@0: href: this.href, michael@0: nodeHref: docHref, michael@0: disabled: this.rawSheet.disabled, michael@0: title: this.rawSheet.title, michael@0: system: !CssLogic.isContentStylesheet(this.rawSheet), michael@0: styleSheetIndex: this.styleSheetIndex michael@0: } michael@0: michael@0: try { michael@0: form.ruleCount = this.rawSheet.cssRules.length; michael@0: } michael@0: catch(e) { michael@0: // stylesheet had an @import rule that wasn't loaded yet michael@0: this.getCSSRules().then(() => { michael@0: this._notifyPropertyChanged("ruleCount"); michael@0: }); michael@0: } michael@0: return form; michael@0: }, michael@0: michael@0: /** michael@0: * Toggle the disabled property of the style sheet michael@0: * michael@0: * @return {object} michael@0: * 'disabled' - the disabled state after toggling. michael@0: */ michael@0: toggleDisabled: method(function() { michael@0: this.rawSheet.disabled = !this.rawSheet.disabled; michael@0: this._notifyPropertyChanged("disabled"); michael@0: michael@0: return this.rawSheet.disabled; michael@0: }, { michael@0: response: { disabled: RetVal("boolean")} michael@0: }), michael@0: michael@0: /** michael@0: * Send an event notifying that a property of the stylesheet michael@0: * has changed. michael@0: * michael@0: * @param {string} property michael@0: * Name of the changed property michael@0: */ michael@0: _notifyPropertyChanged: function(property) { michael@0: events.emit(this, "property-change", property, this.form()[property]); michael@0: }, michael@0: michael@0: /** michael@0: * Protocol method to get the text of this stylesheet. michael@0: */ michael@0: getText: method(function() { michael@0: return this._getText().then((text) => { michael@0: return new LongStringActor(this.conn, text || ""); michael@0: }); michael@0: }, { michael@0: response: { michael@0: text: RetVal("longstring") michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Fetch the text for this stylesheet from the cache or network. Return michael@0: * cached text if it's already been fetched. michael@0: * michael@0: * @return {Promise} michael@0: * Promise that resolves with a string text of the stylesheet. michael@0: */ michael@0: _getText: function() { michael@0: if (this.text) { michael@0: return promise.resolve(this.text); michael@0: } michael@0: michael@0: if (!this.href) { michael@0: // this is an inline