Web Inspector: Don't throw exceptions in WebInspector.Color
[WebKit-https.git] / Source / WebCore / inspector / front-end / Spectrum.js
1 /*
2  * Copyright (C) 2011 Brian Grinstead 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  *
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  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission.
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 /**
30  * @constructor
31  * @extends {WebInspector.View}
32  */
33 WebInspector.Spectrum = function()
34 {
35     WebInspector.View.call(this);
36     this.registerRequiredCSS("spectrum.css");
37
38     this.element.className = "spectrum-container";
39     this.element.tabIndex = 0;
40
41     var topElement = this.element.createChild("div", "spectrum-top");
42     topElement.createChild("div", "spectrum-fill");
43
44     var topInnerElement = topElement.createChild("div", "spectrum-top-inner fill");
45     this._draggerElement = topInnerElement.createChild("div", "spectrum-color");
46     this._dragHelperElement = this._draggerElement.createChild("div", "spectrum-sat fill").createChild("div", "spectrum-val fill").createChild("div", "spectrum-dragger");
47
48     this._sliderElement = topInnerElement.createChild("div", "spectrum-hue");
49     this.slideHelper = this._sliderElement.createChild("div", "spectrum-slider");
50
51     var rangeContainer = this.element.createChild("div", "spectrum-range-container");
52     var alphaLabel = rangeContainer.createChild("label");
53     alphaLabel.textContent = WebInspector.UIString("\u03B1:");
54
55     this._alphaElement = rangeContainer.createChild("input", "spectrum-range");
56     this._alphaElement.setAttribute("type", "range");
57     this._alphaElement.setAttribute("min", "0");
58     this._alphaElement.setAttribute("max", "100");
59     this._alphaElement.addEventListener("change", alphaDrag.bind(this), false);
60
61     var swatchElement = document.createElement("span");
62     swatchElement.className = "swatch";
63     this._swatchInnerElement = swatchElement.createChild("span", "swatch-inner");
64
65     var displayContainer = this.element.createChild("div");
66     displayContainer.appendChild(swatchElement);
67     this._displayElement = displayContainer.createChild("span", "source-code spectrum-display-value");
68
69     WebInspector.Spectrum.draggable(this._sliderElement, hueDrag.bind(this));
70     WebInspector.Spectrum.draggable(this._draggerElement, colorDrag.bind(this), colorDragStart.bind(this));
71
72     function hueDrag(element, dragX, dragY)
73     {
74         this.hsv[0] = (dragY / this.slideHeight);
75
76         this._onchange();
77     }
78
79     var initialHelperOffset;
80
81     function colorDragStart(element, dragX, dragY)
82     {
83         initialHelperOffset = { x: this._dragHelperElement.offsetLeft, y: this._dragHelperElement.offsetTop };
84     }
85
86     function colorDrag(element, dragX, dragY, event)
87     {
88         if (event.shiftKey) {
89             if (Math.abs(dragX - initialHelperOffset.x) >= Math.abs(dragY - initialHelperOffset.y))
90                 dragY = initialHelperOffset.y;
91             else
92                 dragX = initialHelperOffset.x;
93         }
94
95         this.hsv[1] = dragX / this.dragWidth;
96         this.hsv[2] = (this.dragHeight - dragY) / this.dragHeight;
97
98         this._onchange();
99     }
100
101     function alphaDrag()
102     {
103         this.hsv[3] = this._alphaElement.value / 100;
104
105         this._onchange();
106     }
107 };
108
109 WebInspector.Spectrum.Events = {
110     ColorChanged: "ColorChanged"
111 };
112
113 WebInspector.Spectrum.hsvaToRGBA = function(h, s, v, a)
114 {
115     var r, g, b;
116
117     var i = Math.floor(h * 6);
118     var f = h * 6 - i;
119     var p = v * (1 - s);
120     var q = v * (1 - f * s);
121     var t = v * (1 - (1 - f) * s);
122
123     switch(i % 6) {
124     case 0:
125         r = v, g = t, b = p;
126         break;
127     case 1:
128         r = q, g = v, b = p;
129         break;
130     case 2:
131         r = p, g = v, b = t;
132         break;
133     case 3:
134         r = p, g = q, b = v;
135         break;
136     case 4:
137         r = t, g = p, b = v;
138         break;
139     case 5:
140         r = v, g = p, b = q;
141         break;
142     }
143
144     return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), a];
145 };
146
147 WebInspector.Spectrum.rgbaToHSVA = function(r, g, b, a)
148 {
149     r = r / 255;
150     g = g / 255;
151     b = b / 255;
152
153     var max = Math.max(r, g, b);
154     var min = Math.min(r, g, b);
155     var h;
156     var s;
157     var v = max;
158
159     var d = max - min;
160     s = max ? d / max : 0;
161
162     if(max === min) {
163         // Achromatic.
164         h = 0;
165     } else {
166         switch(max) {
167         case r:
168             h = (g - b) / d + (g < b ? 6 : 0);
169             break;
170         case g:
171             h = (b - r) / d + 2;
172             break;
173         case b:
174             h = (r - g) / d + 4;
175             break;
176         }
177         h /= 6;
178     }
179     return [h, s, v, a];
180 };
181
182 //FIXME: migrate to WebInspector.installDragHandle
183 /**
184  * @param {Function=} onmove
185  * @param {Function=} onstart
186  * @param {Function=} onstop
187  */
188 WebInspector.Spectrum.draggable = function(element, onmove, onstart, onstop) {
189
190     var doc = document;
191     var dragging;
192     var offset;
193     var scrollOffset;
194     var maxHeight;
195     var maxWidth;
196
197     function consume(e)
198     {
199         e.consume(true);
200     }
201
202     function move(e)
203     {
204         if (dragging) {
205             var dragX = Math.max(0, Math.min(e.pageX - offset.left + scrollOffset.left, maxWidth));
206             var dragY = Math.max(0, Math.min(e.pageY - offset.top + scrollOffset.top, maxHeight));
207
208             if (onmove)
209                 onmove(element, dragX, dragY, e);
210         }
211     }
212
213     function start(e)
214     {
215         var rightClick = e.which ? (e.which === 3) : (e.button === 2);
216
217         if (!rightClick && !dragging) {
218
219             if (onstart)
220                 onstart(element, e)
221
222             dragging = true;
223             maxHeight = element.clientHeight;
224             maxWidth = element.clientWidth;
225
226             scrollOffset = element.scrollOffset();
227             offset = element.totalOffset();
228
229             doc.addEventListener("selectstart", consume, false);
230             doc.addEventListener("dragstart", consume, false);
231             doc.addEventListener("mousemove", move, false);
232             doc.addEventListener("mouseup", stop, false);
233
234             move(e);
235             consume(e);
236         }
237     }
238
239     function stop(e)
240     {
241         if (dragging) {
242             doc.removeEventListener("selectstart", consume, false);
243             doc.removeEventListener("dragstart", consume, false);
244             doc.removeEventListener("mousemove", move, false);
245             doc.removeEventListener("mouseup", stop, false);
246
247             if (onstop)
248                 onstop(element, e);
249         }
250
251         dragging = false;
252     }
253
254     element.addEventListener("mousedown", start, false);
255 };
256
257 WebInspector.Spectrum.prototype = {
258     /**
259      * @type {WebInspector.Color}
260      */
261     set color(color)
262     {
263         var rgba = (color.rgba || color.rgb).slice(0);
264
265         if (rgba.length === 3)
266             rgba[3] = 1;
267
268         this.hsv = WebInspector.Spectrum.rgbaToHSVA(rgba[0], rgba[1], rgba[2], rgba[3]);
269     },
270
271     get color()
272     {
273         var rgba = WebInspector.Spectrum.hsvaToRGBA(this.hsv[0], this.hsv[1], this.hsv[2], this.hsv[3]);
274         var color;
275
276         if (rgba[3] === 1)
277             color = WebInspector.Color.fromRGB(rgba[0], rgba[1], rgba[2]);
278         else
279             color = WebInspector.Color.fromRGBA(rgba[0], rgba[1], rgba[2], rgba[3]);
280
281         var colorValue = color.toString(this.outputColorFormat);
282         if (!colorValue)
283             colorValue = color.toString(); // this.outputColorFormat can be invalid for current color (e.g. "nickname").
284         return WebInspector.Color.parse(colorValue);
285     },
286
287     get outputColorFormat()
288     {
289         var cf = WebInspector.Color.Format;
290         var format = this._originalFormat;
291
292         if (this.hsv[3] === 1) {
293             // Simplify transparent formats.
294             if (format === cf.RGBA)
295                 format = cf.RGB;
296             else if (format === cf.HSLA)
297                 format = cf.HSL;
298         } else {
299             // Everything except HSL(A) should be returned as RGBA if transparency is involved.
300             if (format === cf.HSL || format === cf.HSLA)
301                 format = cf.HSLA;
302             else
303                 format = cf.RGBA;
304         }
305
306         return format;
307     },
308
309     get colorHueOnly()
310     {
311         var rgba = WebInspector.Spectrum.hsvaToRGBA(this.hsv[0], 1, 1, 1);
312         return WebInspector.Color.fromRGBA(rgba[0], rgba[1], rgba[2], rgba[3]);
313     },
314
315     set displayText(text)
316     {
317         this._displayElement.textContent = text;
318     },
319
320     _onchange: function()
321     {
322         this._updateUI();
323         this.dispatchEventToListeners(WebInspector.Spectrum.Events.ColorChanged, this.color);
324     },
325
326     _updateHelperLocations: function()
327     {
328         var h = this.hsv[0];
329         var s = this.hsv[1];
330         var v = this.hsv[2];
331
332         // Where to show the little circle that displays your current selected color.
333         var dragX = s * this.dragWidth;
334         var dragY = this.dragHeight - (v * this.dragHeight);
335
336         dragX = Math.max(-this._dragHelperElementHeight,
337                         Math.min(this.dragWidth - this._dragHelperElementHeight, dragX - this._dragHelperElementHeight));
338         dragY = Math.max(-this._dragHelperElementHeight,
339                         Math.min(this.dragHeight - this._dragHelperElementHeight, dragY - this._dragHelperElementHeight));
340
341         this._dragHelperElement.positionAt(dragX, dragY);
342
343         // Where to show the bar that displays your current selected hue.
344         var slideY = (h * this.slideHeight) - this.slideHelperHeight;
345         this.slideHelper.style.top = slideY + "px";
346
347         this._alphaElement.value = this.hsv[3] * 100;
348     },
349
350     _updateUI: function()
351     {
352         this._updateHelperLocations();
353
354         var rgb = (this.color.rgba || this.color.rgb).slice(0);
355
356         if (rgb.length === 3)
357             rgb[3] = 1;
358
359         var rgbHueOnly = this.colorHueOnly.rgb;
360
361         var flatColor = "rgb(" + rgbHueOnly[0] + ", " + rgbHueOnly[1] + ", " + rgbHueOnly[2] + ")";
362         var fullColor = "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ")";
363
364         this._draggerElement.style.backgroundColor = flatColor;
365         this._swatchInnerElement.style.backgroundColor = fullColor;
366
367         this._alphaElement.value = this.hsv[3] * 100;
368     },
369
370     wasShown: function()
371     {
372         this.slideHeight = this._sliderElement.offsetHeight;
373         this.dragWidth = this._draggerElement.offsetWidth;
374         this.dragHeight = this._draggerElement.offsetHeight;
375         this._dragHelperElementHeight = this._dragHelperElement.offsetHeight / 2;
376         this.slideHelperHeight = this.slideHelper.offsetHeight / 2;
377         this._updateUI();
378     },
379
380     __proto__: WebInspector.View.prototype
381 }
382
383 /**
384  * @constructor
385  * @extends {WebInspector.Object}
386  */
387 WebInspector.SpectrumPopupHelper = function()
388 {
389     this._spectrum = new WebInspector.Spectrum();
390     this._spectrum.element.addEventListener("keydown", this._onKeyDown.bind(this), false);
391
392     this._popover = new WebInspector.Popover();
393     this._popover.setCanShrink(false);
394     this._popover.element.addEventListener("mousedown", consumeEvent, false);
395
396     this._hideProxy = this.hide.bind(this, true);
397 }
398
399 WebInspector.SpectrumPopupHelper.Events = {
400     Hidden: "Hidden"
401 };
402
403 WebInspector.SpectrumPopupHelper.prototype = {
404     /**
405      * @return {WebInspector.Spectrum}
406      */
407     spectrum: function()
408     {
409         return this._spectrum;
410     },
411
412     toggle: function(element, color, format)
413     {
414         if (this._popover.isShowing())
415             this.hide(true);
416         else
417             this.show(element, color, format);
418
419         return this._popover.isShowing();
420     },
421
422     show: function(element, color, format)
423     {
424         if (this._popover.isShowing()) {
425             if (this._anchorElement === element)
426                 return false;
427
428             // Reopen the picker for another anchor element.
429             this.hide(true);
430         }
431
432         this._anchorElement = element;
433
434         this._spectrum.color = color;
435         this._spectrum._originalFormat = format || color.format;
436         this.reposition(element);
437
438         document.addEventListener("mousedown", this._hideProxy, false);
439         window.addEventListener("blur", this._hideProxy, false);
440         return true;
441     },
442
443     reposition: function(element)
444     {
445         if (!this._previousFocusElement)
446             this._previousFocusElement = WebInspector.currentFocusElement();
447         this._popover.showView(this._spectrum, element);
448         WebInspector.setCurrentFocusElement(this._spectrum.element);
449     },
450
451     /**
452      * @param {boolean=} commitEdit
453      */
454     hide: function(commitEdit)
455     {
456         if (!this._popover.isShowing())
457             return;
458         this._popover.hide();
459
460         document.removeEventListener("mousedown", this._hideProxy, false);
461         window.removeEventListener("blur", this._hideProxy, false);
462
463         this.dispatchEventToListeners(WebInspector.SpectrumPopupHelper.Events.Hidden, !!commitEdit);
464
465         WebInspector.setCurrentFocusElement(this._previousFocusElement);
466         delete this._previousFocusElement;
467
468         delete this._anchorElement;
469     },
470
471     _onKeyDown: function(event)
472     {
473         if (event.keyIdentifier === "Enter") {
474             this.hide(true);
475             event.consume(true);
476             return;
477         }
478         if (event.keyIdentifier === "U+001B") { // Escape key
479             this.hide(false);
480             event.consume(true);
481         }
482     },
483
484     __proto__: WebInspector.Object.prototype
485 }
486
487 /**
488  * @constructor
489  */
490 WebInspector.ColorSwatch = function()
491 {
492     this.element = document.createElement("span");
493     this._swatchInnerElement = this.element.createChild("span", "swatch-inner");
494     this.element.title = WebInspector.UIString("Click to open a colorpicker. Shift-click to change color format");
495     this.element.className = "swatch";
496     this.element.addEventListener("mousedown", consumeEvent, false);
497     this.element.addEventListener("dblclick", consumeEvent, false);
498 }
499
500 WebInspector.ColorSwatch.prototype = {
501     /**
502      * @param {string} colorString
503      */
504     setColorString: function(colorString)
505     {
506         this._swatchInnerElement.style.backgroundColor = colorString;
507     }
508 }