michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const EventEmitter = require("devtools/toolkit/event-emitter"); michael@0: michael@0: /** michael@0: * Spectrum creates a color picker widget in any container you give it. michael@0: * michael@0: * Simple usage example: michael@0: * michael@0: * const {Spectrum} = require("devtools/shared/widgets/Spectrum"); michael@0: * let s = new Spectrum(containerElement, [255, 126, 255, 1]); michael@0: * s.on("changed", (event, rgba, color) => { michael@0: * console.log("rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ", " + rgba[3] + ")"); michael@0: * }); michael@0: * s.show(); michael@0: * s.destroy(); michael@0: * michael@0: * Note that the color picker is hidden by default and you need to call show to michael@0: * make it appear. This 2 stages initialization helps in cases you are creating michael@0: * the color picker in a parent element that hasn't been appended anywhere yet michael@0: * or that is hidden. Calling show() when the parent element is appended and michael@0: * visible will allow spectrum to correctly initialize its various parts. michael@0: * michael@0: * Fires the following events: michael@0: * - changed : When the user changes the current color michael@0: */ michael@0: function Spectrum(parentEl, rgb) { michael@0: EventEmitter.decorate(this); michael@0: michael@0: this.element = parentEl.ownerDocument.createElement('div'); michael@0: this.parentEl = parentEl; michael@0: michael@0: this.element.className = "spectrum-container"; michael@0: this.element.innerHTML = [ michael@0: "
", michael@0: "
", michael@0: "
", michael@0: "
", michael@0: "
", michael@0: "
", michael@0: "
", michael@0: "
", michael@0: "
", michael@0: "
", michael@0: "
", michael@0: "
", michael@0: "
", michael@0: "
", michael@0: "
", michael@0: "
", michael@0: "
", michael@0: "
", michael@0: "
", michael@0: "
", michael@0: ].join(""); michael@0: michael@0: this.onElementClick = this.onElementClick.bind(this); michael@0: this.element.addEventListener("click", this.onElementClick, false); michael@0: michael@0: this.parentEl.appendChild(this.element); michael@0: michael@0: this.slider = this.element.querySelector(".spectrum-hue"); michael@0: this.slideHelper = this.element.querySelector(".spectrum-slider"); michael@0: Spectrum.draggable(this.slider, this.onSliderMove.bind(this)); michael@0: michael@0: this.dragger = this.element.querySelector(".spectrum-color"); michael@0: this.dragHelper = this.element.querySelector(".spectrum-dragger"); michael@0: Spectrum.draggable(this.dragger, this.onDraggerMove.bind(this)); michael@0: michael@0: this.alphaSlider = this.element.querySelector(".spectrum-alpha"); michael@0: this.alphaSliderInner = this.element.querySelector(".spectrum-alpha-inner"); michael@0: this.alphaSliderHelper = this.element.querySelector(".spectrum-alpha-handle"); michael@0: Spectrum.draggable(this.alphaSliderInner, this.onAlphaSliderMove.bind(this)); michael@0: michael@0: if (rgb) { michael@0: this.rgb = rgb; michael@0: this.updateUI(); michael@0: } michael@0: } michael@0: michael@0: module.exports.Spectrum = Spectrum; michael@0: michael@0: Spectrum.hsvToRgb = function(h, s, v, a) { michael@0: let r, g, b; michael@0: michael@0: let i = Math.floor(h * 6); michael@0: let f = h * 6 - i; michael@0: let p = v * (1 - s); michael@0: let q = v * (1 - f * s); michael@0: let t = v * (1 - (1 - f) * s); michael@0: michael@0: switch(i % 6) { michael@0: case 0: r = v, g = t, b = p; break; michael@0: case 1: r = q, g = v, b = p; break; michael@0: case 2: r = p, g = v, b = t; break; michael@0: case 3: r = p, g = q, b = v; break; michael@0: case 4: r = t, g = p, b = v; break; michael@0: case 5: r = v, g = p, b = q; break; michael@0: } michael@0: michael@0: return [r * 255, g * 255, b * 255, a]; michael@0: }; michael@0: michael@0: Spectrum.rgbToHsv = function(r, g, b, a) { michael@0: r = r / 255; michael@0: g = g / 255; michael@0: b = b / 255; michael@0: michael@0: let max = Math.max(r, g, b), min = Math.min(r, g, b); michael@0: let h, s, v = max; michael@0: michael@0: let d = max - min; michael@0: s = max == 0 ? 0 : d / max; michael@0: michael@0: if(max == min) { michael@0: h = 0; // achromatic michael@0: } michael@0: else { michael@0: switch(max) { michael@0: case r: h = (g - b) / d + (g < b ? 6 : 0); break; michael@0: case g: h = (b - r) / d + 2; break; michael@0: case b: h = (r - g) / d + 4; break; michael@0: } michael@0: h /= 6; michael@0: } michael@0: return [h, s, v, a]; michael@0: }; michael@0: michael@0: Spectrum.getOffset = function(el) { michael@0: let curleft = 0, curtop = 0; michael@0: if (el.offsetParent) { michael@0: while (el) { michael@0: curleft += el.offsetLeft; michael@0: curtop += el.offsetTop; michael@0: el = el.offsetParent; michael@0: } michael@0: } michael@0: return { michael@0: left: curleft, michael@0: top: curtop michael@0: }; michael@0: }; michael@0: michael@0: Spectrum.draggable = function(element, onmove, onstart, onstop) { michael@0: onmove = onmove || function() {}; michael@0: onstart = onstart || function() {}; michael@0: onstop = onstop || function() {}; michael@0: michael@0: let doc = element.ownerDocument; michael@0: let dragging = false; michael@0: let offset = {}; michael@0: let maxHeight = 0; michael@0: let maxWidth = 0; michael@0: michael@0: function prevent(e) { michael@0: e.stopPropagation(); michael@0: e.preventDefault(); michael@0: } michael@0: michael@0: function move(e) { michael@0: if (dragging) { michael@0: let pageX = e.pageX; michael@0: let pageY = e.pageY; michael@0: michael@0: let dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth)); michael@0: let dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight)); michael@0: michael@0: onmove.apply(element, [dragX, dragY]); michael@0: } michael@0: } michael@0: michael@0: function start(e) { michael@0: let rightclick = e.which === 3; michael@0: michael@0: if (!rightclick && !dragging) { michael@0: if (onstart.apply(element, arguments) !== false) { michael@0: dragging = true; michael@0: maxHeight = element.offsetHeight; michael@0: maxWidth = element.offsetWidth; michael@0: michael@0: offset = Spectrum.getOffset(element); michael@0: michael@0: move(e); michael@0: michael@0: doc.addEventListener("selectstart", prevent, false); michael@0: doc.addEventListener("dragstart", prevent, false); michael@0: doc.addEventListener("mousemove", move, false); michael@0: doc.addEventListener("mouseup", stop, false); michael@0: michael@0: prevent(e); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function stop() { michael@0: if (dragging) { michael@0: doc.removeEventListener("selectstart", prevent, false); michael@0: doc.removeEventListener("dragstart", prevent, false); michael@0: doc.removeEventListener("mousemove", move, false); michael@0: doc.removeEventListener("mouseup", stop, false); michael@0: onstop.apply(element, arguments); michael@0: } michael@0: dragging = false; michael@0: } michael@0: michael@0: element.addEventListener("mousedown", start, false); michael@0: }; michael@0: michael@0: Spectrum.prototype = { michael@0: set rgb(color) { michael@0: this.hsv = Spectrum.rgbToHsv(color[0], color[1], color[2], color[3]); michael@0: }, michael@0: michael@0: get rgb() { michael@0: let rgb = Spectrum.hsvToRgb(this.hsv[0], this.hsv[1], this.hsv[2], this.hsv[3]); michael@0: return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]), Math.round(rgb[3]*100)/100]; michael@0: }, michael@0: michael@0: get rgbNoSatVal() { michael@0: let rgb = Spectrum.hsvToRgb(this.hsv[0], 1, 1); michael@0: return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]), rgb[3]]; michael@0: }, michael@0: michael@0: get rgbCssString() { michael@0: let rgb = this.rgb; michael@0: return "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ")"; michael@0: }, michael@0: michael@0: show: function() { michael@0: this.element.classList.add('spectrum-show'); michael@0: michael@0: this.slideHeight = this.slider.offsetHeight; michael@0: this.dragWidth = this.dragger.offsetWidth; michael@0: this.dragHeight = this.dragger.offsetHeight; michael@0: this.dragHelperHeight = this.dragHelper.offsetHeight; michael@0: this.slideHelperHeight = this.slideHelper.offsetHeight; michael@0: this.alphaSliderWidth = this.alphaSliderInner.offsetWidth; michael@0: this.alphaSliderHelperWidth = this.alphaSliderHelper.offsetWidth; michael@0: michael@0: this.updateUI(); michael@0: }, michael@0: michael@0: onElementClick: function(e) { michael@0: e.stopPropagation(); michael@0: }, michael@0: michael@0: onSliderMove: function(dragX, dragY) { michael@0: this.hsv[0] = (dragY / this.slideHeight); michael@0: this.updateUI(); michael@0: this.onChange(); michael@0: }, michael@0: michael@0: onDraggerMove: function(dragX, dragY) { michael@0: this.hsv[1] = dragX / this.dragWidth; michael@0: this.hsv[2] = (this.dragHeight - dragY) / this.dragHeight; michael@0: this.updateUI(); michael@0: this.onChange(); michael@0: }, michael@0: michael@0: onAlphaSliderMove: function(dragX, dragY) { michael@0: this.hsv[3] = dragX / this.alphaSliderWidth; michael@0: this.updateUI(); michael@0: this.onChange(); michael@0: }, michael@0: michael@0: onChange: function() { michael@0: this.emit("changed", this.rgb, this.rgbCssString); michael@0: }, michael@0: michael@0: updateHelperLocations: function() { michael@0: // If the UI hasn't been shown yet then none of the dimensions will be correct michael@0: if (!this.element.classList.contains('spectrum-show')) michael@0: return; michael@0: michael@0: let h = this.hsv[0]; michael@0: let s = this.hsv[1]; michael@0: let v = this.hsv[2]; michael@0: michael@0: // Placing the color dragger michael@0: let dragX = s * this.dragWidth; michael@0: let dragY = this.dragHeight - (v * this.dragHeight); michael@0: let helperDim = this.dragHelperHeight/2; michael@0: michael@0: dragX = Math.max( michael@0: -helperDim, michael@0: Math.min(this.dragWidth - helperDim, dragX - helperDim) michael@0: ); michael@0: dragY = Math.max( michael@0: -helperDim, michael@0: Math.min(this.dragHeight - helperDim, dragY - helperDim) michael@0: ); michael@0: michael@0: this.dragHelper.style.top = dragY + "px"; michael@0: this.dragHelper.style.left = dragX + "px"; michael@0: michael@0: // Placing the hue slider michael@0: let slideY = (h * this.slideHeight) - this.slideHelperHeight/2; michael@0: this.slideHelper.style.top = slideY + "px"; michael@0: michael@0: // Placing the alpha slider michael@0: let alphaSliderX = (this.hsv[3] * this.alphaSliderWidth) - (this.alphaSliderHelperWidth / 2); michael@0: this.alphaSliderHelper.style.left = alphaSliderX + "px"; michael@0: }, michael@0: michael@0: updateUI: function() { michael@0: this.updateHelperLocations(); michael@0: michael@0: let rgb = this.rgb; michael@0: let rgbNoSatVal = this.rgbNoSatVal; michael@0: michael@0: let flatColor = "rgb(" + rgbNoSatVal[0] + ", " + rgbNoSatVal[1] + ", " + rgbNoSatVal[2] + ")"; michael@0: let fullColor = "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ")"; michael@0: michael@0: this.dragger.style.backgroundColor = flatColor; michael@0: michael@0: var rgbNoAlpha = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")"; michael@0: var rgbAlpha0 = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ", 0)"; michael@0: var alphaGradient = "linear-gradient(to right, " + rgbAlpha0 + ", " + rgbNoAlpha + ")"; michael@0: this.alphaSliderInner.style.background = alphaGradient; michael@0: }, michael@0: michael@0: destroy: function() { michael@0: this.element.removeEventListener("click", this.onElementClick, false); michael@0: michael@0: this.parentEl.removeChild(this.element); michael@0: michael@0: this.slider = null; michael@0: this.dragger = null; michael@0: this.alphaSlider = this.alphaSliderInner = this.alphaSliderHelper = null; michael@0: this.parentEl = null; michael@0: this.element = null; michael@0: } michael@0: };