From d13f3cb8b33d9b1dcad8a2125b2a26ff1723574f Mon Sep 17 00:00:00 2001 From: Heather Arthur Date: Thu, 19 Dec 2013 11:32:12 -0500 Subject: [PATCH] Bug 949556 - Add Firefox 26-28 backwards compatibility to Style Editor. r=paul --- b2g/chrome/content/shell.js | 1 + .../devtools/styleeditor/styleeditor-panel.js | 17 +- toolkit/devtools/server/actors/styleeditor.js | 446 +++----- toolkit/devtools/server/actors/styles.js | 2 +- toolkit/devtools/server/actors/stylesheets.js | 986 ++++++++++++++++++ toolkit/devtools/server/main.js | 2 + 6 files changed, 1131 insertions(+), 323 deletions(-) create mode 100644 toolkit/devtools/server/actors/stylesheets.js diff --git a/b2g/chrome/content/shell.js b/b2g/chrome/content/shell.js index 07b64c7d54de..be0456240e58 100644 --- a/b2g/chrome/content/shell.js +++ b/b2g/chrome/content/shell.js @@ -1067,6 +1067,7 @@ let RemoteDebugger = { } DebuggerServer.registerModule("devtools/server/actors/inspector"); DebuggerServer.registerModule("devtools/server/actors/styleeditor"); + DebuggerServer.registerModule("devtools/server/actors/stylesheets"); DebuggerServer.enableWebappsContentActor = true; } DebuggerServer.addActors('chrome://browser/content/dbg-browser-actors.js'); diff --git a/browser/devtools/styleeditor/styleeditor-panel.js b/browser/devtools/styleeditor/styleeditor-panel.js index 9940fabd15db..ad13ad250a6c 100644 --- a/browser/devtools/styleeditor/styleeditor-panel.js +++ b/browser/devtools/styleeditor/styleeditor-panel.js @@ -16,7 +16,10 @@ Cu.import("resource:///modules/devtools/StyleEditorUI.jsm"); Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm"); loader.lazyGetter(this, "StyleSheetsFront", - () => require("devtools/server/actors/styleeditor").StyleSheetsFront); + () => require("devtools/server/actors/stylesheets").StyleSheetsFront); + +loader.lazyGetter(this, "StyleEditorFront", + () => require("devtools/server/actors/styleeditor").StyleEditorFront); this.StyleEditorPanel = function StyleEditorPanel(panelWin, toolbox) { EventEmitter.decorate(this); @@ -54,14 +57,20 @@ StyleEditorPanel.prototype = { targetPromise.then(() => { this.target.on("close", this.destroy); - this._debuggee = StyleSheetsFront(this.target.client, this.target.form); - + if (this.target.form.styleSheetsActor) { + this._debuggee = StyleSheetsFront(this.target.client, this.target.form); + } + else { + /* We're talking to a pre-Firefox 29 server-side */ + this._debuggee = StyleEditorFront(this.target.client, this.target.form); + } this.UI = new StyleEditorUI(this._debuggee, this.target, this._panelDoc); this.UI.on("error", this._showError); this.isReady = true; + deferred.resolve(this); - }) + }, console.error); return deferred.promise; }, diff --git a/toolkit/devtools/server/actors/styleeditor.js b/toolkit/devtools/server/actors/styleeditor.js index dff2afb60492..d377912137cb 100644 --- a/toolkit/devtools/server/actors/styleeditor.js +++ b/toolkit/devtools/server/actors/styleeditor.js @@ -33,24 +33,23 @@ transition-property: all !important;\ let LOAD_ERROR = "error-load"; exports.register = function(handle) { - handle.addTabActor(StyleSheetsActor, "styleSheetsActor"); - handle.addGlobalActor(StyleSheetsActor, "styleSheetsActor"); + handle.addTabActor(StyleEditorActor, "styleEditorActor"); + handle.addGlobalActor(StyleEditorActor, "styleEditorActor"); }; exports.unregister = function(handle) { - handle.removeTabActor(StyleSheetsActor); - handle.removeGlobalActor(StyleSheetsActor); + handle.removeTabActor(StyleEditorActor); + handle.removeGlobalActor(StyleEditorActor); }; -types.addActorType("stylesheet"); -types.addActorType("originalsource"); +types.addActorType("old-stylesheet"); /** - * Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the + * Creates a StyleEditorActor. StyleEditorActor provides remote access to the * stylesheets of a document. */ -let StyleSheetsActor = protocol.ActorClass({ - typeName: "stylesheets", +let StyleEditorActor = protocol.ActorClass({ + typeName: "styleeditor", /** * The window we work with, taken from the parent actor. @@ -62,6 +61,13 @@ let StyleSheetsActor = protocol.ActorClass({ */ get document() this.window.document, + events: { + "document-load" : { + type: "documentLoad", + styleSheets: Arg(0, "array:old-stylesheet") + } + }, + form: function() { return { actor: this.actorID }; @@ -77,7 +83,7 @@ let StyleSheetsActor = protocol.ActorClass({ }, /** - * Destroy the current StyleSheetsActor instance. + * Destroy the current StyleEditorActor instance. */ destroy: function() { @@ -85,40 +91,46 @@ let StyleSheetsActor = protocol.ActorClass({ }, /** - * Protocol method for getting a list of StyleSheetActors representing - * all the style sheets in this document. + * Called by client when target navigates to a new document. + * Adds load listeners to document. */ - getStyleSheets: method(function() { - let deferred = promise.defer(); + newDocument: method(function() { + // delete previous document's actors + this._clearStyleSheetActors(); - let window = this.window; - var domReady = () => { - window.removeEventListener("DOMContentLoaded", domReady, true); + // Note: listening for load won't be necessary once + // https://bugzilla.mozilla.org/show_bug.cgi?id=839103 is fixed + if (this.document.readyState == "complete") { + this._onDocumentLoaded(); + } + else { + this.window.addEventListener("load", this._onDocumentLoaded, false); + } + return {}; + }), - let documents = [this.document]; - let actors = []; - for (let doc of documents) { - let sheets = this._addStyleSheets(doc.styleSheets); - actors = actors.concat(sheets); - // Recursively handle style sheets of the documents in iframes. - for (let iframe of doc.getElementsByTagName("iframe")) { - documents.push(iframe.contentDocument); - } - } - deferred.resolve(actors); - }; - - if (window.document.readyState === "loading") { - window.addEventListener("DOMContentLoaded", domReady, true); - } else { - domReady(); + /** + * Event handler for document loaded event. Add actor for each stylesheet + * and send an event notifying of the load + */ + _onDocumentLoaded: function(event) { + if (event) { + this.window.removeEventListener("load", this._onDocumentLoaded, false); } - return deferred.promise; - }, { - request: {}, - response: { styleSheets: RetVal("array:stylesheet") } - }), + let documents = [this.document]; + var forms = []; + for (let doc of documents) { + let sheetForms = this._addStyleSheets(doc.styleSheets); + forms = forms.concat(sheetForms); + // Recursively handle style sheets of the documents in iframes. + for (let iframe of doc.getElementsByTagName("iframe")) { + documents.push(iframe.contentDocument); + } + } + + events.emit(this, "document-load", forms); + }, /** * Add all the stylesheets to the map and create an actor for each one @@ -145,6 +157,27 @@ let StyleSheetsActor = protocol.ActorClass({ return actors; }, + /** + * 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 OldStyleSheetActor(styleSheet, this); + + this.manage(actor); + this._sheets.set(styleSheet, actor); + + return actor; + }, + /** * Get all the stylesheets @imported from a stylesheet. * @@ -177,27 +210,6 @@ let StyleSheetsActor = protocol.ActorClass({ return imported; }, - /** - * 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. */ @@ -217,7 +229,7 @@ let StyleSheetsActor = protocol.ActorClass({ * @return {object} * Object with 'styelSheet' property for form on new actor. */ - addStyleSheet: method(function(text) { + newStyleSheet: 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"); @@ -231,28 +243,43 @@ let StyleSheetsActor = protocol.ActorClass({ return actor; }, { request: { text: Arg(0, "string") }, - response: { styleSheet: RetVal("stylesheet") } + response: { styleSheet: RetVal("old-stylesheet") } }) }); /** - * The corresponding Front object for the StyleSheetsActor. + * The corresponding Front object for the StyleEditorActor. */ -let StyleSheetsFront = protocol.FrontClass(StyleSheetsActor, { +let StyleEditorFront = protocol.FrontClass(StyleEditorActor, { initialize: function(client, tabForm) { protocol.Front.prototype.initialize.call(this, client); - this.actorID = tabForm.styleSheetsActor; + this.actorID = tabForm.styleEditorActor; client.addActorPool(this); this.manage(this); + }, + + getStyleSheets: function() { + let deferred = promise.defer(); + + events.once(this, "document-load", (styleSheets) => { + deferred.resolve(styleSheets); + }); + this.newDocument(); + + return deferred.promise; + }, + + addStyleSheet: function(text) { + return this.newStyleSheet(text); } }); /** * A StyleSheetActor represents a stylesheet on the server. */ -let StyleSheetActor = protocol.ActorClass({ - typeName: "stylesheet", +let OldStyleSheetActor = protocol.ActorClass({ + typeName: "old-stylesheet", events: { "property-change" : { @@ -260,16 +287,17 @@ let StyleSheetActor = protocol.ActorClass({ property: Arg(0, "string"), value: Arg(1, "json") }, + "source-load" : { + type: "sourceLoad", + source: Arg(0, "string") + }, "style-applied" : { type: "styleApplied" } }, - /* List of original sources that generated this stylesheet */ - _originalSources: null, - toString: function() { - return "[StyleSheetActor " + this.actorID + "]"; + return "[OldStyleSheetActor " + this.actorID + "]"; }, /** @@ -399,17 +427,14 @@ let StyleSheetActor = protocol.ActorClass({ 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 || ""); + /** + * Fetch the source of the style sheet from its URL. Send a "sourceLoad" + * event when it's been fetched. + */ + fetchSource: method(function() { + this._getText().then((content) => { + events.emit(this, "source-load", this.text); }); - }, { - response: { - text: RetVal("longstring") - } }), /** @@ -442,163 +467,6 @@ let StyleSheetActor = protocol.ActorClass({ }); }, - /** - * Protocol method to get the original source (actors) for this - * stylesheet if it has uses source maps. - */ - getOriginalSources: method(function() { - if (this._originalSources) { - return promise.resolve(this._originalSources); - } - return this._fetchOriginalSources(); - }, { - request: {}, - response: { - originalSources: RetVal("nullable:array:originalsource") - } - }), - - /** - * Fetch the original sources (actors) for this style sheet using its - * source map. If they've already been fetched, returns cached array. - * - * @return {Promise} - * Promise that resolves with an array of OriginalSourceActors - */ - _fetchOriginalSources: function() { - this._clearOriginalSources(); - this._originalSources = []; - - return this.getSourceMap().then((sourceMap) => { - if (!sourceMap) { - return null; - } - for (let url of sourceMap.sources) { - let actor = new OriginalSourceActor(url, sourceMap, this); - - this.manage(actor); - this._originalSources.push(actor); - } - return this._originalSources; - }) - }, - - /** - * Get the SourceMapConsumer for this stylesheet's source map, if - * it exists. Saves the consumer for later queries. - * - * @return {Promise} - * A promise that resolves with a SourceMapConsumer, or null. - */ - getSourceMap: function() { - if (this._sourceMap) { - return this._sourceMap; - } - return this._fetchSourceMap(); - }, - - /** - * Fetch the source map for this stylesheet. - * - * @return {Promise} - * A promise that resolves with a SourceMapConsumer, or null. - */ - _fetchSourceMap: function() { - let deferred = promise.defer(); - - this._getText().then((content) => { - let url = this._extractSourceMapUrl(content); - if (!url) { - // no source map for this stylesheet - deferred.resolve(null); - return; - }; - - url = normalize(url, this.href); - - let map = fetch(url, { loadFromCache: false, window: this.window }) - .then(({content}) => { - let map = new SourceMapConsumer(content); - this._setSourceMapRoot(map, url, this.href); - this._sourceMap = promise.resolve(map); - - deferred.resolve(map); - return map; - }, deferred.reject); - - this._sourceMap = map; - }, deferred.reject); - - return deferred.promise; - }, - - /** - * Clear and unmanage the original source actors for this stylesheet. - */ - _clearOriginalSources: function() { - for (actor in this._originalSources) { - this.unmanage(actor); - } - this._originalSources = null; - }, - - /** - * Sets the source map's sourceRoot to be relative to the source map url. - */ - _setSourceMapRoot: function(aSourceMap, aAbsSourceMapURL, aScriptURL) { - const base = dirname( - aAbsSourceMapURL.indexOf("data:") === 0 - ? aScriptURL - : aAbsSourceMapURL); - aSourceMap.sourceRoot = aSourceMap.sourceRoot - ? normalize(aSourceMap.sourceRoot, base) - : base; - }, - - /** - * Get the source map url specified in the text of a stylesheet. - * - * @param {string} content - * The text of the style sheet. - * @return {string} - * Url of source map. - */ - _extractSourceMapUrl: function(content) { - var matches = /sourceMappingURL\=([^\s\*]*)/.exec(content); - if (matches) { - return matches[1]; - } - return null; - }, - - /** - * Protocol method that gets the location in the original source of a - * line, column pair in this stylesheet, if its source mapped, otherwise - * a promise of the same location. - */ - getOriginalLocation: method(function(line, column) { - return this.getSourceMap().then((sourceMap) => { - if (sourceMap) { - return sourceMap.originalPositionFor({ line: line, column: column }); - } - return { - source: this.href, - line: line, - column: column - } - }); - }, { - request: { - line: Arg(0, "number"), - column: Arg(1, "number") - }, - response: RetVal(types.addDictType("originallocationresponse", { - source: "string", - line: "number", - column: "number" - })) - }), - /** * Get the charset of the stylesheet according to the character set rules * defined in . @@ -685,7 +553,7 @@ let StyleSheetActor = protocol.ActorClass({ _insertTransistionRule: function() { // Insert the global transition rule // Use a ref count to make sure we do not add it multiple times.. and remove - // it only when all pending StyleSheets-generated transitions ended. + // it only when all pending StyleEditor-generated transitions ended. if (this._transitionRefCount == 0) { this.rawSheet.insertRule(TRANSITION_RULE, this.rawSheet.cssRules.length); this.document.documentElement.classList.add(TRANSITION_CLASS); @@ -717,7 +585,7 @@ let StyleSheetActor = protocol.ActorClass({ /** * StyleSheetFront is the client-side counterpart to a StyleSheetActor. */ -var StyleSheetFront = protocol.FrontClass(StyleSheetActor, { +var OldStyleSheetFront = protocol.FrontClass(OldStyleSheetActor, { initialize: function(conn, form, ctx, detail) { protocol.Front.prototype.initialize.call(this, conn, form, ctx, detail); @@ -744,6 +612,22 @@ var StyleSheetFront = protocol.FrontClass(StyleSheetActor, { this._form = form; }, + getText: function() { + let deferred = promise.defer(); + + events.once(this, "source-load", (source) => { + let longStr = new ShortLongString(source); + deferred.resolve(longStr); + }); + this.fetchSource(); + + return deferred.promise; + }, + + getOriginalSources: function() { + return promise.resolve([]); + }, + get href() this._form.href, get nodeHref() this._form.nodeHref, get disabled() !!this._form.disabled, @@ -753,89 +637,15 @@ var StyleSheetFront = protocol.FrontClass(StyleSheetActor, { get ruleCount() this._form.ruleCount }); -/** - * Actor representing an original source of a style sheet that was specified - * in a source map. - */ -let OriginalSourceActor = protocol.ActorClass({ - typeName: "originalsource", - - 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, - parentSource: this.parentActor.actorID - }; - }, - - _getText: function() { - if (this.text) { - return promise.resolve(this.text); - } - return fetch(this.url, { window: this.window }).then(({content}) => { - this.text = content; - return content; - }); - }, - - /** - * Protocol method to get the text of this source. - */ - getText: method(function() { - return this._getText().then((text) => { - return new LongStringActor(this.conn, text || ""); - }); - }, { - response: { - text: RetVal("longstring") - } - }) -}) - -/** - * The client-side counterpart for an OriginalSourceActor. - */ -let OriginalSourceFront = protocol.FrontClass(OriginalSourceActor, { - initialize: function(client, form) { - protocol.Front.prototype.initialize.call(this, client, form); - - this.isOriginalSource = true; - }, - - form: function(form, detail) { - if (detail === "actorid") { - this.actorID = form; - return; - } - this.actorID = form.actor; - this._form = form; - }, - - get href() this._form.url, - get url() this._form.url -}); - - XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () { return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); }); -exports.StyleSheetsActor = StyleSheetsActor; -exports.StyleSheetsFront = StyleSheetsFront; +exports.StyleEditorActor = StyleEditorActor; +exports.StyleEditorFront = StyleEditorFront; -exports.StyleSheetActor = StyleSheetActor; -exports.StyleSheetFront = StyleSheetFront; +exports.OldStyleSheetActor = OldStyleSheetActor; +exports.OldStyleSheetFront = OldStyleSheetFront; /** diff --git a/toolkit/devtools/server/actors/styles.js b/toolkit/devtools/server/actors/styles.js index e5ebbd292e91..b9d33ca92b7b 100644 --- a/toolkit/devtools/server/actors/styles.js +++ b/toolkit/devtools/server/actors/styles.js @@ -11,7 +11,7 @@ const {Arg, Option, method, RetVal, types} = protocol; const events = require("sdk/event/core"); const object = require("sdk/util/object"); const { Class } = require("sdk/core/heritage"); -const { StyleSheetActor } = require("devtools/server/actors/styleeditor"); +const { StyleSheetActor } = require("devtools/server/actors/stylesheets"); loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm"); loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic); diff --git a/toolkit/devtools/server/actors/stylesheets.js b/toolkit/devtools/server/actors/stylesheets.js new file mode 100644 index 000000000000..dff2afb60492 --- /dev/null +++ b/toolkit/devtools/server/actors/stylesheets.js @@ -0,0 +1,986 @@ +/* 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'); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/devtools/SourceMap.jsm"); + +const promise = require("sdk/core/promise"); +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_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); + + let documents = [this.document]; + let actors = []; + for (let doc of documents) { + let sheets = this._addStyleSheets(doc.styleSheets); + actors = actors.concat(sheets); + // Recursively handle style sheets of the documents in iframes. + for (let iframe of doc.getElementsByTagName("iframe")) { + documents.push(iframe.contentDocument); + } + } + deferred.resolve(actors); + }; + + 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 to the map and create an actor for each one + * if not already created. Send event that there are new stylesheets. + * + * @param {[DOMStyleSheet]} styleSheets + * Stylesheets to add + * @return {[object]} + * Array of actors for each StyleSheetActor created + */ + _addStyleSheets: function(styleSheets) + { + let sheets = []; + for (let i = 0; i < styleSheets.length; i++) { + let styleSheet = styleSheets[i]; + sheets.push(styleSheet); + + // Get all sheets, including imported ones + let imports = this._getImported(styleSheet); + sheets = sheets.concat(imports); + } + let actors = sheets.map(this._createStyleSheetActor.bind(this)); + + return actors; + }, + + /** + * Get all the stylesheets @imported from a stylesheet. + * + * @param {DOMStyleSheet} styleSheet + * Style sheet to search + * @return {array} + * All the imported stylesheets + */ + _getImported: function(styleSheet) { + let imported = []; + + for (let i = 0; i < styleSheet.cssRules.length; i++) { + let rule = styleSheet.cssRules[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; + } + imported.push(rule.styleSheet); + + // recurse imports in this stylesheet as well + imported = imported.concat(this._getImported(rule.styleSheet)); + } + else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) { + // @import rules must precede all others except @charset + break; + } + } + return imported; + }, + + /** + * 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; + + // if this sheet has an @import, then it's rules are loaded async + let ownerNode = this.rawSheet.ownerNode; + if (ownerNode) { + let onSheetLoaded = function(event) { + ownerNode.removeEventListener("load", onSheetLoaded, false); + this._notifyPropertyChanged("ruleCount"); + }.bind(this); + + ownerNode.addEventListener("load", onSheetLoaded, false); + } + }, + + /** + * 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.rawSheet.ownerNode) { + if (this.rawSheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) { + docHref = this.rawSheet.ownerNode.location.href; + } + if (this.rawSheet.ownerNode.ownerDocument) { + docHref = this.rawSheet.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 + } + 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