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