Web Inspector: Styles: unbalanced quotes and parenthesis aren't displayed as property...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / SpreadsheetStyleProperty.js
1 /*
2  * Copyright (C) 2017 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
27 {
28     constructor(delegate, property, options = {})
29     {
30         super();
31
32         console.assert(property instanceof WI.CSSProperty);
33
34         this._delegate = delegate || null;
35         this._property = property;
36         this._readOnly = options.readOnly || false;
37         this._element = document.createElement("div");
38
39         this._contentElement = null;
40         this._nameElement = null;
41         this._valueElement = null;
42         this._jumpToEffectivePropertyButton = null;
43
44         this._nameTextField = null;
45         this._valueTextField = null;
46
47         this._selected = false;
48         this._hasInvalidVariableValue = false;
49
50         this.update();
51         property.addEventListener(WI.CSSProperty.Event.OverriddenStatusChanged, this.updateStatus, this);
52         property.addEventListener(WI.CSSProperty.Event.Changed, this.updateStatus, this);
53
54         if (!this._readOnly) {
55             this._element.tabIndex = -1;
56             property.addEventListener(WI.CSSProperty.Event.ModifiedChanged, this.updateStatus, this);
57
58             this._element.addEventListener("blur", (event) => {
59                 // Keep selection after tabbing out of Web Inspector window and back.
60                 if (document.activeElement === this._element)
61                     return;
62
63                 if (this._delegate.spreadsheetStylePropertyBlur)
64                     this._delegate.spreadsheetStylePropertyBlur(event, this);
65             });
66
67             this._element.addEventListener("mouseenter", (event) => {
68                 if (this._delegate.spreadsheetStylePropertyMouseEnter)
69                     this._delegate.spreadsheetStylePropertyMouseEnter(event, this);
70             });
71
72             new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, WI.KeyboardShortcut.Key.Slash, () => {
73                 this._toggle();
74                 this._select();
75             }, this._element);
76
77             this._element.copyHandler = this;
78         }
79     }
80
81     // Public
82
83     get element() { return this._element; }
84     get property() { return this._property; }
85     get enabled() { return this._property.enabled; }
86
87     set index(index)
88     {
89         this._element.dataset.propertyIndex = index;
90     }
91
92     get selected()
93     {
94         return this._selected;
95     }
96
97     set selected(value)
98     {
99         if (value === this._selected)
100             return;
101
102         this._selected = value;
103         this.updateStatus();
104     }
105
106     startEditingName()
107     {
108         if (!this._nameTextField)
109             return;
110
111         this._nameTextField.startEditing();
112     }
113
114     startEditingValue()
115     {
116         if (!this._valueTextField)
117             return;
118
119         this._valueTextField.startEditing();
120     }
121
122     detached()
123     {
124         if (this._nameTextField)
125             this._nameTextField.detached();
126
127         if (this._valueTextField)
128             this._valueTextField.detached();
129     }
130
131     hidden()
132     {
133         if (this._nameTextField && this._nameTextField.editing)
134             this._nameTextField.element.blur();
135         else if (this._valueTextField && this._valueTextField.editing)
136             this._valueTextField.element.blur();
137     }
138
139     remove(replacement = null)
140     {
141         if (this._delegate && typeof this._delegate.spreadsheetStylePropertyWillRemove === "function")
142             this._delegate.spreadsheetStylePropertyWillRemove(this);
143
144         this.element.remove();
145
146         if (replacement)
147             this._property.replaceWithText(replacement);
148         else
149             this._property.remove();
150
151         this.detached();
152     }
153
154     update()
155     {
156         this.element.removeChildren();
157
158         if (this._isEditable()) {
159             this._checkboxElement = this.element.appendChild(document.createElement("input"));
160             this._checkboxElement.classList.add("property-toggle");
161             this._checkboxElement.type = "checkbox";
162             this._checkboxElement.checked = this._property.enabled;
163             this._checkboxElement.tabIndex = -1;
164             this._checkboxElement.addEventListener("click", (event) => {
165                 event.stopPropagation();
166                 this._toggle();
167                 console.assert(this._checkboxElement.checked === this._property.enabled);
168             });
169         }
170
171         this._contentElement = this.element.appendChild(document.createElement("span"));
172         this._contentElement.className = "content";
173
174         if (!this._property.enabled)
175             this._contentElement.append("/* ");
176
177         this._nameElement = this._contentElement.appendChild(document.createElement("span"));
178         this._nameElement.classList.add("name");
179         this._nameElement.textContent = this._property.name;
180
181         let colonElement = this._contentElement.appendChild(document.createElement("span"));
182         colonElement.classList.add("colon");
183         colonElement.textContent = ": ";
184
185         this._valueElement = this._contentElement.appendChild(document.createElement("span"));
186         this._valueElement.classList.add("value");
187         this._renderValue(this._property.rawValue);
188
189         if (this._isEditable() && this._property.enabled) {
190             this._nameElement.tabIndex = 0;
191             this._nameElement.addEventListener("beforeinput", this._handleNameBeforeInput.bind(this));
192             this._nameElement.addEventListener("paste", this._handleNamePaste.bind(this));
193
194             this._nameTextField = new WI.SpreadsheetTextField(this, this._nameElement, this._nameCompletionDataProvider.bind(this));
195
196             this._valueElement.tabIndex = 0;
197             this._valueElement.addEventListener("beforeinput", this._handleValueBeforeInput.bind(this));
198
199             this._valueTextField = new WI.SpreadsheetTextField(this, this._valueElement, this._valueCompletionDataProvider.bind(this));
200         }
201
202         if (this._isEditable()) {
203             this._setupJumpToSymbol(this._nameElement);
204             this._setupJumpToSymbol(this._valueElement);
205         }
206
207         let semicolonElement = this._contentElement.appendChild(document.createElement("span"));
208         semicolonElement.classList.add("semicolon");
209         semicolonElement.textContent = ";";
210
211         if (this._property.enabled) {
212             this._warningElement = this.element.appendChild(document.createElement("span"));
213             this._warningElement.className = "warning";
214         } else
215             this._contentElement.append(" */");
216
217         if (!this._property.implicit && this._property.ownerStyle.type === WI.CSSStyleDeclaration.Type.Computed) {
218             let effectiveProperty = this._property.ownerStyle.nodeStyles.effectivePropertyForName(this._property.name);
219             if (effectiveProperty && !effectiveProperty.styleSheetTextRange)
220                 effectiveProperty = effectiveProperty.relatedShorthandProperty;
221
222             let ownerRule = effectiveProperty ? effectiveProperty.ownerStyle.ownerRule : null;
223
224             let arrowElement = this._contentElement.appendChild(WI.createGoToArrowButton());
225             arrowElement.addEventListener("click", (event) => {
226                 if (!effectiveProperty || !ownerRule || !event.altKey) {
227                     if (this._delegate.spreadsheetStylePropertyShowProperty)
228                         this._delegate.spreadsheetStylePropertyShowProperty(this, this._property);
229                     return;
230                 }
231
232                 let sourceCode = ownerRule.sourceCodeLocation.sourceCode;
233                 let {startLine, startColumn} = effectiveProperty.styleSheetTextRange;
234                 WI.showSourceCodeLocation(sourceCode.createSourceCodeLocation(startLine, startColumn), {
235                     ignoreNetworkTab: true,
236                     ignoreSearchTab: true,
237                 });
238             });
239
240             if (effectiveProperty && ownerRule)
241                 arrowElement.title = WI.UIString("Option-click to show source");
242         }
243
244         this.updateStatus();
245     }
246
247     updateStatus()
248     {
249         let duplicatePropertyExistsBelow = (cssProperty) => {
250             let propertyFound = false;
251
252             for (let property of this._property.ownerStyle.enabledProperties) {
253                 if (property === cssProperty)
254                     propertyFound = true;
255                 else if (property.name === cssProperty.name && propertyFound)
256                     return true;
257             }
258
259             return false;
260         };
261
262         let classNames = [WI.SpreadsheetStyleProperty.StyleClassName];
263         let elementTitle = "";
264
265         if (this._property.overridden) {
266             if (!this._jumpToEffectivePropertyButton && this._delegate && this._delegate.spreadsheetStylePropertySelectByProperty && WI.settings.experimentalEnableStylesJumpToEffective.value) {
267                 console.assert(this._property.overridingProperty, `Overridden property is missing overridingProperty: ${this._property.formattedText}`);
268                 if (this._property.overridingProperty) {
269                     this._jumpToEffectivePropertyButton = WI.createGoToArrowButton();
270                     this._jumpToEffectivePropertyButton.classList.add("select-effective-property");
271                     this._jumpToEffectivePropertyButton.dataset.value = this._property.overridingProperty.rawValue;
272                     this._element.append(this._jumpToEffectivePropertyButton);
273
274                     this._jumpToEffectivePropertyButton.addEventListener("click", (event) => {
275                         console.assert(this._property.overridingProperty);
276                         event.stop();
277                         this._delegate.spreadsheetStylePropertySelectByProperty(this._property.overridingProperty);
278                     });
279                 }
280             }
281
282             classNames.push("overridden");
283             if (duplicatePropertyExistsBelow(this._property)) {
284                 classNames.push("has-warning");
285                 elementTitle = WI.UIString("Duplicate property");
286             }
287         }
288
289         if (this._property.implicit)
290             classNames.push("implicit");
291
292         if (this._property.ownerStyle.inherited && !this._property.inherited)
293             classNames.push("not-inherited");
294
295         if (!this._property.valid && this._property.hasOtherVendorNameOrKeyword())
296             classNames.push("other-vendor");
297         else if (this._hasInvalidVariableValue || (!this._property.valid && this._property.value !== "")) {
298             let propertyNameIsValid = false;
299             if (WI.CSSCompletions.cssNameCompletions)
300                 propertyNameIsValid = WI.CSSCompletions.cssNameCompletions.isValidPropertyName(this._property.name);
301
302             classNames.push("has-warning");
303
304             if (!propertyNameIsValid) {
305                 classNames.push("invalid-name");
306                 elementTitle = WI.UIString("Unsupported property name");
307             } else {
308                 classNames.push("invalid-value");
309                 elementTitle = WI.UIString("Unsupported property value");
310             }
311         }
312
313         if (!this._property.enabled)
314             classNames.push("disabled");
315
316         if (this._property.modified && this._property.name && this._property.rawValue)
317             classNames.push("modified");
318
319         if (this._selected)
320             classNames.push("selected");
321
322         this._element.className = classNames.join(" ");
323         this._element.title = elementTitle;
324     }
325
326     applyFilter(filterText)
327     {
328         let matchesName = this._nameElement.textContent.includes(filterText);
329         this._nameElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.FilterMatchSectionClassName, !!matchesName);
330
331         let matchesValue = this._valueElement.textContent.includes(filterText);
332         this._valueElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.FilterMatchSectionClassName, !!matchesValue);
333
334         let matches = matchesName || matchesValue;
335         this._contentElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.NoFilterMatchInPropertyClassName, !matches);
336         return matches;
337     }
338
339     handleCopyEvent(event)
340     {
341         this._delegate.spreadsheetStylePropertyCopy(event, this);
342     }
343
344     // SpreadsheetTextField delegate
345
346     spreadsheetTextFieldWillStartEditing(textField)
347     {
348         let isEditingName = textField === this._nameTextField;
349         textField.value = isEditingName ? this._property.name : this._property.rawValue;
350     }
351
352     spreadsheetTextFieldDidChange(textField)
353     {
354         if (textField === this._valueTextField)
355             this._handleValueChange();
356         else if (textField === this._nameTextField)
357             this._handleNameChange();
358     }
359
360     spreadsheetTextFieldDidCommit(textField, {direction})
361     {
362         let willRemoveProperty = false;
363         let isEditingName = textField === this._nameTextField;
364
365         if (!this._property.name || (!this._property.rawValue && !isEditingName && direction === "forward"))
366             willRemoveProperty = true;
367
368         if (!isEditingName && !willRemoveProperty)
369             this._renderValue(this._property.rawValue);
370
371         if (direction === "forward") {
372             if (isEditingName && !willRemoveProperty) {
373                 // Move focus from the name to the value.
374                 this._valueTextField.startEditing();
375                 return;
376             }
377         } else {
378             if (!isEditingName) {
379                 // Move focus from the value to the name.
380                 this._nameTextField.startEditing();
381                 return;
382             }
383         }
384
385         if (typeof this._delegate.spreadsheetStylePropertyFocusMoved === "function") {
386             // Move focus away from the current property, to the next or previous one, if exists, or to the next or previous rule, if exists.
387             this._delegate.spreadsheetStylePropertyFocusMoved(this, {direction, willRemoveProperty});
388         }
389
390         if (willRemoveProperty)
391             this.remove();
392     }
393
394     spreadsheetTextFieldDidBlur(textField, event, changed)
395     {
396         let focusedOutsideThisProperty = event.relatedTarget !== this._nameElement && event.relatedTarget !== this._valueElement;
397         if (focusedOutsideThisProperty && (!this._nameTextField.value.trim() || !this._valueTextField.value.trim())) {
398             this.remove();
399             return;
400         }
401
402         if (textField === this._valueTextField)
403             this._renderValue(this._property.rawValue);
404
405         if (typeof this._delegate.spreadsheetStylePropertyFocusMoved === "function")
406             this._delegate.spreadsheetStylePropertyFocusMoved(this, {direction: null});
407
408         if (changed && window.DOMAgent)
409             DOMAgent.markUndoableState();
410     }
411
412     spreadsheetTextFieldDidBackspace(textField)
413     {
414         if (textField === this._nameTextField)
415             this.spreadsheetTextFieldDidCommit(textField, {direction: "backward"});
416         else if (textField === this._valueTextField)
417             this._nameTextField.startEditing();
418     }
419
420     spreadsheetTextFieldDidPressEsc(textField, textBeforeEditing)
421     {
422         let isNewProperty = !textBeforeEditing;
423         if (isNewProperty)
424             this.remove();
425         else if (this._delegate.spreadsheetStylePropertyDidPressEsc)
426             this._delegate.spreadsheetStylePropertyDidPressEsc(this);
427     }
428
429     // Private
430
431     _toggle()
432     {
433         this._property.commentOut(this.property.enabled);
434         this.update();
435     }
436
437     _select()
438     {
439         if (this._delegate && this._delegate.spreadsheetStylePropertySelect) {
440             let index = parseInt(this._element.dataset.propertyIndex);
441             this._delegate.spreadsheetStylePropertySelect(index);
442         }
443     }
444
445     _isEditable()
446     {
447         return !this._readOnly && this._property.editable;
448     }
449
450     _renderValue(value)
451     {
452         this._hasInvalidVariableValue = false;
453
454         const maxValueLength = 150;
455         let tokens = WI.tokenizeCSSValue(value);
456
457         if (this._property.enabled) {
458             // FIXME: <https://webkit.org/b/178636> Web Inspector: Styles: Make inline widgets work with CSS functions (var(), calc(), etc.)
459
460             // CSS variables may contain color - display color picker for them.
461             if (this._property.variable || WI.CSSKeywordCompletions.isColorAwareProperty(this._property.name)) {
462                 tokens = this._addGradientTokens(tokens);
463                 tokens = this._addColorTokens(tokens);
464             }
465             tokens = this._addTimingFunctionTokens(tokens, "cubic-bezier");
466             tokens = this._addTimingFunctionTokens(tokens, "spring");
467             tokens = this._addVariableTokens(tokens);
468         }
469
470         tokens = tokens.map((token) => {
471             if (token instanceof Element)
472                 return token;
473
474             let className = "";
475
476             if (token.type) {
477                 if (token.type.includes("string"))
478                     className = "token-string";
479                 else if (token.type.includes("link"))
480                     className = "token-link";
481                 else if (token.type.includes("comment"))
482                     className = "token-comment";
483             }
484
485             if (className) {
486                 let span = document.createElement("span");
487                 span.classList.add(className);
488                 span.textContent = token.value.truncateMiddle(maxValueLength);
489
490                 if (token.type && token.type.includes("link"))
491                     span.addEventListener("contextmenu", this._handleLinkContextMenu.bind(this, token));
492
493                 return span;
494             }
495
496             return token.value;
497         });
498
499         this._valueElement.removeChildren();
500         this._valueElement.append(...tokens);
501     }
502
503     _createInlineSwatch(type, text, valueObject)
504     {
505         let tokenElement = document.createElement("span");
506         let innerElement = document.createElement("span");
507         innerElement.textContent = text;
508
509         let readOnly = !this._isEditable();
510         let swatch = new WI.InlineSwatch(type, valueObject, readOnly);
511
512         swatch.addEventListener(WI.InlineSwatch.Event.ValueChanged, (event) => {
513             let value = event.data.value && event.data.value.toString();
514             if (!value)
515                 return;
516
517             innerElement.textContent = value;
518             this._handleValueChange();
519
520             if (type === WI.InlineSwatch.Type.Variable)
521                 this._renderValue(this._property.rawValue);
522         }, this);
523
524         if (this._delegate && typeof this._delegate.stylePropertyInlineSwatchActivated === "function") {
525             swatch.addEventListener(WI.InlineSwatch.Event.Activated, () => {
526                 this._delegate.stylePropertyInlineSwatchActivated();
527             });
528         }
529
530         if (this._delegate && typeof this._delegate.stylePropertyInlineSwatchDeactivated === "function") {
531             swatch.addEventListener(WI.InlineSwatch.Event.Deactivated, () => {
532                 this._delegate.stylePropertyInlineSwatchDeactivated();
533             });
534         }
535
536         tokenElement.append(swatch.element, innerElement);
537
538         return tokenElement;
539     }
540
541     _addGradientTokens(tokens)
542     {
543         let gradientRegex = /^(repeating-)?(linear|radial)-gradient$/i;
544         let newTokens = [];
545         let gradientStartIndex = NaN;
546         let openParenthesis = 0;
547
548         for (let i = 0; i < tokens.length; i++) {
549             let token = tokens[i];
550             if (token.type && token.type.includes("atom") && gradientRegex.test(token.value)) {
551                 gradientStartIndex = i;
552                 openParenthesis = 0;
553             } else if (token.value === "(" && !isNaN(gradientStartIndex))
554                 openParenthesis++;
555             else if (token.value === ")" && !isNaN(gradientStartIndex)) {
556                 openParenthesis--;
557                 if (openParenthesis > 0) {
558                     // Matched a CSS function inside of the gradient.
559                     continue;
560                 }
561
562                 let rawTokens = tokens.slice(gradientStartIndex, i + 1);
563                 let text = rawTokens.map((token) => token.value).join("");
564                 let gradient = WI.Gradient.fromString(text);
565                 if (gradient)
566                     newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Gradient, text, gradient));
567                 else
568                     newTokens.push(...rawTokens);
569
570                 gradientStartIndex = NaN;
571             } else if (isNaN(gradientStartIndex))
572                 newTokens.push(token);
573         }
574
575         return newTokens;
576     }
577
578     _addColorTokens(tokens)
579     {
580         let newTokens = [];
581
582         let pushPossibleColorToken = (text, ...rawTokens) => {
583             let color = WI.Color.fromString(text);
584             if (color)
585                 newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Color, text, color));
586             else
587                 newTokens.push(...rawTokens);
588         };
589
590         let colorFunctionStartIndex = NaN;
591
592         for (let i = 0; i < tokens.length; i++) {
593             let token = tokens[i];
594             if (token.type && token.type.includes("hex-color")) {
595                 // Hex
596                 pushPossibleColorToken(token.value, token);
597             } else if (WI.Color.FunctionNames.has(token.value) && token.type && (token.type.includes("atom") || token.type.includes("keyword"))) {
598                 // Color Function start
599                 colorFunctionStartIndex = i;
600             } else if (isNaN(colorFunctionStartIndex) && token.type && (token.type.includes("atom") || token.type.includes("keyword"))) {
601                 // Color keyword
602                 pushPossibleColorToken(token.value, token);
603             } else if (!isNaN(colorFunctionStartIndex)) {
604                 // Color Function end
605                 if (token.value !== ")")
606                     continue;
607
608                 let rawTokens = tokens.slice(colorFunctionStartIndex, i + 1);
609                 let text = rawTokens.map((token) => token.value).join("");
610                 pushPossibleColorToken(text, ...rawTokens);
611                 colorFunctionStartIndex = NaN;
612             } else
613                 newTokens.push(token);
614         }
615
616         return newTokens;
617     }
618
619     _addTimingFunctionTokens(tokens, tokenType)
620     {
621         let newTokens = [];
622         let startIndex = NaN;
623         let openParenthesis = 0;
624
625         for (let i = 0; i < tokens.length; i++) {
626             let token = tokens[i];
627             if (token.value === tokenType && token.type && token.type.includes("atom")) {
628                 startIndex = i;
629                 openParenthesis = 0;
630             } else if (token.value === "(" && !isNaN(startIndex))
631                 openParenthesis++;
632             else if (token.value === ")" && !isNaN(startIndex)) {
633
634                 openParenthesis--;
635                 if (openParenthesis > 0)
636                     continue;
637
638                 let rawTokens = tokens.slice(startIndex, i + 1);
639                 let text = rawTokens.map((token) => token.value).join("");
640
641                 let valueObject;
642                 let inlineSwatchType;
643                 if (tokenType === "cubic-bezier") {
644                     valueObject = WI.CubicBezier.fromString(text);
645                     inlineSwatchType = WI.InlineSwatch.Type.Bezier;
646                 } else if (tokenType === "spring") {
647                     valueObject = WI.Spring.fromString(text);
648                     inlineSwatchType = WI.InlineSwatch.Type.Spring;
649                 }
650
651                 if (valueObject)
652                     newTokens.push(this._createInlineSwatch(inlineSwatchType, text, valueObject));
653                 else
654                     newTokens.push(...rawTokens);
655
656                 startIndex = NaN;
657             } else if (isNaN(startIndex))
658                 newTokens.push(token);
659         }
660
661         return newTokens;
662     }
663
664     _addVariableTokens(tokens)
665     {
666         let newTokens = [];
667         let startIndex = NaN;
668         let openParenthesis = 0;
669
670         for (let i = 0; i < tokens.length; i++) {
671             let token = tokens[i];
672             if (token.value === "var" && token.type && token.type.includes("atom")) {
673                 startIndex = i;
674                 openParenthesis = 0;
675             } else if (token.value === "(" && !isNaN(startIndex))
676                 ++openParenthesis;
677             else if (token.value === ")" && !isNaN(startIndex)) {
678                 --openParenthesis;
679                 if (openParenthesis > 0)
680                     continue;
681
682                 let rawTokens = tokens.slice(startIndex, i + 1);
683                 let tokenValues = rawTokens.map((token) => token.value);
684                 let variableName = tokenValues.find((value, i) => value.startsWith("--") && /\bvariable-2\b/.test(rawTokens[i].type));
685
686                 const dontCreateIfMissing = true;
687                 let variableProperty = this._property.ownerStyle.nodeStyles.computedStyle.propertyForName(variableName, dontCreateIfMissing);
688                 if (variableProperty) {
689                     let valueObject = variableProperty.value.trim();
690                     newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Variable, tokenValues.join(""), valueObject));
691                 } else {
692                     this._hasInvalidVariableValue = true;
693                     newTokens.push(...rawTokens);
694                 }
695
696                 startIndex = NaN;
697             } else if (isNaN(startIndex))
698                 newTokens.push(token);
699         }
700
701         return newTokens;
702     }
703
704     _handleNameChange()
705     {
706         this._property.name = this._nameElement.textContent.trim();
707     }
708
709     _handleValueChange()
710     {
711         this._property.rawValue = this._valueElement.textContent.trim();
712     }
713
714     _handleNameBeforeInput(event)
715     {
716         if (event.data !== ":" || event.inputType !== "insertText")
717             return;
718
719         event.preventDefault();
720         this._nameTextField.discardCompletion();
721         this._valueTextField.startEditing();
722     }
723
724     _handleNamePaste(event)
725     {
726         let text = event.clipboardData.getData("text/plain");
727         if (!text || !text.includes(":"))
728             return;
729
730         event.preventDefault();
731
732         this.remove(text);
733
734         if (this._delegate.spreadsheetStylePropertyAddBlankPropertySoon) {
735             this._delegate.spreadsheetStylePropertyAddBlankPropertySoon(this, {
736                 index: parseInt(this._element.dataset.propertyIndex) + 1,
737             });
738         }
739     }
740
741     _nameCompletionDataProvider(prefix, options = {})
742     {
743         let completions;
744         if (!prefix && options.allowEmptyPrefix)
745             completions = WI.CSSCompletions.cssNameCompletions.values;
746         else
747             completions = WI.CSSCompletions.cssNameCompletions.startsWith(prefix);
748         return {prefix, completions};
749     }
750
751     _handleValueBeforeInput(event)
752     {
753         if (event.data !== ";" || event.inputType !== "insertText")
754             return;
755
756         let text = this._valueTextField.valueWithoutSuggestion();
757         let selection = window.getSelection();
758         if (!selection.rangeCount || selection.getRangeAt(0).endOffset !== text.length)
759             return;
760
761         let unbalancedCharacters = WI.CSSCompletions.completeUnbalancedValue(text);
762         if (unbalancedCharacters)
763             return;
764
765         event.preventDefault();
766         this._valueTextField.stopEditing();
767         this.spreadsheetTextFieldDidCommit(this._valueTextField, {direction: "forward"});
768     }
769
770     _valueCompletionDataProvider(prefix)
771     {
772         // For "border: 1px so|", we want to suggest "solid" based on "so" prefix.
773         let match = prefix.match(/[a-z0-9()-]+$/i);
774
775         // Clicking on the value of `height: 100%` shouldn't show any completions.
776         if (!match && prefix)
777             return {completions: [], prefix: ""};
778
779         prefix = match ? match[0] : "";
780         let propertyName = this._nameElement.textContent.trim();
781         return {
782             prefix,
783             completions: WI.CSSKeywordCompletions.forProperty(propertyName).startsWith(prefix)
784         };
785     }
786
787     _setupJumpToSymbol(element)
788     {
789         element.addEventListener("mousedown", (event) => {
790             if (event.button !== 0)
791                 return;
792
793             if (!WI.modifierKeys.metaKey)
794                 return;
795
796             if (element.isContentEditable)
797                 return;
798
799             let sourceCodeLocation = null;
800             if (this._property.ownerStyle.ownerRule)
801                 sourceCodeLocation = this._property.ownerStyle.ownerRule.sourceCodeLocation;
802
803             if (!sourceCodeLocation)
804                 return;
805
806             let range = this._property.styleSheetTextRange;
807             const options = {
808                 ignoreNetworkTab: true,
809                 ignoreSearchTab: true,
810             };
811             let sourceCode = sourceCodeLocation.sourceCode;
812             WI.showSourceCodeLocation(sourceCode.createSourceCodeLocation(range.startLine, range.startColumn), options);
813         });
814     }
815
816     _handleLinkContextMenu(token, event)
817     {
818         let contextMenu = WI.ContextMenu.createFromEvent(event);
819
820         let resolveURL = (url) => {
821             let ownerStyle = this._property.ownerStyle;
822             if (!ownerStyle)
823                 return url;
824
825             let ownerStyleSheet = ownerStyle.ownerStyleSheet;
826             if (!ownerStyleSheet) {
827                 let ownerRule = ownerStyle.ownerRule;
828                 if (ownerRule)
829                     ownerStyleSheet = ownerRule.ownerStyleSheet;
830             }
831             if (ownerStyleSheet) {
832                 if (ownerStyleSheet.url)
833                     return absoluteURL(url, ownerStyleSheet.url);
834
835                 let parentFrame = ownerStyleSheet.parentFrame;
836                 if (parentFrame)
837                     return absoluteURL(url, parentFrame.url);
838             }
839
840             let node = ownerStyle.node;
841             if (!node) {
842                 let nodeStyles = ownerStyle.nodeStyles;
843                 if (!nodeStyles) {
844                     let ownerRule = ownerStyle.ownerRule;
845                     if (ownerRule)
846                         nodeStyles = ownerRule.nodeStyles;
847                 }
848                 if (nodeStyles)
849                     node = nodeStyles.node;
850             }
851             if (node) {
852                 let ownerDocument = node.ownerDocument;
853                 if (ownerDocument)
854                     return absoluteURL(url, node.ownerDocument.documentURL);
855             }
856
857             return url;
858         };
859
860         WI.appendContextMenuItemsForURL(contextMenu, resolveURL(token.value));
861     }
862 };
863
864 WI.SpreadsheetStyleProperty.StyleClassName = "property";