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