/* 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"; require("devtools/shared/fronts/stylesheets"); const { Front, FrontClassWithSpec, custom, preEvent } = require("devtools/shared/protocol"); const { pageStyleSpec, styleRuleSpec } = require("devtools/shared/specs/styles"); const promise = require("promise"); const { Task } = require("devtools/shared/task"); const { Class } = require("sdk/core/heritage"); const { RuleRewriter } = require("devtools/shared/css/parsing-utils"); /** * PageStyleFront, the front object for the PageStyleActor */ const PageStyleFront = FrontClassWithSpec(pageStyleSpec, { initialize: function (conn, form, ctx, detail) { Front.prototype.initialize.call(this, conn, form, ctx, detail); this.inspector = this.parent(); }, form: function (form, detail) { if (detail === "actorid") { this.actorID = form; return; } this._form = form; }, destroy: function () { Front.prototype.destroy.call(this); }, get walker() { return this.inspector.walker; }, get supportsAuthoredStyles() { return this._form.traits && this._form.traits.authoredStyles; }, getMatchedSelectors: custom(function (node, property, options) { return this._getMatchedSelectors(node, property, options).then(ret => { return ret.matched; }); }, { impl: "_getMatchedSelectors" }), getApplied: custom(Task.async(function* (node, options = {}) { // If the getApplied method doesn't recreate the style cache itself, this // means a call to cssLogic.highlight is required before trying to access // the applied rules. Issue a request to getLayout if this is the case. // See https://bugzilla.mozilla.org/show_bug.cgi?id=1103993#c16. if (!this._form.traits || !this._form.traits.getAppliedCreatesStyleCache) { yield this.getLayout(node); } let ret = yield this._getApplied(node, options); return ret.entries; }), { impl: "_getApplied" }), addNewRule: custom(function (node, pseudoClasses) { let addPromise; if (this.supportsAuthoredStyles) { addPromise = this._addNewRule(node, pseudoClasses, true); } else { addPromise = this._addNewRule(node, pseudoClasses); } return addPromise.then(ret => { return ret.entries[0]; }); }, { impl: "_addNewRule" }) }); exports.PageStyleFront = PageStyleFront; /** * StyleRuleFront, the front for the StyleRule actor. */ const StyleRuleFront = FrontClassWithSpec(styleRuleSpec, { initialize: function (client, form, ctx, detail) { Front.prototype.initialize.call(this, client, form, ctx, detail); }, destroy: function () { Front.prototype.destroy.call(this); }, form: function (form, detail) { if (detail === "actorid") { this.actorID = form; return; } this.actorID = form.actor; this._form = form; if (this._mediaText) { this._mediaText = null; } }, /** * Ensure _form is updated when location-changed is emitted. */ _locationChangedPre: preEvent("location-changed", function (line, column) { this._clearOriginalLocation(); this._form.line = line; this._form.column = column; }), /** * Return a new RuleModificationList or RuleRewriter for this node. * A RuleRewriter will be returned when the rule's canSetRuleText * trait is true; otherwise a RuleModificationList will be * returned. * * @param {CssPropertiesFront} cssProperties * This is needed by the RuleRewriter. * @return {RuleModificationList} */ startModifyingProperties: function (cssProperties) { if (this.canSetRuleText) { return new RuleRewriter(cssProperties.isKnown, this, this.authoredText); } return new RuleModificationList(this); }, get type() { return this._form.type; }, get line() { return this._form.line || -1; }, get column() { return this._form.column || -1; }, get cssText() { return this._form.cssText; }, get authoredText() { return this._form.authoredText || this._form.cssText; }, get declarations() { return this._form.declarations || []; }, get keyText() { return this._form.keyText; }, get name() { return this._form.name; }, get selectors() { return this._form.selectors; }, get media() { return this._form.media; }, get mediaText() { if (!this._form.media) { return null; } if (this._mediaText) { return this._mediaText; } this._mediaText = this.media.join(", "); return this._mediaText; }, get parentRule() { return this.conn.getActor(this._form.parentRule); }, get parentStyleSheet() { return this.conn.getActor(this._form.parentStyleSheet); }, get element() { return this.conn.getActor(this._form.element); }, get href() { if (this._form.href) { return this._form.href; } let sheet = this.parentStyleSheet; return sheet ? sheet.href : ""; }, get nodeHref() { let sheet = this.parentStyleSheet; return sheet ? sheet.nodeHref : ""; }, get supportsModifySelectorUnmatched() { return this._form.traits && this._form.traits.modifySelectorUnmatched; }, get canSetRuleText() { return this._form.traits && this._form.traits.canSetRuleText; }, get location() { return { source: this.parentStyleSheet, href: this.href, line: this.line, column: this.column }; }, _clearOriginalLocation: function () { this._originalLocation = null; }, getOriginalLocation: function () { if (this._originalLocation) { return promise.resolve(this._originalLocation); } let parentSheet = this.parentStyleSheet; if (!parentSheet) { // This rule doesn't belong to a stylesheet so it is an inline style. // Inline styles do not have any mediaText so we can return early. return promise.resolve(this.location); } return parentSheet.getOriginalLocation(this.line, this.column) .then(({ fromSourceMap, source, line, column }) => { let location = { href: source, line: line, column: column, mediaText: this.mediaText }; if (fromSourceMap === false) { location.source = this.parentStyleSheet; } if (!source) { location.href = this.href; } this._originalLocation = location; return location; }); }, modifySelector: custom(Task.async(function* (node, value) { let response; if (this.supportsModifySelectorUnmatched) { // If the debugee supports adding unmatched rules (post FF41) if (this.canSetRuleText) { response = yield this.modifySelector2(node, value, true); } else { response = yield this.modifySelector2(node, value); } } else { response = yield this._modifySelector(value); } if (response.ruleProps) { response.ruleProps = response.ruleProps.entries[0]; } return response; }), { impl: "_modifySelector" }), setRuleText: custom(function (newText) { this._form.authoredText = newText; return this._setRuleText(newText); }, { impl: "_setRuleText" }) }); exports.StyleRuleFront = StyleRuleFront; /** * Convenience API for building a list of attribute modifications * for the `modifyProperties` request. A RuleModificationList holds a * list of modifications that will be applied to a StyleRuleActor. * The modifications are processed in the order in which they are * added to the RuleModificationList. * * Objects of this type expose the same API as @see RuleRewriter. * This lets the inspector use (mostly) the same code, regardless of * whether the server implements setRuleText. */ var RuleModificationList = Class({ /** * Initialize a RuleModificationList. * @param {StyleRuleFront} rule the associated rule */ initialize: function (rule) { this.rule = rule; this.modifications = []; }, /** * Apply the modifications in this object to the associated rule. * * @return {Promise} A promise which will be resolved when the modifications * are complete; @see StyleRuleActor.modifyProperties. */ apply: function () { return this.rule.modifyProperties(this.modifications); }, /** * Add a "set" entry to the modification list. * * @param {Number} index index of the property in the rule. * This can be -1 in the case where * the rule does not support setRuleText; * generally for setting properties * on an element's style. * @param {String} name the property's name * @param {String} value the property's value * @param {String} priority the property's priority, either the empty * string or "important" */ setProperty: function (index, name, value, priority) { this.modifications.push({ type: "set", name: name, value: value, priority: priority }); }, /** * Add a "remove" entry to the modification list. * * @param {Number} index index of the property in the rule. * This can be -1 in the case where * the rule does not support setRuleText; * generally for setting properties * on an element's style. * @param {String} name the name of the property to remove */ removeProperty: function (index, name) { this.modifications.push({ type: "remove", name: name }); }, /** * Rename a property. This implementation acts like * |removeProperty|, because |setRuleText| is not available. * * @param {Number} index index of the property in the rule. * This can be -1 in the case where * the rule does not support setRuleText; * generally for setting properties * on an element's style. * @param {String} name current name of the property * * This parameter is also passed, but as it is not used in this * implementation, it is omitted. It is documented here as this * code also defined the interface implemented by @see RuleRewriter. * @param {String} newName new name of the property */ renameProperty: function (index, name) { this.removeProperty(index, name); }, /** * Enable or disable a property. This implementation acts like * |removeProperty| when disabling, or a no-op when enabling, * because |setRuleText| is not available. * * @param {Number} index index of the property in the rule. * This can be -1 in the case where * the rule does not support setRuleText; * generally for setting properties * on an element's style. * @param {String} name current name of the property * @param {Boolean} isEnabled true if the property should be enabled; * false if it should be disabled */ setPropertyEnabled: function (index, name, isEnabled) { if (!isEnabled) { this.removeProperty(index, name); } }, /** * Create a new property. This implementation does nothing, because * |setRuleText| is not available. * * These parameters are passed, but as they are not used in this * implementation, they are omitted. They are documented here as * this code also defined the interface implemented by @see * RuleRewriter. * * @param {Number} index index of the property in the rule. * This can be -1 in the case where * the rule does not support setRuleText; * generally for setting properties * on an element's style. * @param {String} name name of the new property * @param {String} value value of the new property * @param {String} priority priority of the new property; either * the empty string or "important" * @param {Boolean} enabled True if the new property should be * enabled, false if disabled */ createProperty: function () { // Nothing. }, });