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