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