Web Inspector: Styles: variable swatch not shown for var() with a fallback
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / SpreadsheetStyleProperty.js
index 91b49d4..ed1d7b5 100644 (file)
@@ -25,7 +25,7 @@
 
 WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
 {
-    constructor(delegate, property)
+    constructor(delegate, property, options = {})
     {
         super();
 
@@ -33,17 +33,17 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
 
         this._delegate = delegate || null;
         this._property = property;
+        this._readOnly = options.readOnly || false;
         this._element = document.createElement("div");
 
         this._contentElement = null;
         this._nameElement = null;
         this._valueElement = null;
+        this._jumpToEffectivePropertyButton = null;
 
         this._nameTextField = null;
         this._valueTextField = null;
 
-        this._property.__propertyView = this;
-
         this._selected = false;
         this._hasInvalidVariableValue = false;
 
@@ -51,18 +51,28 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         property.addEventListener(WI.CSSProperty.Event.OverriddenStatusChanged, this.updateStatus, this);
         property.addEventListener(WI.CSSProperty.Event.Changed, this.updateStatus, this);
 
-        if (WI.settings.experimentalEnableMultiplePropertiesSelection.value && this._property.editable) {
+        if (!this._readOnly) {
             this._element.tabIndex = -1;
+            property.addEventListener(WI.CSSProperty.Event.ModifiedChanged, this.updateStatus, this);
 
             this._element.addEventListener("blur", (event) => {
-                this._delegate.spreadsheetStylePropertyBlur(event, this);
+                // Keep selection after tabbing out of Web Inspector window and back.
+                if (document.activeElement === this._element)
+                    return;
+
+                if (this._delegate.spreadsheetStylePropertyBlur)
+                    this._delegate.spreadsheetStylePropertyBlur(event, this);
             });
 
             this._element.addEventListener("mouseenter", (event) => {
-                this._delegate.spreadsheetStylePropertyMouseEnter(event, this);
+                if (this._delegate.spreadsheetStylePropertyMouseEnter)
+                    this._delegate.spreadsheetStylePropertyMouseEnter(event, this);
             });
 
-            this._element.copyHandler = this;
+            new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, WI.KeyboardShortcut.Key.Slash, () => {
+                this._toggle();
+                this._select();
+            }, this._element);
         }
     }
 
@@ -70,8 +80,6 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
 
     get element() { return this._element; }
     get property() { return this._property; }
-    get nameTextField() { return this._nameTextField; }
-    get valueTextField() { return this._valueTextField; }
     get enabled() { return this._property.enabled; }
 
     set index(index)
@@ -93,10 +101,24 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         this.updateStatus();
     }
 
-    detached()
+    startEditingName()
+    {
+        if (!this._nameTextField)
+            return;
+
+        this._nameTextField.startEditing();
+    }
+
+    startEditingValue()
     {
-        this._property.__propertyView = null;
+        if (!this._valueTextField)
+            return;
 
+        this._valueTextField.startEditing();
+    }
+
+    detached()
+    {
         if (this._nameTextField)
             this._nameTextField.detached();
 
@@ -112,13 +134,11 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             this._valueTextField.element.blur();
     }
 
-    highlight()
-    {
-        this._element.classList.add("highlighted");
-    }
-
     remove(replacement = null)
     {
+        if (this._delegate && typeof this._delegate.spreadsheetStylePropertyWillRemove === "function")
+            this._delegate.spreadsheetStylePropertyWillRemove(this);
+
         this.element.remove();
 
         if (replacement)
@@ -127,16 +147,13 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             this._property.remove();
 
         this.detached();
-
-        if (this._delegate && typeof this._delegate.spreadsheetStylePropertyRemoved === "function")
-            this._delegate.spreadsheetStylePropertyRemoved(this);
     }
 
     update()
     {
         this.element.removeChildren();
 
-        if (this._property.editable) {
+        if (this._isEditable()) {
             this._checkboxElement = this.element.appendChild(document.createElement("input"));
             this._checkboxElement.classList.add("property-toggle");
             this._checkboxElement.type = "checkbox";
@@ -144,9 +161,8 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             this._checkboxElement.tabIndex = -1;
             this._checkboxElement.addEventListener("click", (event) => {
                 event.stopPropagation();
-                let disabled = !this._checkboxElement.checked;
-                this._property.commentOut(disabled);
-                this.update();
+                this._toggle();
+                console.assert(this._checkboxElement.checked === this._property.enabled);
             });
         }
 
@@ -161,13 +177,14 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         this._nameElement.textContent = this._property.name;
 
         let colonElement = this._contentElement.appendChild(document.createElement("span"));
+        colonElement.classList.add("colon");
         colonElement.textContent = ": ";
 
         this._valueElement = this._contentElement.appendChild(document.createElement("span"));
         this._valueElement.classList.add("value");
         this._renderValue(this._property.rawValue);
 
-        if (this._property.editable && this._property.enabled) {
+        if (this._isEditable() && this._property.enabled) {
             this._nameElement.tabIndex = 0;
             this._nameElement.addEventListener("beforeinput", this._handleNameBeforeInput.bind(this));
             this._nameElement.addEventListener("paste", this._handleNamePaste.bind(this));
@@ -180,12 +197,13 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             this._valueTextField = new WI.SpreadsheetTextField(this, this._valueElement, this._valueCompletionDataProvider.bind(this));
         }
 
-        if (this._property.editable) {
+        if (this._isEditable()) {
             this._setupJumpToSymbol(this._nameElement);
             this._setupJumpToSymbol(this._valueElement);
         }
 
         let semicolonElement = this._contentElement.appendChild(document.createElement("span"));
+        semicolonElement.classList.add("semicolon");
         semicolonElement.textContent = ";";
 
         if (this._property.enabled) {
@@ -229,7 +247,7 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         let duplicatePropertyExistsBelow = (cssProperty) => {
             let propertyFound = false;
 
-            for (let property of this._property.ownerStyle.properties) {
+            for (let property of this._property.ownerStyle.enabledProperties) {
                 if (property === cssProperty)
                     propertyFound = true;
                 else if (property.name === cssProperty.name && propertyFound)
@@ -243,6 +261,22 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         let elementTitle = "";
 
         if (this._property.overridden) {
+            if (!this._jumpToEffectivePropertyButton && this._delegate && this._delegate.spreadsheetStylePropertySelectByProperty && WI.settings.experimentalEnableStylesJumpToEffective.value) {
+                console.assert(this._property.overridingProperty, `Overridden property is missing overridingProperty: ${this._property.formattedText}`);
+                if (this._property.overridingProperty) {
+                    this._jumpToEffectivePropertyButton = WI.createGoToArrowButton();
+                    this._jumpToEffectivePropertyButton.classList.add("select-effective-property");
+                    this._jumpToEffectivePropertyButton.dataset.value = this._property.overridingProperty.rawValue;
+                    this._element.append(this._jumpToEffectivePropertyButton);
+
+                    this._jumpToEffectivePropertyButton.addEventListener("click", (event) => {
+                        console.assert(this._property.overridingProperty);
+                        event.stop();
+                        this._delegate.spreadsheetStylePropertySelectByProperty(this._property.overridingProperty);
+                    });
+                }
+            }
+
             classNames.push("overridden");
             if (duplicatePropertyExistsBelow(this._property)) {
                 classNames.push("has-warning");
@@ -277,6 +311,9 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         if (!this._property.enabled)
             classNames.push("disabled");
 
+        if (this._property.modified && this._property.name && this._property.rawValue)
+            classNames.push("modified");
+
         if (this._selected)
             classNames.push("selected");
 
@@ -297,11 +334,6 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         return matches;
     }
 
-    handleCopyEvent(event)
-    {
-        this._delegate.spreadsheetStylePropertyCopy(event, this);
-    }
-
     // SpreadsheetTextField delegate
 
     spreadsheetTextFieldWillStartEditing(textField)
@@ -320,16 +352,14 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
 
     spreadsheetTextFieldDidCommit(textField, {direction})
     {
-        let propertyName = this._nameTextField.value.trim();
-        let propertyValue = this._valueTextField.value.trim();
         let willRemoveProperty = false;
         let isEditingName = textField === this._nameTextField;
 
-        if (!propertyName || (!propertyValue && !isEditingName && direction === "forward"))
+        if (!this._property.name || (!this._property.rawValue && !isEditingName && direction === "forward"))
             willRemoveProperty = true;
 
         if (!isEditingName && !willRemoveProperty)
-            this._renderValue(propertyValue);
+            this._renderValue(this._property.rawValue);
 
         if (direction === "forward") {
             if (isEditingName && !willRemoveProperty) {
@@ -354,7 +384,7 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             this.remove();
     }
 
-    spreadsheetTextFieldDidBlur(textField, event)
+    spreadsheetTextFieldDidBlur(textField, event, changed)
     {
         let focusedOutsideThisProperty = event.relatedTarget !== this._nameElement && event.relatedTarget !== this._valueElement;
         if (focusedOutsideThisProperty && (!this._nameTextField.value.trim() || !this._valueTextField.value.trim())) {
@@ -363,10 +393,13 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         }
 
         if (textField === this._valueTextField)
-            this._renderValue(this._valueElement.textContent);
+            this._renderValue(this._property.rawValue);
 
         if (typeof this._delegate.spreadsheetStylePropertyFocusMoved === "function")
             this._delegate.spreadsheetStylePropertyFocusMoved(this, {direction: null});
+
+        if (changed && window.DOMAgent)
+            DOMAgent.markUndoableState();
     }
 
     spreadsheetTextFieldDidBackspace(textField)
@@ -377,8 +410,36 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             this._nameTextField.startEditing();
     }
 
+    spreadsheetTextFieldDidPressEsc(textField, textBeforeEditing)
+    {
+        let isNewProperty = !textBeforeEditing;
+        if (isNewProperty)
+            this.remove();
+        else if (this._delegate.spreadsheetStylePropertyDidPressEsc)
+            this._delegate.spreadsheetStylePropertyDidPressEsc(this);
+    }
+
     // Private
 
+    _toggle()
+    {
+        this._property.commentOut(this.property.enabled);
+        this.update();
+    }
+
+    _select()
+    {
+        if (this._delegate && this._delegate.spreadsheetStylePropertySelect) {
+            let index = parseInt(this._element.dataset.propertyIndex);
+            this._delegate.spreadsheetStylePropertySelect(index);
+        }
+    }
+
+    _isEditable()
+    {
+        return !this._readOnly && this._property.editable;
+    }
+
     _renderValue(value)
     {
         this._hasInvalidVariableValue = false;
@@ -386,14 +447,8 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         const maxValueLength = 150;
         let tokens = WI.tokenizeCSSValue(value);
 
-        if (this._property.enabled) {
-            // FIXME: <https://webkit.org/b/178636> Web Inspector: Styles: Make inline widgets work with CSS functions (var(), calc(), etc.)
-            tokens = this._addGradientTokens(tokens);
-            tokens = this._addColorTokens(tokens);
-            tokens = this._addTimingFunctionTokens(tokens, "cubic-bezier");
-            tokens = this._addTimingFunctionTokens(tokens, "spring");
-            tokens = this._addVariableTokens(tokens);
-        }
+        if (this._property.enabled)
+            tokens = this._replaceSpecialTokens(tokens);
 
         tokens = tokens.map((token) => {
             if (token instanceof Element)
@@ -428,13 +483,20 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         this._valueElement.append(...tokens);
     }
 
-    _createInlineSwatch(type, text, valueObject)
+    _createInlineSwatch(type, contents, valueObject)
     {
         let tokenElement = document.createElement("span");
         let innerElement = document.createElement("span");
-        innerElement.textContent = text;
+        for (let item of contents) {
+            if (item instanceof Node)
+                innerElement.appendChild(item);
+            else if (typeof item === "object")
+                innerElement.append(item.value);
+            else
+                innerElement.append(item);
+        }
 
-        let readOnly = !this._property.editable;
+        let readOnly = !this._isEditable();
         let swatch = new WI.InlineSwatch(type, valueObject, readOnly);
 
         swatch.addEventListener(WI.InlineSwatch.Event.ValueChanged, (event) => {
@@ -444,36 +506,53 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
 
             innerElement.textContent = value;
             this._handleValueChange();
+
+            if (type === WI.InlineSwatch.Type.Variable)
+                this._renderValue(this._property.rawValue);
         }, this);
 
-        if (typeof this._delegate.stylePropertyInlineSwatchActivated === "function") {
+        if (type === WI.InlineSwatch.Type.Variable) {
+            swatch.value = () => {
+                return this._property.ownerStyle.nodeStyles.computedStyle.resolveVariableValue(innerElement.textContent);
+            };
+        }
+
+        if (this._delegate && typeof this._delegate.stylePropertyInlineSwatchActivated === "function") {
             swatch.addEventListener(WI.InlineSwatch.Event.Activated, () => {
-                this._swatchActive = true;
                 this._delegate.stylePropertyInlineSwatchActivated();
             });
         }
 
-        if (typeof this._delegate.stylePropertyInlineSwatchDeactivated === "function") {
+        if (this._delegate && typeof this._delegate.stylePropertyInlineSwatchDeactivated === "function") {
             swatch.addEventListener(WI.InlineSwatch.Event.Deactivated, () => {
-                this._swatchActive = false;
                 this._delegate.stylePropertyInlineSwatchDeactivated();
             });
         }
 
         tokenElement.append(swatch.element, innerElement);
 
-        // Prevent the value from editing when clicking on the swatch.
-        if (WI.settings.experimentalEnableMultiplePropertiesSelection.value) {
-            swatch.element.addEventListener("click", (event) => {
-                if (this._swatchActive)
-                    event.stop();
-            });
-        } else
-            swatch.element.addEventListener("mousedown", (event) => { event.stop(); });
-
         return tokenElement;
     }
 
+    _replaceSpecialTokens(tokens)
+    {
+        // FIXME: <https://webkit.org/b/178636> Web Inspector: Styles: Make inline widgets work with CSS functions (var(), calc(), etc.)
+
+        tokens = this._addVariableTokens(tokens);
+
+        if (this._property.variable || WI.CSSKeywordCompletions.isColorAwareProperty(this._property.name)) {
+            tokens = this._addGradientTokens(tokens);
+            tokens = this._addColorTokens(tokens);
+        }
+
+        if (this._property.variable || WI.CSSKeywordCompletions.isTimingFunctionAwareProperty(this._property.name)) {
+            tokens = this._addTimingFunctionTokens(tokens, "cubic-bezier");
+            tokens = this._addTimingFunctionTokens(tokens, "spring");
+        }
+
+        return tokens;
+    }
+
     _addGradientTokens(tokens)
     {
         let gradientRegex = /^(repeating-)?(linear|radial)-gradient$/i;
@@ -499,7 +578,7 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
                 let text = rawTokens.map((token) => token.value).join("");
                 let gradient = WI.Gradient.fromString(text);
                 if (gradient)
-                    newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Gradient, text, gradient));
+                    newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Gradient, rawTokens, gradient));
                 else
                     newTokens.push(...rawTokens);
 
@@ -518,7 +597,7 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         let pushPossibleColorToken = (text, ...rawTokens) => {
             let color = WI.Color.fromString(text);
             if (color)
-                newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Color, text, color));
+                newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Color, rawTokens, color));
             else
                 newTokens.push(...rawTokens);
         };
@@ -533,7 +612,7 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             } else if (WI.Color.FunctionNames.has(token.value) && token.type && (token.type.includes("atom") || token.type.includes("keyword"))) {
                 // Color Function start
                 colorFunctionStartIndex = i;
-            } else if (isNaN(colorFunctionStartIndex) && token.type && token.type.includes("keyword")) {
+            } else if (isNaN(colorFunctionStartIndex) && token.type && (token.type.includes("atom") || token.type.includes("keyword"))) {
                 // Color keyword
                 pushPossibleColorToken(token.value, token);
             } else if (!isNaN(colorFunctionStartIndex)) {
@@ -585,7 +664,7 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
                 }
 
                 if (valueObject)
-                    newTokens.push(this._createInlineSwatch(inlineSwatchType, text, valueObject));
+                    newTokens.push(this._createInlineSwatch(inlineSwatchType, rawTokens, valueObject));
                 else
                     newTokens.push(...rawTokens);
 
@@ -606,8 +685,10 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         for (let i = 0; i < tokens.length; i++) {
             let token = tokens[i];
             if (token.value === "var" && token.type && token.type.includes("atom")) {
-                startIndex = i;
-                openParenthesis = 0;
+                if (isNaN(startIndex)) {
+                    startIndex = i;
+                    openParenthesis = 0;
+                }
             } else if (token.value === "(" && !isNaN(startIndex))
                 ++openParenthesis;
             else if (token.value === ")" && !isNaN(startIndex)) {
@@ -616,14 +697,22 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
                     continue;
 
                 let rawTokens = tokens.slice(startIndex, i + 1);
-                let tokenValues = rawTokens.map((token) => token.value);
-                let variableName = tokenValues.find((value, i) => value.startsWith("--") && /\bvariable-2\b/.test(rawTokens[i].type));
-
-                const dontCreateIfMissing = true;
-                let variableProperty = this._property.ownerStyle.nodeStyles.computedStyle.propertyForName(variableName, dontCreateIfMissing);
-                if (variableProperty) {
-                    let valueObject = variableProperty.value.trim();
-                    newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Variable, tokenValues.join(""), valueObject));
+                let variableNameIndex = rawTokens.findIndex((token) => token.value.startsWith("--") && /\bvariable-2\b/.test(token.type));
+                if (variableNameIndex !== -1) {
+                    let contents = [];
+                    let fallbackStartIndex = rawTokens.findIndex((value, i) => i > variableNameIndex + 1 && /\bm-css\b/.test(value.type));
+                    if (fallbackStartIndex !== -1) {
+                        contents = contents.concat(rawTokens.slice(0, fallbackStartIndex));
+                        contents = contents.concat(this._replaceSpecialTokens(rawTokens.slice(fallbackStartIndex, i)));
+                    } else
+                        contents = contents.concat(rawTokens.slice(0, i));
+                    contents.push(token);
+
+                    let text = rawTokens.reduce((accumulator, token) => accumulator + token.value, "");
+                    if (this._property.ownerStyle.nodeStyles.computedStyle.resolveVariableValue(text))
+                        newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Variable, contents));
+                    else
+                        newTokens = newTokens.concat(contents);
                 } else {
                     this._hasInvalidVariableValue = true;
                     newTokens.push(...rawTokens);
@@ -674,9 +763,14 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         }
     }
 
-    _nameCompletionDataProvider(prefix)
+    _nameCompletionDataProvider(prefix, options = {})
     {
-        return WI.CSSCompletions.cssNameCompletions.startsWith(prefix);
+        let completions;
+        if (!prefix && options.allowEmptyPrefix)
+            completions = WI.CSSCompletions.cssNameCompletions.values;
+        else
+            completions = WI.CSSCompletions.cssNameCompletions.startsWith(prefix);
+        return {prefix, completions};
     }
 
     _handleValueBeforeInput(event)
@@ -689,20 +783,8 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         if (!selection.rangeCount || selection.getRangeAt(0).endOffset !== text.length)
             return;
 
-        // Find the first and last index (if any) of a quote character to ensure that the string
-        // doesn't contain unbalanced quotes. If so, then there's no way that the semicolon could be
-        // part of a string within the value, so we can assume that it's the property "terminator".
-        const quoteRegex = /["']/g;
-        let start = -1;
-        let end = text.length;
-        let match = null;
-        while (match = quoteRegex.exec(text)) {
-            if (start < 0)
-                start = match.index;
-            end = match.index + 1;
-        }
-
-        if (start !== -1 && !text.substring(start, end).hasMatchingEscapedQuotes())
+        let unbalancedCharacters = WI.CSSCompletions.completeUnbalancedValue(text);
+        if (unbalancedCharacters)
             return;
 
         event.preventDefault();
@@ -712,8 +794,19 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
 
     _valueCompletionDataProvider(prefix)
     {
+        // For "border: 1px so|", we want to suggest "solid" based on "so" prefix.
+        let match = prefix.match(/[a-z0-9()-]+$/i);
+
+        // Clicking on the value of `height: 100%` shouldn't show any completions.
+        if (!match && prefix)
+            return {completions: [], prefix: ""};
+
+        prefix = match ? match[0] : "";
         let propertyName = this._nameElement.textContent.trim();
-        return WI.CSSKeywordCompletions.forProperty(propertyName).startsWith(prefix);
+        return {
+            prefix,
+            completions: WI.CSSKeywordCompletions.forProperty(propertyName).startsWith(prefix)
+        };
     }
 
     _setupJumpToSymbol(element)