Wed, 31 Dec 2014 06:55:50 +0100
Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const EventEmitter = require("devtools/toolkit/event-emitter");
9 /**
10 * Spectrum creates a color picker widget in any container you give it.
11 *
12 * Simple usage example:
13 *
14 * const {Spectrum} = require("devtools/shared/widgets/Spectrum");
15 * let s = new Spectrum(containerElement, [255, 126, 255, 1]);
16 * s.on("changed", (event, rgba, color) => {
17 * console.log("rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ", " + rgba[3] + ")");
18 * });
19 * s.show();
20 * s.destroy();
21 *
22 * Note that the color picker is hidden by default and you need to call show to
23 * make it appear. This 2 stages initialization helps in cases you are creating
24 * the color picker in a parent element that hasn't been appended anywhere yet
25 * or that is hidden. Calling show() when the parent element is appended and
26 * visible will allow spectrum to correctly initialize its various parts.
27 *
28 * Fires the following events:
29 * - changed : When the user changes the current color
30 */
31 function Spectrum(parentEl, rgb) {
32 EventEmitter.decorate(this);
34 this.element = parentEl.ownerDocument.createElement('div');
35 this.parentEl = parentEl;
37 this.element.className = "spectrum-container";
38 this.element.innerHTML = [
39 "<div class='spectrum-top'>",
40 "<div class='spectrum-fill'></div>",
41 "<div class='spectrum-top-inner'>",
42 "<div class='spectrum-color spectrum-box'>",
43 "<div class='spectrum-sat'>",
44 "<div class='spectrum-val'>",
45 "<div class='spectrum-dragger'></div>",
46 "</div>",
47 "</div>",
48 "</div>",
49 "<div class='spectrum-hue spectrum-box'>",
50 "<div class='spectrum-slider spectrum-slider-control'></div>",
51 "</div>",
52 "</div>",
53 "</div>",
54 "<div class='spectrum-alpha spectrum-checker spectrum-box'>",
55 "<div class='spectrum-alpha-inner'>",
56 "<div class='spectrum-alpha-handle spectrum-slider-control'></div>",
57 "</div>",
58 "</div>",
59 ].join("");
61 this.onElementClick = this.onElementClick.bind(this);
62 this.element.addEventListener("click", this.onElementClick, false);
64 this.parentEl.appendChild(this.element);
66 this.slider = this.element.querySelector(".spectrum-hue");
67 this.slideHelper = this.element.querySelector(".spectrum-slider");
68 Spectrum.draggable(this.slider, this.onSliderMove.bind(this));
70 this.dragger = this.element.querySelector(".spectrum-color");
71 this.dragHelper = this.element.querySelector(".spectrum-dragger");
72 Spectrum.draggable(this.dragger, this.onDraggerMove.bind(this));
74 this.alphaSlider = this.element.querySelector(".spectrum-alpha");
75 this.alphaSliderInner = this.element.querySelector(".spectrum-alpha-inner");
76 this.alphaSliderHelper = this.element.querySelector(".spectrum-alpha-handle");
77 Spectrum.draggable(this.alphaSliderInner, this.onAlphaSliderMove.bind(this));
79 if (rgb) {
80 this.rgb = rgb;
81 this.updateUI();
82 }
83 }
85 module.exports.Spectrum = Spectrum;
87 Spectrum.hsvToRgb = function(h, s, v, a) {
88 let r, g, b;
90 let i = Math.floor(h * 6);
91 let f = h * 6 - i;
92 let p = v * (1 - s);
93 let q = v * (1 - f * s);
94 let t = v * (1 - (1 - f) * s);
96 switch(i % 6) {
97 case 0: r = v, g = t, b = p; break;
98 case 1: r = q, g = v, b = p; break;
99 case 2: r = p, g = v, b = t; break;
100 case 3: r = p, g = q, b = v; break;
101 case 4: r = t, g = p, b = v; break;
102 case 5: r = v, g = p, b = q; break;
103 }
105 return [r * 255, g * 255, b * 255, a];
106 };
108 Spectrum.rgbToHsv = function(r, g, b, a) {
109 r = r / 255;
110 g = g / 255;
111 b = b / 255;
113 let max = Math.max(r, g, b), min = Math.min(r, g, b);
114 let h, s, v = max;
116 let d = max - min;
117 s = max == 0 ? 0 : d / max;
119 if(max == min) {
120 h = 0; // achromatic
121 }
122 else {
123 switch(max) {
124 case r: h = (g - b) / d + (g < b ? 6 : 0); break;
125 case g: h = (b - r) / d + 2; break;
126 case b: h = (r - g) / d + 4; break;
127 }
128 h /= 6;
129 }
130 return [h, s, v, a];
131 };
133 Spectrum.getOffset = function(el) {
134 let curleft = 0, curtop = 0;
135 if (el.offsetParent) {
136 while (el) {
137 curleft += el.offsetLeft;
138 curtop += el.offsetTop;
139 el = el.offsetParent;
140 }
141 }
142 return {
143 left: curleft,
144 top: curtop
145 };
146 };
148 Spectrum.draggable = function(element, onmove, onstart, onstop) {
149 onmove = onmove || function() {};
150 onstart = onstart || function() {};
151 onstop = onstop || function() {};
153 let doc = element.ownerDocument;
154 let dragging = false;
155 let offset = {};
156 let maxHeight = 0;
157 let maxWidth = 0;
159 function prevent(e) {
160 e.stopPropagation();
161 e.preventDefault();
162 }
164 function move(e) {
165 if (dragging) {
166 let pageX = e.pageX;
167 let pageY = e.pageY;
169 let dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth));
170 let dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight));
172 onmove.apply(element, [dragX, dragY]);
173 }
174 }
176 function start(e) {
177 let rightclick = e.which === 3;
179 if (!rightclick && !dragging) {
180 if (onstart.apply(element, arguments) !== false) {
181 dragging = true;
182 maxHeight = element.offsetHeight;
183 maxWidth = element.offsetWidth;
185 offset = Spectrum.getOffset(element);
187 move(e);
189 doc.addEventListener("selectstart", prevent, false);
190 doc.addEventListener("dragstart", prevent, false);
191 doc.addEventListener("mousemove", move, false);
192 doc.addEventListener("mouseup", stop, false);
194 prevent(e);
195 }
196 }
197 }
199 function stop() {
200 if (dragging) {
201 doc.removeEventListener("selectstart", prevent, false);
202 doc.removeEventListener("dragstart", prevent, false);
203 doc.removeEventListener("mousemove", move, false);
204 doc.removeEventListener("mouseup", stop, false);
205 onstop.apply(element, arguments);
206 }
207 dragging = false;
208 }
210 element.addEventListener("mousedown", start, false);
211 };
213 Spectrum.prototype = {
214 set rgb(color) {
215 this.hsv = Spectrum.rgbToHsv(color[0], color[1], color[2], color[3]);
216 },
218 get rgb() {
219 let rgb = Spectrum.hsvToRgb(this.hsv[0], this.hsv[1], this.hsv[2], this.hsv[3]);
220 return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]), Math.round(rgb[3]*100)/100];
221 },
223 get rgbNoSatVal() {
224 let rgb = Spectrum.hsvToRgb(this.hsv[0], 1, 1);
225 return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]), rgb[3]];
226 },
228 get rgbCssString() {
229 let rgb = this.rgb;
230 return "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ")";
231 },
233 show: function() {
234 this.element.classList.add('spectrum-show');
236 this.slideHeight = this.slider.offsetHeight;
237 this.dragWidth = this.dragger.offsetWidth;
238 this.dragHeight = this.dragger.offsetHeight;
239 this.dragHelperHeight = this.dragHelper.offsetHeight;
240 this.slideHelperHeight = this.slideHelper.offsetHeight;
241 this.alphaSliderWidth = this.alphaSliderInner.offsetWidth;
242 this.alphaSliderHelperWidth = this.alphaSliderHelper.offsetWidth;
244 this.updateUI();
245 },
247 onElementClick: function(e) {
248 e.stopPropagation();
249 },
251 onSliderMove: function(dragX, dragY) {
252 this.hsv[0] = (dragY / this.slideHeight);
253 this.updateUI();
254 this.onChange();
255 },
257 onDraggerMove: function(dragX, dragY) {
258 this.hsv[1] = dragX / this.dragWidth;
259 this.hsv[2] = (this.dragHeight - dragY) / this.dragHeight;
260 this.updateUI();
261 this.onChange();
262 },
264 onAlphaSliderMove: function(dragX, dragY) {
265 this.hsv[3] = dragX / this.alphaSliderWidth;
266 this.updateUI();
267 this.onChange();
268 },
270 onChange: function() {
271 this.emit("changed", this.rgb, this.rgbCssString);
272 },
274 updateHelperLocations: function() {
275 // If the UI hasn't been shown yet then none of the dimensions will be correct
276 if (!this.element.classList.contains('spectrum-show'))
277 return;
279 let h = this.hsv[0];
280 let s = this.hsv[1];
281 let v = this.hsv[2];
283 // Placing the color dragger
284 let dragX = s * this.dragWidth;
285 let dragY = this.dragHeight - (v * this.dragHeight);
286 let helperDim = this.dragHelperHeight/2;
288 dragX = Math.max(
289 -helperDim,
290 Math.min(this.dragWidth - helperDim, dragX - helperDim)
291 );
292 dragY = Math.max(
293 -helperDim,
294 Math.min(this.dragHeight - helperDim, dragY - helperDim)
295 );
297 this.dragHelper.style.top = dragY + "px";
298 this.dragHelper.style.left = dragX + "px";
300 // Placing the hue slider
301 let slideY = (h * this.slideHeight) - this.slideHelperHeight/2;
302 this.slideHelper.style.top = slideY + "px";
304 // Placing the alpha slider
305 let alphaSliderX = (this.hsv[3] * this.alphaSliderWidth) - (this.alphaSliderHelperWidth / 2);
306 this.alphaSliderHelper.style.left = alphaSliderX + "px";
307 },
309 updateUI: function() {
310 this.updateHelperLocations();
312 let rgb = this.rgb;
313 let rgbNoSatVal = this.rgbNoSatVal;
315 let flatColor = "rgb(" + rgbNoSatVal[0] + ", " + rgbNoSatVal[1] + ", " + rgbNoSatVal[2] + ")";
316 let fullColor = "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ")";
318 this.dragger.style.backgroundColor = flatColor;
320 var rgbNoAlpha = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
321 var rgbAlpha0 = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ", 0)";
322 var alphaGradient = "linear-gradient(to right, " + rgbAlpha0 + ", " + rgbNoAlpha + ")";
323 this.alphaSliderInner.style.background = alphaGradient;
324 },
326 destroy: function() {
327 this.element.removeEventListener("click", this.onElementClick, false);
329 this.parentEl.removeChild(this.element);
331 this.slider = null;
332 this.dragger = null;
333 this.alphaSlider = this.alphaSliderInner = this.alphaSliderHelper = null;
334 this.parentEl = null;
335 this.element = null;
336 }
337 };