summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/rules/views/text-property-editor.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/inspector/rules/views/text-property-editor.js1598
1 files changed, 1598 insertions, 0 deletions
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 0000000000..28340c8a14
--- /dev/null
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -0,0 +1,1598 @@
+/* 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 {
+ InplaceEditor,
+ editableField,
+} = require("resource://devtools/client/shared/inplace-editor.js");
+const {
+ createChild,
+ appendText,
+ advanceValidate,
+ blurOnMultipleProperties,
+} = require("resource://devtools/client/inspector/shared/utils.js");
+const { throttle } = require("resource://devtools/shared/throttle.js");
+const {
+ style: { ELEMENT_STYLE },
+} = require("resource://devtools/shared/constants.js");
+
+loader.lazyRequireGetter(
+ this,
+ "openContentLink",
+ "resource://devtools/client/shared/link.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ ["parseDeclarations", "parseSingleValue"],
+ "resource://devtools/shared/css/parsing-utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "findCssSelector",
+ "resource://devtools/shared/inspector/css-logic.js",
+ true
+);
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
+});
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const inlineCompatibilityWarningEnabled = Services.prefs.getBoolPref(
+ "devtools.inspector.ruleview.inline-compatibility-warning.enabled"
+);
+
+const SHARED_SWATCH_CLASS = "ruleview-swatch";
+const COLOR_SWATCH_CLASS = "ruleview-colorswatch";
+const BEZIER_SWATCH_CLASS = "ruleview-bezierswatch";
+const LINEAR_EASING_SWATCH_CLASS = "ruleview-lineareasingswatch";
+const FILTER_SWATCH_CLASS = "ruleview-filterswatch";
+const ANGLE_SWATCH_CLASS = "ruleview-angleswatch";
+const FONT_FAMILY_CLASS = "ruleview-font-family";
+const SHAPE_SWATCH_CLASS = "ruleview-shapeswatch";
+
+/*
+ * 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}`,
+ `.${LINEAR_EASING_SWATCH_CLASS}`,
+ `.${FILTER_SWATCH_CLASS}`,
+ `.${ANGLE_SWATCH_CLASS}`,
+ "a",
+];
+
+/*
+ * Speeds at which we update the value when the user is dragging its mouse
+ * over a value.
+ */
+const SLOW_DRAGGING_SPEED = 0.1;
+const DEFAULT_DRAGGING_SPEED = 1;
+const FAST_DRAGGING_SPEED = 10;
+
+// Deadzone in pixels where dragging should not update the value.
+const DRAGGING_DEADZONE_DISTANCE = 5;
+
+const DRAGGABLE_VALUE_CLASSNAME = "ruleview-propertyvalue-draggable";
+const IS_DRAGGING_CLASSNAME = "ruleview-propertyvalue-dragging";
+
+// In order to highlight the used fonts in font-family properties, we
+// retrieve the list of used fonts from the server. That always
+// returns the actually used font family name(s). If the property's
+// authored value is sans-serif for instance, the used font might be
+// arial instead. So we need the list of all generic font family
+// names to underline those when we find them.
+const GENERIC_FONT_FAMILIES = [
+ "serif",
+ "sans-serif",
+ "cursive",
+ "fantasy",
+ "monospace",
+ "system-ui",
+];
+
+/**
+ * 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.cssProperties = this.ruleView.cssProperties;
+ 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;
+
+ this.toolbox = this.ruleView.inspector.toolbox;
+ this.telemetry = this.toolbox.telemetry;
+
+ this._isDragging = false;
+ this._hasDragged = false;
+ this._draggingController = null;
+ this._draggingValueCache = null;
+
+ this.getGridlineNames = this.getGridlineNames.bind(this);
+ this.update = this.update.bind(this);
+ this.updatePropertyState = this.updatePropertyState.bind(this);
+ this._onDraggablePreferenceChanged =
+ this._onDraggablePreferenceChanged.bind(this);
+ this._onEnableChanged = this._onEnableChanged.bind(this);
+ this._onEnableClicked = this._onEnableClicked.bind(this);
+ this._onExpandClicked = this._onExpandClicked.bind(this);
+ this._onNameDone = this._onNameDone.bind(this);
+ this._onStartEditing = this._onStartEditing.bind(this);
+ this._onSwatchCommit = this._onSwatchCommit.bind(this);
+ this._onSwatchPreview = this._onSwatchPreview.bind(this);
+ this._onSwatchRevert = this._onSwatchRevert.bind(this);
+ this._onValidate = this.ruleView.debounce(this._previewValue, 10, this);
+ this._onValueDone = this._onValueDone.bind(this);
+
+ this._draggingOnMouseDown = this._draggingOnMouseDown.bind(this);
+ this._draggingOnMouseMove = throttle(this._draggingOnMouseMove, 30, this);
+ this._draggingOnMouseUp = this._draggingOnMouseUp.bind(this);
+ this._draggingOnKeydown = this._draggingOnKeydown.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;
+ },
+
+ // Exposed for tests.
+ get _DRAGGING_DEADZONE_DISTANCE() {
+ return DRAGGING_DEADZONE_DISTANCE;
+ },
+
+ /**
+ * Create the property editor's DOM.
+ */
+ _create() {
+ this.element = this.doc.createElementNS(HTML_NS, "li");
+ this.element.classList.add("ruleview-property");
+ this.element.dataset.declarationId = this.prop.id;
+ 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, "input", {
+ type: "checkbox",
+ class: "ruleview-enableproperty",
+ "aria-labelledby": this.prop.id,
+ tabindex: "-1",
+ });
+
+ 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-color3",
+ tabindex: this.ruleEditor.isEditable ? "0" : "-1",
+ id: this.prop.id,
+ });
+
+ appendText(this.nameContainer, ": ");
+
+ // 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);
+
+ // 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;
+
+ appendText(this.valueContainer, ";");
+
+ this.warning = createChild(this.container, "div", {
+ class: "ruleview-warning",
+ hidden: "",
+ title: l10n("rule.warning.title"),
+ });
+
+ this.unusedState = createChild(this.container, "div", {
+ class: "ruleview-unused-warning",
+ hidden: "",
+ });
+
+ if (inlineCompatibilityWarningEnabled) {
+ this.compatibilityState = createChild(this.container, "div", {
+ class: "ruleview-compatibility-warning",
+ hidden: "",
+ });
+ }
+
+ // 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();
+ });
+
+ // Holds the viewers for the computed properties.
+ // will be populated in |_updateComputed|.
+ this.computed = createChild(this.element, "ul", {
+ class: "ruleview-computedlist",
+ });
+
+ // Holds the viewers for the overridden shorthand properties.
+ // will be populated in |_updateShorthandOverridden|.
+ this.shorthandOverridden = createChild(this.element, "ul", {
+ class: "ruleview-overridden-items",
+ });
+
+ // Only bind event handlers if the rule is editable.
+ if (this.ruleEditor.isEditable) {
+ this.enable.addEventListener("click", this._onEnableClicked, true);
+ this.enable.addEventListener("change", this._onEnableChanged, 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();
+ }
+ });
+
+ 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,
+ });
+
+ // Auto blur name field on multiple CSS rules get pasted in.
+ this.nameContainer.addEventListener(
+ "paste",
+ blurOnMultipleProperties(this.cssProperties)
+ );
+
+ 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();
+ }
+ });
+
+ // 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 => {
+ const clickedEl = event.target;
+ if (clickedEl === this.valueSpan) {
+ return;
+ }
+ this._hasPendingClick = true;
+
+ const matchedSelector = ACTIONABLE_ELEMENTS_SELECTORS.find(selector =>
+ clickedEl.matches(selector)
+ );
+ if (matchedSelector) {
+ const similarElements = [
+ ...this.valueSpan.querySelectorAll(matchedSelector),
+ ];
+ this._clickedElementOptions = {
+ selector: matchedSelector,
+ index: similarElements.indexOf(clickedEl),
+ };
+ }
+ });
+
+ this.valueSpan.addEventListener("mouseup", event => {
+ // if we have dragged, we will handle the pending click in _draggingOnMouseUp instead
+ if (this._hasDragged) {
+ return;
+ }
+ this._clickedElementOptions = null;
+ this._hasPendingClick = false;
+ });
+
+ this.valueSpan.addEventListener("click", event => {
+ const target = event.target;
+
+ if (target.nodeName === "a") {
+ event.stopPropagation();
+ event.preventDefault();
+ openContentLink(target.href);
+ }
+ });
+
+ this.ruleView.on(
+ "draggable-preference-updated",
+ this._onDraggablePreferenceChanged
+ );
+ if (this._isDraggableProperty(this.prop)) {
+ this._addDraggingCapability();
+ }
+
+ 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,
+ defaultIncrement: this.prop.name === "opacity" ? 0.1 : 1,
+ popup: this.popup,
+ multiline: true,
+ maxWidth: () => this.container.getBoundingClientRect().width,
+ cssProperties: this.cssProperties,
+ cssVariables:
+ this.rule.elementStyle.variablesMap.get(this.rule.pseudoElement) ||
+ [],
+ getGridLineNames: this.getGridlineNames,
+ showSuggestCompletionOnEmpty: true,
+ });
+ }
+ },
+
+ /**
+ * Get the grid line names of the grid that the currently selected element is
+ * contained in.
+ *
+ * @return {Object} Contains the names of the cols and rows as arrays
+ * {cols: [], rows: []}.
+ */
+ async getGridlineNames() {
+ const gridLineNames = { cols: [], rows: [] };
+ const layoutInspector =
+ await this.ruleView.inspector.walker.getLayoutInspector();
+ const gridFront = await layoutInspector.getCurrentGrid(
+ this.ruleView.inspector.selection.nodeFront
+ );
+
+ if (gridFront) {
+ const gridFragments = gridFront.gridFragments;
+
+ for (const gridFragment of gridFragments) {
+ for (const rowLine of gridFragment.rows.lines) {
+ // We specifically ignore implicit line names created from implicitly named
+ // areas. This is because showing implicit line names can be confusing for
+ // designers who may have used a line name with "-start" or "-end" and created
+ // an implicitly named grid area without meaning to.
+ let gridArea;
+
+ for (const name of rowLine.names) {
+ const rowLineName =
+ name.substring(0, name.lastIndexOf("-start")) ||
+ name.substring(0, name.lastIndexOf("-end"));
+ gridArea = gridFragment.areas.find(
+ area => area.name === rowLineName
+ );
+
+ if (
+ rowLine.type === "implicit" &&
+ gridArea &&
+ gridArea.type === "implicit"
+ ) {
+ continue;
+ }
+ gridLineNames.rows.push(name);
+ }
+ }
+
+ for (const colLine of gridFragment.cols.lines) {
+ let gridArea;
+
+ for (const name of colLine.names) {
+ const colLineName =
+ name.substring(0, name.lastIndexOf("-start")) ||
+ name.substring(0, name.lastIndexOf("-end"));
+ gridArea = gridFragment.areas.find(
+ area => area.name === colLineName
+ );
+
+ if (
+ colLine.type === "implicit" &&
+ gridArea &&
+ gridArea.type === "implicit"
+ ) {
+ continue;
+ }
+ gridLineNames.cols.push(name);
+ }
+ }
+ }
+ }
+
+ // Emit message for test files
+ this.ruleView.inspector.emit("grid-line-names-updated");
+ return gridLineNames;
+ },
+
+ /**
+ * Get the path from which to resolve requests for this
+ * rule's stylesheet.
+ *
+ * @return {String} the stylesheet's href.
+ */
+ get sheetHref() {
+ const domRule = this.rule.domRule;
+ if (domRule) {
+ return domRule.href || domRule.nodeHref;
+ }
+ return undefined;
+ },
+
+ /**
+ * Populate the span based on changes to the TextProperty.
+ */
+ // eslint-disable-next-line complexity
+ update() {
+ if (this.ruleView.isDestroyed) {
+ return;
+ }
+
+ this.updatePropertyState();
+
+ const name = this.prop.name;
+ this.nameSpan.textContent = name;
+
+ // Combine the property's value and priority into one string for
+ // the value.
+ const store = this.rule.elementStyle.store;
+ let val = store.userProperties.getProperty(
+ this.rule.domRule,
+ name,
+ this.prop.value
+ );
+ if (this.prop.priority) {
+ val += " !" + this.prop.priority;
+ }
+
+ const propDirty = store.userProperties.contains(this.rule.domRule, name);
+
+ if (propDirty) {
+ this.element.setAttribute("dirty", "");
+ } else {
+ this.element.removeAttribute("dirty");
+ }
+
+ const outputParser = this.ruleView._outputParser;
+ const 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,
+ flexClass: "ruleview-flex js-toggle-flexbox-highlighter",
+ gridClass: "ruleview-grid js-toggle-grid-highlighter",
+ linearEasingClass: "ruleview-lineareasing",
+ linearEasingSwatchClass:
+ SHARED_SWATCH_CLASS + " " + LINEAR_EASING_SWATCH_CLASS,
+ shapeClass: "ruleview-shape",
+ shapeSwatchClass: SHAPE_SWATCH_CLASS,
+ // Only ask the parser to convert colors to the default color type specified by the
+ // user if the property hasn't been changed yet.
+ defaultColorType: !propDirty,
+ urlClass: "theme-link",
+ fontFamilyClass: FONT_FAMILY_CLASS,
+ baseURI: this.sheetHref,
+ unmatchedVariableClass: "ruleview-unmatched-variable",
+ matchedVariableClass: "ruleview-variable",
+ getVariableValue: varName =>
+ this.rule.elementStyle.getVariable(varName, this.rule.pseudoElement),
+ };
+ const frag = outputParser.parseCssProperty(name, val, parserOptions);
+
+ // Save the initial value as the last committed value,
+ // for restoring after pressing escape.
+ if (!this.committed) {
+ this.committed = {
+ name,
+ value: frag.textContent,
+ priority: this.prop.priority,
+ };
+ }
+
+ // Save focused element inside value span if one exists before wiping the innerHTML
+ let focusedElSelector = null;
+ if (this.valueSpan.contains(this.doc.activeElement)) {
+ focusedElSelector = findCssSelector(this.doc.activeElement);
+ }
+
+ this.valueSpan.innerHTML = "";
+ this.valueSpan.appendChild(frag);
+ if (
+ this.valueSpan.textProperty?.name === "grid-template-areas" &&
+ this.isValid() &&
+ (this.valueSpan.innerText.includes(`"`) ||
+ this.valueSpan.innerText.includes(`'`))
+ ) {
+ this._formatGridTemplateAreasValue();
+ }
+
+ this.ruleView.emit("property-value-updated", {
+ rule: this.prop.rule,
+ property: name,
+ value: val,
+ });
+
+ // Highlight the currently used font in font-family properties.
+ // If we cannot find a match, highlight the first generic family instead.
+ const fontFamilySpans = this.valueSpan.querySelectorAll(
+ "." + FONT_FAMILY_CLASS
+ );
+ if (fontFamilySpans.length && this.prop.enabled && !this.prop.overridden) {
+ this.rule.elementStyle
+ .getUsedFontFamilies()
+ .then(families => {
+ const usedFontFamilies = families.map(font => font.toLowerCase());
+ let foundMatchingFamily = false;
+ let firstGenericSpan = null;
+
+ for (const span of fontFamilySpans) {
+ const authoredFont = span.textContent.toLowerCase();
+
+ if (
+ !firstGenericSpan &&
+ GENERIC_FONT_FAMILIES.includes(authoredFont)
+ ) {
+ firstGenericSpan = span;
+ }
+
+ if (usedFontFamilies.includes(authoredFont)) {
+ span.classList.add("used-font");
+ foundMatchingFamily = true;
+ }
+ }
+
+ if (!foundMatchingFamily && firstGenericSpan) {
+ firstGenericSpan.classList.add("used-font");
+ }
+
+ this.ruleView.emit("font-highlighted", this.valueSpan);
+ })
+ .catch(e =>
+ console.error("Could not get the list of font families", e)
+ );
+ }
+
+ // Attach the color picker tooltip to the color swatches
+ this._colorSwatchSpans = this.valueSpan.querySelectorAll(
+ "." + COLOR_SWATCH_CLASS
+ );
+ if (this.ruleEditor.isEditable) {
+ for (const span of this._colorSwatchSpans) {
+ // Adding this swatch to the list of swatches our colorpicker
+ // knows about
+ this.ruleView.tooltips.getTooltip("colorPicker").addSwatch(span, {
+ onShow: this._onStartEditing,
+ onPreview: this._onSwatchPreview,
+ onCommit: this._onSwatchCommit,
+ onRevert: this._onSwatchRevert,
+ });
+ const title = l10n("rule.colorSwatch.tooltip");
+ span.setAttribute("title", title);
+ span.dataset.propertyName = this.nameSpan.textContent;
+ }
+ }
+
+ // Attach the cubic-bezier tooltip to the bezier swatches
+ this._bezierSwatchSpans = this.valueSpan.querySelectorAll(
+ "." + BEZIER_SWATCH_CLASS
+ );
+ if (this.ruleEditor.isEditable) {
+ for (const span of this._bezierSwatchSpans) {
+ // Adding this swatch to the list of swatches our colorpicker
+ // knows about
+ this.ruleView.tooltips.getTooltip("cubicBezier").addSwatch(span, {
+ onShow: this._onStartEditing,
+ onPreview: this._onSwatchPreview,
+ onCommit: this._onSwatchCommit,
+ onRevert: this._onSwatchRevert,
+ });
+ const title = l10n("rule.bezierSwatch.tooltip");
+ span.setAttribute("title", title);
+ }
+ }
+
+ // Attach the linear easing tooltip to the linear easing swatches
+ this._linearEasingSwatchSpans = this.valueSpan.querySelectorAll(
+ "." + LINEAR_EASING_SWATCH_CLASS
+ );
+ if (this.ruleEditor.isEditable) {
+ for (const span of this._linearEasingSwatchSpans) {
+ // Adding this swatch to the list of swatches our colorpicker
+ // knows about
+ this.ruleView.tooltips
+ .getTooltip("linearEaseFunction")
+ .addSwatch(span, {
+ onShow: this._onStartEditing,
+ onPreview: this._onSwatchPreview,
+ onCommit: this._onSwatchCommit,
+ onRevert: this._onSwatchRevert,
+ });
+ span.setAttribute("title", l10n("rule.bezierSwatch.tooltip"));
+ }
+ }
+
+ // Attach the filter editor tooltip to the filter swatch
+ const span = this.valueSpan.querySelector("." + FILTER_SWATCH_CLASS);
+ if (this.ruleEditor.isEditable) {
+ if (span) {
+ parserOptions.filterSwatch = true;
+
+ this.ruleView.tooltips.getTooltip("filterEditor").addSwatch(
+ span,
+ {
+ onShow: this._onStartEditing,
+ onPreview: this._onSwatchPreview,
+ onCommit: this._onSwatchCommit,
+ onRevert: this._onSwatchRevert,
+ },
+ outputParser,
+ parserOptions
+ );
+ const title = l10n("rule.filterSwatch.tooltip");
+ span.setAttribute("title", title);
+ }
+ }
+
+ this.angleSwatchSpans = this.valueSpan.querySelectorAll(
+ "." + ANGLE_SWATCH_CLASS
+ );
+ if (this.ruleEditor.isEditable) {
+ for (const angleSpan of this.angleSwatchSpans) {
+ angleSpan.on("unit-change", this._onSwatchCommit);
+ const title = l10n("rule.angleSwatch.tooltip");
+ angleSpan.setAttribute("title", title);
+ }
+ }
+
+ const nodeFront = this.ruleView.inspector.selection.nodeFront;
+
+ const flexToggle = this.valueSpan.querySelector(".ruleview-flex");
+ if (flexToggle) {
+ flexToggle.setAttribute("title", l10n("rule.flexToggle.tooltip"));
+ flexToggle.classList.toggle(
+ "active",
+ this.ruleView.inspector.highlighters.getNodeForActiveHighlighter(
+ this.ruleView.inspector.highlighters.TYPES.FLEXBOX
+ ) === nodeFront
+ );
+ }
+
+ const gridToggle = this.valueSpan.querySelector(".ruleview-grid");
+ if (gridToggle) {
+ gridToggle.setAttribute("title", l10n("rule.gridToggle.tooltip"));
+ gridToggle.classList.toggle(
+ "active",
+ this.ruleView.highlighters.gridHighlighters.has(nodeFront)
+ );
+ gridToggle.toggleAttribute(
+ "disabled",
+ !this.ruleView.highlighters.canGridHighlighterToggle(nodeFront)
+ );
+ }
+
+ const shapeToggle = this.valueSpan.querySelector(".ruleview-shapeswatch");
+ if (shapeToggle) {
+ const mode =
+ "css" +
+ name
+ .split("-")
+ .map(s => {
+ return s[0].toUpperCase() + s.slice(1);
+ })
+ .join("");
+ shapeToggle.setAttribute("data-mode", mode);
+ }
+
+ // 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 we are dragging, we don't need to handle the pending click
+ if (this._hasPendingClick && !this._isDragging) {
+ this._hasPendingClick = false;
+ let elToClick;
+
+ if (this._clickedElementOptions !== null) {
+ const { selector, index } = this._clickedElementOptions;
+ elToClick = this.valueSpan.querySelectorAll(selector)[index];
+
+ this._clickedElementOptions = null;
+ }
+
+ if (!elToClick) {
+ elToClick = this.valueSpan;
+ }
+ elToClick.click();
+ }
+
+ // Populate the computed styles and shorthand overridden styles.
+ this._updateComputed();
+ this._updateShorthandOverridden();
+
+ // Update the rule property highlight.
+ this.ruleView._updatePropertyHighlight(this);
+
+ // Restore focus back to the element whose markup was recreated above.
+ if (focusedElSelector) {
+ const elementToFocus = this.doc.querySelector(focusedElSelector);
+ if (elementToFocus) {
+ elementToFocus.focus();
+ }
+ }
+ },
+
+ _onStartEditing() {
+ this.element.classList.remove("ruleview-overridden");
+ this.filterProperty.hidden = true;
+ this.enable.style.visibility = "hidden";
+ this.expander.style.display = "none";
+ },
+
+ get shouldShowComputedExpander() {
+ // Only show the expander to reveal computed properties if:
+ // - the computed properties are actually different from the current property (i.e
+ // these are longhands while the current property is the shorthand)
+ // - all of the computed properties have defined values. In case the current property
+ // value contains CSS variables, then the computed properties will be missing and we
+ // want to avoid showing them.
+ return (
+ this.prop.computed.some(c => c.name !== this.prop.name) &&
+ !this.prop.computed.every(c => !c.value)
+ );
+ },
+
+ /**
+ * Update the visibility of the enable checkbox, the warning indicator, the used
+ * indicator and the filter property, as well as the overridden state of the property.
+ */
+ updatePropertyState() {
+ if (this.prop.enabled) {
+ this.enable.style.removeProperty("visibility");
+ } else {
+ this.enable.style.visibility = "visible";
+ }
+
+ this.enable.checked = this.prop.enabled;
+
+ this.warning.title = !this.isNameValid()
+ ? l10n("rule.warningName.title")
+ : l10n("rule.warning.title");
+
+ this.warning.hidden = this.editing || this.isValid();
+ this.filterProperty.hidden =
+ this.editing ||
+ !this.isValid() ||
+ !this.prop.overridden ||
+ this.ruleEditor.rule.isUnmatched;
+
+ this.expander.style.display = this.shouldShowComputedExpander
+ ? "inline-block"
+ : "none";
+
+ 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");
+ }
+
+ this.updatePropertyUsedIndicator();
+
+ if (inlineCompatibilityWarningEnabled) {
+ this.updatePropertyCompatibilityIndicator();
+ }
+ },
+
+ updatePropertyUsedIndicator() {
+ const { used } = this.prop.isUsed();
+
+ if (this.editing || this.prop.overridden || !this.prop.enabled || used) {
+ this.element.classList.remove("unused");
+ this.unusedState.hidden = true;
+ } else {
+ this.element.classList.add("unused");
+ this.unusedState.hidden = false;
+ }
+ },
+
+ async updatePropertyCompatibilityIndicator() {
+ const { isCompatible } = await this.prop.isCompatible();
+
+ if (this.editing || isCompatible) {
+ this.compatibilityState.hidden = true;
+ } else {
+ this.compatibilityState.hidden = false;
+ }
+ },
+
+ /**
+ * Update the indicator for computed styles. The computed styles themselves
+ * are populated on demand, when they become visible.
+ */
+ _updateComputed() {
+ this.computed.innerHTML = "";
+
+ this.expander.style.display =
+ !this.editing && this.shouldShowComputedExpander
+ ? "inline-block"
+ : "none";
+
+ this._populatedComputed = false;
+ if (this.expander.hasAttribute("open")) {
+ this._populateComputed();
+ }
+ },
+
+ /**
+ * Populate the list of computed styles.
+ */
+ _populateComputed() {
+ if (this._populatedComputed) {
+ return;
+ }
+ this._populatedComputed = true;
+
+ for (const computed of this.prop.computed) {
+ // Don't bother to duplicate information already
+ // shown in the text property.
+ if (computed.name === this.prop.name) {
+ continue;
+ }
+
+ // Store the computed style element for easy access when highlighting
+ // styles
+ computed.element = this._createComputedListItem(
+ this.computed,
+ computed,
+ "ruleview-computed"
+ );
+ }
+ },
+
+ /**
+ * Update the indicator for overridden shorthand styles. The shorthand
+ * overridden styles themselves are populated on demand, when they
+ * become visible.
+ */
+ _updateShorthandOverridden() {
+ this.shorthandOverridden.innerHTML = "";
+
+ this._populatedShorthandOverridden = false;
+ this._populateShorthandOverridden();
+ },
+
+ /**
+ * Populate the list of overridden shorthand styles.
+ */
+ _populateShorthandOverridden() {
+ if (
+ this._populatedShorthandOverridden ||
+ this.prop.overridden ||
+ !this.shouldShowComputedExpander
+ ) {
+ return;
+ }
+ this._populatedShorthandOverridden = true;
+
+ for (const computed of this.prop.computed) {
+ // Don't display duplicate information or show properties
+ // that are completely overridden.
+ if (computed.name === this.prop.name || !computed.overridden) {
+ continue;
+ }
+
+ this._createComputedListItem(
+ this.shorthandOverridden,
+ computed,
+ "ruleview-overridden-item"
+ );
+ }
+ },
+
+ /**
+ * Creates and populates a list item with the computed CSS property.
+ */
+ _createComputedListItem(parentEl, computed, className) {
+ const li = createChild(parentEl, "li", {
+ class: className,
+ });
+
+ if (computed.overridden) {
+ li.classList.add("ruleview-overridden");
+ }
+
+ const nameContainer = createChild(li, "span", {
+ class: "ruleview-namecontainer",
+ });
+
+ createChild(nameContainer, "span", {
+ class: "ruleview-propertyname theme-fg-color3",
+ textContent: computed.name,
+ });
+ appendText(nameContainer, ": ");
+
+ const outputParser = this.ruleView._outputParser;
+ const frag = outputParser.parseCssProperty(computed.name, computed.value, {
+ colorSwatchClass: "ruleview-swatch ruleview-colorswatch",
+ urlClass: "theme-link",
+ baseURI: this.sheetHref,
+ fontFamilyClass: "ruleview-font-family",
+ });
+
+ // Store the computed property value that was parsed for output
+ computed.parsedValue = frag.textContent;
+
+ const propertyContainer = createChild(li, "span", {
+ class: "ruleview-propertyvaluecontainer",
+ });
+
+ createChild(propertyContainer, "span", {
+ class: "ruleview-propertyvalue theme-fg-color1",
+ child: frag,
+ });
+ appendText(propertyContainer, ";");
+
+ return li;
+ },
+
+ /**
+ * Handle updates to the preference which disables/enables the feature to
+ * edit size properties on drag.
+ */
+ _onDraggablePreferenceChanged() {
+ if (this._isDraggableProperty(this.prop)) {
+ this._addDraggingCapability();
+ } else {
+ this._removeDraggingCapacity();
+ }
+ },
+
+ /**
+ * Stop clicks propogating down the tree from the enable / disable checkbox.
+ */
+ _onEnableClicked(event) {
+ event.stopPropagation();
+ },
+
+ /**
+ * Handles clicks on the disabled property.
+ */
+ _onEnableChanged(event) {
+ this.prop.setEnabled(this.enable.checked);
+ event.stopPropagation();
+ this.telemetry.recordEvent("edit_rule", "ruleview");
+ },
+
+ /**
+ * 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(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");
+ this.shorthandOverridden.hidden = false;
+ this._populateShorthandOverridden();
+ } else {
+ this.expander.setAttribute("open", "true");
+ this.computed.setAttribute("user-open", "");
+ this.shorthandOverridden.hidden = true;
+ 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() {
+ 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() {
+ 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(value, commit, direction) {
+ const isNameUnchanged =
+ (!commit && !this.ruleEditor.isEditing) || this.committed.name === value;
+ if (this.prop.value && isNameUnchanged) {
+ return;
+ }
+
+ this.telemetry.recordEvent("edit_rule", "ruleview");
+
+ // 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.
+ const 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(direction) {
+ if (this._colorSwatchSpans && this._colorSwatchSpans.length) {
+ for (const span of this._colorSwatchSpans) {
+ this.ruleView.tooltips.getTooltip("colorPicker").removeSwatch(span);
+ span.off("unit-change", this._onSwatchCommit);
+ }
+ }
+
+ if (this.angleSwatchSpans && this.angleSwatchSpans.length) {
+ for (const span of this.angleSwatchSpans) {
+ span.off("unit-change", this._onSwatchCommit);
+ }
+ }
+
+ this.ruleView.off(
+ "draggable-preference-updated",
+ this._onDraggablePreferenceChanged
+ );
+
+ this.element.remove();
+ 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(value = "", commit, direction) {
+ const parsedProperties = this._getValueAndExtraProperties(value);
+ const val = parseSingleValue(
+ this.cssProperties.isKnown,
+ parsedProperties.firstValue
+ );
+ const 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;
+ }
+
+ // Check if unit of value changed to add dragging feature
+ if (this._isDraggableProperty(val)) {
+ this._addDraggingCapability();
+ } else {
+ this._removeDraggingCapacity();
+ }
+
+ this.telemetry.recordEvent("edit_rule", "ruleview");
+
+ // 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() {
+ this._onValueDone(this.valueSpan.textContent, true);
+ this.update();
+ },
+
+ /**
+ * Called when the swatch editor wants to preview a value change.
+ */
+ _onSwatchPreview() {
+ 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() {
+ 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(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 = [];
+
+ const 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,
+ 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(value, reverting = false) {
+ // Since function call is debounced, we need to make sure we are still
+ // editing, and any selector modifications have been completed
+ if (!reverting && (!this.editing || this.ruleEditor.isEditing)) {
+ return;
+ }
+
+ const val = parseSingleValue(this.cssProperties.isKnown, value);
+ this.ruleEditor.rule.previewPropertyValue(
+ this.prop,
+ val.value,
+ val.priority
+ );
+ },
+
+ /**
+ * Check if the event passed has a "small increment" modifier
+ * Alt on macosx and ctrl on other OSs
+ *
+ * @param {KeyboardEvent} event
+ * @returns {Boolean}
+ */
+ _hasSmallIncrementModifier(event) {
+ const modifier =
+ lazy.AppConstants.platform === "macosx" ? "altKey" : "ctrlKey";
+ return event[modifier] === true;
+ },
+
+ /**
+ * Parses the value to check if it is a dimension
+ * e.g. if the input is "128px" it will return an object like
+ * { groups: { value: "128", unit: "px"}}
+ *
+ * @param {String} value
+ * @returns {Object|null}
+ */
+ _parseDimension(value) {
+ // The regex handles values like +1, -1, 1e4, .4, 1.3e-4, 1.567
+ const cssDimensionRegex =
+ /^(?<value>[+-]?(\d*\.)?\d+(e[+-]?\d+)?)(?<unit>(%|[a-zA-Z]+))$/;
+ return value.match(cssDimensionRegex);
+ },
+
+ /**
+ * Check if a textProperty value is supported to add the dragging feature
+ *
+ * @param {TextProperty} textProperty
+ * @returns {Boolean}
+ */
+ _isDraggableProperty(textProperty) {
+ // Check if the feature is explicitly disabled.
+ if (!this.ruleView.draggablePropertiesEnabled) {
+ return false;
+ }
+ // temporary way of fixing the bug when editing inline styles
+ // otherwise the textPropertyEditor object is destroyed on each value edit
+ // See Bug 1755024
+ if (this.rule.domRule.type == ELEMENT_STYLE) {
+ return false;
+ }
+
+ const nbValues = textProperty.value.split(" ").length;
+ if (nbValues > 1) {
+ // we do not support values like "1px solid red" yet
+ // See 1755025
+ return false;
+ }
+
+ const dimensionMatchObj = this._parseDimension(textProperty.value);
+ return !!dimensionMatchObj;
+ },
+
+ _draggingOnMouseDown(event) {
+ this._isDragging = true;
+ this.valueSpan.setPointerCapture(event.pointerId);
+ this._draggingController = new AbortController();
+ const { signal } = this._draggingController;
+
+ // turn off user-select in CSS when we drag
+ this.valueSpan.classList.add(IS_DRAGGING_CLASSNAME);
+
+ const dimensionObj = this._parseDimension(this.prop.value);
+ const { value, unit } = dimensionObj.groups;
+ this._draggingValueCache = {
+ isInDeadzone: true,
+ previousScreenX: event.screenX,
+ value: parseFloat(value),
+ unit,
+ };
+
+ this.valueSpan.addEventListener("mousemove", this._draggingOnMouseMove, {
+ signal,
+ });
+ this.valueSpan.addEventListener("mouseup", this._draggingOnMouseUp, {
+ signal,
+ });
+ this.valueSpan.addEventListener("keydown", this._draggingOnKeydown, {
+ signal,
+ });
+ },
+
+ _draggingOnMouseMove(event) {
+ if (!this._isDragging) {
+ return;
+ }
+
+ const { isInDeadzone, previousScreenX } = this._draggingValueCache;
+ let deltaX = event.screenX - previousScreenX;
+
+ // If `isInDeadzone` is still true, the user has not previously left the deadzone.
+ if (isInDeadzone) {
+ // If the mouse is still in the deadzone, bail out immediately.
+ if (Math.abs(deltaX) < DRAGGING_DEADZONE_DISTANCE) {
+ return;
+ }
+
+ // Otherwise, remove the DRAGGING_DEADZONE_DISTANCE from the current deltaX, so that
+ // the value does not update too abruptly.
+ deltaX =
+ Math.sign(deltaX) * (Math.abs(deltaX) - DRAGGING_DEADZONE_DISTANCE);
+
+ // Update the state to remember the user is out of the deadzone.
+ this._draggingValueCache.isInDeadzone = false;
+ }
+
+ let draggingSpeed = DEFAULT_DRAGGING_SPEED;
+ if (event.shiftKey) {
+ draggingSpeed = FAST_DRAGGING_SPEED;
+ } else if (this._hasSmallIncrementModifier(event)) {
+ draggingSpeed = SLOW_DRAGGING_SPEED;
+ }
+
+ const delta = deltaX * draggingSpeed;
+ this._draggingValueCache.previousScreenX = event.screenX;
+ this._draggingValueCache.value += delta;
+
+ if (delta == 0) {
+ return;
+ }
+
+ const { value, unit } = this._draggingValueCache;
+ // We use toFixed to avoid the case where value is too long, 9.00001px for example
+ const roundedValue = Number.isInteger(value) ? value : value.toFixed(1);
+ this.prop.setValue(roundedValue + unit, this.prop.priority);
+ this.ruleView.emitForTests("property-updated-by-dragging");
+ this._hasDragged = true;
+ },
+
+ _draggingOnMouseUp(event) {
+ if (!this._isDragging) {
+ return;
+ }
+ if (this._hasDragged) {
+ this.committed.value = this.prop.value;
+ this.prop.setEnabled(true);
+ }
+ this._onStopDragging(event);
+ },
+
+ _draggingOnKeydown(event) {
+ if (event.key == "Escape") {
+ this.prop.setValue(this.committed.value, this.committed.priority);
+ this._onStopDragging(event);
+ event.preventDefault();
+ }
+ },
+
+ _onStopDragging(event) {
+ // childHasDragged is used to stop the propagation of a click event when we
+ // release the mouse in the ruleview.
+ // The click event is not emitted when we have a pending click on the text property.
+ if (this._hasDragged && !this._hasPendingClick) {
+ this.ruleView.childHasDragged = true;
+ }
+ this._isDragging = false;
+ this._hasDragged = false;
+ this._draggingValueCache = null;
+ this.valueSpan.releasePointerCapture(event.pointerId);
+ this.valueSpan.classList.remove(IS_DRAGGING_CLASSNAME);
+ this._draggingController.abort();
+ },
+
+ /**
+ * add event listeners to add the ability to modify any size value
+ * by dragging the mouse horizontally
+ */
+ _addDraggingCapability() {
+ if (this.valueSpan.classList.contains(DRAGGABLE_VALUE_CLASSNAME)) {
+ return;
+ }
+ this.valueSpan.classList.add(DRAGGABLE_VALUE_CLASSNAME);
+ this.valueSpan.addEventListener("mousedown", this._draggingOnMouseDown);
+ },
+
+ _removeDraggingCapacity() {
+ if (!this.valueSpan.classList.contains(DRAGGABLE_VALUE_CLASSNAME)) {
+ return;
+ }
+ this._draggingController = null;
+ this.valueSpan.classList.remove(DRAGGABLE_VALUE_CLASSNAME);
+ this.valueSpan.removeEventListener("mousedown", this._draggingOnMouseDown);
+ },
+
+ /**
+ * 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 name + value pair is valid, false otherwise.
+ */
+ isValid() {
+ return this.prop.isValid();
+ },
+
+ /**
+ * Validate the name of this property.
+ * @return {Boolean} true if the property name is valid, false otherwise.
+ */
+ isNameValid() {
+ return this.prop.isNameValid();
+ },
+
+ /**
+ * Display grid-template-area value strings each on their own line
+ * to display it in an ascii-art style matrix
+ */
+ _formatGridTemplateAreasValue() {
+ this.valueSpan.classList.add("ruleview-propertyvalue-break-spaces");
+
+ let quoteSymbolsUsed = [];
+
+ const getQuoteSymbolsUsed = cssValue => {
+ const regex = /\"|\'/g;
+ const found = cssValue.match(regex);
+ quoteSymbolsUsed = found.filter((_, i) => i % 2 === 0);
+ };
+
+ getQuoteSymbolsUsed(this.valueSpan.innerText);
+
+ this.valueSpan.innerText = this.valueSpan.innerText
+ .split('"')
+ .filter(s => s !== "")
+ .map(s => s.split("'"))
+ .flat()
+ .map(s => s.trim().replace(/\s+/g, " "))
+ .filter(s => s.length)
+ .map(line => line.split(" "))
+ .map((line, i, lines) =>
+ line.map((col, j) =>
+ col.padEnd(Math.max(...lines.map(l => l[j].length)), " ")
+ )
+ )
+ .map(
+ (line, i) =>
+ `\n${quoteSymbolsUsed[i]}` + line.join(" ") + quoteSymbolsUsed[i]
+ )
+ .join(" ");
+ },
+};
+
+module.exports = TextPropertyEditor;