summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/rules/views/rule-editor.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/inspector/rules/views/rule-editor.js842
1 files changed, 842 insertions, 0 deletions
diff --git a/devtools/client/inspector/rules/views/rule-editor.js b/devtools/client/inspector/rules/views/rule-editor.js
new file mode 100644
index 0000000000..35c4b41e37
--- /dev/null
+++ b/devtools/client/inspector/rules/views/rule-editor.js
@@ -0,0 +1,842 @@
+/* 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 { l10n } = require("resource://devtools/shared/inspector/css-logic.js");
+const {
+ PSEUDO_CLASSES,
+} = require("resource://devtools/shared/css/constants.js");
+const {
+ style: { ELEMENT_STYLE },
+} = require("resource://devtools/shared/constants.js");
+const Rule = require("resource://devtools/client/inspector/rules/models/rule.js");
+const {
+ InplaceEditor,
+ editableField,
+ editableItem,
+} = require("resource://devtools/client/shared/inplace-editor.js");
+const TextPropertyEditor = require("resource://devtools/client/inspector/rules/views/text-property-editor.js");
+const {
+ createChild,
+ blurOnMultipleProperties,
+ promiseWarn,
+} = require("resource://devtools/client/inspector/shared/utils.js");
+const {
+ parseNamedDeclarations,
+ parsePseudoClassesAndAttributes,
+ SELECTOR_ATTRIBUTE,
+ SELECTOR_ELEMENT,
+ SELECTOR_PSEUDO_CLASS,
+} = require("resource://devtools/shared/css/parsing-utils.js");
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+const CssLogic = require("resource://devtools/shared/inspector/css-logic.js");
+
+loader.lazyRequireGetter(
+ this,
+ "Tools",
+ "resource://devtools/client/definitions.js",
+ true
+);
+
+const STYLE_INSPECTOR_PROPERTIES =
+ "devtools/shared/locales/styleinspector.properties";
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
+
+/**
+ * RuleEditor is responsible for the following:
+ * Owns a Rule object and creates a list of TextPropertyEditors
+ * for its TextProperties.
+ * Manages creation of new text properties.
+ *
+ * @param {CssRuleView} ruleView
+ * The CssRuleView containg the document holding this rule editor.
+ * @param {Rule} rule
+ * The Rule object we're editing.
+ */
+function RuleEditor(ruleView, rule) {
+ EventEmitter.decorate(this);
+
+ this.ruleView = ruleView;
+ this.doc = this.ruleView.styleDocument;
+ this.toolbox = this.ruleView.inspector.toolbox;
+ this.telemetry = this.toolbox.telemetry;
+ this.rule = rule;
+
+ this.isEditable = !rule.isSystem;
+ // Flag that blocks updates of the selector and properties when it is
+ // being edited
+ this.isEditing = false;
+
+ this._onNewProperty = this._onNewProperty.bind(this);
+ this._newPropertyDestroy = this._newPropertyDestroy.bind(this);
+ this._onSelectorDone = this._onSelectorDone.bind(this);
+ this._locationChanged = this._locationChanged.bind(this);
+ this.updateSourceLink = this.updateSourceLink.bind(this);
+ this._onToolChanged = this._onToolChanged.bind(this);
+ this._updateLocation = this._updateLocation.bind(this);
+ this._onSourceClick = this._onSourceClick.bind(this);
+
+ this.rule.domRule.on("location-changed", this._locationChanged);
+ this.toolbox.on("tool-registered", this._onToolChanged);
+ this.toolbox.on("tool-unregistered", this._onToolChanged);
+
+ this._create();
+}
+
+RuleEditor.prototype = {
+ destroy() {
+ this.rule.domRule.off("location-changed");
+ this.toolbox.off("tool-registered", this._onToolChanged);
+ this.toolbox.off("tool-unregistered", this._onToolChanged);
+
+ if (this._unsubscribeSourceMap) {
+ this._unsubscribeSourceMap();
+ }
+ },
+
+ get sourceMapURLService() {
+ if (!this._sourceMapURLService) {
+ // sourceMapURLService is a lazy getter in the toolbox.
+ this._sourceMapURLService = this.toolbox.sourceMapURLService;
+ }
+
+ return this._sourceMapURLService;
+ },
+
+ get isSelectorEditable() {
+ const trait =
+ this.isEditable &&
+ this.rule.domRule.type !== ELEMENT_STYLE &&
+ this.rule.domRule.type !== CSSRule.KEYFRAME_RULE;
+
+ // Do not allow editing anonymousselectors until we can
+ // detect mutations on pseudo elements in Bug 1034110.
+ return trait && !this.rule.elementStyle.element.isAnonymous;
+ },
+
+ _create() {
+ this.element = this.doc.createElement("div");
+ this.element.className = "ruleview-rule devtools-monospace";
+ this.element.dataset.ruleId = this.rule.domRule.actorID;
+ this.element.setAttribute("uneditable", !this.isEditable);
+ this.element.setAttribute("unmatched", this.rule.isUnmatched);
+ this.element._ruleEditor = this;
+
+ // Give a relative position for the inplace editor's measurement
+ // span to be placed absolutely against.
+ this.element.style.position = "relative";
+
+ // Add the source link.
+ this.source = createChild(this.element, "div", {
+ class: "ruleview-rule-source theme-link",
+ });
+ this.source.addEventListener("click", this._onSourceClick);
+
+ const sourceLabel = this.doc.createElement("span");
+ sourceLabel.classList.add("ruleview-rule-source-label");
+ this.source.appendChild(sourceLabel);
+
+ this.updateSourceLink();
+
+ if (this.rule.domRule.ancestorData.length) {
+ const parts = this.rule.domRule.ancestorData.map(
+ (ancestorData, index) => {
+ if (ancestorData.type == "container") {
+ const container = this.doc.createElement("li");
+ container.classList.add("container-query");
+ container.setAttribute("data-ancestor-index", index);
+
+ createChild(container, "span", {
+ class: "container-query-declaration",
+ textContent: `@container${
+ ancestorData.containerName
+ ? " " + ancestorData.containerName
+ : ""
+ }`,
+ });
+
+ container.classList.add("has-tooltip");
+
+ const jumpToNodeButton = createChild(container, "button", {
+ class: "open-inspector",
+ title: l10n("rule.containerQuery.selectContainerButton.tooltip"),
+ });
+
+ let containerNodeFront;
+ const getNodeFront = async () => {
+ if (!containerNodeFront) {
+ const res = await this.rule.domRule.getQueryContainerForNode(
+ index,
+ this.rule.inherited ||
+ this.ruleView.inspector.selection.nodeFront
+ );
+ containerNodeFront = res.node;
+ }
+ return containerNodeFront;
+ };
+
+ jumpToNodeButton.addEventListener("click", async () => {
+ const front = await getNodeFront();
+ if (!front) {
+ return;
+ }
+ this.ruleView.inspector.selection.setNodeFront(front);
+ await this.ruleView.inspector.highlighters.hideHighlighterType(
+ this.ruleView.inspector.highlighters.TYPES.BOXMODEL
+ );
+ });
+ container.append(jumpToNodeButton);
+
+ container.addEventListener("mouseenter", async () => {
+ const front = await getNodeFront();
+ if (!front) {
+ return;
+ }
+
+ await this.ruleView.inspector.highlighters.showHighlighterTypeForNode(
+ this.ruleView.inspector.highlighters.TYPES.BOXMODEL,
+ front
+ );
+ });
+ container.addEventListener("mouseleave", async () => {
+ await this.ruleView.inspector.highlighters.hideHighlighterType(
+ this.ruleView.inspector.highlighters.TYPES.BOXMODEL
+ );
+ });
+
+ createChild(container, "span", {
+ // Add a space between the container name (or @container if there's no name)
+ // and the query so the title, which is computed from the DOM, displays correctly.
+ textContent: " " + ancestorData.containerQuery,
+ });
+ return container;
+ }
+ if (ancestorData.type == "layer") {
+ return `@layer${
+ ancestorData.value ? " " + ancestorData.value : ""
+ }`;
+ }
+ if (ancestorData.type == "media") {
+ return `@media ${ancestorData.value}`;
+ }
+
+ if (ancestorData.type == "supports") {
+ return `@supports ${ancestorData.conditionText}`;
+ }
+
+ if (ancestorData.type == "import") {
+ return `@import ${ancestorData.value}`;
+ }
+
+ // We shouldn't get here as `type` should only match to what can be set in
+ // the StyleRuleActor form, but just in case, let's return an empty string.
+ console.warn("Unknown ancestor data type:", ancestorData.type);
+ return ``;
+ }
+ );
+
+ this.ancestorDataEl = createChild(this.element, "ul", {
+ class: "ruleview-rule-ancestor-data theme-link",
+ });
+
+ for (const part of parts) {
+ if (typeof part == "string") {
+ createChild(this.ancestorDataEl, "li", {
+ textContent: part,
+ });
+ } else {
+ this.ancestorDataEl.append(part);
+ }
+ }
+ }
+
+ const code = createChild(this.element, "div", {
+ class: "ruleview-code",
+ });
+
+ const header = createChild(code, "div", {});
+
+ this.selectorText = createChild(header, "span", {
+ class: "ruleview-selectorcontainer",
+ tabindex: this.isSelectorEditable ? "0" : "-1",
+ });
+
+ if (this.isSelectorEditable) {
+ this.selectorText.addEventListener("click", event => {
+ // Clicks within the selector shouldn't propagate any further.
+ event.stopPropagation();
+ });
+
+ editableField({
+ element: this.selectorText,
+ done: this._onSelectorDone,
+ cssProperties: this.rule.cssProperties,
+ });
+ }
+
+ if (this.rule.domRule.type !== CSSRule.KEYFRAME_RULE) {
+ let selector = "";
+ if (this.rule.domRule.selectors) {
+ // This is a "normal" rule with a selector.
+ selector = this.rule.domRule.selectors.join(", ");
+ // Otherwise, the rule is either inherited or inline, and selectors will
+ // be computed on demand when the highlighter is requested.
+ }
+
+ const isHighlighted = this.ruleView.isSelectorHighlighted(selector);
+ // Handling of click events is delegated to CssRuleView.handleEvent()
+ createChild(header, "span", {
+ class:
+ "ruleview-selectorhighlighter js-toggle-selector-highlighter" +
+ (isHighlighted ? " highlighted" : ""),
+ "data-selector": selector,
+ title: l10n("rule.selectorHighlighter.tooltip"),
+ });
+ }
+
+ this.openBrace = createChild(header, "span", {
+ class: "ruleview-ruleopen",
+ textContent: " {",
+ });
+
+ this.propertyList = createChild(code, "ul", {
+ class: "ruleview-propertylist",
+ });
+
+ this.populate();
+
+ this.closeBrace = createChild(code, "div", {
+ class: "ruleview-ruleclose",
+ tabindex: this.isEditable ? "0" : "-1",
+ textContent: "}",
+ });
+
+ if (this.isEditable) {
+ // A newProperty editor should only be created when no editor was
+ // previously displayed. Since the editors are cleared on blur,
+ // check this.ruleview.isEditing on mousedown
+ this._ruleViewIsEditing = false;
+
+ code.addEventListener("mousedown", () => {
+ this._ruleViewIsEditing = this.ruleView.isEditing;
+ });
+
+ code.addEventListener("click", event => {
+ const selection = this.doc.defaultView.getSelection();
+ if (selection.isCollapsed && !this._ruleViewIsEditing) {
+ this.newProperty();
+ }
+ // Cleanup the _ruleViewIsEditing flag
+ this._ruleViewIsEditing = false;
+ });
+
+ this.element.addEventListener("mousedown", () => {
+ this.doc.defaultView.focus();
+ });
+
+ // Create a property editor when the close brace is clicked.
+ editableItem({ element: this.closeBrace }, () => {
+ this.newProperty();
+ });
+ }
+ },
+
+ /**
+ * Called when a tool is registered or unregistered.
+ */
+ _onToolChanged() {
+ // When the source editor is registered, update the source links
+ // to be clickable; and if it is unregistered, update the links to
+ // be unclickable. However, some links are never clickable, so
+ // filter those out first.
+ if (this.source.getAttribute("unselectable") === "permanent") {
+ // Nothing.
+ } else if (this.toolbox.isToolRegistered("styleeditor")) {
+ this.source.removeAttribute("unselectable");
+ } else {
+ this.source.setAttribute("unselectable", "true");
+ }
+ },
+
+ /**
+ * Event handler called when a property changes on the
+ * StyleRuleActor.
+ */
+ _locationChanged() {
+ this.updateSourceLink();
+ },
+
+ _onSourceClick() {
+ if (this.source.hasAttribute("unselectable")) {
+ return;
+ }
+
+ const { inspector } = this.ruleView;
+ if (Tools.styleEditor.isToolSupported(inspector.toolbox)) {
+ inspector.toolbox.viewSourceInStyleEditorByResource(
+ this.rule.sheet,
+ this.rule.ruleLine,
+ this.rule.ruleColumn
+ );
+ }
+ },
+
+ /**
+ * Update the text of the source link to reflect whether we're showing
+ * original sources or not. This is a callback for
+ * SourceMapURLService.subscribeByID, which see.
+ *
+ * @param {Object | null} originalLocation
+ * The original position object (url/line/column) or null.
+ */
+ _updateLocation(originalLocation) {
+ let displayURL = this.rule.sheet?.href;
+ const constructed = this.rule.sheet?.constructed;
+ let line = this.rule.ruleLine;
+ if (originalLocation) {
+ displayURL = originalLocation.url;
+ line = originalLocation.line;
+ }
+
+ let sourceTextContent = CssLogic.shortSource({
+ constructed,
+ href: displayURL,
+ });
+ let title = displayURL ? displayURL : sourceTextContent;
+ if (line > 0) {
+ sourceTextContent += ":" + line;
+ title += ":" + line;
+ }
+
+ const sourceLabel = this.element.querySelector(
+ ".ruleview-rule-source-label"
+ );
+ sourceLabel.setAttribute("title", title);
+ sourceLabel.textContent = sourceTextContent;
+ },
+
+ updateSourceLink() {
+ if (this.rule.isSystem) {
+ const sourceLabel = this.element.querySelector(
+ ".ruleview-rule-source-label"
+ );
+ const title = this.rule.title;
+ const sourceHref = this.rule.sheet?.href || title;
+
+ const uaLabel = STYLE_INSPECTOR_L10N.getStr("rule.userAgentStyles");
+ sourceLabel.textContent = uaLabel + " " + title;
+
+ // Special case about:PreferenceStyleSheet, as it is generated on the
+ // fly and the URI is not registered with the about: handler.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37
+ if (sourceHref === "about:PreferenceStyleSheet") {
+ this.source.setAttribute("unselectable", "permanent");
+ sourceLabel.textContent = uaLabel;
+ sourceLabel.removeAttribute("title");
+ }
+ } else {
+ this._updateLocation(null);
+ }
+
+ if (
+ this.rule.sheet &&
+ !this.rule.isSystem &&
+ this.rule.domRule.type !== ELEMENT_STYLE
+ ) {
+ // Only get the original source link if the rule isn't a system
+ // rule and if it isn't an inline rule.
+ if (this._unsubscribeSourceMap) {
+ this._unsubscribeSourceMap();
+ }
+ this._unsubscribeSourceMap = this.sourceMapURLService.subscribeByID(
+ this.rule.sheet.resourceId,
+ this.rule.ruleLine,
+ this.rule.ruleColumn,
+ this._updateLocation
+ );
+ // Set "unselectable" appropriately.
+ this._onToolChanged();
+ } else if (this.rule.domRule.type === ELEMENT_STYLE) {
+ this.source.setAttribute("unselectable", "permanent");
+ } else {
+ // Set "unselectable" appropriately.
+ this._onToolChanged();
+ }
+
+ Promise.resolve().then(() => {
+ this.emit("source-link-updated");
+ });
+ },
+
+ /**
+ * Update the rule editor with the contents of the rule.
+ *
+ * @param {Boolean} reset
+ * True to completely reset the rule editor before populating.
+ */
+ populate(reset) {
+ // Clear out existing viewers.
+ while (this.selectorText.hasChildNodes()) {
+ this.selectorText.removeChild(this.selectorText.lastChild);
+ }
+
+ // If selector text comes from a css rule, highlight selectors that
+ // actually match. For custom selector text (such as for the 'element'
+ // style, just show the text directly.
+ if (this.rule.domRule.type === ELEMENT_STYLE) {
+ this.selectorText.textContent = this.rule.selectorText;
+ } else if (this.rule.domRule.type === CSSRule.KEYFRAME_RULE) {
+ this.selectorText.textContent = this.rule.domRule.keyText;
+ } else {
+ this.rule.domRule.selectors.forEach((selector, i) => {
+ if (i !== 0) {
+ createChild(this.selectorText, "span", {
+ class: "ruleview-selector-separator",
+ textContent: ", ",
+ });
+ }
+
+ const containerClass =
+ this.rule.matchedSelectors.indexOf(selector) > -1
+ ? "ruleview-selector-matched"
+ : "ruleview-selector-unmatched";
+ const selectorContainer = createChild(this.selectorText, "span", {
+ class: containerClass,
+ });
+
+ const parsedSelector = parsePseudoClassesAndAttributes(selector);
+
+ for (const selectorText of parsedSelector) {
+ let selectorClass = "";
+
+ switch (selectorText.type) {
+ case SELECTOR_ATTRIBUTE:
+ selectorClass = "ruleview-selector-attribute";
+ break;
+ case SELECTOR_ELEMENT:
+ selectorClass = "ruleview-selector";
+ break;
+ case SELECTOR_PSEUDO_CLASS:
+ selectorClass = PSEUDO_CLASSES.some(
+ pseudo => selectorText.value === pseudo
+ )
+ ? "ruleview-selector-pseudo-class-lock"
+ : "ruleview-selector-pseudo-class";
+ break;
+ default:
+ break;
+ }
+
+ createChild(selectorContainer, "span", {
+ textContent: selectorText.value,
+ class: selectorClass,
+ });
+ }
+ });
+ }
+
+ if (reset) {
+ while (this.propertyList.hasChildNodes()) {
+ this.propertyList.removeChild(this.propertyList.lastChild);
+ }
+ }
+
+ for (const prop of this.rule.textProps) {
+ if (!prop.editor && !prop.invisible) {
+ const editor = new TextPropertyEditor(this, prop);
+ this.propertyList.appendChild(editor.element);
+ } else if (prop.editor) {
+ // If an editor already existed, append it to the bottom now to make sure the
+ // order of editors in the DOM follow the order of the rule's properties.
+ this.propertyList.appendChild(prop.editor.element);
+ }
+ }
+ },
+
+ /**
+ * Programatically add a new property to the rule.
+ *
+ * @param {String} name
+ * Property name.
+ * @param {String} value
+ * Property value.
+ * @param {String} priority
+ * Property priority.
+ * @param {Boolean} enabled
+ * True if the property should be enabled.
+ * @param {TextProperty} siblingProp
+ * Optional, property next to which the new property will be added.
+ * @return {TextProperty}
+ * The new property
+ */
+ addProperty(name, value, priority, enabled, siblingProp) {
+ const prop = this.rule.createProperty(
+ name,
+ value,
+ priority,
+ enabled,
+ siblingProp
+ );
+ const index = this.rule.textProps.indexOf(prop);
+ const editor = new TextPropertyEditor(this, prop);
+
+ // Insert this node before the DOM node that is currently at its new index
+ // in the property list. There is currently one less node in the DOM than
+ // in the property list, so this causes it to appear after siblingProp.
+ // If there is no node at its index, as is the case where this is the last
+ // node being inserted, then this behaves as appendChild.
+ this.propertyList.insertBefore(
+ editor.element,
+ this.propertyList.children[index]
+ );
+
+ return prop;
+ },
+
+ /**
+ * Programatically add a list of new properties to the rule. Focus the UI
+ * to the proper location after adding (either focus the value on the
+ * last property if it is empty, or create a new property and focus it).
+ *
+ * @param {Array} properties
+ * Array of properties, which are objects with this signature:
+ * {
+ * name: {string},
+ * value: {string},
+ * priority: {string}
+ * }
+ * @param {TextProperty} siblingProp
+ * Optional, the property next to which all new props should be added.
+ */
+ addProperties(properties, siblingProp) {
+ if (!properties || !properties.length) {
+ return;
+ }
+
+ let lastProp = siblingProp;
+ for (const p of properties) {
+ const isCommented = Boolean(p.commentOffsets);
+ const enabled = !isCommented;
+ lastProp = this.addProperty(
+ p.name,
+ p.value,
+ p.priority,
+ enabled,
+ lastProp
+ );
+ }
+
+ // Either focus on the last value if incomplete, or start a new one.
+ if (lastProp && lastProp.value.trim() === "") {
+ lastProp.editor.valueSpan.click();
+ } else {
+ this.newProperty();
+ }
+ },
+
+ /**
+ * Create a text input for a property name. If a non-empty property
+ * name is given, we'll create a real TextProperty and add it to the
+ * rule.
+ */
+ newProperty() {
+ // If we're already creating a new property, ignore this.
+ if (!this.closeBrace.hasAttribute("tabindex")) {
+ return;
+ }
+
+ // While we're editing a new property, it doesn't make sense to
+ // start a second new property editor, so disable focusing the
+ // close brace for now.
+ this.closeBrace.removeAttribute("tabindex");
+
+ this.newPropItem = createChild(this.propertyList, "li", {
+ class: "ruleview-property ruleview-newproperty",
+ });
+
+ this.newPropSpan = createChild(this.newPropItem, "span", {
+ class: "ruleview-propertyname",
+ tabindex: "0",
+ });
+
+ this.multipleAddedProperties = null;
+
+ this.editor = new InplaceEditor({
+ element: this.newPropSpan,
+ done: this._onNewProperty,
+ destroy: this._newPropertyDestroy,
+ advanceChars: ":",
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
+ popup: this.ruleView.popup,
+ cssProperties: this.rule.cssProperties,
+ });
+
+ // Auto-close the input if multiple rules get pasted into new property.
+ this.editor.input.addEventListener(
+ "paste",
+ blurOnMultipleProperties(this.rule.cssProperties)
+ );
+ },
+
+ /**
+ * Called when the new property input has been dismissed.
+ *
+ * @param {String} value
+ * The value in the editor.
+ * @param {Boolean} commit
+ * True if the value should be committed.
+ */
+ _onNewProperty(value, commit) {
+ if (!value || !commit) {
+ return;
+ }
+
+ // parseDeclarations allows for name-less declarations, but in the present
+ // case, we're creating a new declaration, it doesn't make sense to accept
+ // these entries
+ this.multipleAddedProperties = parseNamedDeclarations(
+ this.rule.cssProperties.isKnown,
+ value,
+ true
+ );
+
+ // Blur the editor field now and deal with adding declarations later when
+ // the field gets destroyed (see _newPropertyDestroy)
+ this.editor.input.blur();
+
+ this.telemetry.recordEvent("edit_rule", "ruleview");
+ },
+
+ /**
+ * Called when the new property editor is destroyed.
+ * This is where the properties (type TextProperty) are actually being
+ * added, since we want to wait until after the inplace editor `destroy`
+ * event has been fired to keep consistent UI state.
+ */
+ _newPropertyDestroy() {
+ // We're done, make the close brace focusable again.
+ this.closeBrace.setAttribute("tabindex", "0");
+
+ this.propertyList.removeChild(this.newPropItem);
+ delete this.newPropItem;
+ delete this.newPropSpan;
+
+ // If properties were added, we want to focus the proper element.
+ // If the last new property has no value, focus the value on it.
+ // Otherwise, start a new property and focus that field.
+ if (this.multipleAddedProperties && this.multipleAddedProperties.length) {
+ this.addProperties(this.multipleAddedProperties);
+ }
+ },
+
+ /**
+ * Called when the selector's inplace editor is closed.
+ * Ignores the change if the user pressed escape, otherwise
+ * commits it.
+ *
+ * @param {String} value
+ * The value contained in the editor.
+ * @param {Boolean} commit
+ * True if the change should be applied.
+ * @param {Number} direction
+ * The move focus direction number.
+ */
+ async _onSelectorDone(value, commit, direction) {
+ if (
+ !commit ||
+ this.isEditing ||
+ value === "" ||
+ value === this.rule.selectorText
+ ) {
+ return;
+ }
+
+ const ruleView = this.ruleView;
+ const elementStyle = ruleView._elementStyle;
+ const element = elementStyle.element;
+
+ this.isEditing = true;
+
+ // Remove highlighter for the previous selector.
+ if (this.ruleView.isSelectorHighlighted(this.rule.selectorText)) {
+ await this.ruleView.toggleSelectorHighlighter(this.rule.selectorText);
+ }
+
+ try {
+ const response = await this.rule.domRule.modifySelector(element, value);
+
+ // We recompute the list of applied styles, because editing a
+ // selector might cause this rule's position to change.
+ const applied = await elementStyle.pageStyle.getApplied(element, {
+ inherited: true,
+ matchedSelectors: true,
+ filter: elementStyle.showUserAgentStyles ? "ua" : undefined,
+ });
+
+ this.isEditing = false;
+
+ const { ruleProps, isMatching } = response;
+ if (!ruleProps) {
+ // Notify for changes, even when nothing changes,
+ // just to allow tests being able to track end of this request.
+ ruleView.emit("ruleview-invalid-selector");
+ return;
+ }
+
+ ruleProps.isUnmatched = !isMatching;
+ const newRule = new Rule(elementStyle, ruleProps);
+ const editor = new RuleEditor(ruleView, newRule);
+ const rules = elementStyle.rules;
+
+ let newRuleIndex = applied.findIndex(r => r.rule == ruleProps.rule);
+ const oldIndex = rules.indexOf(this.rule);
+
+ // If the selector no longer matches, then we leave the rule in
+ // the same relative position.
+ if (newRuleIndex === -1) {
+ newRuleIndex = oldIndex;
+ }
+
+ // Remove the old rule and insert the new rule.
+ rules.splice(oldIndex, 1);
+ rules.splice(newRuleIndex, 0, newRule);
+ elementStyle._changed();
+ elementStyle.onRuleUpdated();
+
+ // We install the new editor in place of the old -- you might
+ // think we would replicate the list-modification logic above,
+ // but that is complicated due to the way the UI installs
+ // pseudo-element rules and the like.
+ this.element.parentNode.replaceChild(editor.element, this.element);
+
+ editor._moveSelectorFocus(direction);
+ } catch (err) {
+ this.isEditing = false;
+ promiseWarn(err);
+ }
+ },
+
+ /**
+ * Handle moving the focus change after a tab or return keypress in the
+ * selector inplace editor.
+ *
+ * @param {Number} direction
+ * The move focus direction number.
+ */
+ _moveSelectorFocus(direction) {
+ if (!direction || direction === Services.focus.MOVEFOCUS_BACKWARD) {
+ return;
+ }
+
+ if (this.rule.textProps.length) {
+ this.rule.textProps[0].editor.nameSpan.click();
+ } else {
+ this.propertyList.click();
+ }
+ },
+};
+
+module.exports = RuleEditor;