Web Inspector: Styles Redesign: Inline widgets don't hide when starting editing by...
[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 if (textField === this._valueTextField)
239             this._renderValue(this._valueElement.textContent);
240     }
241
242     // Private
243
244     _renderValue(value)
245     {
246         const maxValueLength = 150;
247         let tokens = WI.tokenizeCSSValue(value);
248
249         if (this._property.enabled) {
250             // Don't show color widgets for CSS gradients, show dedicated gradient widgets instead.
251             // FIXME: <https://webkit.org/b/178404> Web Inspector: [PARITY] Styles Redesign: Add bezier curve, color gradient, and CSS variable inline widgets
252             tokens = this._addColorTokens(tokens);
253         }
254
255         tokens = tokens.map((token) => {
256             if (token instanceof Element)
257                 return token;
258
259             let className = "";
260
261             if (token.type) {
262                 if (token.type.includes("string"))
263                     className = "token-string";
264                 else if (token.type.includes("link"))
265                     className = "token-link";
266                 else if (token.type.includes("comment"))
267                     className = "token-comment";
268             }
269
270             if (className) {
271                 let span = document.createElement("span");
272                 span.classList.add(className);
273                 span.textContent = token.value.trimMiddle(maxValueLength);
274                 return span;
275             }
276
277             return token.value;
278         });
279
280         this._valueElement.removeChildren();
281         this._valueElement.append(...tokens);
282     }
283
284     _addColorTokens(tokens)
285     {
286         let newTokens = [];
287
288         let createColorTokenElement = (colorString, color) => {
289             let colorTokenElement = document.createElement("span");
290             colorTokenElement.className = "token-color";
291
292             let innerElement = document.createElement("span");
293             innerElement.className = "token-color-value";
294             innerElement.textContent = colorString;
295
296             if (color) {
297                 let readOnly = !this._property.editable;
298                 let swatch = new WI.InlineSwatch(WI.InlineSwatch.Type.Color, color, readOnly);
299
300                 swatch.addEventListener(WI.InlineSwatch.Event.ValueChanged, (event) => {
301                     let value = event.data && event.data.value && event.data.value.toString();
302                     console.assert(value, "Color value is empty.");
303                     if (!value)
304                         return;
305
306                     innerElement.textContent = value;
307                     this._handleValueChange();
308                 }, this);
309
310                 colorTokenElement.append(swatch.element);
311
312                 // Prevent the value from editing when clicking on the swatch.
313                 swatch.element.addEventListener("mousedown", (event) => { event.stop(); });
314             }
315
316             colorTokenElement.append(innerElement);
317             return colorTokenElement;
318         };
319
320         let pushPossibleColorToken = (text, ...tokens) => {
321             let color = WI.Color.fromString(text);
322             if (color)
323                 newTokens.push(createColorTokenElement(text, color));
324             else
325                 newTokens.push(...tokens);
326         };
327
328         let colorFunctionStartIndex = NaN;
329
330         for (let i = 0; i < tokens.length; i++) {
331             let token = tokens[i];
332             if (token.type && token.type.includes("hex-color")) {
333                 // Hex
334                 pushPossibleColorToken(token.value, token);
335             } else if (WI.Color.FunctionNames.has(token.value) && token.type && (token.type.includes("atom") || token.type.includes("keyword"))) {
336                 // Color Function start
337                 colorFunctionStartIndex = i;
338             } else if (isNaN(colorFunctionStartIndex) && token.type && token.type.includes("keyword")) {
339                 // Color keyword
340                 pushPossibleColorToken(token.value, token);
341             } else if (!isNaN(colorFunctionStartIndex)) {
342                 // Color Function end
343                 if (token.value !== ")")
344                     continue;
345
346                 let rawTokens = tokens.slice(colorFunctionStartIndex, i + 1);
347                 let text = rawTokens.map((token) => token.value).join("");
348                 pushPossibleColorToken(text, ...rawTokens);
349                 colorFunctionStartIndex = NaN;
350             } else
351                 newTokens.push(token);
352         }
353
354         return newTokens;
355     }
356
357     _handleNameChange()
358     {
359         this._property.name = this._nameElement.textContent.trim();
360     }
361
362     _handleValueChange()
363     {
364         this._property.rawValue = this._valueElement.textContent.trim();
365     }
366
367     _nameCompletionDataProvider(prefix)
368     {
369         return WI.CSSCompletions.cssNameCompletions.startsWith(prefix);
370     }
371
372     _valueCompletionDataProvider(prefix)
373     {
374         return WI.CSSKeywordCompletions.forProperty(this._property.name).startsWith(prefix);
375     }
376
377     _setupJumpToSymbol(element)
378     {
379         element.addEventListener("mousedown", (event) => {
380             if (event.button !== 0)
381                 return;
382
383             if (!WI.modifierKeys.metaKey)
384                 return;
385
386             if (element.isContentEditable)
387                 return;
388
389             let sourceCodeLocation = null;
390             if (this._property.ownerStyle.ownerRule)
391                 sourceCodeLocation = this._property.ownerStyle.ownerRule.sourceCodeLocation;
392
393             if (!sourceCodeLocation)
394                 return;
395
396             let range = this._property.styleSheetTextRange;
397             const options = {
398                 ignoreNetworkTab: true,
399                 ignoreSearchTab: true,
400             };
401             let sourceCode = sourceCodeLocation.sourceCode;
402             WI.showSourceCodeLocation(sourceCode.createSourceCodeLocation(range.startLine, range.startColumn), options);
403         });
404     }
405 };
406
407 WI.SpreadsheetStyleProperty.CommitCoalesceDelay = 250;