Web Inspector: Styles Redesign: hook up autocompletion to property names and values
[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._update();
46         property.addEventListener(WI.CSSProperty.Event.OverriddenStatusChanged, this._update, this);
47     }
48
49     // Public
50
51     get element() { return this._element; }
52     get nameTextField() { return this._nameTextField; }
53     get valueTextField() { return this._valueTextField; }
54
55     detached()
56     {
57         if (this._nameTextField)
58             this._nameTextField.detached();
59
60         if (this._valueTextField)
61             this._valueTextField.detached();
62     }
63
64     // Private
65
66     _remove()
67     {
68         this.element.remove();
69         this._property.remove();
70         this.detached();
71
72         if (this._delegate && typeof this._delegate.spreadsheetStylePropertyRemoved === "function")
73             this._delegate.spreadsheetStylePropertyRemoved(this);
74     }
75
76     _update()
77     {
78         this.element.removeChildren();
79         this.element.className = "";
80
81         let duplicatePropertyExistsBelow = (cssProperty) => {
82             let propertyFound = false;
83
84             for (let property of this._property.ownerStyle.properties) {
85                 if (property === cssProperty)
86                     propertyFound = true;
87                 else if (property.name === cssProperty.name && propertyFound)
88                     return true;
89             }
90
91             return false;
92         };
93
94         let classNames = ["property"];
95
96         if (this._property.overridden)
97             classNames.push("overridden");
98
99         if (this._property.implicit)
100             classNames.push("implicit");
101
102         if (this._property.ownerStyle.inherited && !this._property.inherited)
103             classNames.push("not-inherited");
104
105         if (!this._property.valid && this._property.hasOtherVendorNameOrKeyword())
106             classNames.push("other-vendor");
107         else if (!this._property.valid) {
108             let propertyNameIsValid = false;
109             if (WI.CSSCompletions.cssNameCompletions)
110                 propertyNameIsValid = WI.CSSCompletions.cssNameCompletions.isValidPropertyName(this._property.name);
111
112             if (!propertyNameIsValid || duplicatePropertyExistsBelow(this._property))
113                 classNames.push("invalid");
114         }
115
116         if (!this._property.enabled)
117             classNames.push("disabled");
118
119         this._element.classList.add(...classNames);
120
121         if (this._property.editable) {
122             this._checkboxElement = this.element.appendChild(document.createElement("input"));
123             this._checkboxElement.classList.add("property-toggle");
124             this._checkboxElement.type = "checkbox";
125             this._checkboxElement.checked = this._property.enabled;
126             this._checkboxElement.tabIndex = -1;
127             this._checkboxElement.addEventListener("change", () => {
128                 let disabled = !this._checkboxElement.checked;
129                 this._property.commentOut(disabled);
130                 this._update();
131             });
132         }
133
134         if (!this._property.enabled)
135             this.element.append("/* ");
136
137         this._nameElement = this.element.appendChild(document.createElement("span"));
138         this._nameElement.classList.add("name");
139         this._nameElement.textContent = this._property.name;
140
141         this.element.append(": ");
142
143         this._valueElement = this.element.appendChild(document.createElement("span"));
144         this._valueElement.classList.add("value");
145         this._valueElement.textContent = this._property.rawValue;
146
147         if (this._property.editable && this._property.enabled) {
148             this._nameElement.tabIndex = 0;
149             this._nameTextField = new WI.SpreadsheetTextField(this, this._nameElement, this._nameCompletionDataProvider.bind(this));
150
151             this._valueElement.tabIndex = 0;
152             this._valueTextField = new WI.SpreadsheetTextField(this, this._valueElement, this._valueCompletionDataProvider.bind(this));
153         }
154
155         this.element.append(";");
156
157         if (!this._property.enabled)
158             this.element.append(" */");
159     }
160
161     spreadsheetTextFieldDidChange(textField)
162     {
163         if (textField === this._valueTextField)
164             this.debounce(WI.SpreadsheetStyleProperty.CommitCoalesceDelay)._handleValueChange();
165         else if (textField === this._nameTextField)
166             this.debounce(WI.SpreadsheetStyleProperty.CommitCoalesceDelay)._handleNameChange();
167     }
168
169     spreadsheetTextFieldDidCommit(textField, {direction})
170     {
171         let propertyName = this._nameTextField.value.trim();
172         let propertyValue = this._valueTextField.value.trim();
173         let willRemoveProperty = false;
174
175         // Remove a property with an empty name or value. However, a newly added property
176         // has an empty name and value at first. Don't remove it when moving focus from
177         // the name to the value for the first time.
178         if (!propertyName || (!this._newlyAdded && !propertyValue))
179             willRemoveProperty = true;
180
181         let isEditingName = textField === this._nameTextField;
182
183         if (propertyName && isEditingName)
184             this._newlyAdded = false;
185
186         if (direction === "forward") {
187             if (isEditingName && !willRemoveProperty) {
188                 // Move focus from the name to the value.
189                 this._valueTextField.startEditing();
190                 return;
191             }
192         } else {
193             if (!isEditingName) {
194                 // Move focus from the value to the name.
195                 this._nameTextField.startEditing();
196                 return;
197             }
198         }
199
200         if (typeof this._delegate.spreadsheetCSSStyleDeclarationEditorFocusMoved === "function") {
201             // Move focus away from the current property, to the next or previous one, if exists, or to the next or previous rule, if exists.
202             this._delegate.spreadsheetCSSStyleDeclarationEditorFocusMoved({direction, willRemoveProperty, movedFromProperty: this});
203         }
204
205         if (willRemoveProperty)
206             this._remove();
207     }
208
209     spreadsheetTextFieldDidBlur(textField)
210     {
211         if (textField.value.trim() === "")
212             this._remove();
213     }
214
215     _handleNameChange()
216     {
217         this._property.name = this._nameElement.textContent.trim();
218     }
219
220     _handleValueChange()
221     {
222         this._property.rawValue = this._valueElement.textContent.trim();
223     }
224
225     _nameCompletionDataProvider(prefix)
226     {
227         return WI.CSSCompletions.cssNameCompletions.startsWith(prefix);
228     }
229
230     _valueCompletionDataProvider(prefix)
231     {
232         return WI.CSSKeywordCompletions.forProperty(this._property.name).startsWith(prefix);
233     }
234 };
235
236 WI.SpreadsheetStyleProperty.CommitCoalesceDelay = 250;