e6cdb9013d86914daa3168bf0a5f9cad5c6e3004
[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._focused = false;
39         this._inlineSwatchActive = false;
40
41         this._showsImplicitProperties = false;
42         this._alwaysShowPropertyNames = new Set;
43         this._propertyVisibilityMode = WI.SpreadsheetCSSStyleDeclarationEditor.PropertyVisibilityMode.ShowAll;
44         this._hideFilterNonMatchingProperties = false;
45         this._sortPropertiesByName = false;
46
47         this._propertyPendingStartEditing = null;
48         this._pendingAddBlankPropertyIndexOffset = NaN;
49         this._filterText = null;
50
51         this._anchorIndex = NaN;
52         this._focusIndex = NaN;
53     }
54
55     // Public
56
57     initialLayout()
58     {
59         if (!this._style)
60             return;
61
62         this.element.addEventListener("focus", () => { this.focused = true; }, true);
63         this.element.addEventListener("blur", (event) => {
64             let focusedElement = event.relatedTarget;
65             if (focusedElement && this.element.contains(focusedElement))
66                 return;
67
68             this.focused = false;
69         }, true);
70
71         this.element.addEventListener("keydown", this._handleKeyDown.bind(this));
72     }
73
74     layout()
75     {
76         // Prevent layout of properties when one of them is being edited. A full layout resets
77         // the focus, text selection, and completion state <http://webkit.org/b/182619>.
78         if (this.editing && !this._propertyPendingStartEditing && isNaN(this._pendingAddBlankPropertyIndexOffset))
79             return;
80
81         super.layout();
82
83         this.element.removeChildren();
84
85         let properties = this.propertiesToRender;
86         this.element.classList.toggle("no-properties", !properties.length);
87
88         // FIXME: Only re-layout properties that have been modified and preserve focus whenever possible.
89         this._propertyViews = [];
90
91         let propertyViewPendingStartEditing = null;
92         for (let index = 0; index < properties.length; index++) {
93             let property = properties[index];
94             let propertyView = new WI.SpreadsheetStyleProperty(this, property);
95             propertyView.index = index;
96             this.element.append(propertyView.element);
97             this._propertyViews.push(propertyView);
98
99             if (property === this._propertyPendingStartEditing)
100                 propertyViewPendingStartEditing = propertyView;
101         }
102
103         if (propertyViewPendingStartEditing) {
104             propertyViewPendingStartEditing.startEditingName();
105             this._propertyPendingStartEditing = null;
106         }
107
108         if (this._filterText)
109             this.applyFilter(this._filterText);
110
111         if (!isNaN(this._pendingAddBlankPropertyIndexOffset))
112             this.addBlankProperty(this._propertyViews.length - 1 - this._pendingAddBlankPropertyIndexOffset);
113         else if (this.hasSelectedProperties())
114             this.selectProperties(this._anchorIndex, this._focusIndex);
115
116         this._updateDebugLockStatus();
117     }
118
119     detached()
120     {
121         this._inlineSwatchActive = false;
122         this.focused = false;
123
124         for (let propertyView of this._propertyViews)
125             propertyView.detached();
126     }
127
128     hidden()
129     {
130         for (let propertyView of this._propertyViews)
131             propertyView.hidden();
132     }
133
134     get style()
135     {
136         return this._style;
137     }
138
139     set style(style)
140     {
141         if (this._style === style)
142             return;
143
144         if (this._style)
145             this._style.removeEventListener(WI.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this);
146
147         this._style = style || null;
148
149         if (this._style)
150             this._style.addEventListener(WI.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this);
151
152         this.needsLayout();
153     }
154
155     get editing()
156     {
157         return this._focused || this._inlineSwatchActive;
158     }
159
160     set focused(value)
161     {
162         this._focused = value;
163         this._updateStyleLock();
164     }
165
166     set inlineSwatchActive(value)
167     {
168         this._inlineSwatchActive = value;
169         this._updateStyleLock();
170     }
171
172     set showsImplicitProperties(value)
173     {
174         if (value === this._showsImplicitProperties)
175             return;
176
177         this._showsImplicitProperties = value;
178
179         this.needsLayout();
180     }
181
182     set alwaysShowPropertyNames(propertyNames)
183     {
184         this._alwaysShowPropertyNames = new Set(propertyNames);
185
186         this.needsLayout();
187     }
188
189     set propertyVisibilityMode(propertyVisibilityMode)
190     {
191         if (this._propertyVisibilityMode === propertyVisibilityMode)
192             return;
193
194         this._propertyVisibilityMode = propertyVisibilityMode;
195
196         this.needsLayout();
197     }
198
199     set hideFilterNonMatchingProperties(value)
200     {
201         if (value === this._hideFilterNonMatchingProperties)
202             return;
203
204         this._hideFilterNonMatchingProperties = value;
205
206         this.needsLayout();
207     }
208
209     set sortPropertiesByName(value)
210     {
211         if (value === this._sortPropertiesByName)
212             return;
213
214         this._sortPropertiesByName = value;
215         this.needsLayout();
216     }
217
218     get propertiesToRender()
219     {
220         let properties = [];
221         if (!this._style)
222             return properties;
223
224         if (this._style._styleSheetTextRange)
225             properties = this._style.allVisibleProperties;
226         else
227             properties = this._style.allProperties;
228
229         if (this._sortPropertiesByName)
230             properties.sort((a, b) => a.name.extendedLocaleCompare(b.name));
231
232         return properties.filter((property) => {
233             if (!property.variable && this._propertyVisibilityMode === WI.SpreadsheetCSSStyleDeclarationEditor.PropertyVisibilityMode.HideNonVariables)
234                 return false;
235
236             if (property.variable && this._propertyVisibilityMode === WI.SpreadsheetCSSStyleDeclarationEditor.PropertyVisibilityMode.HideVariables)
237                 return false;
238
239             return !property.implicit || this._showsImplicitProperties || this._alwaysShowPropertyNames.has(property.canonicalName);
240         });
241     }
242
243     get selectionRange()
244     {
245         let startIndex = Math.min(this._anchorIndex, this._focusIndex);
246         let endIndex = Math.max(this._anchorIndex, this._focusIndex);
247         return [startIndex, endIndex];
248     }
249
250     startEditingFirstProperty()
251     {
252         let firstEditableProperty = this._editablePropertyAfter(-1);
253         if (firstEditableProperty)
254             firstEditableProperty.startEditingName();
255         else {
256             const appendAfterLast = -1;
257             this.addBlankProperty(appendAfterLast);
258         }
259     }
260
261     startEditingLastProperty()
262     {
263         let lastEditableProperty = this._editablePropertyBefore(this._propertyViews.length);
264         if (lastEditableProperty)
265             lastEditableProperty.startEditingValue();
266         else {
267             const appendAfterLast = -1;
268             this.addBlankProperty(appendAfterLast);
269         }
270     }
271
272     highlightProperty(property)
273     {
274         let propertiesMatch = (cssProperty) => {
275             if (cssProperty.attached && !cssProperty.overridden) {
276                 if (cssProperty.canonicalName === property.canonicalName || hasMatchingLonghandProperty(cssProperty))
277                     return true;
278             }
279
280             return false;
281         };
282
283         let hasMatchingLonghandProperty = (cssProperty) => {
284             let cssProperties = cssProperty.relatedLonghandProperties;
285
286             if (!cssProperties.length)
287                 return false;
288
289             for (let property of cssProperties) {
290                 if (propertiesMatch(property))
291                     return true;
292             }
293
294             return false;
295         };
296
297         for (let i = 0; i < this._propertyViews.length; ++i) {
298             if (propertiesMatch(this._propertyViews[i].property)) {
299                 this.selectProperties(i, i);
300                 return true;
301             }
302         }
303
304         return false;
305     }
306
307     addBlankProperty(index)
308     {
309         this._pendingAddBlankPropertyIndexOffset = NaN;
310
311         if (index === -1) {
312             // Append to the end.
313             index = this._propertyViews.length;
314         }
315
316         this._propertyPendingStartEditing = this._style.newBlankProperty(index);
317         this.needsLayout();
318     }
319
320     hasSelectedProperties()
321     {
322         return !isNaN(this._anchorIndex) && !isNaN(this._focusIndex);
323     }
324
325     selectProperties(anchorIndex, focusIndex)
326     {
327         console.assert(anchorIndex < this._propertyViews.length, `anchorIndex (${anchorIndex}) is greater than the last property index (${this._propertyViews.length})`);
328         console.assert(focusIndex < this._propertyViews.length, `focusIndex (${focusIndex}) is greater than the last property index (${this._propertyViews.length})`);
329
330         if (isNaN(anchorIndex) || isNaN(focusIndex)) {
331             console.error(`Nothing to select. anchorIndex (${anchorIndex}) and focusIndex (${focusIndex}) must be numbers.`);
332             this.deselectProperties();
333             return;
334         }
335
336         this._anchorIndex = anchorIndex;
337         this._focusIndex = focusIndex;
338
339         let [startIndex, endIndex] = this.selectionRange;
340
341         for (let i = 0; i < this._propertyViews.length; ++i) {
342             let propertyView = this._propertyViews[i];
343             let isSelected = i >= startIndex && i <= endIndex;
344             propertyView.selected = isSelected;
345         }
346
347         this._suppressBlur = true;
348         let property = this._propertyViews[focusIndex];
349         property.element.focus();
350         this._suppressBlur = false;
351     }
352
353     extendSelectedProperties(focusIndex)
354     {
355         this.selectProperties(this._anchorIndex, focusIndex);
356     }
357
358     deselectProperties()
359     {
360         for (let propertyView  of this._propertyViews)
361             propertyView.selected = false;
362
363         this._focused = false;
364         this._anchorIndex = NaN;
365         this._focusIndex = NaN;
366     }
367
368     applyFilter(filterText)
369     {
370         this._filterText = filterText;
371
372         if (!this.didInitialLayout)
373             return;
374
375         let matches = false;
376         for (let propertyView of this._propertyViews) {
377             if (propertyView.applyFilter(this._filterText)) {
378                 matches = true;
379
380                 if (this._hideFilterNonMatchingProperties)
381                     this.element.append(propertyView.element);
382             } else if (this._hideFilterNonMatchingProperties)
383                 propertyView.element.remove();
384         }
385
386         this.dispatchEventToListeners(WI.SpreadsheetCSSStyleDeclarationEditor.Event.FilterApplied, {matches});
387     }
388
389     // SpreadsheetStyleProperty delegate
390
391     spreadsheetStylePropertyBlur(event, property)
392     {
393         if (this._suppressBlur)
394             return;
395
396         if (this._delegate.spreadsheetCSSStyleDeclarationEditorPropertyBlur)
397             this._delegate.spreadsheetCSSStyleDeclarationEditorPropertyBlur(event, property);
398     }
399
400     spreadsheetStylePropertyMouseEnter(event, property)
401     {
402         if (this._delegate.spreadsheetCSSStyleDeclarationEditorPropertyMouseEnter)
403             this._delegate.spreadsheetCSSStyleDeclarationEditorPropertyMouseEnter(event, property);
404     }
405
406     spreadsheetStylePropertyFocusMoved(propertyView, {direction, willRemoveProperty})
407     {
408         this._updatePropertiesStatus();
409
410         if (!direction)
411             return;
412
413         let movedFromIndex = this._propertyViews.indexOf(propertyView);
414         console.assert(movedFromIndex !== -1, "Property doesn't exist, focusing on a selector as a fallback.");
415         if (movedFromIndex === -1) {
416             if (this._style.selectorEditable)
417                 this._delegate.spreadsheetCSSStyleDeclarationEditorStartEditingRuleSelector();
418
419             return;
420         }
421
422         if (direction === "forward") {
423             // Move from the value to the next enabled property's name.
424             let propertyView = this._editablePropertyAfter(movedFromIndex);
425             if (propertyView)
426                 propertyView.startEditingName();
427             else {
428                 if (willRemoveProperty) {
429                     const delta = 1;
430                     this._delegate.spreadsheetCSSStyleDeclarationEditorStartEditingAdjacentRule(this, delta);
431                 } else {
432                     const appendAfterLast = -1;
433                     this.addBlankProperty(appendAfterLast);
434                 }
435             }
436         } else {
437             let propertyView = this._editablePropertyBefore(movedFromIndex);
438             if (propertyView) {
439                 // Move from the property's name to the previous enabled property's value.
440                 propertyView.startEditingValue();
441             } else {
442                 // Move from the first property's name to the rule's selector.
443                 if (this._style.selectorEditable)
444                     this._delegate.spreadsheetCSSStyleDeclarationEditorStartEditingRuleSelector();
445                 else {
446                     const delta = -1;
447                     this._delegate.spreadsheetCSSStyleDeclarationEditorStartEditingAdjacentRule(this, delta);
448                 }
449             }
450         }
451     }
452
453     spreadsheetStylePropertyAddBlankPropertySoon(propertyView, {index})
454     {
455         if (isNaN(index))
456             index = this._propertyViews.length;
457         this._pendingAddBlankPropertyIndexOffset = this._propertyViews.length - index;
458     }
459
460     spreadsheetStylePropertyCopy(event)
461     {
462         if (!this.hasSelectedProperties())
463             return;
464
465         let formattedProperties = [];
466         let [startIndex, endIndex] = this.selectionRange;
467         for (let i = startIndex; i <= endIndex; ++i) {
468             let propertyView = this._propertyViews[i];
469             formattedProperties.push(propertyView.property.formattedText);
470         }
471
472         event.clipboardData.setData("text/plain", formattedProperties.join("\n"));
473         event.stop();
474     }
475
476     spreadsheetStylePropertyRemoved(propertyView)
477     {
478         this._propertyViews.remove(propertyView);
479
480         for (let index = 0; index < this._propertyViews.length; index++)
481             this._propertyViews[index].index = index;
482
483         this.focused = false;
484     }
485
486     spreadsheetStylePropertyShowProperty(propertyView, property)
487     {
488         if (this._delegate.spreadsheetCSSStyleDeclarationEditorShowProperty)
489             this._delegate.spreadsheetCSSStyleDeclarationEditorShowProperty(this, property);
490     }
491
492     spreadsheetStylePropertyDidPressEsc(propertyView)
493     {
494         let index = this._propertyViews.indexOf(propertyView);
495         console.assert(index !== -1, `Can't find StyleProperty to select (${propertyView.property.name})`);
496         if (index !== -1)
497             this.selectProperties(index, index);
498     }
499
500     stylePropertyInlineSwatchActivated()
501     {
502         this.inlineSwatchActive = true;
503     }
504
505     stylePropertyInlineSwatchDeactivated()
506     {
507         this.inlineSwatchActive = false;
508     }
509
510     // Private
511
512     _handleKeyDown(event)
513     {
514         if (!this.hasSelectedProperties() || !this._propertyViews.length)
515             return;
516
517         if (event.key === "ArrowUp" || event.key === "ArrowDown") {
518             let delta = event.key === "ArrowUp" ? -1 : 1;
519             let focusIndex = Number.constrain(this._focusIndex + delta, 0, this._propertyViews.length - 1);
520
521             if (event.shiftKey)
522                 this.extendSelectedProperties(focusIndex);
523             else
524                 this.selectProperties(focusIndex, focusIndex);
525
526             event.stop();
527         } else if (event.key === "Tab" || event.key === "Enter") {
528             if (!this.style.editable)
529                 return;
530
531             let property = this._propertyViews[this._focusIndex];
532             if (property && property.enabled) {
533                 event.stop();
534                 property.startEditingName();
535             }
536         } else if (event.key === "Backspace") {
537             let [startIndex, endIndex] = this.selectionRange;
538
539             let propertyIndexToSelect = NaN;
540             if (endIndex + 1 !== this._propertyViews.length)
541                 propertyIndexToSelect = startIndex;
542             else if (startIndex > 0)
543                 propertyIndexToSelect = startIndex - 1;
544
545             this.deselectProperties();
546
547             for (let i = endIndex; i >= startIndex; i--)
548                 this._propertyViews[i].remove();
549
550             if (!isNaN(propertyIndexToSelect))
551                 this.selectProperties(propertyIndexToSelect, propertyIndexToSelect);
552
553             event.stop();
554
555         } else if ((event.code === "Space" && !event.shiftKey && !event.metaKey && !event.ctrlKey) || (event.key === "/" && event.commandOrControlKey && !event.shiftKey)) {
556             if (!this.style.editable)
557                 return;
558
559             let [startIndex, endIndex] = this.selectionRange;
560
561             // Toggle the first selected property and set this state to all selected properties.
562             let disabled = this._propertyViews[startIndex].property.enabled;
563
564             for (let i = endIndex; i >= startIndex; --i) {
565                 let propertyView = this._propertyViews[i];
566                 propertyView.property.commentOut(disabled);
567                 propertyView.update();
568             }
569
570             event.stop();
571
572         } else if (event.key === "a" && event.commandOrControlKey) {
573
574             this.selectProperties(0, this._propertyViews.length - 1);
575             event.stop();
576
577         } else if (event.key === "Esc")
578             this.deselectProperties();
579     }
580
581     _editablePropertyAfter(propertyIndex)
582     {
583         for (let index = propertyIndex + 1; index < this._propertyViews.length; index++) {
584             let property = this._propertyViews[index];
585             if (property.enabled)
586                 return property;
587         }
588
589         return null;
590     }
591
592     _editablePropertyBefore(propertyIndex)
593     {
594         for (let index = propertyIndex - 1; index >= 0; index--) {
595             let property = this._propertyViews[index];
596             if (property.enabled)
597                 return property;
598         }
599
600         return null;
601     }
602
603     _propertiesChanged(event)
604     {
605         if (this.editing && isNaN(this._pendingAddBlankPropertyIndexOffset))
606             this._updatePropertiesStatus();
607         else
608             this.needsLayout();
609     }
610
611     _updatePropertiesStatus()
612     {
613         for (let propertyView of this._propertyViews)
614             propertyView.updateStatus();
615     }
616
617     _updateStyleLock()
618     {
619         if (!this._style)
620             return;
621
622         this._style.locked = this._focused || this._inlineSwatchActive;
623         this._updateDebugLockStatus();
624     }
625
626     _updateDebugLockStatus()
627     {
628         if (!this._style || !WI.settings.enableStyleEditingDebugMode.value)
629             return;
630
631         this.element.classList.toggle("debug-style-locked", this._style.locked);
632     }
633 };
634
635 WI.SpreadsheetCSSStyleDeclarationEditor.Event = {
636     FilterApplied: "spreadsheet-css-style-declaration-editor-filter-applied",
637 };
638
639 WI.SpreadsheetCSSStyleDeclarationEditor.StyleClassName = "spreadsheet-style-declaration-editor";
640
641 WI.SpreadsheetCSSStyleDeclarationEditor.PropertyVisibilityMode = {
642     ShowAll: Symbol("variable-visibility-show-all"),
643     HideVariables: Symbol("variable-visibility-hide-variables"),
644     HideNonVariables: Symbol("variable-visibility-hide-non-variables"),
645 };