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