|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 const EventEmitter = require("devtools/toolkit/event-emitter"); |
|
8 |
|
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); |
|
33 |
|
34 this.element = parentEl.ownerDocument.createElement('div'); |
|
35 this.parentEl = parentEl; |
|
36 |
|
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(""); |
|
60 |
|
61 this.onElementClick = this.onElementClick.bind(this); |
|
62 this.element.addEventListener("click", this.onElementClick, false); |
|
63 |
|
64 this.parentEl.appendChild(this.element); |
|
65 |
|
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)); |
|
69 |
|
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)); |
|
73 |
|
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)); |
|
78 |
|
79 if (rgb) { |
|
80 this.rgb = rgb; |
|
81 this.updateUI(); |
|
82 } |
|
83 } |
|
84 |
|
85 module.exports.Spectrum = Spectrum; |
|
86 |
|
87 Spectrum.hsvToRgb = function(h, s, v, a) { |
|
88 let r, g, b; |
|
89 |
|
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); |
|
95 |
|
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 } |
|
104 |
|
105 return [r * 255, g * 255, b * 255, a]; |
|
106 }; |
|
107 |
|
108 Spectrum.rgbToHsv = function(r, g, b, a) { |
|
109 r = r / 255; |
|
110 g = g / 255; |
|
111 b = b / 255; |
|
112 |
|
113 let max = Math.max(r, g, b), min = Math.min(r, g, b); |
|
114 let h, s, v = max; |
|
115 |
|
116 let d = max - min; |
|
117 s = max == 0 ? 0 : d / max; |
|
118 |
|
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 }; |
|
132 |
|
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 }; |
|
147 |
|
148 Spectrum.draggable = function(element, onmove, onstart, onstop) { |
|
149 onmove = onmove || function() {}; |
|
150 onstart = onstart || function() {}; |
|
151 onstop = onstop || function() {}; |
|
152 |
|
153 let doc = element.ownerDocument; |
|
154 let dragging = false; |
|
155 let offset = {}; |
|
156 let maxHeight = 0; |
|
157 let maxWidth = 0; |
|
158 |
|
159 function prevent(e) { |
|
160 e.stopPropagation(); |
|
161 e.preventDefault(); |
|
162 } |
|
163 |
|
164 function move(e) { |
|
165 if (dragging) { |
|
166 let pageX = e.pageX; |
|
167 let pageY = e.pageY; |
|
168 |
|
169 let dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth)); |
|
170 let dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight)); |
|
171 |
|
172 onmove.apply(element, [dragX, dragY]); |
|
173 } |
|
174 } |
|
175 |
|
176 function start(e) { |
|
177 let rightclick = e.which === 3; |
|
178 |
|
179 if (!rightclick && !dragging) { |
|
180 if (onstart.apply(element, arguments) !== false) { |
|
181 dragging = true; |
|
182 maxHeight = element.offsetHeight; |
|
183 maxWidth = element.offsetWidth; |
|
184 |
|
185 offset = Spectrum.getOffset(element); |
|
186 |
|
187 move(e); |
|
188 |
|
189 doc.addEventListener("selectstart", prevent, false); |
|
190 doc.addEventListener("dragstart", prevent, false); |
|
191 doc.addEventListener("mousemove", move, false); |
|
192 doc.addEventListener("mouseup", stop, false); |
|
193 |
|
194 prevent(e); |
|
195 } |
|
196 } |
|
197 } |
|
198 |
|
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 } |
|
209 |
|
210 element.addEventListener("mousedown", start, false); |
|
211 }; |
|
212 |
|
213 Spectrum.prototype = { |
|
214 set rgb(color) { |
|
215 this.hsv = Spectrum.rgbToHsv(color[0], color[1], color[2], color[3]); |
|
216 }, |
|
217 |
|
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 }, |
|
222 |
|
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 }, |
|
227 |
|
228 get rgbCssString() { |
|
229 let rgb = this.rgb; |
|
230 return "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ")"; |
|
231 }, |
|
232 |
|
233 show: function() { |
|
234 this.element.classList.add('spectrum-show'); |
|
235 |
|
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; |
|
243 |
|
244 this.updateUI(); |
|
245 }, |
|
246 |
|
247 onElementClick: function(e) { |
|
248 e.stopPropagation(); |
|
249 }, |
|
250 |
|
251 onSliderMove: function(dragX, dragY) { |
|
252 this.hsv[0] = (dragY / this.slideHeight); |
|
253 this.updateUI(); |
|
254 this.onChange(); |
|
255 }, |
|
256 |
|
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 }, |
|
263 |
|
264 onAlphaSliderMove: function(dragX, dragY) { |
|
265 this.hsv[3] = dragX / this.alphaSliderWidth; |
|
266 this.updateUI(); |
|
267 this.onChange(); |
|
268 }, |
|
269 |
|
270 onChange: function() { |
|
271 this.emit("changed", this.rgb, this.rgbCssString); |
|
272 }, |
|
273 |
|
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; |
|
278 |
|
279 let h = this.hsv[0]; |
|
280 let s = this.hsv[1]; |
|
281 let v = this.hsv[2]; |
|
282 |
|
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; |
|
287 |
|
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 ); |
|
296 |
|
297 this.dragHelper.style.top = dragY + "px"; |
|
298 this.dragHelper.style.left = dragX + "px"; |
|
299 |
|
300 // Placing the hue slider |
|
301 let slideY = (h * this.slideHeight) - this.slideHelperHeight/2; |
|
302 this.slideHelper.style.top = slideY + "px"; |
|
303 |
|
304 // Placing the alpha slider |
|
305 let alphaSliderX = (this.hsv[3] * this.alphaSliderWidth) - (this.alphaSliderHelperWidth / 2); |
|
306 this.alphaSliderHelper.style.left = alphaSliderX + "px"; |
|
307 }, |
|
308 |
|
309 updateUI: function() { |
|
310 this.updateHelperLocations(); |
|
311 |
|
312 let rgb = this.rgb; |
|
313 let rgbNoSatVal = this.rgbNoSatVal; |
|
314 |
|
315 let flatColor = "rgb(" + rgbNoSatVal[0] + ", " + rgbNoSatVal[1] + ", " + rgbNoSatVal[2] + ")"; |
|
316 let fullColor = "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ")"; |
|
317 |
|
318 this.dragger.style.backgroundColor = flatColor; |
|
319 |
|
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 }, |
|
325 |
|
326 destroy: function() { |
|
327 this.element.removeEventListener("click", this.onElementClick, false); |
|
328 |
|
329 this.parentEl.removeChild(this.element); |
|
330 |
|
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 }; |