/** * Custom Color Picker Component * Consistent across all operating systems and browsers */ class ColorPicker { constructor(container, options = {}) { this.container = typeof container === 'string' ? document.querySelector(container) : container; this.options = { initialColor: options.initialColor || '#FF0000', onColorChange: options.onColorChange || null, showHexInput: options.showHexInput !== false, ...options }; this.currentColor = this.options.initialColor; this.isOpen = false; this.init(); } init() { this.createPicker(); this.setupEventListeners(); this.updateColor(this.options.initialColor); } createPicker() { this.container.innerHTML = ''; this.container.className = 'color-picker-container'; // Color preview button this.previewBtn = document.createElement('button'); this.previewBtn.className = 'color-picker-preview'; this.previewBtn.type = 'button'; this.previewBtn.style.backgroundColor = this.currentColor; this.previewBtn.setAttribute('aria-label', 'Open color picker'); // Dropdown panel this.panel = document.createElement('div'); this.panel.className = 'color-picker-panel'; this.panel.style.display = 'none'; // Main color area (hue/saturation) this.mainArea = document.createElement('div'); this.mainArea.className = 'color-picker-main'; this.mainCanvas = document.createElement('canvas'); this.mainCanvas.width = 200; this.mainCanvas.height = 200; this.mainCanvas.className = 'color-picker-canvas'; this.mainArea.appendChild(this.mainCanvas); // Main area cursor this.mainCursor = document.createElement('div'); this.mainCursor.className = 'color-picker-cursor'; this.mainArea.appendChild(this.mainCursor); // Hue slider this.hueArea = document.createElement('div'); this.hueArea.className = 'color-picker-hue'; this.hueCanvas = document.createElement('canvas'); this.hueCanvas.width = 20; this.hueCanvas.height = 200; this.hueCanvas.className = 'color-picker-canvas'; this.hueArea.appendChild(this.hueCanvas); // Hue slider cursor this.hueCursor = document.createElement('div'); this.hueCursor.className = 'color-picker-hue-cursor'; this.hueArea.appendChild(this.hueCursor); // Controls section this.controls = document.createElement('div'); this.controls.className = 'color-picker-controls'; // Hex input if (this.options.showHexInput) { this.hexInput = document.createElement('input'); this.hexInput.type = 'text'; this.hexInput.className = 'color-picker-hex'; this.hexInput.placeholder = '#000000'; this.hexInput.maxLength = 7; this.controls.appendChild(this.hexInput); } // RGB inputs and sliders this.rgbContainer = document.createElement('div'); this.rgbContainer.className = 'color-picker-rgb'; ['R', 'G', 'B'].forEach((label, index) => { const wrapper = document.createElement('div'); wrapper.className = 'color-picker-rgb-item'; wrapper.dataset.channel = label.toLowerCase(); const labelEl = document.createElement('label'); labelEl.textContent = label; const slider = document.createElement('input'); slider.type = 'range'; slider.className = 'color-picker-rgb-slider'; slider.min = 0; slider.max = 255; slider.value = 0; slider.dataset.channel = label.toLowerCase(); const input = document.createElement('input'); input.type = 'number'; input.className = 'color-picker-rgb-input'; input.min = 0; input.max = 255; input.value = 0; input.dataset.channel = label.toLowerCase(); wrapper.appendChild(labelEl); wrapper.appendChild(slider); wrapper.appendChild(input); this.rgbContainer.appendChild(wrapper); this[`rgb${label}Slider`] = slider; this[`rgb${label}`] = input; }); this.controls.appendChild(this.rgbContainer); // Assemble panel const pickerArea = document.createElement('div'); pickerArea.className = 'color-picker-area'; pickerArea.appendChild(this.mainArea); pickerArea.appendChild(this.hueArea); this.panel.appendChild(pickerArea); this.panel.appendChild(this.controls); // Assemble container this.container.appendChild(this.previewBtn); this.container.appendChild(this.panel); // Draw canvases this.drawHueCanvas(); this.drawMainCanvas(1.0); // Start with full saturation } setupEventListeners() { // Toggle panel this.previewBtn.addEventListener('click', (e) => { e.stopPropagation(); this.toggle(); }); // Close on outside click document.addEventListener('click', (e) => { if (!this.container.contains(e.target) && this.isOpen) { this.close(); } }); // Main area interaction let isMainDragging = false; this.mainCanvas.addEventListener('mousedown', (e) => { isMainDragging = true; this.handleMainAreaClick(e); }); this.mainCanvas.addEventListener('mousemove', (e) => { if (isMainDragging) { this.handleMainAreaClick(e); } }); document.addEventListener('mouseup', () => { isMainDragging = false; }); // Touch support for main area this.mainCanvas.addEventListener('touchstart', (e) => { e.preventDefault(); isMainDragging = true; this.handleMainAreaClick(e.touches[0]); }); this.mainCanvas.addEventListener('touchmove', (e) => { e.preventDefault(); if (isMainDragging) { this.handleMainAreaClick(e.touches[0]); } }); this.mainCanvas.addEventListener('touchend', () => { isMainDragging = false; }); // Hue slider interaction let isHueDragging = false; this.hueCanvas.addEventListener('mousedown', (e) => { isHueDragging = true; this.handleHueClick(e); }); this.hueCanvas.addEventListener('mousemove', (e) => { if (isHueDragging) { this.handleHueClick(e); } }); document.addEventListener('mouseup', () => { isHueDragging = false; }); // Touch support for hue slider this.hueCanvas.addEventListener('touchstart', (e) => { e.preventDefault(); isHueDragging = true; this.handleHueClick(e.touches[0]); }); this.hueCanvas.addEventListener('touchmove', (e) => { e.preventDefault(); if (isHueDragging) { this.handleHueClick(e.touches[0]); } }); this.hueCanvas.addEventListener('touchend', () => { isHueDragging = false; }); // Hex input if (this.hexInput) { this.hexInput.addEventListener('input', (e) => { const value = e.target.value; if (/^#[0-9A-Fa-f]{6}$/.test(value)) { this.updateColor(value); } }); this.hexInput.addEventListener('blur', (e) => { const value = e.target.value; if (!/^#[0-9A-Fa-f]{6}$/.test(value) && value.length > 0) { e.target.value = this.currentColor; } }); } // RGB inputs and sliders ['R', 'G', 'B'].forEach(label => { // Slider change this[`rgb${label}Slider`].addEventListener('input', (e) => { const value = parseInt(e.target.value) || 0; this[`rgb${label}`].value = value; const r = parseInt(this.rgbR.value) || 0; const g = parseInt(this.rgbG.value) || 0; const b = parseInt(this.rgbB.value) || 0; const hex = this.rgbToHex(r, g, b); this.updateColor(hex, false); // Don't update RGB inputs/sliders to avoid loop }); // Input change this[`rgb${label}`].addEventListener('input', (e) => { let value = parseInt(e.target.value) || 0; value = Math.max(0, Math.min(255, value)); // Clamp to 0-255 e.target.value = value; this[`rgb${label}Slider`].value = value; const r = parseInt(this.rgbR.value) || 0; const g = parseInt(this.rgbG.value) || 0; const b = parseInt(this.rgbB.value) || 0; const hex = this.rgbToHex(r, g, b); this.updateColor(hex, false); // Don't update RGB inputs/sliders to avoid loop }); }); } drawHueCanvas() { const ctx = this.hueCanvas.getContext('2d'); const gradient = ctx.createLinearGradient(0, 0, 0, 200); for (let i = 0; i <= 6; i++) { const hue = i * 60; gradient.addColorStop(i / 6, `hsl(${hue}, 100%, 50%)`); } ctx.fillStyle = gradient; ctx.fillRect(0, 0, 20, 200); } drawMainCanvas(hue) { const ctx = this.mainCanvas.getContext('2d'); // Saturation gradient (left to right) const satGradient = ctx.createLinearGradient(0, 0, 200, 0); satGradient.addColorStop(0, `hsl(${hue}, 0%, 50%)`); satGradient.addColorStop(1, `hsl(${hue}, 100%, 50%)`); ctx.fillStyle = satGradient; ctx.fillRect(0, 0, 200, 200); // Brightness gradient (top to bottom) const brightGradient = ctx.createLinearGradient(0, 0, 0, 200); brightGradient.addColorStop(0, 'rgba(255, 255, 255, 0)'); brightGradient.addColorStop(1, 'rgba(0, 0, 0, 1)'); ctx.fillStyle = brightGradient; ctx.fillRect(0, 0, 200, 200); } handleMainAreaClick(e) { const rect = this.mainCanvas.getBoundingClientRect(); const x = Math.max(0, Math.min(200, e.clientX - rect.left)); const y = Math.max(0, Math.min(200, e.clientY - rect.top)); const saturation = x / 200; const brightness = 1 - (y / 200); this.updateColorFromHSB(this.hue, saturation, brightness); this.updateCursor(x, y); } handleHueClick(e) { const rect = this.hueCanvas.getBoundingClientRect(); const y = Math.max(0, Math.min(200, e.clientY - rect.top)); const hue = (y / 200) * 360; this.hue = hue; this.drawMainCanvas(hue); this.updateHueCursor(y); // Recalculate color with new hue const rect2 = this.mainCanvas.getBoundingClientRect(); const x = parseFloat(this.mainCursor.style.left) || 0; const y2 = parseFloat(this.mainCursor.style.top) || 0; const saturation = x / 200; const brightness = 1 - (y2 / 200); this.updateColorFromHSB(hue, saturation, brightness); } updateColorFromHSB(h, s, v) { const rgb = this.hsbToRgb(h, s, v); const hex = this.rgbToHex(rgb.r, rgb.g, rgb.b); this.updateColor(hex); } hsbToRgb(h, s, v) { h = h / 360; const i = Math.floor(h * 6); const f = h * 6 - i; const p = v * (1 - s); const q = v * (1 - f * s); const t = v * (1 - (1 - f) * s); let r, g, b; 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: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) }; } rgbToHex(r, g, b) { return '#' + [r, g, b].map(x => { const hex = x.toString(16); return hex.length === 1 ? '0' + hex : hex; }).join('').toUpperCase(); } hexToRgb(hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; } rgbToHsb(r, g, b) { r /= 255; g /= 255; b /= 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const diff = max - min; let h = 0; if (diff !== 0) { if (max === r) { h = ((g - b) / diff) % 6) * 60; } else if (max === g) { h = ((b - r) / diff + 2) * 60; } else { h = ((r - g) / diff + 4) * 60; } } if (h < 0) h += 360; const s = max === 0 ? 0 : diff / max; const v = max; return { h, s, v }; } updateColor(hex, updateInputs = true) { this.currentColor = hex.toUpperCase(); this.previewBtn.style.backgroundColor = this.currentColor; const rgb = this.hexToRgb(this.currentColor); if (!rgb) return; const hsb = this.rgbToHsb(rgb.r, rgb.g, rgb.b); this.hue = hsb.h; // Update main canvas this.drawMainCanvas(this.hue); // Update cursors const x = hsb.s * 200; const y = (1 - hsb.v) * 200; this.updateCursor(x, y); this.updateHueCursor((this.hue / 360) * 200); // Update inputs if (updateInputs) { if (this.hexInput) { this.hexInput.value = this.currentColor; } if (this.rgbR) { this.rgbR.value = rgb.r; this.rgbG.value = rgb.g; this.rgbB.value = rgb.b; } if (this.rgbRSlider) { this.rgbRSlider.value = rgb.r; this.rgbGSlider.value = rgb.g; this.rgbBSlider.value = rgb.b; } } // Callback if (this.options.onColorChange) { this.options.onColorChange(this.currentColor); } } updateCursor(x, y) { this.mainCursor.style.left = `${x}px`; this.mainCursor.style.top = `${y}px`; } updateHueCursor(y) { this.hueCursor.style.top = `${y}px`; } toggle() { this.isOpen ? this.close() : this.open(); } open() { this.panel.style.display = 'block'; this.isOpen = true; } close() { this.panel.style.display = 'none'; this.isOpen = false; } getColor() { return this.currentColor; } setColor(color) { this.updateColor(color); } } // Export for use in other scripts if (typeof module !== 'undefined' && module.exports) { module.exports = ColorPicker; }