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