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