Web Inspector: Styles: variable swatch not shown for var() with a fallback
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / SpreadsheetStyleProperty.js
index 931059a..ed1d7b5 100644 (file)
@@ -25,7 +25,7 @@
 
 WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
 {
-    constructor(delegate, property, newlyAdded = false)
+    constructor(delegate, property, options = {})
     {
         super();
 
@@ -33,31 +33,92 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
 
         this._delegate = delegate || null;
         this._property = property;
-        this._newlyAdded = newlyAdded;
+        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;
 
-        this._update();
-        property.addEventListener(WI.CSSProperty.Event.OverriddenStatusChanged, this._update, this);
+        this.update();
+        property.addEventListener(WI.CSSProperty.Event.OverriddenStatusChanged, this.updateStatus, this);
+        property.addEventListener(WI.CSSProperty.Event.Changed, this.updateStatus, this);
+
+        if (!this._readOnly) {
+            this._element.tabIndex = -1;
+            property.addEventListener(WI.CSSProperty.Event.ModifiedChanged, this.updateStatus, this);
+
+            this._element.addEventListener("blur", (event) => {
+                // 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) => {
+                if (this._delegate.spreadsheetStylePropertyMouseEnter)
+                    this._delegate.spreadsheetStylePropertyMouseEnter(event, this);
+            });
+
+            new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, WI.KeyboardShortcut.Key.Slash, () => {
+                this._toggle();
+                this._select();
+            }, this._element);
+        }
     }
 
     // Public
 
     get element() { return this._element; }
-    get nameTextField() { return this._nameTextField; }
-    get valueTextField() { return this._valueTextField; }
+    get property() { return this._property; }
+    get enabled() { return this._property.enabled; }
 
-    detached()
+    set index(index)
+    {
+        this._element.dataset.propertyIndex = index;
+    }
+
+    get selected()
+    {
+        return this._selected;
+    }
+
+    set selected(value)
     {
-        this._property.__propertyView = null;
+        if (value === this._selected)
+            return;
 
+        this._selected = value;
+        this.updateStatus();
+    }
+
+    startEditingName()
+    {
+        if (!this._nameTextField)
+            return;
+
+        this._nameTextField.startEditing();
+    }
+
+    startEditingValue()
+    {
+        if (!this._valueTextField)
+            return;
+
+        this._valueTextField.startEditing();
+    }
+
+    detached()
+    {
         if (this._nameTextField)
             this._nameTextField.detached();
 
@@ -65,32 +126,128 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             this._valueTextField.detached();
     }
 
-    highlight()
+    hidden()
     {
-        this._element.classList.add("highlighted");
+        if (this._nameTextField && this._nameTextField.editing)
+            this._nameTextField.element.blur();
+        else if (this._valueTextField && this._valueTextField.editing)
+            this._valueTextField.element.blur();
     }
 
-    // Private
-
-    _remove()
+    remove(replacement = null)
     {
+        if (this._delegate && typeof this._delegate.spreadsheetStylePropertyWillRemove === "function")
+            this._delegate.spreadsheetStylePropertyWillRemove(this);
+
         this.element.remove();
-        this._property.remove();
-        this.detached();
 
-        if (this._delegate && typeof this._delegate.spreadsheetStylePropertyRemoved === "function")
-            this._delegate.spreadsheetStylePropertyRemoved(this);
+        if (replacement)
+            this._property.replaceWithText(replacement);
+        else
+            this._property.remove();
+
+        this.detached();
     }
 
-    _update()
+    update()
     {
         this.element.removeChildren();
-        this.element.className = "";
 
+        if (this._isEditable()) {
+            this._checkboxElement = this.element.appendChild(document.createElement("input"));
+            this._checkboxElement.classList.add("property-toggle");
+            this._checkboxElement.type = "checkbox";
+            this._checkboxElement.checked = this._property.enabled;
+            this._checkboxElement.tabIndex = -1;
+            this._checkboxElement.addEventListener("click", (event) => {
+                event.stopPropagation();
+                this._toggle();
+                console.assert(this._checkboxElement.checked === this._property.enabled);
+            });
+        }
+
+        this._contentElement = this.element.appendChild(document.createElement("span"));
+        this._contentElement.className = "content";
+
+        if (!this._property.enabled)
+            this._contentElement.append("/* ");
+
+        this._nameElement = this._contentElement.appendChild(document.createElement("span"));
+        this._nameElement.classList.add("name");
+        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._isEditable() && this._property.enabled) {
+            this._nameElement.tabIndex = 0;
+            this._nameElement.addEventListener("beforeinput", this._handleNameBeforeInput.bind(this));
+            this._nameElement.addEventListener("paste", this._handleNamePaste.bind(this));
+
+            this._nameTextField = new WI.SpreadsheetTextField(this, this._nameElement, this._nameCompletionDataProvider.bind(this));
+
+            this._valueElement.tabIndex = 0;
+            this._valueElement.addEventListener("beforeinput", this._handleValueBeforeInput.bind(this));
+
+            this._valueTextField = new WI.SpreadsheetTextField(this, this._valueElement, this._valueCompletionDataProvider.bind(this));
+        }
+
+        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) {
+            this._warningElement = this.element.appendChild(document.createElement("span"));
+            this._warningElement.className = "warning";
+        } else
+            this._contentElement.append(" */");
+
+        if (!this._property.implicit && this._property.ownerStyle.type === WI.CSSStyleDeclaration.Type.Computed) {
+            let effectiveProperty = this._property.ownerStyle.nodeStyles.effectivePropertyForName(this._property.name);
+            if (effectiveProperty && !effectiveProperty.styleSheetTextRange)
+                effectiveProperty = effectiveProperty.relatedShorthandProperty;
+
+            let ownerRule = effectiveProperty ? effectiveProperty.ownerStyle.ownerRule : null;
+
+            let arrowElement = this._contentElement.appendChild(WI.createGoToArrowButton());
+            arrowElement.addEventListener("click", (event) => {
+                if (!effectiveProperty || !ownerRule || !event.altKey) {
+                    if (this._delegate.spreadsheetStylePropertyShowProperty)
+                        this._delegate.spreadsheetStylePropertyShowProperty(this, this._property);
+                    return;
+                }
+
+                let sourceCode = ownerRule.sourceCodeLocation.sourceCode;
+                let {startLine, startColumn} = effectiveProperty.styleSheetTextRange;
+                WI.showSourceCodeLocation(sourceCode.createSourceCodeLocation(startLine, startColumn), {
+                    ignoreNetworkTab: true,
+                    ignoreSearchTab: true,
+                });
+            });
+
+            if (effectiveProperty && ownerRule)
+                arrowElement.title = WI.UIString("Option-click to show source");
+        }
+
+        this.updateStatus();
+    }
+
+    updateStatus()
+    {
         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)
@@ -100,10 +257,32 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             return false;
         };
 
-        let classNames = ["property"];
+        let classNames = [WI.SpreadsheetStyleProperty.StyleClassName];
+        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);
+                    });
+                }
+            }
 
-        if (this._property.overridden)
             classNames.push("overridden");
+            if (duplicatePropertyExistsBelow(this._property)) {
+                classNames.push("has-warning");
+                elementTitle = WI.UIString("Duplicate property");
+            }
+        }
 
         if (this._property.implicit)
             classNames.push("implicit");
@@ -113,63 +292,46 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
 
         if (!this._property.valid && this._property.hasOtherVendorNameOrKeyword())
             classNames.push("other-vendor");
-        else if (!this._property.valid) {
+        else if (this._hasInvalidVariableValue || (!this._property.valid && this._property.value !== "")) {
             let propertyNameIsValid = false;
             if (WI.CSSCompletions.cssNameCompletions)
                 propertyNameIsValid = WI.CSSCompletions.cssNameCompletions.isValidPropertyName(this._property.name);
 
-            if (!propertyNameIsValid || duplicatePropertyExistsBelow(this._property))
-                classNames.push("invalid");
-        }
-
-        if (!this._property.enabled)
-            classNames.push("disabled");
-
-        this._element.classList.add(...classNames);
+            classNames.push("has-warning");
 
-        if (this._property.editable) {
-            this._checkboxElement = this.element.appendChild(document.createElement("input"));
-            this._checkboxElement.classList.add("property-toggle");
-            this._checkboxElement.type = "checkbox";
-            this._checkboxElement.checked = this._property.enabled;
-            this._checkboxElement.tabIndex = -1;
-            this._checkboxElement.addEventListener("change", () => {
-                let disabled = !this._checkboxElement.checked;
-                this._property.commentOut(disabled);
-                this._update();
-            });
+            if (!propertyNameIsValid) {
+                classNames.push("invalid-name");
+                elementTitle = WI.UIString("Unsupported property name");
+            } else {
+                classNames.push("invalid-value");
+                elementTitle = WI.UIString("Unsupported property value");
+            }
         }
 
         if (!this._property.enabled)
-            this.element.append("/* ");
-
-        this._nameElement = this.element.appendChild(document.createElement("span"));
-        this._nameElement.classList.add("name");
-        this._nameElement.textContent = this._property.name;
-
-        this.element.append(": ");
+            classNames.push("disabled");
 
-        this._valueElement = this.element.appendChild(document.createElement("span"));
-        this._valueElement.classList.add("value");
-        this._renderValue(this._property.rawValue);
+        if (this._property.modified && this._property.name && this._property.rawValue)
+            classNames.push("modified");
 
-        if (this._property.editable && this._property.enabled) {
-            this._nameElement.tabIndex = 0;
-            this._nameTextField = new WI.SpreadsheetTextField(this, this._nameElement, this._nameCompletionDataProvider.bind(this));
+        if (this._selected)
+            classNames.push("selected");
 
-            this._valueElement.tabIndex = 0;
-            this._valueTextField = new WI.SpreadsheetTextField(this, this._valueElement, this._valueCompletionDataProvider.bind(this));
-        }
+        this._element.className = classNames.join(" ");
+        this._element.title = elementTitle;
+    }
 
-        if (this._property.editable) {
-            this._setupJumpToSymbol(this._nameElement);
-            this._setupJumpToSymbol(this._valueElement);
-        }
+    applyFilter(filterText)
+    {
+        let matchesName = this._nameElement.textContent.includes(filterText);
+        this._nameElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.FilterMatchSectionClassName, !!matchesName);
 
-        this.element.append(";");
+        let matchesValue = this._valueElement.textContent.includes(filterText);
+        this._valueElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.FilterMatchSectionClassName, !!matchesValue);
 
-        if (!this._property.enabled)
-            this.element.append(" */");
+        let matches = matchesName || matchesValue;
+        this._contentElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.NoFilterMatchInPropertyClassName, !matches);
+        return matches;
     }
 
     // SpreadsheetTextField delegate
@@ -183,30 +345,21 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
     spreadsheetTextFieldDidChange(textField)
     {
         if (textField === this._valueTextField)
-            this.debounce(WI.SpreadsheetStyleProperty.CommitCoalesceDelay)._handleValueChange();
+            this._handleValueChange();
         else if (textField === this._nameTextField)
-            this.debounce(WI.SpreadsheetStyleProperty.CommitCoalesceDelay)._handleNameChange();
+            this._handleNameChange();
     }
 
     spreadsheetTextFieldDidCommit(textField, {direction})
     {
-        let propertyName = this._nameTextField.value.trim();
-        let propertyValue = this._valueTextField.value.trim();
         let willRemoveProperty = false;
+        let isEditingName = textField === this._nameTextField;
 
-        // Remove a property with an empty name or value. However, a newly added property
-        // has an empty name and value at first. Don't remove it when moving focus from
-        // the name to the value for the first time.
-        if (!propertyName || (!this._newlyAdded && !propertyValue))
+        if (!this._property.name || (!this._property.rawValue && !isEditingName && direction === "forward"))
             willRemoveProperty = true;
 
-        let isEditingName = textField === this._nameTextField;
-
         if (!isEditingName && !willRemoveProperty)
-            this._renderValue(propertyValue);
-
-        if (propertyName && isEditingName)
-            this._newlyAdded = false;
+            this._renderValue(this._property.rawValue);
 
         if (direction === "forward") {
             if (isEditingName && !willRemoveProperty) {
@@ -222,35 +375,80 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             }
         }
 
-        if (typeof this._delegate.spreadsheetCSSStyleDeclarationEditorFocusMoved === "function") {
+        if (typeof this._delegate.spreadsheetStylePropertyFocusMoved === "function") {
             // Move focus away from the current property, to the next or previous one, if exists, or to the next or previous rule, if exists.
-            this._delegate.spreadsheetCSSStyleDeclarationEditorFocusMoved({direction, willRemoveProperty, movedFromProperty: this});
+            this._delegate.spreadsheetStylePropertyFocusMoved(this, {direction, willRemoveProperty});
         }
 
         if (willRemoveProperty)
-            this._remove();
+            this.remove();
     }
 
-    spreadsheetTextFieldDidBlur(textField)
+    spreadsheetTextFieldDidBlur(textField, event, changed)
     {
-        if (textField.value.trim() === "")
-            this._remove();
+        let focusedOutsideThisProperty = event.relatedTarget !== this._nameElement && event.relatedTarget !== this._valueElement;
+        if (focusedOutsideThisProperty && (!this._nameTextField.value.trim() || !this._valueTextField.value.trim())) {
+            this.remove();
+            return;
+        }
+
+        if (textField === this._valueTextField)
+            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)
+    {
+        if (textField === this._nameTextField)
+            this.spreadsheetTextFieldDidCommit(textField, {direction: "backward"});
         else if (textField === this._valueTextField)
-            this._renderValue(this._valueElement.textContent);
+            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;
+
         const maxValueLength = 150;
         let tokens = WI.tokenizeCSSValue(value);
 
-        if (this._property.enabled) {
-            // Don't show color widgets for CSS gradients, show dedicated gradient widgets instead.
-            // FIXME: <https://webkit.org/b/178404> Web Inspector: [PARITY] Styles Redesign: Add bezier curve, color gradient, and CSS variable inline widgets
-            tokens = this._addColorTokens(tokens);
-        }
+        if (this._property.enabled)
+            tokens = this._replaceSpecialTokens(tokens);
 
         tokens = tokens.map((token) => {
             if (token instanceof Element)
@@ -270,7 +468,11 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             if (className) {
                 let span = document.createElement("span");
                 span.classList.add(className);
-                span.textContent = token.value.trimMiddle(maxValueLength);
+                span.textContent = token.value.truncateMiddle(maxValueLength);
+
+                if (token.type && token.type.includes("link"))
+                    span.addEventListener("contextmenu", this._handleLinkContextMenu.bind(this, token));
+
                 return span;
             }
 
@@ -281,48 +483,123 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         this._valueElement.append(...tokens);
     }
 
-    _addColorTokens(tokens)
+    _createInlineSwatch(type, contents, valueObject)
     {
-        let newTokens = [];
+        let tokenElement = document.createElement("span");
+        let innerElement = document.createElement("span");
+        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 createColorTokenElement = (colorString, color) => {
-            let colorTokenElement = document.createElement("span");
-            colorTokenElement.className = "token-color";
+        let readOnly = !this._isEditable();
+        let swatch = new WI.InlineSwatch(type, valueObject, readOnly);
 
-            let innerElement = document.createElement("span");
-            innerElement.className = "token-color-value";
-            innerElement.textContent = colorString;
+        swatch.addEventListener(WI.InlineSwatch.Event.ValueChanged, (event) => {
+            let value = event.data.value && event.data.value.toString();
+            if (!value)
+                return;
 
-            if (color) {
-                let readOnly = !this._property.editable;
-                let swatch = new WI.InlineSwatch(WI.InlineSwatch.Type.Color, color, readOnly);
+            innerElement.textContent = value;
+            this._handleValueChange();
 
-                swatch.addEventListener(WI.InlineSwatch.Event.ValueChanged, (event) => {
-                    let value = event.data && event.data.value && event.data.value.toString();
-                    console.assert(value, "Color value is empty.");
-                    if (!value)
-                        return;
+            if (type === WI.InlineSwatch.Type.Variable)
+                this._renderValue(this._property.rawValue);
+        }, this);
 
-                    innerElement.textContent = value;
-                    this._handleValueChange();
-                }, this);
+        if (type === WI.InlineSwatch.Type.Variable) {
+            swatch.value = () => {
+                return this._property.ownerStyle.nodeStyles.computedStyle.resolveVariableValue(innerElement.textContent);
+            };
+        }
 
-                colorTokenElement.append(swatch.element);
+        if (this._delegate && typeof this._delegate.stylePropertyInlineSwatchActivated === "function") {
+            swatch.addEventListener(WI.InlineSwatch.Event.Activated, () => {
+                this._delegate.stylePropertyInlineSwatchActivated();
+            });
+        }
 
-                // Prevent the value from editing when clicking on the swatch.
-                swatch.element.addEventListener("mousedown", (event) => { event.stop(); });
-            }
+        if (this._delegate && typeof this._delegate.stylePropertyInlineSwatchDeactivated === "function") {
+            swatch.addEventListener(WI.InlineSwatch.Event.Deactivated, () => {
+                this._delegate.stylePropertyInlineSwatchDeactivated();
+            });
+        }
 
-            colorTokenElement.append(innerElement);
-            return colorTokenElement;
-        };
+        tokenElement.append(swatch.element, innerElement);
+
+        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;
+        let newTokens = [];
+        let gradientStartIndex = NaN;
+        let openParenthesis = 0;
+
+        for (let i = 0; i < tokens.length; i++) {
+            let token = tokens[i];
+            if (token.type && token.type.includes("atom") && gradientRegex.test(token.value)) {
+                gradientStartIndex = i;
+                openParenthesis = 0;
+            } else if (token.value === "(" && !isNaN(gradientStartIndex))
+                openParenthesis++;
+            else if (token.value === ")" && !isNaN(gradientStartIndex)) {
+                openParenthesis--;
+                if (openParenthesis > 0) {
+                    // Matched a CSS function inside of the gradient.
+                    continue;
+                }
+
+                let rawTokens = tokens.slice(gradientStartIndex, i + 1);
+                let text = rawTokens.map((token) => token.value).join("");
+                let gradient = WI.Gradient.fromString(text);
+                if (gradient)
+                    newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Gradient, rawTokens, gradient));
+                else
+                    newTokens.push(...rawTokens);
+
+                gradientStartIndex = NaN;
+            } else if (isNaN(gradientStartIndex))
+                newTokens.push(token);
+        }
+
+        return newTokens;
+    }
+
+    _addColorTokens(tokens)
+    {
+        let newTokens = [];
 
-        let pushPossibleColorToken = (text, ...tokens) => {
+        let pushPossibleColorToken = (text, ...rawTokens) => {
             let color = WI.Color.fromString(text);
             if (color)
-                newTokens.push(createColorTokenElement(text, color));
+                newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Color, rawTokens, color));
             else
-                newTokens.push(...tokens);
+                newTokens.push(...rawTokens);
         };
 
         let colorFunctionStartIndex = NaN;
@@ -335,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)) {
@@ -354,6 +631,101 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         return newTokens;
     }
 
+    _addTimingFunctionTokens(tokens, tokenType)
+    {
+        let newTokens = [];
+        let startIndex = NaN;
+        let openParenthesis = 0;
+
+        for (let i = 0; i < tokens.length; i++) {
+            let token = tokens[i];
+            if (token.value === tokenType && token.type && token.type.includes("atom")) {
+                startIndex = i;
+                openParenthesis = 0;
+            } else if (token.value === "(" && !isNaN(startIndex))
+                openParenthesis++;
+            else if (token.value === ")" && !isNaN(startIndex)) {
+
+                openParenthesis--;
+                if (openParenthesis > 0)
+                    continue;
+
+                let rawTokens = tokens.slice(startIndex, i + 1);
+                let text = rawTokens.map((token) => token.value).join("");
+
+                let valueObject;
+                let inlineSwatchType;
+                if (tokenType === "cubic-bezier") {
+                    valueObject = WI.CubicBezier.fromString(text);
+                    inlineSwatchType = WI.InlineSwatch.Type.Bezier;
+                } else if (tokenType === "spring") {
+                    valueObject = WI.Spring.fromString(text);
+                    inlineSwatchType = WI.InlineSwatch.Type.Spring;
+                }
+
+                if (valueObject)
+                    newTokens.push(this._createInlineSwatch(inlineSwatchType, rawTokens, valueObject));
+                else
+                    newTokens.push(...rawTokens);
+
+                startIndex = NaN;
+            } else if (isNaN(startIndex))
+                newTokens.push(token);
+        }
+
+        return newTokens;
+    }
+
+    _addVariableTokens(tokens)
+    {
+        let newTokens = [];
+        let startIndex = NaN;
+        let openParenthesis = 0;
+
+        for (let i = 0; i < tokens.length; i++) {
+            let token = tokens[i];
+            if (token.value === "var" && token.type && token.type.includes("atom")) {
+                if (isNaN(startIndex)) {
+                    startIndex = i;
+                    openParenthesis = 0;
+                }
+            } else if (token.value === "(" && !isNaN(startIndex))
+                ++openParenthesis;
+            else if (token.value === ")" && !isNaN(startIndex)) {
+                --openParenthesis;
+                if (openParenthesis > 0)
+                    continue;
+
+                let rawTokens = tokens.slice(startIndex, i + 1);
+                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);
+                }
+
+                startIndex = NaN;
+            } else if (isNaN(startIndex))
+                newTokens.push(token);
+        }
+
+        return newTokens;
+    }
+
     _handleNameChange()
     {
         this._property.name = this._nameElement.textContent.trim();
@@ -364,14 +736,77 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
         this._property.rawValue = this._valueElement.textContent.trim();
     }
 
-    _nameCompletionDataProvider(prefix)
+    _handleNameBeforeInput(event)
     {
-        return WI.CSSCompletions.cssNameCompletions.startsWith(prefix);
+        if (event.data !== ":" || event.inputType !== "insertText")
+            return;
+
+        event.preventDefault();
+        this._nameTextField.discardCompletion();
+        this._valueTextField.startEditing();
+    }
+
+    _handleNamePaste(event)
+    {
+        let text = event.clipboardData.getData("text/plain");
+        if (!text || !text.includes(":"))
+            return;
+
+        event.preventDefault();
+
+        this.remove(text);
+
+        if (this._delegate.spreadsheetStylePropertyAddBlankPropertySoon) {
+            this._delegate.spreadsheetStylePropertyAddBlankPropertySoon(this, {
+                index: parseInt(this._element.dataset.propertyIndex) + 1,
+            });
+        }
+    }
+
+    _nameCompletionDataProvider(prefix, options = {})
+    {
+        let completions;
+        if (!prefix && options.allowEmptyPrefix)
+            completions = WI.CSSCompletions.cssNameCompletions.values;
+        else
+            completions = WI.CSSCompletions.cssNameCompletions.startsWith(prefix);
+        return {prefix, completions};
+    }
+
+    _handleValueBeforeInput(event)
+    {
+        if (event.data !== ";" || event.inputType !== "insertText")
+            return;
+
+        let text = this._valueTextField.valueWithoutSuggestion();
+        let selection = window.getSelection();
+        if (!selection.rangeCount || selection.getRangeAt(0).endOffset !== text.length)
+            return;
+
+        let unbalancedCharacters = WI.CSSCompletions.completeUnbalancedValue(text);
+        if (unbalancedCharacters)
+            return;
+
+        event.preventDefault();
+        this._valueTextField.stopEditing();
+        this.spreadsheetTextFieldDidCommit(this._valueTextField, {direction: "forward"});
     }
 
     _valueCompletionDataProvider(prefix)
     {
-        return WI.CSSKeywordCompletions.forProperty(this._property.name).startsWith(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 {
+            prefix,
+            completions: WI.CSSKeywordCompletions.forProperty(propertyName).startsWith(prefix)
+        };
     }
 
     _setupJumpToSymbol(element)
@@ -402,6 +837,53 @@ WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
             WI.showSourceCodeLocation(sourceCode.createSourceCodeLocation(range.startLine, range.startColumn), options);
         });
     }
+
+    _handleLinkContextMenu(token, event)
+    {
+        let contextMenu = WI.ContextMenu.createFromEvent(event);
+
+        let resolveURL = (url) => {
+            let ownerStyle = this._property.ownerStyle;
+            if (!ownerStyle)
+                return url;
+
+            let ownerStyleSheet = ownerStyle.ownerStyleSheet;
+            if (!ownerStyleSheet) {
+                let ownerRule = ownerStyle.ownerRule;
+                if (ownerRule)
+                    ownerStyleSheet = ownerRule.ownerStyleSheet;
+            }
+            if (ownerStyleSheet) {
+                if (ownerStyleSheet.url)
+                    return absoluteURL(url, ownerStyleSheet.url);
+
+                let parentFrame = ownerStyleSheet.parentFrame;
+                if (parentFrame)
+                    return absoluteURL(url, parentFrame.url);
+            }
+
+            let node = ownerStyle.node;
+            if (!node) {
+                let nodeStyles = ownerStyle.nodeStyles;
+                if (!nodeStyles) {
+                    let ownerRule = ownerStyle.ownerRule;
+                    if (ownerRule)
+                        nodeStyles = ownerRule.nodeStyles;
+                }
+                if (nodeStyles)
+                    node = nodeStyles.node;
+            }
+            if (node) {
+                let ownerDocument = node.ownerDocument;
+                if (ownerDocument)
+                    return absoluteURL(url, node.ownerDocument.documentURL);
+            }
+
+            return url;
+        };
+
+        WI.appendContextMenuItemsForURL(contextMenu, resolveURL(token.value));
+    }
 };
 
-WI.SpreadsheetStyleProperty.CommitCoalesceDelay = 250;
+WI.SpreadsheetStyleProperty.StyleClassName = "property";