Web Inspector: Styles: variable swatch not shown for var() with a fallback
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / InlineSwatch.js
1 /*
2  * Copyright (C) 2015 Apple Inc. All rights reserved.
3  * Copyright (C) 2016 Devin Rousso <webkit@devinrousso.com>. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
15  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
16  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
18  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
24  * THE POSSIBILITY OF SUCH DAMAGE.
25  */
26
27 WI.InlineSwatch = class InlineSwatch extends WI.Object
28 {
29     constructor(type, value, readOnly)
30     {
31         super();
32
33         this._type = type;
34
35         if (this._type === WI.InlineSwatch.Type.Bezier || this._type === WI.InlineSwatch.Type.Spring)
36             this._swatchElement = WI.ImageUtilities.useSVGSymbol("Images/CubicBezier.svg");
37         else if (this._type === WI.InlineSwatch.Type.Variable)
38             this._swatchElement = WI.ImageUtilities.useSVGSymbol("Images/CSSVariable.svg");
39         else
40             this._swatchElement = document.createElement("span");
41
42         this._swatchElement.classList.add("inline-swatch", this._type.split("-").lastValue);
43
44         if (readOnly)
45             this._swatchElement.classList.add("read-only");
46         else {
47             switch (this._type) {
48             case WI.InlineSwatch.Type.Color:
49                 this._swatchElement.title = WI.UIString("Click to select a color\nShift-click to switch color formats");
50                 break;
51             case WI.InlineSwatch.Type.Gradient:
52                 this._swatchElement.title = WI.UIString("Edit custom gradient");
53                 break;
54             case WI.InlineSwatch.Type.Bezier:
55                 this._swatchElement.title = WI.UIString("Edit \u201Ccubic-bezier\u201D function");
56                 break;
57             case WI.InlineSwatch.Type.Spring:
58                 this._swatchElement.title = WI.UIString("Edit \u201Cspring\u201D function");
59                 break;
60             case WI.InlineSwatch.Type.Variable:
61                 this._swatchElement.title = WI.UIString("Click to view variable value\nShift-click to replace variable with value");
62                 break;
63             case WI.InlineSwatch.Type.Image:
64                 this._swatchElement.title = WI.UIString("View Image");
65                 break;
66             default:
67                 WI.reportInternalError(`Unknown InlineSwatch type "${type}"`);
68                 break;
69             }
70
71             this._swatchElement.addEventListener("click", this._swatchElementClicked.bind(this));
72             if (this._type === WI.InlineSwatch.Type.Color)
73                 this._swatchElement.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this));
74         }
75
76         this._swatchInnerElement = this._swatchElement.createChild("span");
77
78         this._value = value || this._fallbackValue();
79         this._valueEditor = null;
80
81         this._updateSwatch();
82     }
83
84     // Public
85
86     get element()
87     {
88         return this._swatchElement;
89     }
90
91     get value()
92     {
93         if (typeof this._value === "function")
94             return this._value();
95         return this._value;
96     }
97
98     set value(value)
99     {
100         this._value = value;
101         this._updateSwatch(true);
102     }
103
104     // Popover delegate
105
106     didDismissPopover(popover)
107     {
108         if (!this._valueEditor)
109             return;
110
111         if (this._valueEditor.removeListeners)
112             this._valueEditor.removeListeners();
113
114         if (this._valueEditor instanceof WI.Object)
115             this._valueEditor.removeEventListener(null, null, this);
116
117         this._valueEditor = null;
118
119         this.dispatchEventToListeners(WI.InlineSwatch.Event.Deactivated);
120     }
121
122     // Private
123
124     _fallbackValue()
125     {
126         switch (this._type) {
127         case WI.InlineSwatch.Type.Bezier:
128             return WI.CubicBezier.fromString("linear");
129         case WI.InlineSwatch.Type.Gradient:
130             return WI.Gradient.fromString("linear-gradient(transparent, transparent)");
131         case WI.InlineSwatch.Type.Spring:
132             return WI.Spring.fromString("1 100 10 0");
133         case WI.InlineSwatch.Type.Color:
134             return WI.Color.fromString("white");
135         default:
136             return null;
137         }
138     }
139
140     _updateSwatch(dontFireEvents)
141     {
142         let value = this.value;
143
144         if (this._type === WI.InlineSwatch.Type.Color || this._type === WI.InlineSwatch.Type.Gradient)
145             this._swatchInnerElement.style.background = value ? value.toString() : null;
146         else if (this._type === WI.InlineSwatch.Type.Image)
147             this._swatchInnerElement.style.setProperty("background-image", `url(${value.src})`);
148
149         if (!dontFireEvents)
150             this.dispatchEventToListeners(WI.InlineSwatch.Event.ValueChanged, {value});
151     }
152
153     _swatchElementClicked(event)
154     {
155         event.stop();
156
157         let value = this.value;
158
159         if (event.shiftKey && value) {
160             if (this._type === WI.InlineSwatch.Type.Color) {
161                 let nextFormat = value.nextFormat();
162                 console.assert(nextFormat);
163                 if (nextFormat) {
164                     value.format = nextFormat;
165                     this._updateSwatch();
166                 }
167                 return;
168             }
169
170             if (this._type === WI.InlineSwatch.Type.Variable) {
171                 // Force the swatch to replace the displayed text with the variable's value.
172                 this._swatchElement.remove();
173                 this._updateSwatch();
174                 return;
175             }
176         }
177
178         if (this._valueEditor)
179             return;
180
181         if (!value)
182             value = this._fallbackValue();
183
184         let bounds = WI.Rect.rectFromClientRect(this._swatchElement.getBoundingClientRect());
185         let popover = new WI.Popover(this);
186
187         popover.windowResizeHandler = () => {
188             let bounds = WI.Rect.rectFromClientRect(this._swatchElement.getBoundingClientRect());
189             popover.present(bounds.pad(2), [WI.RectEdge.MIN_X]);
190         };
191
192         this._valueEditor = null;
193         switch (this._type) {
194         case WI.InlineSwatch.Type.Color:
195             this._valueEditor = new WI.ColorPicker;
196             this._valueEditor.addEventListener(WI.ColorPicker.Event.ColorChanged, this._valueEditorValueDidChange, this);
197             this._valueEditor.addEventListener(WI.ColorPicker.Event.FormatChanged, (event) => popover.update());
198             break;
199
200         case WI.InlineSwatch.Type.Gradient:
201             this._valueEditor = new WI.GradientEditor;
202             this._valueEditor.addEventListener(WI.GradientEditor.Event.GradientChanged, this._valueEditorValueDidChange, this);
203             this._valueEditor.addEventListener(WI.GradientEditor.Event.ColorPickerToggled, (event) => popover.update());
204             break;
205
206         case WI.InlineSwatch.Type.Bezier:
207             this._valueEditor = new WI.BezierEditor;
208             this._valueEditor.addEventListener(WI.BezierEditor.Event.BezierChanged, this._valueEditorValueDidChange, this);
209             break;
210
211         case WI.InlineSwatch.Type.Spring:
212             this._valueEditor = new WI.SpringEditor;
213             this._valueEditor.addEventListener(WI.SpringEditor.Event.SpringChanged, this._valueEditorValueDidChange, this);
214             break;
215
216         case WI.InlineSwatch.Type.Variable:
217             this._valueEditor = {};
218
219             this._valueEditor.element = document.createElement("div");
220             this._valueEditor.element.classList.add("inline-swatch-variable-popover");
221
222             this._valueEditor.codeMirror = WI.CodeMirrorEditor.create(this._valueEditor.element, {
223                 mode: "css",
224                 readOnly: true,
225             });
226             this._valueEditor.codeMirror.on("update", () => {
227                 popover.update();
228             });
229             break;
230
231         case WI.InlineSwatch.Type.Image:
232             if (value.src) {
233                 this._valueEditor = {};
234                 this._valueEditor.element = document.createElement("img");
235                 this._valueEditor.element.src = value.src;
236                 this._valueEditor.element.classList.add("show-grid");
237                 this._valueEditor.element.style.setProperty("max-width", "50vw");
238                 this._valueEditor.element.style.setProperty("max-height", "50vh");
239             }
240             break;
241         }
242
243         if (!this._valueEditor)
244             return;
245
246         popover.content = this._valueEditor.element;
247         popover.present(bounds.pad(2), [WI.RectEdge.MIN_X]);
248
249         this.dispatchEventToListeners(WI.InlineSwatch.Event.Activated);
250
251         switch (this._type) {
252         case WI.InlineSwatch.Type.Color:
253             this._valueEditor.color = value;
254             break;
255
256         case WI.InlineSwatch.Type.Gradient:
257             this._valueEditor.gradient = value;
258             break;
259
260         case WI.InlineSwatch.Type.Bezier:
261             this._valueEditor.bezier = value;
262             break;
263
264         case WI.InlineSwatch.Type.Spring:
265             this._valueEditor.spring = value;
266             break;
267
268         case WI.InlineSwatch.Type.Variable: {
269             let codeMirror = this._valueEditor.codeMirror;
270             codeMirror.setValue(value);
271
272             const range = null;
273             function optionsForType(type) {
274                 return {
275                     allowedTokens: /\btag\b/,
276                     callback(marker, valueObject, valueString) {
277                         let swatch = new WI.InlineSwatch(type, valueObject, true);
278                         codeMirror.setUniqueBookmark({line: 0, ch: 0}, swatch.element);
279                     }
280                 };
281             }
282             createCodeMirrorColorTextMarkers(codeMirror, range, optionsForType(WI.InlineSwatch.Type.Color));
283             createCodeMirrorGradientTextMarkers(codeMirror, range, optionsForType(WI.InlineSwatch.Type.Gradient));
284             createCodeMirrorCubicBezierTextMarkers(codeMirror, range, optionsForType(WI.InlineSwatch.Type.Bezier));
285             createCodeMirrorSpringTextMarkers(codeMirror, range, optionsForType(WI.InlineSwatch.Type.Spring));
286             break;
287         }
288         }
289     }
290
291     _valueEditorValueDidChange(event)
292     {
293         if (this._type === WI.InlineSwatch.Type.Color)
294             this._value = event.data.color;
295         else if (this._type === WI.InlineSwatch.Type.Gradient)
296             this._value = event.data.gradient;
297         else if (this._type === WI.InlineSwatch.Type.Bezier)
298             this._value = event.data.bezier;
299         else if (this._type === WI.InlineSwatch.Type.Spring)
300             this._value = event.data.spring;
301
302         this._updateSwatch();
303     }
304
305     _handleContextMenuEvent(event)
306     {
307         let value = this.value;
308         if (!value)
309             return;
310
311         let contextMenu = WI.ContextMenu.createFromEvent(event);
312
313         if (value.isKeyword() && value.format !== WI.Color.Format.Keyword) {
314             contextMenu.appendItem(WI.UIString("Format: Keyword"), () => {
315                 value.format = WI.Color.Format.Keyword;
316                 this._updateSwatch();
317             });
318         }
319
320         let hexInfo = this._getNextValidHEXFormat();
321         if (hexInfo) {
322             contextMenu.appendItem(hexInfo.title, () => {
323                 value.format = hexInfo.format;
324                 this._updateSwatch();
325             });
326         }
327
328         if (value.simple && value.format !== WI.Color.Format.HSL) {
329             contextMenu.appendItem(WI.UIString("Format: HSL"), () => {
330                 value.format = WI.Color.Format.HSL;
331                 this._updateSwatch();
332             });
333         } else if (value.format !== WI.Color.Format.HSLA) {
334             contextMenu.appendItem(WI.UIString("Format: HSLA"), () => {
335                 value.format = WI.Color.Format.HSLA;
336                 this._updateSwatch();
337             });
338         }
339
340         if (value.simple && value.format !== WI.Color.Format.RGB) {
341             contextMenu.appendItem(WI.UIString("Format: RGB"), () => {
342                 value.format = WI.Color.Format.RGB;
343                 this._updateSwatch();
344             });
345         } else if (value.format !== WI.Color.Format.RGBA) {
346             contextMenu.appendItem(WI.UIString("Format: RGBA"), () => {
347                 value.format = WI.Color.Format.RGBA;
348                 this._updateSwatch();
349             });
350         }
351     }
352
353     _getNextValidHEXFormat()
354     {
355         if (this._type !== WI.InlineSwatch.Type.Color)
356             return false;
357
358         let value = this.value;
359
360         function hexMatchesCurrentColor(hexInfo) {
361             let nextIsSimple = hexInfo.format === WI.Color.Format.ShortHEX || hexInfo.format === WI.Color.Format.HEX;
362             if (nextIsSimple && !value.simple)
363                 return false;
364
365             let nextIsShort = hexInfo.format === WI.Color.Format.ShortHEX || hexInfo.format === WI.Color.Format.ShortHEXAlpha;
366             if (nextIsShort && !value.canBeSerializedAsShortHEX())
367                 return false;
368
369             return true;
370         }
371
372         const hexFormats = [
373             {
374                 format: WI.Color.Format.ShortHEX,
375                 title: WI.UIString("Format: Short Hex")
376             },
377             {
378                 format: WI.Color.Format.ShortHEXAlpha,
379                 title: WI.UIString("Format: Short Hex with Alpha")
380             },
381             {
382                 format: WI.Color.Format.HEX,
383                 title: WI.UIString("Format: Hex")
384             },
385             {
386                 format: WI.Color.Format.HEXAlpha,
387                 title: WI.UIString("Format: Hex with Alpha")
388             }
389         ];
390
391         let currentColorIsHEX = hexFormats.some((info) => info.format === value.format);
392
393         for (let i = 0; i < hexFormats.length; ++i) {
394             if (currentColorIsHEX && value.format !== hexFormats[i].format)
395                 continue;
396
397             for (let j = ~~currentColorIsHEX; j < hexFormats.length; ++j) {
398                 let nextIndex = (i + j) % hexFormats.length;
399                 if (hexMatchesCurrentColor(hexFormats[nextIndex]))
400                     return hexFormats[nextIndex];
401             }
402             return null;
403         }
404         return hexFormats[0];
405     }
406 };
407
408 WI.InlineSwatch.Type = {
409     Color: "inline-swatch-type-color",
410     Gradient: "inline-swatch-type-gradient",
411     Bezier: "inline-swatch-type-bezier",
412     Spring: "inline-swatch-type-spring",
413     Variable: "inline-swatch-type-variable",
414     Image: "inline-swatch-type-image",
415 };
416
417 WI.InlineSwatch.Event = {
418     ValueChanged: "inline-swatch-value-changed",
419     Activated: "inline-swatch-activated",
420     Deactivated: "inline-swatch-deactivated",
421 };