summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/styleeditor.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/styleeditor.js')
-rw-r--r--devtools/server/actors/styleeditor.js528
1 files changed, 528 insertions, 0 deletions
diff --git a/devtools/server/actors/styleeditor.js b/devtools/server/actors/styleeditor.js
new file mode 100644
index 000000000..5793a2baf
--- /dev/null
+++ b/devtools/server/actors/styleeditor.js
@@ -0,0 +1,528 @@
+/* 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";
+
+const {Cc, Ci} = require("chrome");
+const Services = require("Services");
+const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+const promise = require("promise");
+const events = require("sdk/event/core");
+const protocol = require("devtools/shared/protocol");
+const {Arg, method, RetVal} = protocol;
+const {fetch} = require("devtools/shared/DevToolsUtils");
+const {oldStyleSheetSpec, styleEditorSpec} = require("devtools/shared/specs/styleeditor");
+
+loader.lazyGetter(this, "CssLogic", () => require("devtools/shared/inspector/css-logic"));
+
+var TRANSITION_CLASS = "moz-styleeditor-transitioning";
+var TRANSITION_DURATION_MS = 500;
+var 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;\
+}";
+
+var LOAD_ERROR = "error-load";
+
+var OldStyleSheetActor = protocol.ActorClassWithSpec(oldStyleSheetSpec, {
+ toString: function() {
+ return "[OldStyleSheetActor " + this.actorID + "]";
+ },
+
+ /**
+ * Window of target
+ */
+ get window() {
+ return this._window || this.parentActor.window;
+ },
+
+ /**
+ * Document of target.
+ */
+ get document() {
+ return this.window.document;
+ },
+
+ /**
+ * URL of underlying stylesheet.
+ */
+ get href() {
+ return 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 = (event) => {
+ ownerNode.removeEventListener("load", onSheetLoaded, false);
+ this._notifyPropertyChanged("ruleCount");
+ };
+
+ 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: function () {
+ this.rawSheet.disabled = !this.rawSheet.disabled;
+ this._notifyPropertyChanged("disabled");
+
+ return this.rawSheet.disabled;
+ },
+
+ /**
+ * 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: 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 <style> sheet
+ let content = this.rawSheet.ownerNode.textContent;
+ this.text = content;
+ return promise.resolve(content);
+ }
+
+ let options = {
+ policy: Ci.nsIContentPolicy.TYPE_INTERNAL_STYLESHEET,
+ window: this.window,
+ charset: this._getCSSCharset()
+ };
+
+ return fetch(this.href, options).then(({ content }) => {
+ this.text = content;
+ return content;
+ });
+ },
+
+ /**
+ * Get the charset of the stylesheet according to the character set rules
+ * defined in <http://www.w3.org/TR/CSS2/syndata.html#charset>.
+ * Note that some of the algorithm is implemented in DevToolsUtils.fetch.
+ */
+ _getCSSCharset: function ()
+ {
+ let sheet = this.rawSheet;
+ if (sheet) {
+ // Do we have a @charset rule in the stylesheet?
+ // step 2 of syndata.html (without the BOM check).
+ if (sheet.cssRules) {
+ let rules = sheet.cssRules;
+ if (rules.length
+ && rules.item(0).type == Ci.nsIDOMCSSRule.CHARSET_RULE) {
+ return rules.item(0).encoding;
+ }
+ }
+
+ // step 3: charset attribute of <link> or <style> element, if it exists
+ if (sheet.ownerNode && sheet.ownerNode.getAttribute) {
+ let linkCharset = sheet.ownerNode.getAttribute("charset");
+ if (linkCharset != null) {
+ return linkCharset;
+ }
+ }
+
+ // step 4 (1 of 2): charset of referring stylesheet.
+ let parentSheet = sheet.parentStyleSheet;
+ if (parentSheet && parentSheet.cssRules &&
+ parentSheet.cssRules[0].type == Ci.nsIDOMCSSRule.CHARSET_RULE) {
+ return parentSheet.cssRules[0].encoding;
+ }
+
+ // step 4 (2 of 2): charset of referring document.
+ if (sheet.ownerNode && sheet.ownerNode.ownerDocument.characterSet) {
+ return sheet.ownerNode.ownerDocument.characterSet;
+ }
+ }
+
+ // step 5: default to utf-8.
+ return "UTF-8";
+ },
+
+ /**
+ * Update the style sheet in place with new text.
+ *
+ * @param {object} request
+ * 'text' - new text
+ * 'transition' - whether to do CSS transition for change.
+ */
+ update: function (text, transition) {
+ DOMUtils.parseStyleSheet(this.rawSheet, text);
+
+ this.text = text;
+
+ this._notifyPropertyChanged("ruleCount");
+
+ if (transition) {
+ this._insertTransistionRule();
+ }
+ else {
+ this._notifyStyleApplied();
+ }
+ },
+
+ /**
+ * Insert a catch-all transition rule into the document. Set a timeout
+ * to remove the rule after a certain time.
+ */
+ _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 StyleEditor-generated transitions ended.
+ if (this._transitionRefCount == 0) {
+ this.rawSheet.insertRule(TRANSITION_RULE, this.rawSheet.cssRules.length);
+ this.document.documentElement.classList.add(TRANSITION_CLASS);
+ }
+
+ this._transitionRefCount++;
+
+ // Set up clean up and commit after transition duration (+10% buffer)
+ // @see _onTransitionEnd
+ this.window.setTimeout(this._onTransitionEnd.bind(this),
+ Math.floor(TRANSITION_DURATION_MS * 1.1));
+ },
+
+ /**
+ * This cleans up class and rule added for transition effect and then
+ * notifies that the style has been applied.
+ */
+ _onTransitionEnd: function ()
+ {
+ if (--this._transitionRefCount == 0) {
+ this.document.documentElement.classList.remove(TRANSITION_CLASS);
+ this.rawSheet.deleteRule(this.rawSheet.cssRules.length - 1);
+ }
+
+ events.emit(this, "style-applied");
+ }
+});
+
+exports.OldStyleSheetActor = OldStyleSheetActor;
+
+/**
+ * Creates a StyleEditorActor. StyleEditorActor provides remote access to the
+ * stylesheets of a document.
+ */
+var StyleEditorActor = exports.StyleEditorActor = protocol.ActorClassWithSpec(styleEditorSpec, {
+ /**
+ * The window we work with, taken from the parent actor.
+ */
+ get window() {
+ return this.parentActor.window;
+ },
+
+ /**
+ * The current content document of the window we work with.
+ */
+ get document() {
+ return 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 StyleEditorActor instance.
+ */
+ destroy: function ()
+ {
+ this._sheets.clear();
+ },
+
+ /**
+ * Called by client when target navigates to a new document.
+ * Adds load listeners to document.
+ */
+ newDocument: 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: 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;
+ }
+});
+
+XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+
+exports.StyleEditorActor = StyleEditorActor;
+
+/**
+ * Normalize multiple relative paths towards the base paths on the right.
+ */
+function normalize(...aURLs) {
+ let base = Services.io.newURI(aURLs.pop(), null, null);
+ let url;
+ while ((url = aURLs.pop())) {
+ base = Services.io.newURI(url, null, base);
+ }
+ return base.spec;
+}
+
+function dirname(aPath) {
+ return Services.io.newURI(
+ ".", null, Services.io.newURI(aPath, null, null)).spec;
+}