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