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