4c7e571b2d04e25aef0bfe8e9628cf15aa66b4d8
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / SpreadsheetCSSStyleDeclarationEditor.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.SpreadsheetCSSStyleDeclarationEditor = class SpreadsheetCSSStyleDeclarationEditor extends WI.View
27 {
28     constructor(delegate, style)
29     {
30         super();
31
32         this.element.classList.add(WI.SpreadsheetCSSStyleDeclarationEditor.StyleClassName);
33
34         this._delegate = delegate;
35         this.style = style;
36         this._propertyViews = [];
37
38         this._inlineSwatchActive = false;
39         this._focused = false;
40
41         this._propertyPendingStartEditing = null;
42         this._filterText = null;
43     }
44
45     // Public
46
47     initialLayout()
48     {
49         if (!this.style.editable)
50             return;
51
52         this.element.addEventListener("focus", () => { this.focused = true; }, true);
53         this.element.addEventListener("blur", (event) => {
54             let focusedElement = event.relatedTarget;
55             if (focusedElement && focusedElement.isDescendant(this.element))
56                 return;
57
58             this.focused = false;
59         }, true);
60     }
61
62     layout()
63     {
64         super.layout();
65
66         this.element.removeChildren();
67
68         let properties = this._propertiesToRender;
69         this.element.classList.toggle("no-properties", !properties.length);
70
71         // FIXME: Only re-layout properties that have been modified and preserve focus whenever possible.
72         this._propertyViews = [];
73
74         let propertyViewPendingStartEditing = null;
75         for (let index = 0; index < properties.length; index++) {
76             let property = properties[index];
77             let propertyView = new WI.SpreadsheetStyleProperty(this, property);
78             propertyView.index = index;
79             this.element.append(propertyView.element);
80             this._propertyViews.push(propertyView);
81
82             if (property === this._propertyPendingStartEditing)
83                 propertyViewPendingStartEditing = propertyView;
84         }
85
86         if (propertyViewPendingStartEditing) {
87             propertyViewPendingStartEditing.nameTextField.startEditing();
88             this._propertyPendingStartEditing = null;
89         }
90
91         if (this._filterText)
92             this.applyFilter(this._filterText);
93     }
94
95     detached()
96     {
97         this._inlineSwatchActive = false;
98         this.focused = false;
99
100         for (let propertyView of this._propertyViews)
101             propertyView.detached();
102     }
103
104     get style()
105     {
106         return this._style;
107     }
108
109     set style(style)
110     {
111         if (this._style === style)
112             return;
113
114         if (this._style)
115             this._style.removeEventListener(WI.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this);
116
117         this._style = style || null;
118
119         if (this._style)
120             this._style.addEventListener(WI.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this);
121
122         this.needsLayout();
123     }
124
125     get editing()
126     {
127         return this._focused || this._inlineSwatchActive;
128     }
129
130     set focused(value)
131     {
132         this._focused = value;
133         this._updateStyleLock();
134     }
135
136     set inlineSwatchActive(value)
137     {
138         this._inlineSwatchActive = value;
139         this._updateStyleLock();
140     }
141
142     startEditingFirstProperty()
143     {
144         let firstEditableProperty = this._editablePropertyAfter(-1);
145         if (firstEditableProperty)
146             firstEditableProperty.nameTextField.startEditing();
147         else {
148             const appendAfterLast = -1;
149             this.addBlankProperty(appendAfterLast);
150         }
151     }
152
153     startEditingLastProperty()
154     {
155         let lastEditableProperty = this._editablePropertyBefore(this._propertyViews.length);
156         if (lastEditableProperty)
157             lastEditableProperty.valueTextField.startEditing();
158         else {
159             const appendAfterLast = -1;
160             this.addBlankProperty(appendAfterLast);
161         }
162     }
163
164     highlightProperty(property)
165     {
166         let propertiesMatch = (cssProperty) => {
167             if (cssProperty.attached && !cssProperty.overridden) {
168                 if (cssProperty.canonicalName === property.canonicalName || hasMatchingLonghandProperty(cssProperty))
169                     return true;
170             }
171
172             return false;
173         };
174
175         let hasMatchingLonghandProperty = (cssProperty) => {
176             let cssProperties = cssProperty.relatedLonghandProperties;
177
178             if (!cssProperties.length)
179                 return false;
180
181             for (let property of cssProperties) {
182                 if (propertiesMatch(property))
183                     return true;
184             }
185
186             return false;
187         };
188
189         for (let cssProperty of this.style.properties) {
190             if (propertiesMatch(cssProperty)) {
191                 let propertyView = cssProperty.__propertyView;
192                 if (propertyView) {
193                     propertyView.highlight();
194
195                     if (cssProperty.editable)
196                         propertyView.valueTextField.startEditing();
197                 }
198                 return true;
199             }
200         }
201
202         return false;
203     }
204
205     addBlankProperty(index)
206     {
207         if (index === -1) {
208             // Append to the end.
209             index = this._propertyViews.length;
210         }
211
212         this._propertyPendingStartEditing = this._style.newBlankProperty(index);
213         this.needsLayout();
214     }
215
216     spreadsheetCSSStyleDeclarationEditorFocusMoved({direction, movedFromProperty, willRemoveProperty})
217     {
218         let movedFromIndex = this._propertyViews.indexOf(movedFromProperty);
219         console.assert(movedFromIndex !== -1, "Property doesn't exist, focusing on a selector as a fallback.");
220         if (movedFromIndex === -1) {
221             if (this._style.selectorEditable)
222                 this._delegate.cssStyleDeclarationTextEditorStartEditingRuleSelector();
223
224             return;
225         }
226
227         if (direction === "forward") {
228             // Move from the value to the next enabled property's name.
229             let propertyView = this._editablePropertyAfter(movedFromIndex);
230             if (propertyView)
231                 propertyView.nameTextField.startEditing();
232             else {
233                 if (willRemoveProperty) {
234                     // Move from the last value in the rule to the next rule's selector.
235                     let reverse = false;
236                     this._delegate.cssStyleDeclarationEditorStartEditingAdjacentRule(reverse);
237                 } else {
238                     const appendAfterLast = -1;
239                     this.addBlankProperty(appendAfterLast);
240                 }
241             }
242         } else {
243             let propertyView = this._editablePropertyBefore(movedFromIndex);
244             if (propertyView) {
245                 // Move from the property's name to the previous enabled property's value.
246                 propertyView.valueTextField.startEditing()
247             } else {
248                 // Move from the first property's name to the rule's selector.
249                 if (this._style.selectorEditable)
250                     this._delegate.cssStyleDeclarationTextEditorStartEditingRuleSelector();
251             }
252         }
253     }
254
255     // SpreadsheetStyleProperty delegate
256
257     spreadsheetStylePropertyRemoved(propertyView)
258     {
259         this._propertyViews.remove(propertyView);
260
261         for (let index = 0; index < this._propertyViews.length; index++)
262             this._propertyViews[index].index = index;
263     }
264
265     stylePropertyInlineSwatchActivated()
266     {
267         this.inlineSwatchActive = true;
268     }
269
270     stylePropertyInlineSwatchDeactivated()
271     {
272         this.inlineSwatchActive = false;
273     }
274
275     applyFilter(filterText)
276     {
277         this._filterText = filterText;
278
279         if (!this.didInitialLayout)
280             return;
281
282         let matches = false;
283         for (let propertyView of this._propertyViews) {
284             if (propertyView.applyFilter(this._filterText))
285                 matches = true;
286         }
287
288         this.dispatchEventToListeners(WI.SpreadsheetCSSStyleDeclarationEditor.Event.FilterApplied, {matches});
289     }
290
291     // Private
292
293     get _propertiesToRender()
294     {
295         if (this._style._styleSheetTextRange)
296             return this._style.allVisibleProperties;
297
298         return this._style.allProperties;
299     }
300
301     _editablePropertyAfter(propertyIndex)
302     {
303         for (let index = propertyIndex + 1; index < this._propertyViews.length; index++) {
304             let property = this._propertyViews[index];
305             if (property.enabled)
306                 return property;
307         }
308
309         return null;
310     }
311
312     _editablePropertyBefore(propertyIndex)
313     {
314         for (let index = propertyIndex - 1; index >= 0; index--) {
315             let property = this._propertyViews[index];
316             if (property.enabled)
317                 return property;
318         }
319
320         return null;
321     }
322
323     _propertiesChanged(event)
324     {
325         if (this.editing) {
326             for (let propertyView of this._propertyViews)
327                 propertyView.updateStatus();
328         } else
329             this.needsLayout();
330     }
331
332     _updateStyleLock()
333     {
334         this.style.locked = this._focused || this._inlineSwatchActive;
335     }
336 };
337
338 WI.SpreadsheetCSSStyleDeclarationEditor.Event = {
339     FilterApplied: "spreadsheet-css-style-declaration-editor-filter-applied",
340 };
341
342 WI.SpreadsheetCSSStyleDeclarationEditor.StyleClassName = "spreadsheet-style-declaration-editor";