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: 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_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(StyleEditorActor, "styleEditorActor"); michael@0: handle.addGlobalActor(StyleEditorActor, "styleEditorActor"); michael@0: }; michael@0: michael@0: exports.unregister = function(handle) { michael@0: handle.removeTabActor(StyleEditorActor); michael@0: handle.removeGlobalActor(StyleEditorActor); michael@0: }; michael@0: michael@0: types.addActorType("old-stylesheet"); michael@0: michael@0: /** michael@0: * Creates a StyleEditorActor. StyleEditorActor provides remote access to the michael@0: * stylesheets of a document. michael@0: */ michael@0: let StyleEditorActor = protocol.ActorClass({ michael@0: typeName: "styleeditor", 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: events: { michael@0: "document-load" : { michael@0: type: "documentLoad", michael@0: styleSheets: Arg(0, "array:old-stylesheet") michael@0: } michael@0: }, 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 StyleEditorActor instance. michael@0: */ michael@0: destroy: function() michael@0: { michael@0: this._sheets.clear(); michael@0: }, michael@0: michael@0: /** michael@0: * Called by client when target navigates to a new document. michael@0: * Adds load listeners to document. michael@0: */ michael@0: newDocument: method(function() { michael@0: // delete previous document's actors michael@0: this._clearStyleSheetActors(); michael@0: michael@0: // Note: listening for load won't be necessary once michael@0: // https://bugzilla.mozilla.org/show_bug.cgi?id=839103 is fixed michael@0: if (this.document.readyState == "complete") { michael@0: this._onDocumentLoaded(); michael@0: } michael@0: else { michael@0: this.window.addEventListener("load", this._onDocumentLoaded, false); michael@0: } michael@0: return {}; michael@0: }), michael@0: michael@0: /** michael@0: * Event handler for document loaded event. Add actor for each stylesheet michael@0: * and send an event notifying of the load michael@0: */ michael@0: _onDocumentLoaded: function(event) { michael@0: if (event) { michael@0: this.window.removeEventListener("load", this._onDocumentLoaded, false); michael@0: } michael@0: michael@0: let documents = [this.document]; michael@0: var forms = []; michael@0: for (let doc of documents) { michael@0: let sheetForms = this._addStyleSheets(doc.styleSheets); michael@0: forms = forms.concat(sheetForms); michael@0: // Recursively handle style sheets of the documents in iframes. michael@0: for (let iframe of doc.getElementsByTagName("iframe")) { michael@0: documents.push(iframe.contentDocument); michael@0: } michael@0: } michael@0: michael@0: events.emit(this, "document-load", forms); 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. Send event that there are new stylesheets. michael@0: * michael@0: * @param {[DOMStyleSheet]} styleSheets michael@0: * Stylesheets to add michael@0: * @return {[object]} michael@0: * Array of actors for each StyleSheetActor created michael@0: */ michael@0: _addStyleSheets: function(styleSheets) michael@0: { michael@0: let sheets = []; michael@0: for (let i = 0; i < styleSheets.length; i++) { michael@0: let styleSheet = styleSheets[i]; michael@0: sheets.push(styleSheet); michael@0: michael@0: // Get all sheets, including imported ones michael@0: let imports = this._getImported(styleSheet); michael@0: sheets = sheets.concat(imports); michael@0: } michael@0: let actors = sheets.map(this._createStyleSheetActor.bind(this)); michael@0: michael@0: return actors; 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 OldStyleSheetActor(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: * Get all the stylesheets @imported from a stylesheet. michael@0: * michael@0: * @param {DOMStyleSheet} styleSheet michael@0: * Style sheet to search michael@0: * @return {array} michael@0: * All the imported stylesheets michael@0: */ michael@0: _getImported: function(styleSheet) { michael@0: let imported = []; michael@0: michael@0: for (let i = 0; i < styleSheet.cssRules.length; i++) { michael@0: let rule = styleSheet.cssRules[i]; michael@0: if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) { michael@0: // Associated styleSheet may be null if it has already been seen due to michael@0: // duplicate @imports for the same URL. michael@0: if (!rule.styleSheet) { michael@0: continue; michael@0: } michael@0: imported.push(rule.styleSheet); michael@0: michael@0: // recurse imports in this stylesheet as well michael@0: imported = imported.concat(this._getImported(rule.styleSheet)); 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: return imported; 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: newStyleSheet: 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("old-stylesheet") } michael@0: }) michael@0: }); michael@0: michael@0: /** michael@0: * The corresponding Front object for the StyleEditorActor. michael@0: */ michael@0: let StyleEditorFront = protocol.FrontClass(StyleEditorActor, { michael@0: initialize: function(client, tabForm) { michael@0: protocol.Front.prototype.initialize.call(this, client); michael@0: this.actorID = tabForm.styleEditorActor; michael@0: michael@0: client.addActorPool(this); michael@0: this.manage(this); michael@0: }, michael@0: michael@0: getStyleSheets: function() { michael@0: let deferred = promise.defer(); michael@0: michael@0: events.once(this, "document-load", (styleSheets) => { michael@0: deferred.resolve(styleSheets); michael@0: }); michael@0: this.newDocument(); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: addStyleSheet: function(text) { michael@0: return this.newStyleSheet(text); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * A StyleSheetActor represents a stylesheet on the server. michael@0: */ michael@0: let OldStyleSheetActor = protocol.ActorClass({ michael@0: typeName: "old-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: "source-load" : { michael@0: type: "sourceLoad", michael@0: source: Arg(0, "string") michael@0: }, michael@0: "style-applied" : { michael@0: type: "styleApplied" michael@0: } michael@0: }, michael@0: michael@0: toString: function() { michael@0: return "[OldStyleSheetActor " + 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: // if this sheet has an @import, then it's rules are loaded async michael@0: let ownerNode = this.rawSheet.ownerNode; michael@0: if (ownerNode) { michael@0: let onSheetLoaded = function(event) { michael@0: ownerNode.removeEventListener("load", onSheetLoaded, false); michael@0: this._notifyPropertyChanged("ruleCount"); michael@0: }.bind(this); michael@0: michael@0: ownerNode.addEventListener("load", onSheetLoaded, false); michael@0: } 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: if (this.rawSheet.ownerNode) { michael@0: if (this.rawSheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) { michael@0: docHref = this.rawSheet.ownerNode.location.href; michael@0: } michael@0: if (this.rawSheet.ownerNode.ownerDocument) { michael@0: docHref = this.rawSheet.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: } 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: * Fetch the source of the style sheet from its URL. Send a "sourceLoad" michael@0: * event when it's been fetched. michael@0: */ michael@0: fetchSource: method(function() { michael@0: this._getText().then((content) => { michael@0: events.emit(this, "source-load", this.text); 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