diff -r 000000000000 -r 6474c204b198 toolkit/devtools/server/actors/styleeditor.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolkit/devtools/server/actors/styleeditor.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,796 @@ +/* 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"); + +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_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(StyleEditorActor, "styleEditorActor"); + handle.addGlobalActor(StyleEditorActor, "styleEditorActor"); +}; + +exports.unregister = function(handle) { + handle.removeTabActor(StyleEditorActor); + handle.removeGlobalActor(StyleEditorActor); +}; + +types.addActorType("old-stylesheet"); + +/** + * Creates a StyleEditorActor. StyleEditorActor provides remote access to the + * stylesheets of a document. + */ +let StyleEditorActor = protocol.ActorClass({ + typeName: "styleeditor", + + /** + * 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, + + events: { + "document-load" : { + type: "documentLoad", + styleSheets: Arg(0, "array:old-stylesheet") + } + }, + + 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 StyleEditorActor instance. + */ + destroy: function() + { + this._sheets.clear(); + }, + + /** + * Called by client when target navigates to a new document. + * Adds load listeners to document. + */ + newDocument: method(function() { + // delete previous document's actors + this._clearStyleSheetActors(); + + // 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 {}; + }), + + /** + * 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); + } + + 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 + * 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; + }, + + /** + * 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. + * + * @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; + }, + + /** + * 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. + */ + 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"); + + 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("old-stylesheet") } + }) +}); + +/** + * The corresponding Front object for the StyleEditorActor. + */ +let StyleEditorFront = protocol.FrontClass(StyleEditorActor, { + initialize: function(client, tabForm) { + protocol.Front.prototype.initialize.call(this, client); + 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 OldStyleSheetActor = protocol.ActorClass({ + typeName: "old-stylesheet", + + events: { + "property-change" : { + type: "propertyChange", + property: Arg(0, "string"), + value: Arg(1, "json") + }, + "source-load" : { + type: "sourceLoad", + source: Arg(0, "string") + }, + "style-applied" : { + type: "styleApplied" + } + }, + + toString: function() { + return "[OldStyleSheetActor " + 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]); + }, + + /** + * 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); + }); + }), + + /** + * 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