Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / elements / Spectrum.js
blob0d8cd20ec9d318824d6cc6779f7eab16df6dfab8
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  */
29 /**
30  * @constructor
31  * @extends {WebInspector.VBox}
32  */
33 WebInspector.Spectrum = function()
35     /**
36      * @param {!Element} parentElement
37      */
38     function appendSwitcherIcon(parentElement)
39     {
40         var icon = parentElement.createSVGChild("svg");
41         icon.setAttribute("height", 16);
42         icon.setAttribute("width", 16);
43         var path = icon.createSVGChild("path");
44         path.setAttribute("d", "M5,6 L11,6 L8,2 Z M5,10 L11,10 L8,14 Z");
45         return icon;
46     }
48     WebInspector.VBox.call(this, true);
49     this.registerRequiredCSS("elements/spectrum.css");
50     this.contentElement.tabIndex = 0;
52     this._colorElement = this.contentElement.createChild("div", "spectrum-color");
53     this._colorDragElement = this._colorElement.createChild("div", "spectrum-sat fill").createChild("div", "spectrum-val fill").createChild("div", "spectrum-dragger");
54     var contrastRatioSVG = this._colorElement.createSVGChild("svg", "spectrum-contrast-container fill");
55     this._contrastRatioLine = contrastRatioSVG.createSVGChild("path", "spectrum-contrast-line");
57     var toolbar = new WebInspector.Toolbar(this.contentElement);
58     toolbar.element.classList.add("spectrum-eye-dropper");
59     this._colorPickerButton = new WebInspector.ToolbarButton(WebInspector.UIString("Toggle color picker"), "eyedropper-toolbar-item");
60     this._colorPickerButton.setToggled(true);
61     this._colorPickerButton.addEventListener("click", this._toggleColorPicker.bind(this, undefined));
62     toolbar.appendToolbarItem(this._colorPickerButton);
64     var swatchElement = this.contentElement.createChild("span", "swatch");
65     this._swatchInnerElement = swatchElement.createChild("span", "swatch-inner");
67     this._hueElement = this.contentElement.createChild("div", "spectrum-hue");
68     this._hueSlider = this._hueElement.createChild("div", "spectrum-slider");
69     this._alphaElement = this.contentElement.createChild("div", "spectrum-alpha");
70     this._alphaElementBackground = this._alphaElement.createChild("div", "spectrum-alpha-background");
71     this._alphaSlider = this._alphaElement.createChild("div", "spectrum-slider");
73     var displaySwitcher = this.contentElement.createChild("div", "spectrum-display-switcher spectrum-switcher");
74     appendSwitcherIcon(displaySwitcher);
75     displaySwitcher.addEventListener("click", this._formatViewSwitch.bind(this));
77     // RGBA/HSLA display.
78     this._displayContainer = this.contentElement.createChild("div", "spectrum-text source-code");
79     this._textValues = [];
80     for (var i = 0; i < 4; ++i) {
81         var inputValue = this._displayContainer.createChild("input", "spectrum-text-value");
82         inputValue.maxLength = 4;
83         this._textValues.push(inputValue);
84         inputValue.addEventListener("keydown", this._inputChanged.bind(this), false);
85         inputValue.addEventListener("input", this._inputChanged.bind(this), false);
86         inputValue.addEventListener("mousewheel", this._inputChanged.bind(this), false);
87     }
89     this._textLabels = this._displayContainer.createChild("div", "spectrum-text-label");
91     // HEX display.
92     this._hexContainer = this.contentElement.createChild("div", "spectrum-text spectrum-text-hex source-code");
93     this._hexValue = this._hexContainer.createChild("input", "spectrum-text-value");
94     this._hexValue.maxLength = 7;
95     this._hexValue.addEventListener("keydown", this._inputChanged.bind(this), false);
96     this._hexValue.addEventListener("input", this._inputChanged.bind(this), false);
97     this._hexValue.addEventListener("mousewheel", this._inputChanged.bind(this), false);
99     var label = this._hexContainer.createChild("div", "spectrum-text-label");
100     label.textContent = "HEX";
102     WebInspector.installDragHandle(this._hueElement, dragStart.bind(this, positionHue.bind(this)), positionHue.bind(this), null, "default");
103     WebInspector.installDragHandle(this._alphaElement, dragStart.bind(this, positionAlpha.bind(this)), positionAlpha.bind(this), null, "default");
104     WebInspector.installDragHandle(this._colorElement, dragStart.bind(this, positionColor.bind(this)), positionColor.bind(this), null, "default");
106     this.element.classList.add("palettes-enabled");
107     /** @type {!Map.<string, !WebInspector.Spectrum.Palette>} */
108     this._palettes = new Map();
109     this._palettePanel = this.contentElement.createChild("div", "palette-panel");
110     this._palettePanelShowing = false;
111     this._paletteContainer = this.contentElement.createChild("div", "spectrum-palette");
112     this._paletteContainer.addEventListener("contextmenu", this._showPaletteColorContextMenu.bind(this, -1));
113     WebInspector.installDragHandle(this._paletteContainer, this._paletteDragStart.bind(this), this._paletteDrag.bind(this), this._paletteDragEnd.bind(this), "default");
114     var paletteSwitcher = this.contentElement.createChild("div", "spectrum-palette-switcher spectrum-switcher");
115     appendSwitcherIcon(paletteSwitcher);
116     paletteSwitcher.addEventListener("click", this._togglePalettePanel.bind(this, true));
118     this._deleteIconToolbar = new WebInspector.Toolbar();
119     this._deleteIconToolbar.element.classList.add("delete-color-toolbar");
120     this._deleteButton = new WebInspector.ToolbarButton("", "garbage-collect-toolbar-item");
121     this._deleteIconToolbar.appendToolbarItem(this._deleteButton);
123     var overlay = this.contentElement.createChild("div", "spectrum-overlay fill");
124     overlay.addEventListener("click", this._togglePalettePanel.bind(this, false));
126     this._addColorToolbar = new WebInspector.Toolbar();
127     this._addColorToolbar.element.classList.add("add-color-toolbar");
128     var addColorButton = new WebInspector.ToolbarButton(WebInspector.UIString("Add to palette"), "add-toolbar-item");
129     addColorButton.addEventListener("click", this._addColorToCustomPalette.bind(this));
130     this._addColorToolbar.appendToolbarItem(addColorButton);
132     new WebInspector.Spectrum.PaletteGenerator(this._generatedPaletteLoaded.bind(this));
134     /**
135      * @param {function(!Event)} callback
136      * @param {!Event} event
137      * @return {boolean}
138      * @this {WebInspector.Spectrum}
139      */
140     function dragStart(callback, event)
141     {
142         this._hueAlphaLeft = this._hueElement.totalOffsetLeft();
143         this._colorOffset = this._colorElement.totalOffset();
144         callback(event);
145         return true;
146     }
148     /**
149      * @param {!Event} event
150      * @this {WebInspector.Spectrum}
151      */
152     function positionHue(event)
153     {
154         var hsva = this._hsv.slice();
155         hsva[0] = Number.constrain(1 - (event.x - this._hueAlphaLeft) / this._hueAlphaWidth, 0, 1);
156         this._innerSetColor(hsva,  "", undefined, WebInspector.Spectrum._ChangeSource.Other);
157     }
159     /**
160      * @param {!Event} event
161      * @this {WebInspector.Spectrum}
162      */
163     function positionAlpha(event)
164     {
165         var newAlpha = Math.round((event.x - this._hueAlphaLeft) / this._hueAlphaWidth * 100) / 100;
166         var hsva = this._hsv.slice();
167         hsva[3] = Number.constrain(newAlpha, 0, 1);
168         var colorFormat = undefined;
169         if (hsva[3] !== 1 && (this._colorFormat === WebInspector.Color.Format.ShortHEX || this._colorFormat === WebInspector.Color.Format.HEX || this._colorFormat === WebInspector.Color.Format.Nickname))
170             colorFormat = WebInspector.Color.Format.RGB;
171         this._innerSetColor(hsva, "", colorFormat, WebInspector.Spectrum._ChangeSource.Other);
172     }
174     /**
175      * @param {!Event} event
176      * @this {WebInspector.Spectrum}
177      */
178     function positionColor(event)
179     {
180         var hsva = this._hsv.slice();
181         hsva[1] = Number.constrain((event.x - this._colorOffset.left) / this.dragWidth, 0, 1);
182         hsva[2] = Number.constrain(1 - (event.y - this._colorOffset.top) / this.dragHeight, 0, 1);
183         this._innerSetColor(hsva,  "", undefined, WebInspector.Spectrum._ChangeSource.Other);
184     }
187 WebInspector.Spectrum._ChangeSource = {
188     Input: "Input",
189     Model: "Model",
190     Other: "Other"
193 WebInspector.Spectrum.Events = {
194     ColorChanged: "ColorChanged",
195     SizeChanged: "SizeChanged"
198 WebInspector.Spectrum._colorChipSize = 24;
199 WebInspector.Spectrum._itemsPerPaletteRow = 8;
201 WebInspector.Spectrum.prototype = {
202     _updatePalettePanel: function()
203     {
204         this._palettePanel.removeChildren();
205         var title = this._palettePanel.createChild("div", "palette-title");
206         title.textContent = WebInspector.UIString("Color Palettes");
207         var toolbar = new WebInspector.Toolbar(this._palettePanel);
208         var closeButton = new WebInspector.ToolbarButton("Return to color picker", "delete-toolbar-item");
209         closeButton.addEventListener("click", this._togglePalettePanel.bind(this, false));
210         toolbar.appendToolbarItem(closeButton);
211         for (var palette of this._palettes.values())
212             this._palettePanel.appendChild(this._createPreviewPaletteElement(palette));
213     },
215     /**
216      * @param {boolean} show
217      */
218     _togglePalettePanel: function(show)
219     {
220         if (this._palettePanelShowing === show)
221             return;
222         if (show)
223             this._updatePalettePanel();
224         this._palettePanelShowing = show;
225         this.contentElement.classList.toggle("palette-panel-showing", show);
226     },
228     /**
229      * @param {string} colorText
230      * @param {number=} animationDelay
231      * @return {!Element}
232      */
233     _createPaletteColor: function(colorText, animationDelay)
234     {
235         var element = createElementWithClass("div", "spectrum-palette-color");
236         element.style.background = String.sprintf("linear-gradient(%s, %s), url(Images/checker.png)", colorText, colorText);
237         if (animationDelay)
238             element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 100, delay: animationDelay, fill: "backwards" });
239         element.title = colorText;
240         return element;
241     },
243     /**
244      * @param {!WebInspector.Spectrum.Palette} palette
245      * @param {boolean} animate
246      * @param {!Event=} event
247      */
248     _showPalette: function(palette, animate, event)
249     {
250         this._paletteContainer.removeChildren();
251         for (var i = 0; i < palette.colors.length; i++) {
252             var animationDelay = animate ? i * 100 / palette.colors.length : 0;
253             var colorElement = this._createPaletteColor(palette.colors[i], animationDelay);
254             colorElement.addEventListener("mousedown", this._paletteColorSelected.bind(this, palette.colors[i], palette.matchUserFormat));
255             if (palette.mutable) {
256                 colorElement.__mutable = true;
257                 colorElement.__color = palette.colors[i];
258                 colorElement.addEventListener("contextmenu", this._showPaletteColorContextMenu.bind(this, i));
259             }
260             this._paletteContainer.appendChild(colorElement);
261         }
262         this._paletteContainerMutable = palette.mutable;
264         var numItems = palette.colors.length;
265         if (palette.mutable)
266             numItems++;
267         var rowsNeeded = Math.max(1, Math.ceil(numItems / WebInspector.Spectrum._itemsPerPaletteRow));
268         if (palette.mutable) {
269             this._paletteContainer.appendChild(this._addColorToolbar.element);
270             this._paletteContainer.appendChild(this._deleteIconToolbar.element);
271         } else {
272             this._addColorToolbar.element.remove();
273             this._deleteIconToolbar.element.remove();
274         }
276         this._togglePalettePanel(false);
277         var paletteColorHeight = 12;
278         var paletteMargin = 12;
279         this.element.style.height = (this._paletteContainer.offsetTop + paletteMargin + (paletteColorHeight + paletteMargin) * rowsNeeded) + "px";
280         this.dispatchEventToListeners(WebInspector.Spectrum.Events.SizeChanged);
281     },
283     /**
284      * @param {!Event} e
285      * @return {number}
286      */
287     _slotIndexForEvent: function(e)
288     {
289         var localX = e.pageX - this._paletteContainer.totalOffsetLeft();
290         var localY = e.pageY - this._paletteContainer.totalOffsetTop();
291         var col = Math.min(localX / WebInspector.Spectrum._colorChipSize | 0, WebInspector.Spectrum._itemsPerPaletteRow - 1);
292         var row = (localY / WebInspector.Spectrum._colorChipSize) | 0;
293         return Math.min(row * WebInspector.Spectrum._itemsPerPaletteRow + col, this._customPaletteSetting.get().colors.length - 1);
294     },
296     /**
297      * @param {!Event} e
298      * @return {boolean}
299      */
300     _isDraggingToBin: function(e)
301     {
302         return e.pageX > this._deleteIconToolbar.element.totalOffsetLeft();
303     },
305     /**
306      * @param {!Event} e
307      * @return {boolean}
308      */
309     _paletteDragStart: function(e)
310     {
311         var element = e.deepElementFromPoint();
312         if (!element || !element.__mutable)
313             return false;
315         var index = this._slotIndexForEvent(e);
316         this._dragElement = element;
317         this._dragHotSpotX = e.pageX - (index % WebInspector.Spectrum._itemsPerPaletteRow) * WebInspector.Spectrum._colorChipSize;
318         this._dragHotSpotY = e.pageY - (index / WebInspector.Spectrum._itemsPerPaletteRow | 0) * WebInspector.Spectrum._colorChipSize;
320         this._deleteIconToolbar.element.classList.add("dragging");
321         return true;
322     },
324     /**
325      * @param {!Event} e
326      */
327     _paletteDrag: function(e)
328     {
329         if (e.pageX < this._paletteContainer.totalOffsetLeft() || e.pageY < this._paletteContainer.totalOffsetTop())
330             return;
331         var newIndex = this._slotIndexForEvent(e);
332         var offsetX = e.pageX - (newIndex % WebInspector.Spectrum._itemsPerPaletteRow) * WebInspector.Spectrum._colorChipSize;
333         var offsetY = e.pageY - (newIndex / WebInspector.Spectrum._itemsPerPaletteRow | 0) * WebInspector.Spectrum._colorChipSize;
335         var isDeleting = this._isDraggingToBin(e);
336         this._deleteIconToolbar.element.classList.toggle("delete-color-toolbar-active", isDeleting);
337         var dragElementTransform = "translateX(" + (offsetX - this._dragHotSpotX) + "px) translateY(" + (offsetY - this._dragHotSpotY) + "px)";
338         this._dragElement.style.transform = isDeleting ? dragElementTransform + " scale(0.8)" : dragElementTransform;
339         var children = Array.prototype.slice.call(this._paletteContainer.children);
340         var index = children.indexOf(this._dragElement);
341         /** @type {!Map.<!Element, {left: number, top: number}>} */
342         var swatchOffsets = new Map();
343         for (var swatch of children)
344             swatchOffsets.set(swatch, swatch.totalOffset());
346         if (index !== newIndex)
347             this._paletteContainer.insertBefore(this._dragElement, children[newIndex > index ? newIndex + 1 : newIndex]);
349         for (var swatch of children) {
350             if (swatch === this._dragElement)
351                 continue;
352             var before = swatchOffsets.get(swatch);
353             var after = swatch.totalOffset();
354             if (before.left !== after.left || before.top !== after.top) {
355                 swatch.animate([
356                     { transform: "translateX(" + (before.left - after.left) + "px) translateY(" + (before.top - after.top) + "px)" },
357                     { transform: "none" }], { duration: 100, easing: "cubic-bezier(0, 0, 0.2, 1)" });
358             }
359         }
360     },
362     /**
363      * @param {!Event} e
364      */
365     _paletteDragEnd: function(e)
366     {
367         if (this._isDraggingToBin(e))
368             this._dragElement.remove();
369         this._dragElement.style.removeProperty("transform");
370         var children = this._paletteContainer.children;
371         var colors = [];
372         for (var i = 0; i < children.length; ++i) {
373             if (children[i].__color)
374                 colors.push(children[i].__color);
375         }
376         var palette = this._customPaletteSetting.get();
377         palette.colors = colors;
378         this._customPaletteSetting.set(palette);
379         this._showPalette(this._customPaletteSetting.get(), false);
381         this._deleteIconToolbar.element.classList.remove("dragging");
382         this._deleteIconToolbar.element.classList.remove("delete-color-toolbar-active");
383         this._deleteButton.setToggled(false);
384     },
386     /**
387      * @param {!WebInspector.Spectrum.Palette} generatedPalette
388      */
389     _generatedPaletteLoaded: function(generatedPalette)
390     {
391         if (generatedPalette.colors.length)
392             this._palettes.set(generatedPalette.title, generatedPalette);
393         this._palettes.set(WebInspector.Spectrum.MaterialPalette.title, WebInspector.Spectrum.MaterialPalette);
394         /** @type {!WebInspector.Spectrum.Palette} */
395         var defaultCustomPalette = { title: "Custom", colors: [], mutable: true };
396         this._customPaletteSetting = WebInspector.settings.createSetting("customColorPalette", defaultCustomPalette);
397         this._palettes.set(this._customPaletteSetting.get().title, this._customPaletteSetting.get());
399         this._selectedColorPalette = WebInspector.settings.createSetting("selectedColorPalette", WebInspector.Spectrum.GeneratedPaletteTitle);
400         var paletteToShow = this._palettes.get(this._selectedColorPalette.get() || WebInspector.Spectrum.GeneratedPaletteTitle)
401             || this._palettes.get("Material");
402         if (paletteToShow)
403             this._showPalette(paletteToShow, true);
404     },
406     /**
407      * @param {!WebInspector.Spectrum.Palette} palette
408      * @return {!Element}
409      */
410     _createPreviewPaletteElement: function(palette)
411     {
412         var colorsPerPreviewRow = 5;
413         var previewElement = createElementWithClass("div", "palette-preview");
414         var titleElement = previewElement.createChild("div", "palette-preview-title");
415         titleElement.textContent = palette.title;
416         for (var i = 0; i < colorsPerPreviewRow && i < palette.colors.length; i++)
417             previewElement.appendChild(this._createPaletteColor(palette.colors[i]));
418         for (; i < colorsPerPreviewRow; i++)
419             previewElement.createChild("div", "spectrum-palette-color empty-color");
420         previewElement.addEventListener("click", this._paletteSelected.bind(this, palette));
421         return previewElement;
422     },
424     /**
425      * @param {!WebInspector.Spectrum.Palette} palette
426      */
427     _paletteSelected: function(palette)
428     {
429         this._selectedColorPalette.set(palette.title);
430         this._showPalette(palette, true);
431     },
433     /**
434      * @param {string} colorText
435      * @param {boolean} matchUserFormat
436      */
437     _paletteColorSelected: function(colorText, matchUserFormat)
438     {
439         var color = WebInspector.Color.parse(colorText);
440         if (!color)
441             return;
442         this._innerSetColor(color.hsva(), colorText, matchUserFormat ? this._colorFormat :  color.format(), WebInspector.Spectrum._ChangeSource.Other);
443     },
445     _addColorToCustomPalette: function()
446     {
447         var palette = this._customPaletteSetting.get();
448         palette.colors.push(this.colorString());
449         this._customPaletteSetting.set(palette);
450         this._showPalette(this._customPaletteSetting.get(), false);
451     },
453     /**
454      * @param {number} colorIndex
455      * @param {!Event} event
456      */
457     _showPaletteColorContextMenu: function(colorIndex, event)
458     {
459         if (!this._paletteContainerMutable)
460             return;
461         var contextMenu = new WebInspector.ContextMenu(event);
462         if (colorIndex !== -1) {
463             contextMenu.appendItem(WebInspector.UIString("Remove color"), this._deletePaletteColors.bind(this, colorIndex, false));
464             contextMenu.appendItem(WebInspector.UIString("Remove all to the right"), this._deletePaletteColors.bind(this, colorIndex, true));
465         }
466         contextMenu.appendItem(WebInspector.UIString("Clear palette"), this._deletePaletteColors.bind(this, -1, true));
467         contextMenu.show();
468     },
470     /**
471      * @param {number} colorIndex
472      * @param {boolean} toRight
473      */
474     _deletePaletteColors: function(colorIndex, toRight)
475     {
476         var palette = this._customPaletteSetting.get();
477         if (toRight)
478             palette.colors.splice(colorIndex + 1, palette.colors.length - colorIndex - 1);
479         else
480             palette.colors.splice(colorIndex, 1);
481         this._customPaletteSetting.set(palette);
482         this._showPalette(this._customPaletteSetting.get(), false);
483     },
485     /**
486      * @param {!WebInspector.Color} color
487      * @param {string} colorFormat
488      */
489     setColor: function(color, colorFormat)
490     {
491         this._originalFormat = colorFormat;
492         this._innerSetColor(color.hsva(), "", colorFormat, WebInspector.Spectrum._ChangeSource.Model);
493     },
495     /**
496      * @param {!Array<number>|undefined} hsva
497      * @param {string|undefined} colorString
498      * @param {string|undefined} colorFormat
499      * @param {string} changeSource
500      */
501     _innerSetColor: function(hsva, colorString, colorFormat, changeSource)
502     {
503         if (hsva !== undefined)
504             this._hsv = hsva;
505         if (colorString !== undefined)
506             this._colorString = colorString;
507         if (colorFormat !== undefined) {
508             console.assert(colorFormat !== WebInspector.Color.Format.Original, "Spectrum's color format cannot be Original");
509             if (colorFormat === WebInspector.Color.Format.RGBA)
510                 colorFormat = WebInspector.Color.Format.RGB;
511             else if (colorFormat === WebInspector.Color.Format.HSLA)
512                 colorFormat = WebInspector.Color.Format.HSL;
513             this._colorFormat = colorFormat;
514         }
516         this._updateHelperLocations();
517         this._updateUI();
519         if (changeSource !== WebInspector.Spectrum._ChangeSource.Input)
520             this._updateInput();
521         if (changeSource !== WebInspector.Spectrum._ChangeSource.Model)
522             this.dispatchEventToListeners(WebInspector.Spectrum.Events.ColorChanged, this.colorString());
523     },
525     /**
526      * @param {!WebInspector.Color} color
527      */
528     setContrastColor: function(color)
529     {
530         this._contrastColor = color;
531         this._updateUI();
532     },
534     /**
535      * @return {!WebInspector.Color}
536      */
537     _color: function()
538     {
539         return WebInspector.Color.fromHSVA(this._hsv);
540     },
542     /**
543      * @return {string}
544      */
545     colorString: function()
546     {
547         if (this._colorString)
548             return this._colorString;
549         var cf = WebInspector.Color.Format;
550         var color = this._color();
551         var colorString = color.asString(this._colorFormat);
552         if (colorString)
553             return colorString;
555         if (this._colorFormat === cf.Nickname || this._colorFormat === cf.ShortHEX) {
556             colorString = color.asString(cf.HEX);
557             if (colorString)
558                 return colorString;
559         }
561         console.assert(color.hasAlpha());
562         return this._colorFormat === cf.HSL ? /** @type {string} */(color.asString(cf.HSLA)) : /** @type {string} */(color.asString(cf.RGBA));
563     },
565     _updateHelperLocations: function()
566     {
567         var h = this._hsv[0];
568         var s = this._hsv[1];
569         var v = this._hsv[2];
570         var alpha = this._hsv[3];
572         // Where to show the little circle that displays your current selected color.
573         var dragX = s * this.dragWidth;
574         var dragY = this.dragHeight - (v * this.dragHeight);
576         dragX = Math.max(-this._colorDragElementHeight,
577                         Math.min(this.dragWidth - this._colorDragElementHeight, dragX - this._colorDragElementHeight));
578         dragY = Math.max(-this._colorDragElementHeight,
579                         Math.min(this.dragHeight - this._colorDragElementHeight, dragY - this._colorDragElementHeight));
581         this._colorDragElement.positionAt(dragX, dragY);
583         // Where to show the bar that displays your current selected hue.
584         var hueSlideX = (1 - h) * this._hueAlphaWidth - this.slideHelperWidth;
585         this._hueSlider.style.left = hueSlideX + "px";
586         var alphaSlideX = alpha * this._hueAlphaWidth - this.slideHelperWidth;
587         this._alphaSlider.style.left = alphaSlideX + "px";
588     },
590     _updateInput: function()
591     {
592         var cf = WebInspector.Color.Format;
593         if (this._colorFormat === cf.HEX || this._colorFormat === cf.ShortHEX || this._colorFormat === cf.Nickname) {
594             this._hexContainer.hidden = false;
595             this._displayContainer.hidden = true;
596             if (this._colorFormat === cf.ShortHEX && this._color().canBeShortHex())
597                 this._hexValue.value = this._color().asString(cf.ShortHEX);
598             else
599                 this._hexValue.value = this._color().asString(cf.HEX);
600         } else {
601             // RGBA, HSLA display.
602             this._hexContainer.hidden = true;
603             this._displayContainer.hidden = false;
604             var isRgb = this._colorFormat === cf.RGB;
605             this._textLabels.textContent = isRgb ? "RGBA" : "HSLA";
606             var colorValues = isRgb ? this._color().canonicalRGBA() : this._color().canonicalHSLA();
607             for (var i = 0; i < 3; ++i) {
608                 this._textValues[i].value = colorValues[i];
609                 if (!isRgb && (i === 1 || i === 2))
610                     this._textValues[i].value += "%";
611             }
612             this._textValues[3].value= Math.round(colorValues[3] * 100) / 100;
613         }
614     },
616     /**
617      * @param {number} requiredContrast
618      */
619     _drawContrastRatioLine: function(requiredContrast)
620     {
621         if (!this._contrastColor || !this.dragWidth || !this.dragHeight)
622             return;
624         /** const */ var width = this.dragWidth;
625         /** const */ var height = this.dragHeight;
626         /** const */ var dS = 0.02;
627         /** const */ var epsilon = 0.01;
629         var fgRGBA = [];
630         WebInspector.Color.hsva2rgba(this._hsv, fgRGBA);
631         var fgLuminance = WebInspector.Color.luminance(fgRGBA);
632         var bgRGBA = this._contrastColor.rgba();
633         var bgLuminance = WebInspector.Color.luminance(bgRGBA);
634         var delta = fgLuminance < bgLuminance ? 1 : -1;
636         var lastV = this._hsv[2];
637         var currentSlope = 0;
638         var candidateHSVA = [this._hsv[0], 0, 0, this._hsv[3]];
639         var pathBuilder = [];
640         var candidateRGBA = [];
641         WebInspector.Color.hsva2rgba(candidateHSVA, candidateRGBA);
642         var flattenedRGBA = [];
643         WebInspector.Color.flattenColors(candidateRGBA, bgRGBA, flattenedRGBA);
645         /**
646          * Approach the desired contrast ratio by modifying the given component
647          * from the given starting value.
648          * @param {number} index
649          * @param {number} x
650          * @return {?number}
651          */
652         function approach(index, x)
653         {
654             while (0 <= x && x <= 1) {
655                 candidateHSVA[index] = x;
656                 WebInspector.Color.hsva2rgba(candidateHSVA, candidateRGBA);
657                 WebInspector.Color.flattenColors(candidateRGBA, bgRGBA, flattenedRGBA);
658                 var contrast = WebInspector.Color.calculateContrastRatio(flattenedRGBA, bgRGBA);
659                 var dContrast = contrast - requiredContrast;
660                 if (Math.abs(dContrast) < epsilon) {
661                     return x;
662                 } else {
663                     // 21 is the maximum possible value for contrast ratio:
664                     // http://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html#contrast-ratiodef
665                     x += delta * (dContrast / 21);
666                 }
667             }
668             return null;
669         }
671         for (var s = 0; s < 1 + dS; s += dS) {
672             s = Math.min(1, s);
673             candidateHSVA[1] = s;
674             var v = lastV;
675             v = lastV + currentSlope * dS;
677             v = approach(2, v);
678             if (v === null)
679                 break;
681             currentSlope = (v - lastV) / dS;
683             pathBuilder.push(pathBuilder.length ? "L" : "M");
684             pathBuilder.push(s * width);
685             pathBuilder.push((1 - v) * height);
686         }
688         if (s < 1 + dS) {
689             s -= dS;
690             delta = -delta;
691             candidateHSVA[2] = 1;
692             s = approach(1, s);
693             if (s !== null)
694                 pathBuilder = pathBuilder.concat(["L", s * width, 0])
695         }
697         this._contrastRatioLine.setAttribute("d", pathBuilder.join(" "));
698     },
700     _updateUI: function()
701     {
702         var h = WebInspector.Color.fromHSVA([this._hsv[0], 1, 1, 1]);
703         this._colorElement.style.backgroundColor = /** @type {string} */ (h.asString(WebInspector.Color.Format.RGB));
704         if (Runtime.experiments.isEnabled("colorContrastRatio")) {
705             // TODO(samli): Determine size of text and switch between AA/AAA ratings.
706             this._drawContrastRatioLine(4.5);
707         }
708         this._swatchInnerElement.style.backgroundColor = /** @type {string} */ (this._color().asString(WebInspector.Color.Format.RGBA));
709         // Show border if the swatch is white.
710         this._swatchInnerElement.classList.toggle("swatch-inner-white", this._color().hsla()[2] > 0.9);
711         this._colorDragElement.style.backgroundColor = /** @type {string} */ (this._color().asString(WebInspector.Color.Format.RGBA));
712         var noAlpha = WebInspector.Color.fromHSVA(this._hsv.slice(0,3).concat(1));
713         this._alphaElementBackground.style.backgroundImage = String.sprintf("linear-gradient(to right, rgba(0,0,0,0), %s)", noAlpha.asString(WebInspector.Color.Format.RGB));
714     },
716     _formatViewSwitch: function()
717     {
718         var cf = WebInspector.Color.Format;
719         var format = cf.RGB;
720         if (this._colorFormat === cf.RGB)
721             format = cf.HSL;
722         else if (this._colorFormat === cf.HSL && !this._color().hasAlpha())
723             format = this._originalFormat === cf.ShortHEX ? cf.ShortHEX : cf.HEX;
724         this._innerSetColor(undefined, "", format, WebInspector.Spectrum._ChangeSource.Other);
725     },
727     /**
728      * @param {!Event} event
729      */
730     _inputChanged: function(event)
731     {
732         /**
733          * @param {!Element} element
734          * @return {string}
735          */
736         function elementValue(element)
737         {
738             return element.value;
739         }
741         var inputElement = /** @type {!Element} */(event.currentTarget);
742         var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel");
743         var pageKeyPressed = (event.keyIdentifier === "PageUp" || event.keyIdentifier === "PageDown");
744         if (arrowKeyOrMouseWheelEvent || pageKeyPressed) {
745             var newValue = WebInspector.createReplacementString(inputElement.value, event);
746             if (newValue) {
747                 inputElement.value = newValue;
748                 inputElement.selectionStart = 0;
749                 inputElement.selectionEnd = newValue.length;
750             }
751             event.consume(true);
752         }
754         const cf = WebInspector.Color.Format;
755         var colorString;
756         if (this._colorFormat === cf.HEX || this._colorFormat === cf.ShortHEX) {
757             colorString = this._hexValue.value;
758         } else {
759             var format = this._colorFormat === cf.RGB ? "rgba" : "hsla";
760             var values = this._textValues.map(elementValue).join(",");
761             colorString = String.sprintf("%s(%s)", format, values);
762         }
764         var color = WebInspector.Color.parse(colorString);
765         if (!color)
766             return;
767         var hsv = color.hsva();
768         if (this._colorFormat === cf.HEX || this._colorFormat === cf.ShortHEX)
769             this._colorFormat = color.canBeShortHex() ? cf.ShortHEX : cf.HEX;
770         this._innerSetColor(hsv, colorString, undefined, WebInspector.Spectrum._ChangeSource.Input);
771     },
773     wasShown: function()
774     {
775         this._hueAlphaWidth = this._hueElement.offsetWidth;
776         this.slideHelperWidth = this._hueSlider.offsetWidth / 2;
777         this.dragWidth = this._colorElement.offsetWidth;
778         this.dragHeight = this._colorElement.offsetHeight;
779         this._colorDragElementHeight = this._colorDragElement.offsetHeight / 2;
780         this._innerSetColor(undefined, undefined, undefined, WebInspector.Spectrum._ChangeSource.Model);
781         this._toggleColorPicker(true);
782         WebInspector.targetManager.addModelListener(WebInspector.ResourceTreeModel, WebInspector.ResourceTreeModel.EventTypes.ColorPicked, this._colorPicked, this);
783     },
785     willHide: function()
786     {
787         this._toggleColorPicker(false);
788         WebInspector.targetManager.removeModelListener(WebInspector.ResourceTreeModel, WebInspector.ResourceTreeModel.EventTypes.ColorPicked, this._colorPicked, this);
789     },
791     /**
792      * @param {boolean=} enabled
793      * @param {!WebInspector.Event=} event
794      */
795     _toggleColorPicker: function(enabled, event)
796     {
797         if (enabled === undefined)
798             enabled = !this._colorPickerButton.toggled();
799         this._colorPickerButton.setToggled(enabled);
800         for (var target of WebInspector.targetManager.targets())
801             target.pageAgent().setColorPickerEnabled(enabled);
802     },
804     /**
805      * @param {!WebInspector.Event} event
806      */
807     _colorPicked: function(event)
808     {
809         var rgbColor = /** @type {!DOMAgent.RGBA} */ (event.data);
810         var rgba = [rgbColor.r, rgbColor.g, rgbColor.b, (rgbColor.a / 2.55 | 0) / 100];
811         var color = WebInspector.Color.fromRGBA(rgba);
812         this._innerSetColor(color.hsva(), "", undefined, WebInspector.Spectrum._ChangeSource.Other);
813         InspectorFrontendHost.bringToFront();
814     },
817     __proto__: WebInspector.VBox.prototype
820 /** @typedef {{ title: string, colors: !Array.<string>, mutable: boolean }} */
821 WebInspector.Spectrum.Palette;
822 WebInspector.Spectrum.GeneratedPaletteTitle = "Page colors";
825  * @constructor
826  * @param {function(!WebInspector.Spectrum.Palette)} callback
827  */
828 WebInspector.Spectrum.PaletteGenerator = function(callback)
830     this._callback = callback;
831     var target = WebInspector.targetManager.mainTarget();
832     if (!target)
833         return;
834     var cssModel = WebInspector.CSSStyleModel.fromTarget(target);
835     /** @type {!Map.<string, number>} */
836     this._frequencyMap = new Map();
837     var stylesheetPromises = [];
838     for (var stylesheet of cssModel.allStyleSheets())
839         stylesheetPromises.push(new Promise(this._processStylesheet.bind(this, stylesheet)));
840     Promise.all(stylesheetPromises)
841         .catchException(null)
842         .then(this._finish.bind(this));
845 WebInspector.Spectrum.PaletteGenerator.prototype = {
846     /**
847      * @param {string} a
848      * @param {string} b
849      * @return {number}
850      */
851     _frequencyComparator: function(a, b)
852     {
853         return this._frequencyMap.get(b) - this._frequencyMap.get(a);
854     },
856     _finish: function()
857     {
858         /**
859          * @param {string} a
860          * @param {string} b
861          * @return {number}
862          */
863         function hueComparator(a, b)
864         {
865             var hsva = paletteColors.get(a).hsva();
866             var hsvb = paletteColors.get(b).hsva();
868             // First trim the shades of gray
869             if (hsvb[1] < 0.12 && hsva[1] < 0.12)
870                 return hsvb[2]*hsvb[3] - hsva[2]*hsva[3];
871             if (hsvb[1] < 0.12)
872                 return -1;
873             if (hsva[1] < 0.12)
874                 return 1;
876             // Equal hue -> sort by sat
877             if (hsvb[0] === hsva[0])
878                 return hsvb[1]*hsvb[3] - hsva[1]*hsva[3];
880             return (hsvb[0] + 0.94) % 1 - (hsva[0] + 0.94) % 1;
881         }
883         var colors = this._frequencyMap.keysArray();
884         colors = colors.sort(this._frequencyComparator.bind(this));
885         /** @type {!Map.<string, !WebInspector.Color>} */
886         var paletteColors = new Map();
887         var colorsPerRow = 24;
888         while (paletteColors.size < colorsPerRow && colors.length) {
889             var colorText = colors.shift();
890             var color = WebInspector.Color.parse(colorText);
891             if (!color || color.nickname() === "white" || color.nickname() === "black")
892                 continue;
893             paletteColors.set(colorText, color);
894         }
896         this._callback({ title: WebInspector.Spectrum.GeneratedPaletteTitle, colors: paletteColors.keysArray().sort(hueComparator), mutable: false });
897     },
899     /**
900      * @param {!WebInspector.CSSStyleSheetHeader} stylesheet
901      * @param {function(?)} resolve
902      * @this {WebInspector.Spectrum.PaletteGenerator}
903      */
904     _processStylesheet: function(stylesheet, resolve)
905     {
906         /**
907          * @param {?string} text
908          * @this {WebInspector.Spectrum.PaletteGenerator}
909          */
910         function parseContent(text)
911         {
912             text = text.toLowerCase();
913             var regexResult = text.match(/((?:rgb|hsl)a?\([^)]+\)|#[0-9a-f]{6}|#[0-9a-f]{3})/g) || [];
914             for (var c of regexResult) {
915                 var frequency = this._frequencyMap.get(c) || 0;
916                 this._frequencyMap.set(c, ++frequency);
917             }
918             resolve(null);
919         }
921         stylesheet.requestContent(parseContent.bind(this));
922     }
925 WebInspector.Spectrum.MaterialPalette = { title: "Material", mutable: false, matchUserFormat: true, colors: [
926     "#F44336",
927     "#E91E63",
928     "#9C27B0",
929     "#673AB7",
930     "#3F51B5",
931     "#2196F3",
932     "#03A9F4",
933     "#00BCD4",
934     "#009688",
935     "#4CAF50",
936     "#8BC34A",
937     "#CDDC39",
938     "#FFEB3B",
939     "#FFC107",
940     "#FF9800",
941     "#FF5722",
942     "#795548",
943     "#9E9E9E",
944     "#607D8B"