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