summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/rules/views
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /devtools/client/inspector/rules/views
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/inspector/rules/views')
-rw-r--r--devtools/client/inspector/rules/views/moz.build8
-rw-r--r--devtools/client/inspector/rules/views/rule-editor.js620
-rw-r--r--devtools/client/inspector/rules/views/text-property-editor.js880
3 files changed, 1508 insertions, 0 deletions
diff --git a/devtools/client/inspector/rules/views/moz.build b/devtools/client/inspector/rules/views/moz.build
new file mode 100644
index 000000000..ac0a24d76
--- /dev/null
+++ b/devtools/client/inspector/rules/views/moz.build
@@ -0,0 +1,8 @@
+# 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/.
+
+DevToolsModules(
+ 'rule-editor.js',
+ 'text-property-editor.js',
+)
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 000000000..2587bf19c
--- /dev/null
+++ b/devtools/client/inspector/rules/views/rule-editor.js
@@ -0,0 +1,620 @@
+/* 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("devtools/shared/inspector/css-logic");
+const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
+const {PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils");
+const {Rule} = require("devtools/client/inspector/rules/models/rule");
+const {InplaceEditor, editableField, editableItem} =
+ require("devtools/client/shared/inplace-editor");
+const {TextPropertyEditor} =
+ require("devtools/client/inspector/rules/views/text-property-editor");
+const {
+ createChild,
+ blurOnMultipleProperties,
+ promiseWarn
+} = require("devtools/client/inspector/shared/utils");
+const {
+ parseDeclarations,
+ parsePseudoClassesAndAttributes,
+ SELECTOR_ATTRIBUTE,
+ SELECTOR_ELEMENT,
+ SELECTOR_PSEUDO_CLASS
+} = require("devtools/shared/css/parsing-utils");
+const promise = require("promise");
+const Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {Task} = require("devtools/shared/task");
+
+const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties";
+const {LocalizationHelper} = require("devtools/shared/l10n");
+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.
+ *
+ * One step of a RuleEditor's instantiation is figuring out what's the original
+ * source link to the parent stylesheet (in case of source maps). This step is
+ * asynchronous and is triggered as soon as the RuleEditor is instantiated (see
+ * updateSourceLink). If you need to know when the RuleEditor is done with this,
+ * you need to listen to the source-link-updated event.
+ *
+ * @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.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.rule.domRule.on("location-changed", this._locationChanged);
+ this.toolbox.on("tool-registered", this.updateSourceLink);
+ this.toolbox.on("tool-unregistered", this.updateSourceLink);
+
+ this._create();
+}
+
+RuleEditor.prototype = {
+ destroy: function () {
+ this.rule.domRule.off("location-changed");
+ this.toolbox.off("tool-registered", this.updateSourceLink);
+ this.toolbox.off("tool-unregistered", this.updateSourceLink);
+ },
+
+ get isSelectorEditable() {
+ let trait = this.isEditable &&
+ this.ruleView.inspector.target.client.traits.selectorEditable &&
+ 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: function () {
+ this.element = this.doc.createElement("div");
+ this.element.className = "ruleview-rule theme-separator";
+ 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", function () {
+ if (this.source.hasAttribute("unselectable")) {
+ return;
+ }
+ let rule = this.rule.domRule;
+ this.ruleView.emit("ruleview-linked-clicked", rule);
+ }.bind(this));
+ let sourceLabel = this.doc.createElement("span");
+ sourceLabel.classList.add("ruleview-rule-source-label");
+ this.source.appendChild(sourceLabel);
+
+ this.updateSourceLink();
+
+ let code = createChild(this.element, "div", {
+ class: "ruleview-code"
+ });
+
+ let header = createChild(code, "div", {});
+
+ this.selectorText = createChild(header, "span", {
+ class: "ruleview-selectorcontainer theme-fg-color3",
+ tabindex: this.isSelectorEditable ? "0" : "-1",
+ });
+
+ if (this.isSelectorEditable) {
+ this.selectorText.addEventListener("click", event => {
+ // Clicks within the selector shouldn't propagate any further.
+ event.stopPropagation();
+ }, false);
+
+ editableField({
+ element: this.selectorText,
+ done: this._onSelectorDone,
+ cssProperties: this.rule.cssProperties,
+ contextMenu: this.ruleView.inspector.onTextBoxContextMenu
+ });
+ }
+
+ if (this.rule.domRule.type !== CSSRule.KEYFRAME_RULE) {
+ let selector = this.rule.domRule.selectors
+ ? this.rule.domRule.selectors.join(", ")
+ : this.ruleView.inspector.selectionCssSelector;
+
+ let selectorHighlighter = createChild(header, "span", {
+ class: "ruleview-selectorhighlighter" +
+ (this.ruleView.highlighters.selectorHighlighterShown === selector ?
+ " highlighted" : ""),
+ title: l10n("rule.selectorHighlighter.tooltip")
+ });
+ selectorHighlighter.addEventListener("click", () => {
+ this.ruleView.toggleSelectorHighlighter(selectorHighlighter, selector);
+ });
+ }
+
+ 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", () => {
+ let selection = this.doc.defaultView.getSelection();
+ if (selection.isCollapsed && !this._ruleViewIsEditing) {
+ this.newProperty();
+ }
+ // Cleanup the _ruleViewIsEditing flag
+ this._ruleViewIsEditing = false;
+ }, false);
+
+ this.element.addEventListener("mousedown", () => {
+ this.doc.defaultView.focus();
+ }, false);
+
+ // Create a property editor when the close brace is clicked.
+ editableItem({ element: this.closeBrace }, () => {
+ this.newProperty();
+ });
+ }
+ },
+
+ /**
+ * Event handler called when a property changes on the
+ * StyleRuleActor.
+ */
+ _locationChanged: function () {
+ this.updateSourceLink();
+ },
+
+ updateSourceLink: function () {
+ let sourceLabel = this.element.querySelector(".ruleview-rule-source-label");
+ let title = this.rule.title;
+ let sourceHref = (this.rule.sheet && this.rule.sheet.href) ?
+ this.rule.sheet.href : title;
+ let sourceLine = this.rule.ruleLine > 0 ? ":" + this.rule.ruleLine : "";
+
+ sourceLabel.setAttribute("title", sourceHref + sourceLine);
+
+ if (this.toolbox.isToolRegistered("styleeditor")) {
+ this.source.removeAttribute("unselectable");
+ } else {
+ this.source.setAttribute("unselectable", true);
+ }
+
+ if (this.rule.isSystem) {
+ let 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", "true");
+ sourceLabel.textContent = uaLabel;
+ sourceLabel.removeAttribute("title");
+ }
+ } else {
+ sourceLabel.textContent = title;
+ if (this.rule.ruleLine === -1 && this.rule.domRule.parentStyleSheet) {
+ this.source.setAttribute("unselectable", "true");
+ }
+ }
+
+ let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
+ if (showOrig && !this.rule.isSystem &&
+ this.rule.domRule.type !== ELEMENT_STYLE) {
+ // Only get the original source link if the right pref is set, if the rule
+ // isn't a system rule and if it isn't an inline rule.
+ this.rule.getOriginalSourceStrings().then((strings) => {
+ sourceLabel.textContent = strings.short;
+ sourceLabel.setAttribute("title", strings.full);
+ }, e => console.error(e)).then(() => {
+ this.emit("source-link-updated");
+ });
+ } else {
+ // If we're not getting the original source link, then we can emit the
+ // event immediately (but still asynchronously to give consumers a chance
+ // to register it after having instantiated the RuleEditor).
+ promise.resolve().then(() => {
+ this.emit("source-link-updated");
+ });
+ }
+ },
+
+ /**
+ * Update the rule editor with the contents of the rule.
+ */
+ populate: function () {
+ // 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: ", "
+ });
+ }
+
+ let containerClass =
+ (this.rule.matchedSelectors.indexOf(selector) > -1) ?
+ "ruleview-selector-matched" : "ruleview-selector-unmatched";
+ let selectorContainer = createChild(this.selectorText, "span", {
+ class: containerClass
+ });
+
+ let parsedSelector = parsePseudoClassesAndAttributes(selector);
+
+ for (let 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 = [":active", ":focus", ":hover"].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
+ });
+ }
+ });
+ }
+
+ for (let prop of this.rule.textProps) {
+ if (!prop.editor && !prop.invisible) {
+ let editor = new TextPropertyEditor(this, prop);
+ this.propertyList.appendChild(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: function (name, value, priority, enabled, siblingProp) {
+ let prop = this.rule.createProperty(name, value, priority, enabled,
+ siblingProp);
+ let index = this.rule.textProps.indexOf(prop);
+ let 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: function (properties, siblingProp) {
+ if (!properties || !properties.length) {
+ return;
+ }
+
+ let lastProp = siblingProp;
+ for (let p of properties) {
+ let isCommented = Boolean(p.commentOffsets);
+ let 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: function () {
+ // 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,
+ contextMenu: this.ruleView.inspector.onTextBoxContextMenu
+ });
+
+ // Auto-close the input if multiple rules get pasted into new property.
+ this.editor.input.addEventListener("paste",
+ blurOnMultipleProperties(this.rule.cssProperties), false);
+ },
+
+ /**
+ * 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: function (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 =
+ parseDeclarations(this.rule.cssProperties.isKnown, value, true)
+ .filter(d => d.name);
+
+ // Blur the editor field now and deal with adding declarations later when
+ // the field gets destroyed (see _newPropertyDestroy)
+ this.editor.input.blur();
+ },
+
+ /**
+ * 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: function () {
+ // 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.
+ */
+ _onSelectorDone: Task.async(function* (value, commit, direction) {
+ if (!commit || this.isEditing || value === "" ||
+ value === this.rule.selectorText) {
+ return;
+ }
+
+ let ruleView = this.ruleView;
+ let elementStyle = ruleView._elementStyle;
+ let element = elementStyle.element;
+ let supportsUnmatchedRules =
+ this.rule.domRule.supportsModifySelectorUnmatched;
+
+ this.isEditing = true;
+
+ try {
+ let response = yield this.rule.domRule.modifySelector(element, value);
+
+ if (!supportsUnmatchedRules) {
+ this.isEditing = false;
+
+ if (response) {
+ this.ruleView.refreshPanel();
+ }
+ return;
+ }
+
+ // We recompute the list of applied styles, because editing a
+ // selector might cause this rule's position to change.
+ let applied = yield elementStyle.pageStyle.getApplied(element, {
+ inherited: true,
+ matchedSelectors: true,
+ filter: elementStyle.showUserAgentStyles ? "ua" : undefined
+ });
+
+ this.isEditing = false;
+
+ let {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;
+ let newRule = new Rule(elementStyle, ruleProps);
+ let editor = new RuleEditor(ruleView, newRule);
+ let rules = elementStyle.rules;
+
+ let newRuleIndex = applied.findIndex((r) => r.rule == ruleProps.rule);
+ let 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.markOverriddenAll();
+
+ // 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);
+
+ // Remove highlight for modified selector
+ if (ruleView.highlighters.selectorHighlighterShown) {
+ ruleView.toggleSelectorHighlighter(ruleView.lastSelectorIcon,
+ ruleView.highlighters.selectorHighlighterShown);
+ }
+
+ 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: function (direction) {
+ if (!direction || direction === Services.focus.MOVEFOCUS_BACKWARD) {
+ return;
+ }
+
+ if (this.rule.textProps.length > 0) {
+ this.rule.textProps[0].editor.nameSpan.click();
+ } else {
+ this.propertyList.click();
+ }
+ }
+};
+
+exports.RuleEditor = RuleEditor;
diff --git a/devtools/client/inspector/rules/views/text-property-editor.js b/devtools/client/inspector/rules/views/text-property-editor.js
new file mode 100644
index 000000000..d3015f931
--- /dev/null
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -0,0 +1,880 @@
+/* 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("devtools/shared/inspector/css-logic");
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+const {InplaceEditor, editableField} =
+ require("devtools/client/shared/inplace-editor");
+const {
+ createChild,
+ appendText,
+ advanceValidate,
+ blurOnMultipleProperties
+} = require("devtools/client/inspector/shared/utils");
+const {
+ parseDeclarations,
+ parseSingleValue,
+} = require("devtools/shared/css/parsing-utils");
+const Services = require("Services");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const SHARED_SWATCH_CLASS = "ruleview-swatch";
+const COLOR_SWATCH_CLASS = "ruleview-colorswatch";
+const BEZIER_SWATCH_CLASS = "ruleview-bezierswatch";
+const FILTER_SWATCH_CLASS = "ruleview-filterswatch";
+const ANGLE_SWATCH_CLASS = "ruleview-angleswatch";
+
+/*
+ * An actionable element is an element which on click triggers a specific action
+ * (e.g. shows a color tooltip, opens a link, …).
+ */
+const ACTIONABLE_ELEMENTS_SELECTORS = [
+ `.${COLOR_SWATCH_CLASS}`,
+ `.${BEZIER_SWATCH_CLASS}`,
+ `.${FILTER_SWATCH_CLASS}`,
+ `.${ANGLE_SWATCH_CLASS}`,
+ "a"
+];
+
+/**
+ * TextPropertyEditor is responsible for the following:
+ * Owns a TextProperty object.
+ * Manages changes to the TextProperty.
+ * Can be expanded to display computed properties.
+ * Can mark a property disabled or enabled.
+ *
+ * @param {RuleEditor} ruleEditor
+ * The rule editor that owns this TextPropertyEditor.
+ * @param {TextProperty} property
+ * The text property to edit.
+ */
+function TextPropertyEditor(ruleEditor, property) {
+ this.ruleEditor = ruleEditor;
+ this.ruleView = this.ruleEditor.ruleView;
+ this.doc = this.ruleEditor.doc;
+ this.popup = this.ruleView.popup;
+ this.prop = property;
+ this.prop.editor = this;
+ this.browserWindow = this.doc.defaultView.top;
+ this._populatedComputed = false;
+ this._hasPendingClick = false;
+ this._clickedElementOptions = null;
+
+ const toolbox = this.ruleView.inspector.toolbox;
+ this.cssProperties = getCssProperties(toolbox);
+
+ this._onEnableClicked = this._onEnableClicked.bind(this);
+ this._onExpandClicked = this._onExpandClicked.bind(this);
+ this._onStartEditing = this._onStartEditing.bind(this);
+ this._onNameDone = this._onNameDone.bind(this);
+ this._onValueDone = this._onValueDone.bind(this);
+ this._onSwatchCommit = this._onSwatchCommit.bind(this);
+ this._onSwatchPreview = this._onSwatchPreview.bind(this);
+ this._onSwatchRevert = this._onSwatchRevert.bind(this);
+ this._onValidate = this.ruleView.throttle(this._previewValue, 10, this);
+ this.update = this.update.bind(this);
+ this.updatePropertyState = this.updatePropertyState.bind(this);
+
+ this._create();
+ this.update();
+}
+
+TextPropertyEditor.prototype = {
+ /**
+ * Boolean indicating if the name or value is being currently edited.
+ */
+ get editing() {
+ return !!(this.nameSpan.inplaceEditor || this.valueSpan.inplaceEditor ||
+ this.ruleView.tooltips.isEditing) || this.popup.isOpen;
+ },
+
+ /**
+ * Get the rule to the current text property
+ */
+ get rule() {
+ return this.prop.rule;
+ },
+
+ /**
+ * Create the property editor's DOM.
+ */
+ _create: function () {
+ this.element = this.doc.createElementNS(HTML_NS, "li");
+ this.element.classList.add("ruleview-property");
+ this.element._textPropertyEditor = this;
+
+ this.container = createChild(this.element, "div", {
+ class: "ruleview-propertycontainer"
+ });
+
+ // The enable checkbox will disable or enable the rule.
+ this.enable = createChild(this.container, "div", {
+ class: "ruleview-enableproperty theme-checkbox",
+ tabindex: "-1"
+ });
+
+ // Click to expand the computed properties of the text property.
+ this.expander = createChild(this.container, "span", {
+ class: "ruleview-expander theme-twisty"
+ });
+ this.expander.addEventListener("click", this._onExpandClicked, true);
+
+ this.nameContainer = createChild(this.container, "span", {
+ class: "ruleview-namecontainer"
+ });
+
+ // Property name, editable when focused. Property name
+ // is committed when the editor is unfocused.
+ this.nameSpan = createChild(this.nameContainer, "span", {
+ class: "ruleview-propertyname theme-fg-color5",
+ tabindex: this.ruleEditor.isEditable ? "0" : "-1",
+ });
+
+ appendText(this.nameContainer, ": ");
+
+ // Create a span that will hold the property and semicolon.
+ // Use this span to create a slightly larger click target
+ // for the value.
+ this.valueContainer = createChild(this.container, "span", {
+ class: "ruleview-propertyvaluecontainer"
+ });
+
+ // Property value, editable when focused. Changes to the
+ // property value are applied as they are typed, and reverted
+ // if the user presses escape.
+ this.valueSpan = createChild(this.valueContainer, "span", {
+ class: "ruleview-propertyvalue theme-fg-color1",
+ tabindex: this.ruleEditor.isEditable ? "0" : "-1",
+ });
+
+ // Storing the TextProperty on the elements for easy access
+ // (for instance by the tooltip)
+ this.valueSpan.textProperty = this.prop;
+ this.nameSpan.textProperty = this.prop;
+
+ // If the value is a color property we need to put it through the parser
+ // so that colors can be coerced into the default color type. This prevents
+ // us from thinking that when colors are coerced they have been changed by
+ // the user.
+ let outputParser = this.ruleView._outputParser;
+ let frag = outputParser.parseCssProperty(this.prop.name, this.prop.value);
+ let parsedValue = frag.textContent;
+
+ // Save the initial value as the last committed value,
+ // for restoring after pressing escape.
+ this.committed = { name: this.prop.name,
+ value: parsedValue,
+ priority: this.prop.priority };
+
+ appendText(this.valueContainer, ";");
+
+ this.warning = createChild(this.container, "div", {
+ class: "ruleview-warning",
+ hidden: "",
+ title: l10n("rule.warning.title"),
+ });
+
+ // Filter button that filters for the current property name and is
+ // displayed when the property is overridden by another rule.
+ this.filterProperty = createChild(this.container, "div", {
+ class: "ruleview-overridden-rule-filter",
+ hidden: "",
+ title: l10n("rule.filterProperty.title"),
+ });
+
+ this.filterProperty.addEventListener("click", event => {
+ this.ruleEditor.ruleView.setFilterStyles("`" + this.prop.name + "`");
+ event.stopPropagation();
+ }, false);
+
+ // Holds the viewers for the computed properties.
+ // will be populated in |_updateComputed|.
+ this.computed = createChild(this.element, "ul", {
+ class: "ruleview-computedlist",
+ });
+
+ // Only bind event handlers if the rule is editable.
+ if (this.ruleEditor.isEditable) {
+ this.enable.addEventListener("click", this._onEnableClicked, true);
+
+ this.nameContainer.addEventListener("click", (event) => {
+ // Clicks within the name shouldn't propagate any further.
+ event.stopPropagation();
+
+ // Forward clicks on nameContainer to the editable nameSpan
+ if (event.target === this.nameContainer) {
+ this.nameSpan.click();
+ }
+ }, false);
+
+ editableField({
+ start: this._onStartEditing,
+ element: this.nameSpan,
+ done: this._onNameDone,
+ destroy: this.updatePropertyState,
+ advanceChars: ":",
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
+ popup: this.popup,
+ cssProperties: this.cssProperties,
+ contextMenu: this.ruleView.inspector.onTextBoxContextMenu
+ });
+
+ // Auto blur name field on multiple CSS rules get pasted in.
+ this.nameContainer.addEventListener("paste",
+ blurOnMultipleProperties(this.cssProperties), false);
+
+ this.valueContainer.addEventListener("click", (event) => {
+ // Clicks within the value shouldn't propagate any further.
+ event.stopPropagation();
+
+ // Forward clicks on valueContainer to the editable valueSpan
+ if (event.target === this.valueContainer) {
+ this.valueSpan.click();
+ }
+ }, false);
+
+ // The mousedown event could trigger a blur event on nameContainer, which
+ // will trigger a call to the update function. The update function clears
+ // valueSpan's markup. Thus the regular click event does not bubble up, and
+ // listener's callbacks are not called.
+ // So we need to remember where the user clicks in order to re-trigger the click
+ // after the valueSpan's markup is re-populated. We only need to track this for
+ // valueSpan's child elements, because direct click on valueSpan will always
+ // trigger a click event.
+ this.valueSpan.addEventListener("mousedown", (event) => {
+ let clickedEl = event.target;
+ if (clickedEl === this.valueSpan) {
+ return;
+ }
+ this._hasPendingClick = true;
+
+ let matchedSelector = ACTIONABLE_ELEMENTS_SELECTORS.find(
+ (selector) => clickedEl.matches(selector));
+ if (matchedSelector) {
+ let similarElements = [...this.valueSpan.querySelectorAll(matchedSelector)];
+ this._clickedElementOptions = {
+ selector: matchedSelector,
+ index: similarElements.indexOf(clickedEl)
+ };
+ }
+ }, false);
+
+ this.valueSpan.addEventListener("mouseup", (event) => {
+ this._clickedElementOptions = null;
+ this._hasPendingClick = false;
+ }, false);
+
+ this.valueSpan.addEventListener("click", (event) => {
+ let target = event.target;
+
+ if (target.nodeName === "a") {
+ event.stopPropagation();
+ event.preventDefault();
+ this.browserWindow.openUILinkIn(target.href, "tab");
+ }
+ }, false);
+
+ editableField({
+ start: this._onStartEditing,
+ element: this.valueSpan,
+ done: this._onValueDone,
+ destroy: this.update,
+ validate: this._onValidate,
+ advanceChars: advanceValidate,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
+ property: this.prop,
+ popup: this.popup,
+ multiline: true,
+ maxWidth: () => this.container.getBoundingClientRect().width,
+ cssProperties: this.cssProperties,
+ contextMenu: this.ruleView.inspector.onTextBoxContextMenu
+ });
+ }
+ },
+
+ /**
+ * Get the path from which to resolve requests for this
+ * rule's stylesheet.
+ *
+ * @return {String} the stylesheet's href.
+ */
+ get sheetHref() {
+ let domRule = this.rule.domRule;
+ if (domRule) {
+ return domRule.href || domRule.nodeHref;
+ }
+ return undefined;
+ },
+
+ /**
+ * Populate the span based on changes to the TextProperty.
+ */
+ update: function () {
+ if (this.ruleView.isDestroyed) {
+ return;
+ }
+
+ this.updatePropertyState();
+
+ let name = this.prop.name;
+ this.nameSpan.textContent = name;
+
+ // Combine the property's value and priority into one string for
+ // the value.
+ let store = this.rule.elementStyle.store;
+ let val = store.userProperties.getProperty(this.rule.style, name,
+ this.prop.value);
+ if (this.prop.priority) {
+ val += " !" + this.prop.priority;
+ }
+
+ let propDirty = store.userProperties.contains(this.rule.style, name);
+
+ if (propDirty) {
+ this.element.setAttribute("dirty", "");
+ } else {
+ this.element.removeAttribute("dirty");
+ }
+
+ let outputParser = this.ruleView._outputParser;
+ let parserOptions = {
+ angleClass: "ruleview-angle",
+ angleSwatchClass: SHARED_SWATCH_CLASS + " " + ANGLE_SWATCH_CLASS,
+ bezierClass: "ruleview-bezier",
+ bezierSwatchClass: SHARED_SWATCH_CLASS + " " + BEZIER_SWATCH_CLASS,
+ colorClass: "ruleview-color",
+ colorSwatchClass: SHARED_SWATCH_CLASS + " " + COLOR_SWATCH_CLASS,
+ filterClass: "ruleview-filter",
+ filterSwatchClass: SHARED_SWATCH_CLASS + " " + FILTER_SWATCH_CLASS,
+ gridClass: "ruleview-grid",
+ defaultColorType: !propDirty,
+ urlClass: "theme-link",
+ baseURI: this.sheetHref
+ };
+ let frag = outputParser.parseCssProperty(name, val, parserOptions);
+ this.valueSpan.innerHTML = "";
+ this.valueSpan.appendChild(frag);
+
+ this.ruleView.emit("property-value-updated", this.valueSpan);
+
+ // Attach the color picker tooltip to the color swatches
+ this._colorSwatchSpans =
+ this.valueSpan.querySelectorAll("." + COLOR_SWATCH_CLASS);
+ if (this.ruleEditor.isEditable) {
+ for (let span of this._colorSwatchSpans) {
+ // Adding this swatch to the list of swatches our colorpicker
+ // knows about
+ this.ruleView.tooltips.colorPicker.addSwatch(span, {
+ onShow: this._onStartEditing,
+ onPreview: this._onSwatchPreview,
+ onCommit: this._onSwatchCommit,
+ onRevert: this._onSwatchRevert
+ });
+ span.on("unit-change", this._onSwatchCommit);
+ let title = l10n("rule.colorSwatch.tooltip");
+ span.setAttribute("title", title);
+ }
+ }
+
+ // Attach the cubic-bezier tooltip to the bezier swatches
+ this._bezierSwatchSpans =
+ this.valueSpan.querySelectorAll("." + BEZIER_SWATCH_CLASS);
+ if (this.ruleEditor.isEditable) {
+ for (let span of this._bezierSwatchSpans) {
+ // Adding this swatch to the list of swatches our colorpicker
+ // knows about
+ this.ruleView.tooltips.cubicBezier.addSwatch(span, {
+ onShow: this._onStartEditing,
+ onPreview: this._onSwatchPreview,
+ onCommit: this._onSwatchCommit,
+ onRevert: this._onSwatchRevert
+ });
+ let title = l10n("rule.bezierSwatch.tooltip");
+ span.setAttribute("title", title);
+ }
+ }
+
+ // Attach the filter editor tooltip to the filter swatch
+ let span = this.valueSpan.querySelector("." + FILTER_SWATCH_CLASS);
+ if (this.ruleEditor.isEditable) {
+ if (span) {
+ parserOptions.filterSwatch = true;
+
+ this.ruleView.tooltips.filterEditor.addSwatch(span, {
+ onShow: this._onStartEditing,
+ onPreview: this._onSwatchPreview,
+ onCommit: this._onSwatchCommit,
+ onRevert: this._onSwatchRevert
+ }, outputParser, parserOptions);
+ let title = l10n("rule.filterSwatch.tooltip");
+ span.setAttribute("title", title);
+ }
+ }
+
+ this.angleSwatchSpans =
+ this.valueSpan.querySelectorAll("." + ANGLE_SWATCH_CLASS);
+ if (this.ruleEditor.isEditable) {
+ for (let angleSpan of this.angleSwatchSpans) {
+ angleSpan.on("unit-change", this._onSwatchCommit);
+ let title = l10n("rule.angleSwatch.tooltip");
+ angleSpan.setAttribute("title", title);
+ }
+ }
+
+ let gridToggle = this.valueSpan.querySelector(".ruleview-grid");
+ if (gridToggle) {
+ gridToggle.setAttribute("title", l10n("rule.gridToggle.tooltip"));
+ if (this.ruleView.highlighters.gridHighlighterShown ===
+ this.ruleView.inspector.selection.nodeFront) {
+ gridToggle.classList.add("active");
+ }
+ }
+
+ // Now that we have updated the property's value, we might have a pending
+ // click on the value container. If we do, we have to trigger a click event
+ // on the right element.
+ if (this._hasPendingClick) {
+ this._hasPendingClick = false;
+ let elToClick;
+
+ if (this._clickedElementOptions !== null) {
+ let {selector, index} = this._clickedElementOptions;
+ elToClick = this.valueSpan.querySelectorAll(selector)[index];
+
+ this._clickedElementOptions = null;
+ }
+
+ if (!elToClick) {
+ elToClick = this.valueSpan;
+ }
+ elToClick.click();
+ }
+
+ // Populate the computed styles.
+ this._updateComputed();
+
+ // Update the rule property highlight.
+ this.ruleView._updatePropertyHighlight(this);
+ },
+
+ _onStartEditing: function () {
+ this.element.classList.remove("ruleview-overridden");
+ this.filterProperty.hidden = true;
+ this.enable.style.visibility = "hidden";
+ },
+
+ /**
+ * Update the visibility of the enable checkbox, the warning indicator and
+ * the filter property, as well as the overriden state of the property.
+ */
+ updatePropertyState: function () {
+ if (this.prop.enabled) {
+ this.enable.style.removeProperty("visibility");
+ this.enable.setAttribute("checked", "");
+ } else {
+ this.enable.style.visibility = "visible";
+ this.enable.removeAttribute("checked");
+ }
+
+ this.warning.hidden = this.editing || this.isValid();
+ this.filterProperty.hidden = this.editing ||
+ !this.isValid() ||
+ !this.prop.overridden ||
+ this.ruleEditor.rule.isUnmatched;
+
+ if (!this.editing &&
+ (this.prop.overridden || !this.prop.enabled ||
+ !this.prop.isKnownProperty())) {
+ this.element.classList.add("ruleview-overridden");
+ } else {
+ this.element.classList.remove("ruleview-overridden");
+ }
+ },
+
+ /**
+ * Update the indicator for computed styles. The computed styles themselves
+ * are populated on demand, when they become visible.
+ */
+ _updateComputed: function () {
+ this.computed.innerHTML = "";
+
+ let showExpander = this.prop.computed.some(c => c.name !== this.prop.name);
+ this.expander.style.visibility = showExpander ? "visible" : "hidden";
+
+ this._populatedComputed = false;
+ if (this.expander.hasAttribute("open")) {
+ this._populateComputed();
+ }
+ },
+
+ /**
+ * Populate the list of computed styles.
+ */
+ _populateComputed: function () {
+ if (this._populatedComputed) {
+ return;
+ }
+ this._populatedComputed = true;
+
+ for (let computed of this.prop.computed) {
+ // Don't bother to duplicate information already
+ // shown in the text property.
+ if (computed.name === this.prop.name) {
+ continue;
+ }
+
+ let li = createChild(this.computed, "li", {
+ class: "ruleview-computed"
+ });
+
+ if (computed.overridden) {
+ li.classList.add("ruleview-overridden");
+ }
+
+ createChild(li, "span", {
+ class: "ruleview-propertyname theme-fg-color5",
+ textContent: computed.name
+ });
+ appendText(li, ": ");
+
+ let outputParser = this.ruleView._outputParser;
+ let frag = outputParser.parseCssProperty(
+ computed.name, computed.value, {
+ colorSwatchClass: "ruleview-swatch ruleview-colorswatch",
+ urlClass: "theme-link",
+ baseURI: this.sheetHref
+ }
+ );
+
+ // Store the computed property value that was parsed for output
+ computed.parsedValue = frag.textContent;
+
+ createChild(li, "span", {
+ class: "ruleview-propertyvalue theme-fg-color1",
+ child: frag
+ });
+
+ appendText(li, ";");
+
+ // Store the computed style element for easy access when highlighting
+ // styles
+ computed.element = li;
+ }
+ },
+
+ /**
+ * Handles clicks on the disabled property.
+ */
+ _onEnableClicked: function (event) {
+ let checked = this.enable.hasAttribute("checked");
+ if (checked) {
+ this.enable.removeAttribute("checked");
+ } else {
+ this.enable.setAttribute("checked", "");
+ }
+ this.prop.setEnabled(!checked);
+ event.stopPropagation();
+ },
+
+ /**
+ * Handles clicks on the computed property expander. If the computed list is
+ * open due to user expanding or style filtering, collapse the computed list
+ * and close the expander. Otherwise, add user-open attribute which is used to
+ * expand the computed list and tracks whether or not the computed list is
+ * expanded by manually by the user.
+ */
+ _onExpandClicked: function (event) {
+ if (this.computed.hasAttribute("filter-open") ||
+ this.computed.hasAttribute("user-open")) {
+ this.expander.removeAttribute("open");
+ this.computed.removeAttribute("filter-open");
+ this.computed.removeAttribute("user-open");
+ } else {
+ this.expander.setAttribute("open", "true");
+ this.computed.setAttribute("user-open", "");
+ this._populateComputed();
+ }
+
+ event.stopPropagation();
+ },
+
+ /**
+ * Expands the computed list when a computed property is matched by the style
+ * filtering. The filter-open attribute is used to track whether or not the
+ * computed list was toggled opened by the filter.
+ */
+ expandForFilter: function () {
+ if (!this.computed.hasAttribute("user-open")) {
+ this.expander.setAttribute("open", "true");
+ this.computed.setAttribute("filter-open", "");
+ this._populateComputed();
+ }
+ },
+
+ /**
+ * Collapses the computed list that was expanded by style filtering.
+ */
+ collapseForFilter: function () {
+ this.computed.removeAttribute("filter-open");
+
+ if (!this.computed.hasAttribute("user-open")) {
+ this.expander.removeAttribute("open");
+ }
+ },
+
+ /**
+ * Called when the property name'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.
+ */
+ _onNameDone: function (value, commit, direction) {
+ let isNameUnchanged = (!commit && !this.ruleEditor.isEditing) ||
+ this.committed.name === value;
+ if (this.prop.value && isNameUnchanged) {
+ return;
+ }
+
+ // Remove a property if the name is empty
+ if (!value.trim()) {
+ this.remove(direction);
+ return;
+ }
+
+ // Remove a property if the property value is empty and the property
+ // value is not about to be focused
+ if (!this.prop.value &&
+ direction !== Services.focus.MOVEFOCUS_FORWARD) {
+ this.remove(direction);
+ return;
+ }
+
+ // Adding multiple rules inside of name field overwrites the current
+ // property with the first, then adds any more onto the property list.
+ let properties = parseDeclarations(this.cssProperties.isKnown, value);
+
+ if (properties.length) {
+ this.prop.setName(properties[0].name);
+ this.committed.name = this.prop.name;
+
+ if (!this.prop.enabled) {
+ this.prop.setEnabled(true);
+ }
+
+ if (properties.length > 1) {
+ this.prop.setValue(properties[0].value, properties[0].priority);
+ this.ruleEditor.addProperties(properties.slice(1), this.prop);
+ }
+ }
+ },
+
+ /**
+ * Remove property from style and the editors from DOM.
+ * Begin editing next or previous available property given the focus
+ * direction.
+ *
+ * @param {Number} direction
+ * The move focus direction number.
+ */
+ remove: function (direction) {
+ if (this._colorSwatchSpans && this._colorSwatchSpans.length) {
+ for (let span of this._colorSwatchSpans) {
+ this.ruleView.tooltips.colorPicker.removeSwatch(span);
+ span.off("unit-change", this._onSwatchCommit);
+ }
+ }
+
+ if (this.angleSwatchSpans && this.angleSwatchSpans.length) {
+ for (let span of this.angleSwatchSpans) {
+ span.off("unit-change", this._onSwatchCommit);
+ }
+ }
+
+ this.element.parentNode.removeChild(this.element);
+ this.ruleEditor.rule.editClosestTextProperty(this.prop, direction);
+ this.nameSpan.textProperty = null;
+ this.valueSpan.textProperty = null;
+ this.prop.remove();
+ },
+
+ /**
+ * Called when a value editor closes. If the user pressed escape,
+ * revert to the value this property had before editing.
+ *
+ * @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.
+ */
+ _onValueDone: function (value = "", commit, direction) {
+ let parsedProperties = this._getValueAndExtraProperties(value);
+ let val = parseSingleValue(this.cssProperties.isKnown,
+ parsedProperties.firstValue);
+ let isValueUnchanged = (!commit && !this.ruleEditor.isEditing) ||
+ !parsedProperties.propertiesToAdd.length &&
+ this.committed.value === val.value &&
+ this.committed.priority === val.priority;
+ // If the value is not empty and unchanged, revert the property back to
+ // its original value and enabled or disabled state
+ if (value.trim() && isValueUnchanged) {
+ this.ruleEditor.rule.previewPropertyValue(this.prop, val.value,
+ val.priority);
+ this.rule.setPropertyEnabled(this.prop, this.prop.enabled);
+ return;
+ }
+
+ if (this.isDisplayGrid()) {
+ this.ruleView.highlighters._hideGridHighlighter();
+ }
+
+ // First, set this property value (common case, only modified a property)
+ this.prop.setValue(val.value, val.priority);
+
+ if (!this.prop.enabled) {
+ this.prop.setEnabled(true);
+ }
+
+ this.committed.value = this.prop.value;
+ this.committed.priority = this.prop.priority;
+
+ // If needed, add any new properties after this.prop.
+ this.ruleEditor.addProperties(parsedProperties.propertiesToAdd, this.prop);
+
+ // If the input value is empty and the focus is moving forward to the next
+ // editable field, then remove the whole property.
+ // A timeout is used here to accurately check the state, since the inplace
+ // editor `done` and `destroy` events fire before the next editor
+ // is focused.
+ if (!value.trim() && direction !== Services.focus.MOVEFOCUS_BACKWARD) {
+ setTimeout(() => {
+ if (!this.editing) {
+ this.remove(direction);
+ }
+ }, 0);
+ }
+ },
+
+ /**
+ * Called when the swatch editor wants to commit a value change.
+ */
+ _onSwatchCommit: function () {
+ this._onValueDone(this.valueSpan.textContent, true);
+ this.update();
+ },
+
+ /**
+ * Called when the swatch editor wants to preview a value change.
+ */
+ _onSwatchPreview: function () {
+ this._previewValue(this.valueSpan.textContent);
+ },
+
+ /**
+ * Called when the swatch editor closes from an ESC. Revert to the original
+ * value of this property before editing.
+ */
+ _onSwatchRevert: function () {
+ this._previewValue(this.prop.value, true);
+ this.update();
+ },
+
+ /**
+ * Parse a value string and break it into pieces, starting with the
+ * first value, and into an array of additional properties (if any).
+ *
+ * Example: Calling with "red; width: 100px" would return
+ * { firstValue: "red", propertiesToAdd: [{ name: "width", value: "100px" }] }
+ *
+ * @param {String} value
+ * The string to parse
+ * @return {Object} An object with the following properties:
+ * firstValue: A string containing a simple value, like
+ * "red" or "100px!important"
+ * propertiesToAdd: An array with additional properties, following the
+ * parseDeclarations format of {name,value,priority}
+ */
+ _getValueAndExtraProperties: function (value) {
+ // The inplace editor will prevent manual typing of multiple properties,
+ // but we need to deal with the case during a paste event.
+ // Adding multiple properties inside of value editor sets value with the
+ // first, then adds any more onto the property list (below this property).
+ let firstValue = value;
+ let propertiesToAdd = [];
+
+ let properties = parseDeclarations(this.cssProperties.isKnown, value);
+
+ // Check to see if the input string can be parsed as multiple properties
+ if (properties.length) {
+ // Get the first property value (if any), and any remaining
+ // properties (if any)
+ if (!properties[0].name && properties[0].value) {
+ firstValue = properties[0].value;
+ propertiesToAdd = properties.slice(1);
+ } else if (properties[0].name && properties[0].value) {
+ // In some cases, the value could be a property:value pair
+ // itself. Join them as one value string and append
+ // potentially following properties
+ firstValue = properties[0].name + ": " + properties[0].value;
+ propertiesToAdd = properties.slice(1);
+ }
+ }
+
+ return {
+ propertiesToAdd: propertiesToAdd,
+ firstValue: firstValue
+ };
+ },
+
+ /**
+ * Live preview this property, without committing changes.
+ *
+ * @param {String} value
+ * The value to set the current property to.
+ * @param {Boolean} reverting
+ * True if we're reverting the previously previewed value
+ */
+ _previewValue: function (value, reverting = false) {
+ // Since function call is throttled, we need to make sure we are still
+ // editing, and any selector modifications have been completed
+ if (!reverting && (!this.editing || this.ruleEditor.isEditing)) {
+ return;
+ }
+
+ let val = parseSingleValue(this.cssProperties.isKnown, value);
+ this.ruleEditor.rule.previewPropertyValue(this.prop, val.value,
+ val.priority);
+ },
+
+ /**
+ * Validate this property. Does it make sense for this value to be assigned
+ * to this property name? This does not apply the property value
+ *
+ * @return {Boolean} true if the property value is valid, false otherwise.
+ */
+ isValid: function () {
+ return this.prop.isValid();
+ },
+
+ /**
+ * Returns true if the property is a `display: grid` declaration.
+ *
+ * @return {Boolean} true if the property is a `display: grid` declaration.
+ */
+ isDisplayGrid: function () {
+ return this.prop.name === "display" && this.prop.value === "grid";
+ }
+};
+
+exports.TextPropertyEditor = TextPropertyEditor;