Web Inspector: Styles: Command-click on a property name should jump to definition...
[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, newlyAdded = false)
29     {
30         super();
31
32         console.assert(property instanceof WI.CSSProperty);
33
34         this._delegate = delegate || null;
35         this._property = property;
36         this._newlyAdded = newlyAdded;
37         this._element = document.createElement("div");
38
39         this._nameElement = null;
40         this._valueElement = null;
41
42         this._nameTextField = null;
43         this._valueTextField = null;
44
45         this._property.__propertyView = this;
46
47         this._update();
48         property.addEventListener(WI.CSSProperty.Event.OverriddenStatusChanged, this._update, this);
49     }
50
51     // Public
52
53     get element() { return this._element; }
54     get nameTextField() { return this._nameTextField; }
55     get valueTextField() { return this._valueTextField; }
56
57     detached()
58     {
59         this._property.__propertyView = null;
60
61         if (this._nameTextField)
62             this._nameTextField.detached();
63
64         if (this._valueTextField)
65             this._valueTextField.detached();
66     }
67
68     highlight()
69     {
70         this._element.classList.add("highlighted");
71     }
72
73     // Private
74
75     _remove()
76     {
77         this.element.remove();
78         this._property.remove();
79         this.detached();
80
81         if (this._delegate && typeof this._delegate.spreadsheetStylePropertyRemoved === "function")
82             this._delegate.spreadsheetStylePropertyRemoved(this);
83     }
84
85     _update()
86     {
87         this.element.removeChildren();
88         this.element.className = "";
89
90         let duplicatePropertyExistsBelow = (cssProperty) => {
91             let propertyFound = false;
92
93             for (let property of this._property.ownerStyle.properties) {
94                 if (property === cssProperty)
95                     propertyFound = true;
96                 else if (property.name === cssProperty.name && propertyFound)
97                     return true;
98             }
99
100             return false;
101         };
102
103         let classNames = ["property"];
104
105         if (this._property.overridden)
106             classNames.push("overridden");
107
108         if (this._property.implicit)
109             classNames.push("implicit");
110
111         if (this._property.ownerStyle.inherited && !this._property.inherited)
112             classNames.push("not-inherited");
113
114         if (!this._property.valid && this._property.hasOtherVendorNameOrKeyword())
115             classNames.push("other-vendor");
116         else if (!this._property.valid) {
117             let propertyNameIsValid = false;
118             if (WI.CSSCompletions.cssNameCompletions)
119                 propertyNameIsValid = WI.CSSCompletions.cssNameCompletions.isValidPropertyName(this._property.name);
120
121             if (!propertyNameIsValid || duplicatePropertyExistsBelow(this._property))
122                 classNames.push("invalid");
123         }
124
125         if (!this._property.enabled)
126             classNames.push("disabled");
127
128         this._element.classList.add(...classNames);
129
130         if (this._property.editable) {
131             this._checkboxElement = this.element.appendChild(document.createElement("input"));
132             this._checkboxElement.classList.add("property-toggle");
133             this._checkboxElement.type = "checkbox";
134             this._checkboxElement.checked = this._property.enabled;
135             this._checkboxElement.tabIndex = -1;
136             this._checkboxElement.addEventListener("change", () => {
137                 let disabled = !this._checkboxElement.checked;
138                 this._property.commentOut(disabled);
139                 this._update();
140             });
141         }
142
143         if (!this._property.enabled)
144             this.element.append("/* ");
145
146         this._nameElement = this.element.appendChild(document.createElement("span"));
147         this._nameElement.classList.add("name");
148         this._nameElement.textContent = this._property.name;
149
150         this.element.append(": ");
151
152         this._valueElement = this.element.appendChild(document.createElement("span"));
153         this._valueElement.classList.add("value");
154         this._renderValue(this._property.rawValue);
155
156         if (this._property.editable && this._property.enabled) {
157             this._nameElement.tabIndex = 0;
158             this._nameTextField = new WI.SpreadsheetTextField(this, this._nameElement, this._nameCompletionDataProvider.bind(this));
159
160             this._valueElement.tabIndex = 0;
161             this._valueTextField = new WI.SpreadsheetTextField(this, this._valueElement, this._valueCompletionDataProvider.bind(this));
162         }
163
164         if (this._property.editable) {
165             this._setupJumpToSymbol(this._nameElement);
166             this._setupJumpToSymbol(this._valueElement);
167         }
168
169         this.element.append(";");
170
171         if (!this._property.enabled)
172             this.element.append(" */");
173     }
174
175     // SpreadsheetTextField delegate
176
177     spreadsheetTextFieldWillStartEditing(textField)
178     {
179         let isEditingName = textField === this._nameTextField;
180         textField.value = isEditingName ? this._property.name : this._property.rawValue;
181     }
182
183     spreadsheetTextFieldDidChange(textField)
184     {
185         if (textField === this._valueTextField)
186             this.debounce(WI.SpreadsheetStyleProperty.CommitCoalesceDelay)._handleValueChange();
187         else if (textField === this._nameTextField)
188             this.debounce(WI.SpreadsheetStyleProperty.CommitCoalesceDelay)._handleNameChange();
189     }
190
191     spreadsheetTextFieldDidCommit(textField, {direction})
192     {
193         let propertyName = this._nameTextField.value.trim();
194         let propertyValue = this._valueTextField.value.trim();
195         let willRemoveProperty = false;
196
197         // Remove a property with an empty name or value. However, a newly added property
198         // has an empty name and value at first. Don't remove it when moving focus from
199         // the name to the value for the first time.
200         if (!propertyName || (!this._newlyAdded && !propertyValue))
201             willRemoveProperty = true;
202
203         let isEditingName = textField === this._nameTextField;
204
205         if (!isEditingName && !willRemoveProperty)
206             this._renderValue(propertyValue);
207
208         if (propertyName && isEditingName)
209             this._newlyAdded = false;
210
211         if (direction === "forward") {
212             if (isEditingName && !willRemoveProperty) {
213                 // Move focus from the name to the value.
214                 this._valueTextField.startEditing();
215                 return;
216             }
217         } else {
218             if (!isEditingName) {
219                 // Move focus from the value to the name.
220                 this._nameTextField.startEditing();
221                 return;
222             }
223         }
224
225         if (typeof this._delegate.spreadsheetCSSStyleDeclarationEditorFocusMoved === "function") {
226             // Move focus away from the current property, to the next or previous one, if exists, or to the next or previous rule, if exists.
227             this._delegate.spreadsheetCSSStyleDeclarationEditorFocusMoved({direction, willRemoveProperty, movedFromProperty: this});
228         }
229
230         if (willRemoveProperty)
231             this._remove();
232     }
233
234     spreadsheetTextFieldDidBlur(textField)
235     {
236         if (textField.value.trim() === "")
237             this._remove();
238         else
239             this._renderValue(this._valueElement.textContent);
240     }
241
242     // Private
243
244     _renderValue(value)
245     {
246         const maxValueLength = 150;
247
248         let tokens = WI.tokenizeCSSValue(value).map((token) => {
249             let className = "";
250             if (token.type) {
251                 if (token.type.includes("string"))
252                     className = "token-string";
253                 else if (token.type.includes("link"))
254                     className = "token-link";
255             }
256
257             if (className) {
258                 let span = document.createElement("span");
259                 span.classList.add(className);
260                 span.textContent = token.value.trimMiddle(maxValueLength);
261                 return span;
262             }
263
264             return token.value;
265         });
266
267         this._valueElement.removeChildren();
268         this._valueElement.append(...tokens);
269     }
270
271     _handleNameChange()
272     {
273         this._property.name = this._nameElement.textContent.trim();
274     }
275
276     _handleValueChange()
277     {
278         this._property.rawValue = this._valueElement.textContent.trim();
279     }
280
281     _nameCompletionDataProvider(prefix)
282     {
283         return WI.CSSCompletions.cssNameCompletions.startsWith(prefix);
284     }
285
286     _valueCompletionDataProvider(prefix)
287     {
288         return WI.CSSKeywordCompletions.forProperty(this._property.name).startsWith(prefix);
289     }
290
291     _setupJumpToSymbol(element)
292     {
293         element.addEventListener("mousedown", (event) => {
294             if (event.button !== 0)
295                 return;
296
297             if (!WI.modifierKeys.metaKey)
298                 return;
299
300             if (element.isContentEditable)
301                 return;
302
303             let sourceCodeLocation = null;
304             if (this._property.ownerStyle.ownerRule)
305                 sourceCodeLocation = this._property.ownerStyle.ownerRule.sourceCodeLocation;
306
307             if (!sourceCodeLocation)
308                 return;
309
310             let range = this._property.styleSheetTextRange;
311             const options = {
312                 ignoreNetworkTab: true,
313                 ignoreSearchTab: true,
314             };
315             let sourceCode = sourceCodeLocation.sourceCode;
316             WI.showSourceCodeLocation(sourceCode.createSourceCodeLocation(range.startLine, range.startColumn), options);
317         });
318     }
319 };
320
321 WI.SpreadsheetStyleProperty.CommitCoalesceDelay = 250;