4de1fd4da8a82d4186c0cee22e1935af227ac7c2
[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         return this._value;
94     }
95
96     set value(value)
97     {
98         this._value = value;
99         this._updateSwatch(true);
100     }
101
102     // Popover delegate
103
104     didDismissPopover(popover)
105     {
106         if (!this._valueEditor)
107             return;
108
109         if (this._valueEditor.removeListeners)
110             this._valueEditor.removeListeners();
111
112         if (this._valueEditor instanceof WI.Object)
113             this._valueEditor.removeEventListener(null, null, this);
114
115         this._valueEditor = null;
116
117         this.dispatchEventToListeners(WI.InlineSwatch.Event.Deactivated);
118     }
119
120     // Private
121
122     _fallbackValue()
123     {
124         switch (this._type) {
125         case WI.InlineSwatch.Type.Bezier:
126             return WI.CubicBezier.fromString("linear");
127         case WI.InlineSwatch.Type.Gradient:
128             return WI.Gradient.fromString("linear-gradient(transparent, transparent)");
129         case WI.InlineSwatch.Type.Spring:
130             return WI.Spring.fromString("1 100 10 0");
131         case WI.InlineSwatch.Type.Color:
132             return WI.Color.fromString("white");
133         default:
134             return null;
135         }
136     }
137
138     _updateSwatch(dontFireEvents)
139     {
140         if (this._type === WI.InlineSwatch.Type.Color || this._type === WI.InlineSwatch.Type.Gradient)
141             this._swatchInnerElement.style.background = this._value ? this._value.toString() : null;
142         else if (this._type === WI.InlineSwatch.Type.Image)
143             this._swatchInnerElement.style.setProperty("background-image", `url(${this._value.src})`);
144
145         if (!dontFireEvents)
146             this.dispatchEventToListeners(WI.InlineSwatch.Event.ValueChanged, {value: this._value});
147     }
148
149     _swatchElementClicked(event)
150     {
151         event.stop();
152
153         if (event.shiftKey && this._value) {
154             if (this._type === WI.InlineSwatch.Type.Color) {
155                 let nextFormat = this._value.nextFormat();
156                 console.assert(nextFormat);
157                 if (nextFormat) {
158                     this._value.format = nextFormat;
159                     this._updateSwatch();
160                 }
161                 return;
162             }
163
164             if (this._type === WI.InlineSwatch.Type.Variable) {
165                 // Force the swatch to replace the displayed text with the variable's value.
166                 this._swatchElement.remove();
167                 this._updateSwatch();
168                 return;
169             }
170         }
171
172         if (this._valueEditor)
173             return;
174
175         let bounds = WI.Rect.rectFromClientRect(this._swatchElement.getBoundingClientRect());
176         let popover = new WI.Popover(this);
177
178         popover.windowResizeHandler = () => {
179             let bounds = WI.Rect.rectFromClientRect(this._swatchElement.getBoundingClientRect());
180             popover.present(bounds.pad(2), [WI.RectEdge.MIN_X]);
181         };
182
183         this._valueEditor = null;
184         switch (this._type) {
185         case WI.InlineSwatch.Type.Color:
186             this._valueEditor = new WI.ColorPicker;
187             this._valueEditor.addEventListener(WI.ColorPicker.Event.ColorChanged, this._valueEditorValueDidChange, this);
188             this._valueEditor.addEventListener(WI.ColorPicker.Event.FormatChanged, (event) => popover.update());
189             break;
190
191         case WI.InlineSwatch.Type.Gradient:
192             this._valueEditor = new WI.GradientEditor;
193             this._valueEditor.addEventListener(WI.GradientEditor.Event.GradientChanged, this._valueEditorValueDidChange, this);
194             this._valueEditor.addEventListener(WI.GradientEditor.Event.ColorPickerToggled, (event) => popover.update());
195             break;
196
197         case WI.InlineSwatch.Type.Bezier:
198             this._valueEditor = new WI.BezierEditor;
199             this._valueEditor.addEventListener(WI.BezierEditor.Event.BezierChanged, this._valueEditorValueDidChange, this);
200             break;
201
202         case WI.InlineSwatch.Type.Spring:
203             this._valueEditor = new WI.SpringEditor;
204             this._valueEditor.addEventListener(WI.SpringEditor.Event.SpringChanged, this._valueEditorValueDidChange, this);
205             break;
206
207         case WI.InlineSwatch.Type.Variable:
208             this._valueEditor = {};
209
210             this._valueEditor.element = document.createElement("div");
211             this._valueEditor.element.classList.add("inline-swatch-variable-popover");
212
213             this._valueEditor.codeMirror = WI.CodeMirrorEditor.create(this._valueEditor.element, {
214                 mode: "css",
215                 readOnly: true,
216             });
217             this._valueEditor.codeMirror.on("update", () => {
218                 popover.update();
219             });
220             break;
221
222         case WI.InlineSwatch.Type.Image:
223             this._valueEditor = {};
224             this._valueEditor.element = document.createElement("img");
225             this._valueEditor.element.src = this._value.src;
226             this._valueEditor.element.classList.add("show-grid");
227             this._valueEditor.element.style.setProperty("max-width", "50vw");
228             this._valueEditor.element.style.setProperty("max-height", "50vh");
229             break;
230         }
231
232         if (!this._valueEditor)
233             return;
234
235         popover.content = this._valueEditor.element;
236         popover.present(bounds.pad(2), [WI.RectEdge.MIN_X]);
237
238         this.dispatchEventToListeners(WI.InlineSwatch.Event.Activated);
239
240         let value = this._value || this._fallbackValue();
241         switch (this._type) {
242         case WI.InlineSwatch.Type.Color:
243             this._valueEditor.color = value;
244             break;
245
246         case WI.InlineSwatch.Type.Gradient:
247             this._valueEditor.gradient = value;
248             break;
249
250         case WI.InlineSwatch.Type.Bezier:
251             this._valueEditor.bezier = value;
252             break;
253
254         case WI.InlineSwatch.Type.Spring:
255             this._valueEditor.spring = value;
256             break;
257
258         case WI.InlineSwatch.Type.Variable: {
259             let codeMirror = this._valueEditor.codeMirror;
260             codeMirror.setValue(value);
261
262             const range = null;
263             function optionsForType(type) {
264                 return {
265                     allowedTokens: /\btag\b/,
266                     callback(marker, valueObject, valueString) {
267                         let swatch = new WI.InlineSwatch(type, valueObject, true);
268                         codeMirror.setUniqueBookmark({line: 0, ch: 0}, swatch.element);
269                     }
270                 };
271             }
272             createCodeMirrorColorTextMarkers(codeMirror, range, optionsForType(WI.InlineSwatch.Type.Color));
273             createCodeMirrorGradientTextMarkers(codeMirror, range, optionsForType(WI.InlineSwatch.Type.Gradient));
274             createCodeMirrorCubicBezierTextMarkers(codeMirror, range, optionsForType(WI.InlineSwatch.Type.Bezier));
275             createCodeMirrorSpringTextMarkers(codeMirror, range, optionsForType(WI.InlineSwatch.Type.Spring));
276             break;
277         }
278         }
279     }
280
281     _valueEditorValueDidChange(event)
282     {
283         if (this._type === WI.InlineSwatch.Type.Color)
284             this._value = event.data.color;
285         else if (this._type === WI.InlineSwatch.Type.Gradient)
286             this._value = event.data.gradient;
287         else if (this._type === WI.InlineSwatch.Type.Bezier)
288             this._value = event.data.bezier;
289         else if (this._type === WI.InlineSwatch.Type.Spring)
290             this._value = event.data.spring;
291
292         this._updateSwatch();
293     }
294
295     _handleContextMenuEvent(event)
296     {
297         if (!this._value)
298             return;
299
300         let contextMenu = WI.ContextMenu.createFromEvent(event);
301
302         if (this._value.isKeyword() && this._value.format !== WI.Color.Format.Keyword) {
303             contextMenu.appendItem(WI.UIString("Format: Keyword"), () => {
304                 this._value.format = WI.Color.Format.Keyword;
305                 this._updateSwatch();
306             });
307         }
308
309         let hexInfo = this._getNextValidHEXFormat();
310         if (hexInfo) {
311             contextMenu.appendItem(hexInfo.title, () => {
312                 this._value.format = hexInfo.format;
313                 this._updateSwatch();
314             });
315         }
316
317         if (this._value.simple && this._value.format !== WI.Color.Format.HSL) {
318             contextMenu.appendItem(WI.UIString("Format: HSL"), () => {
319                 this._value.format = WI.Color.Format.HSL;
320                 this._updateSwatch();
321             });
322         } else if (this._value.format !== WI.Color.Format.HSLA) {
323             contextMenu.appendItem(WI.UIString("Format: HSLA"), () => {
324                 this._value.format = WI.Color.Format.HSLA;
325                 this._updateSwatch();
326             });
327         }
328
329         if (this._value.simple && this._value.format !== WI.Color.Format.RGB) {
330             contextMenu.appendItem(WI.UIString("Format: RGB"), () => {
331                 this._value.format = WI.Color.Format.RGB;
332                 this._updateSwatch();
333             });
334         } else if (this._value.format !== WI.Color.Format.RGBA) {
335             contextMenu.appendItem(WI.UIString("Format: RGBA"), () => {
336                 this._value.format = WI.Color.Format.RGBA;
337                 this._updateSwatch();
338             });
339         }
340     }
341
342     _getNextValidHEXFormat()
343     {
344         if (this._type !== WI.InlineSwatch.Type.Color)
345             return false;
346
347         function hexMatchesCurrentColor(hexInfo) {
348             let nextIsSimple = hexInfo.format === WI.Color.Format.ShortHEX || hexInfo.format === WI.Color.Format.HEX;
349             if (nextIsSimple && !this._value.simple)
350                 return false;
351
352             let nextIsShort = hexInfo.format === WI.Color.Format.ShortHEX || hexInfo.format === WI.Color.Format.ShortHEXAlpha;
353             if (nextIsShort && !this._value.canBeSerializedAsShortHEX())
354                 return false;
355
356             return true;
357         }
358
359         const hexFormats = [
360             {
361                 format: WI.Color.Format.ShortHEX,
362                 title: WI.UIString("Format: Short Hex")
363             },
364             {
365                 format: WI.Color.Format.ShortHEXAlpha,
366                 title: WI.UIString("Format: Short Hex with Alpha")
367             },
368             {
369                 format: WI.Color.Format.HEX,
370                 title: WI.UIString("Format: Hex")
371             },
372             {
373                 format: WI.Color.Format.HEXAlpha,
374                 title: WI.UIString("Format: Hex with Alpha")
375             }
376         ];
377
378         let currentColorIsHEX = hexFormats.some((info) => info.format === this._value.format);
379
380         for (let i = 0; i < hexFormats.length; ++i) {
381             if (currentColorIsHEX && this._value.format !== hexFormats[i].format)
382                 continue;
383
384             for (let j = ~~currentColorIsHEX; j < hexFormats.length; ++j) {
385                 let nextIndex = (i + j) % hexFormats.length;
386                 if (hexMatchesCurrentColor.call(this, hexFormats[nextIndex]))
387                     return hexFormats[nextIndex];
388             }
389             return null;
390         }
391         return hexFormats[0];
392     }
393 };
394
395 WI.InlineSwatch.Type = {
396     Color: "inline-swatch-type-color",
397     Gradient: "inline-swatch-type-gradient",
398     Bezier: "inline-swatch-type-bezier",
399     Spring: "inline-swatch-type-spring",
400     Variable: "inline-swatch-type-variable",
401     Image: "inline-swatch-type-image",
402 };
403
404 WI.InlineSwatch.Event = {
405     ValueChanged: "inline-swatch-value-changed",
406     Activated: "inline-swatch-activated",
407     Deactivated: "inline-swatch-deactivated",
408 };