1/** 2 * Custom color picker for settings: SV plane, hue slider, hex + RGB inputs. 3 */ 4(function (global) { 5 'use strict'; 6 7 var openPicker = null; 8 var globalBound = false; 9 10 function clamp(v, min, max) { 11 return Math.max(min, Math.min(max, v)); 12 } 13 14 function hexToRgb(hex) { 15 var raw = String(hex || '').replace('#', '').trim(); 16 if (raw.length === 3) { 17 raw = raw.split('').map(function (c) { return c + c; }).join(''); 18 } 19 if (!/^[0-9a-fA-F]{6}$/.test(raw)) return null; 20 return { 21 r: parseInt(raw.substring(0, 2), 16), 22 g: parseInt(raw.substring(2, 4), 16), 23 b: parseInt(raw.substring(4, 6), 16) 24 }; 25 } 26 27 function rgbToHex(r, g, b) { 28 return '#' + [r, g, b].map(function (x) { 29 var hex = clamp(Math.round(x), 0, 255).toString(16); 30 return hex.length === 1 ? '0' + hex : hex; 31 }).join(''); 32 } 33 34 function rgbToHsv(r, g, b) { 35 r /= 255; 36 g /= 255; 37 b /= 255; 38 var max = Math.max(r, g, b); 39 var min = Math.min(r, g, b); 40 var diff = max - min; 41 var h = 0; 42 if (diff !== 0) { 43 if (max === r) h = ((g - b) / diff) % 6; 44 else if (max === g) h = (b - r) / diff + 2; 45 else h = (r - g) / diff + 4; 46 } 47 h = Math.round(h * 60); 48 if (h < 0) h += 360; 49 var s = max === 0 ? 0 : diff / max; 50 return { h: h, s: Math.round(s * 100), v: Math.round(max * 100) }; 51 } 52 53 function hsvToRgb(h, s, v) { 54 h = ((h % 360) + 360) % 360; 55 s /= 100; 56 v /= 100; 57 var c = v * s; 58 var x = c * (1 - Math.abs((h / 60) % 2 - 1)); 59 var m = v - c; 60 var r = 0; 61 var g = 0; 62 var b = 0; 63 if (h < 60) { r = c; g = x; } 64 else if (h < 120) { r = x; g = c; } 65 else if (h < 180) { g = c; b = x; } 66 else if (h < 240) { g = x; b = c; } 67 else if (h < 300) { r = x; b = c; } 68 else { r = c; b = x; } 69 return { 70 r: Math.round((r + m) * 255), 71 g: Math.round((g + m) * 255), 72 b: Math.round((b + m) * 255) 73 }; 74 } 75 76 function isValidHex(hex) { 77 var raw = String(hex || '').trim(); 78 if (!raw) return false; 79 if (raw[0] !== '#') raw = '#' + raw; 80 return /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(raw); 81 } 82 83 function isCompleteHex(hex) { 84 var raw = String(hex || '').trim(); 85 if (!raw) return false; 86 if (raw[0] !== '#') raw = '#' + raw; 87 return /^#[0-9a-fA-F]{6}$/.test(raw); 88 } 89 90 function normalizeHex(hex) { 91 if (!isValidHex(hex)) return null; 92 var rgb = hexToRgb(hex); 93 if (!rgb) return null; 94 return rgbToHex(rgb.r, rgb.g, rgb.b); 95 } 96 97 function bindGlobalListeners() { 98 if (globalBound) return; 99 globalBound = true; 100 101 document.addEventListener('click', function (e) { 102 if (!openPicker || !openPicker.root) return; 103 if (openPicker.root.contains(e.target)) return; 104 if (openPicker.floatAnchor && openPicker.floatAnchor.contains(e.target)) return; 105 openPicker.close(); 106 }); 107 108 document.addEventListener('keydown', function (e) { 109 if (e.key === 'Escape' && openPicker) { 110 openPicker.close(); 111 e.stopPropagation(); 112 } 113 }); 114 } 115 116 function mount(root, cfg) { 117 cfg = cfg || {}; 118 var id = cfg.idPrefix || cfg.id || ('color-' + Math.random().toString(36).slice(2, 8)); 119 var onChange = typeof cfg.onChange === 'function' ? cfg.onChange : null; 120 var onCommit = typeof cfg.onCommit === 'function' ? cfg.onCommit : onChange; 121 var hsv = { h: 330, s: 70, v: 100 }; 122 var hex = '#ff4da6'; 123 var syncing = false; 124 var draggingPlane = false; 125 var rgbEmitTimer = null; 126 127 root.classList.add('settings-color-picker'); 128 129 root.innerHTML = 130 '<button type="button" class="settings-color-picker__trigger" id="' + id + '-trigger"' + 131 ' aria-haspopup="dialog" aria-expanded="false" aria-controls="' + id + '-panel">' + 132 '<span class="settings-color-picker__swatch" id="' + id + '-swatch" aria-hidden="true"></span>' + 133 '<span class="settings-color-picker__value" id="' + id + '-preview">#ff4da6</span>' + 134 '<span class="settings-color-picker__chevron" aria-hidden="true">' + 135 '<i class="fa-solid fa-chevron-down fa-width-auto"></i></span>' + 136 '</button>' + 137 '<div class="settings-color-picker__panel" id="' + id + '-panel" hidden role="dialog" aria-label="Pick a color">' + 138 '<div class="settings-color-picker__plane-wrap" id="' + id + '-plane-wrap">' + 139 '<div class="settings-color-picker__plane" id="' + id + '-plane"></div>' + 140 '<div class="settings-color-picker__plane-cursor" id="' + id + '-plane-cursor"></div>' + 141 '</div>' + 142 '<input type="range" class="settings-color-picker__hue" id="' + id + '-hue" min="0" max="360" step="1" value="330" aria-label="Hue">' + 143 '<div class="settings-color-picker__tabs" role="tablist">' + 144 '<button type="button" class="settings-color-picker__tab is-active" data-tab="hex" role="tab" aria-selected="true">HEX</button>' + 145 '<button type="button" class="settings-color-picker__tab" data-tab="rgb" role="tab" aria-selected="false">RGB</button>' + 146 '</div>' + 147 '<div class="settings-color-picker__pane is-active" data-pane="hex">' + 148 '<label class="settings-color-picker__input-label">Hex</label>' + 149 '<input type="text" class="settings-color-picker__hex" id="' + id + '-hex" spellcheck="false" autocomplete="off" placeholder="#ff4da6">' + 150 '</div>' + 151 '<div class="settings-color-picker__pane" data-pane="rgb" hidden>' + 152 '<div class="settings-color-picker__rgb-row">' + 153 '<label class="settings-color-picker__rgb-field"><span>R</span>' + 154 '<input type="number" class="settings-color-picker__rgb" id="' + id + '-r" min="0" max="255" step="1"></label>' + 155 '<label class="settings-color-picker__rgb-field"><span>G</span>' + 156 '<input type="number" class="settings-color-picker__rgb" id="' + id + '-g" min="0" max="255" step="1"></label>' + 157 '<label class="settings-color-picker__rgb-field"><span>B</span>' + 158 '<input type="number" class="settings-color-picker__rgb" id="' + id + '-b" min="0" max="255" step="1"></label>' + 159 '</div>' + 160 '</div>' + 161 '</div>'; 162 163 var trigger = root.querySelector('#' + id + '-trigger'); 164 var panel = root.querySelector('#' + id + '-panel'); 165 var swatch = root.querySelector('#' + id + '-swatch'); 166 var preview = root.querySelector('#' + id + '-preview'); 167 var planeWrap = root.querySelector('#' + id + '-plane-wrap'); 168 var plane = root.querySelector('#' + id + '-plane'); 169 var planeCursor = root.querySelector('#' + id + '-plane-cursor'); 170 var hueInput = root.querySelector('#' + id + '-hue'); 171 var hexInput = root.querySelector('#' + id + '-hex'); 172 var rInput = root.querySelector('#' + id + '-r'); 173 var gInput = root.querySelector('#' + id + '-g');
174 var bInput = root.querySelector('#' + id + '-b'); 175 var tabs = root.querySelectorAll('.settings-color-picker__tab'); 176 var panes = root.querySelectorAll('.settings-color-picker__pane'); 177 178 function emit(live) { 179 if (live && onChange) onChange(hex); 180 if (!live && onCommit) onCommit(hex); 181 } 182 183 function scheduleRgbEmit() { 184 if (rgbEmitTimer) clearTimeout(rgbEmitTimer); 185 rgbEmitTimer = setTimeout(function () { 186 rgbEmitTimer = null; 187 emit(true); 188 }, 280); 189 } 190 191 function updatePlaneVisuals() { 192 plane.style.backgroundColor = 'hsl(' + hsv.h + ', 100%, 50%)'; 193 var wrapRect = planeWrap.getBoundingClientRect(); 194 var x = (hsv.s / 100) * wrapRect.width; 195 var y = (1 - hsv.v / 100) * wrapRect.height; 196 planeCursor.style.left = x + 'px'; 197 planeCursor.style.top = y + 'px'; 198 } 199 200 function syncUi(skip) { 201 syncing = true; 202 var rgb = hsvToRgb(hsv.h, hsv.s, hsv.v); 203 hex = rgbToHex(rgb.r, rgb.g, rgb.b); 204 swatch.style.backgroundColor = hex; 205 preview.textContent = hex; 206 hueInput.value = String(hsv.h); 207 if (skip !== 'hex') hexInput.value = hex; 208 if (skip !== 'rgb') { 209 rInput.value = String(rgb.r); 210 gInput.value = String(rgb.g); 211 bInput.value = String(rgb.b); 212 } 213 updatePlaneVisuals(); 214 syncing = false; 215 } 216 217 function setFromHsv(next, skip, doEmit) { 218 hsv.h = clamp(next.h, 0, 360); 219 hsv.s = clamp(next.s, 0, 100); 220 hsv.v = clamp(next.v, 0, 100); 221 syncUi(skip); 222 if (doEmit !== false) emit(true); 223 } 224 225 function setFromRgb(rgb, skip, doEmit) { 226 var nextHsv = rgbToHsv(rgb.r, rgb.g, rgb.b); 227 hsv.h = nextHsv.h; 228 hsv.s = nextHsv.s; 229 hsv.v = nextHsv.v; 230 syncUi(skip); 231 if (doEmit !== false) emit(true); 232 } 233 234 function setFromHex(nextHex, skip, doEmit) { 235 var normalized = normalizeHex(nextHex); 236 if (!normalized) return false; 237 var rgb = hexToRgb(normalized); 238 if (!rgb) return false; 239 setFromRgb(rgb, skip, doEmit); 240 return true; 241 } 242 243 function pickFromPlane(clientX, clientY) { 244 var rect = planeWrap.getBoundingClientRect(); 245 var x = clamp(clientX - rect.left, 0, rect.width); 246 var y = clamp(clientY - rect.top, 0, rect.height); 247 var s = rect.width ? (x / rect.width) * 100 : 0; 248 var v = rect.height ? (1 - y / rect.height) * 100 : 0; 249 setFromHsv({ h: hsv.h, s: s, v: v }, null, true); 250 } 251 252 function setActiveTab(tabName) {
253 tabs.forEach(function (tab) { 254 var active = tab.dataset.tab === tabName; 255 tab.classList.toggle('is-active', active); 256 tab.setAttribute('aria-selected', active ? 'true' : 'false'); 257 }); 258 panes.forEach(function (pane) { 259 var active = pane.dataset.pane === tabName; 260 pane.classList.toggle('is-active', active); 261 pane.hidden = !active; 262 }); 263 } 264 265 var floatAnchor = null; 266 if (global.PupwifeSettingsBubbleSelect && typeof global.PupwifeSettingsBubbleSelect.createFloatingDropdown === 'function') { 267 floatAnchor = global.PupwifeSettingsBubbleSelect.createFloatingDropdown({ 268 root: root, 269 trigger: trigger, 270 panel: panel, 271 floatingClass: 'settings-color-picker__panel--floating' 272 }); 273 } 274 275 var api = { 276 root: root, 277 panel: panel, 278 floatAnchor: floatAnchor, 279 close: function () { 280 if (floatAnchor) floatAnchor.close(); 281 panel.hidden = true; 282 trigger.setAttribute('aria-expanded', 'false'); 283 root.classList.remove('is-open'); 284 draggingPlane = false; 285 if (openPicker === api) openPicker = null; 286 }, 287 open: function () { 288 if (openPicker && openPicker !== api) openPicker.close(); 289 if (global.PupwifeSettingsBubbleSelect && typeof global.PupwifeSettingsBubbleSelect.closeOpen === 'function') { 290 global.PupwifeSettingsBubbleSelect.closeOpen(); 291 } 292 panel.hidden = false; 293 trigger.setAttribute('aria-expanded', 'true'); 294 root.classList.add('is-open'); 295 if (floatAnchor) floatAnchor.open(); 296 openPicker = api; 297 updatePlaneVisuals(); 298 }, 299 setValue: function (nextHex) { 300 setFromHex(nextHex || '#ff4da6', null, false); 301 }, 302 getValue: function () { return hex; } 303 }; 304 305 trigger.addEventListener('click', function () { 306 if (panel.hidden) api.open(); 307 else api.close(); 308 }); 309 310 hueInput.addEventListener('input', function () { 311 if (syncing) return; 312 setFromHsv({ 313 h: parseInt(hueInput.value, 10) || 0, 314 s: hsv.s, 315 v: hsv.v 316 }, null, true); 317 }); 318 319 planeWrap.addEventListener('mousedown', function (e) { 320 e.preventDefault(); 321 draggingPlane = true; 322 pickFromPlane(e.clientX, e.clientY); 323 }); 324 325 global.addEventListener('mousemove', function (e) { 326 if (!draggingPlane) return; 327 pickFromPlane(e.clientX, e.clientY); 328 }); 329 330 global.addEventListener('mouseup', function () { 331 draggingPlane = false; 332 }); 333 334 planeWrap.addEventListener('touchstart', function (e) { 335 if (!e.touches || !e.touches[0]) return; 336 draggingPlane = true; 337 pickFromPlane(e.touches[0].clientX, e.touches[0].clientY); 338 }, { passive: true }); 339 340 planeWrap.addEventListener('touchmove', function (e) { 341 if (!draggingPlane || !e.touches || !e.touches[0]) return; 342 pickFromPlane(e.touches[0].clientX, e.touches[0].clientY); 343 }, { passive: true }); 344 345 planeWrap.addEventListener('touchend', function () { 346 draggingPlane = false; 347 }); 348 349 hexInput.addEventListener('input', function () { 350 if (syncing) return; 351 var raw = hexInput.value.trim(); 352 if (raw && raw[0] !== '#') raw = '#' + raw; 353 if (isCompleteHex(raw)) { 354 setFromHex(raw, 'hex', true); 355 } 356 }); 357 358 hexInput.addEventListener('blur', function () { 359 var normalized = normalizeHex(hexInput.value); 360 if (normalized) setFromHex(normalized, 'hex', false); 361 else syncUi('hex'); 362 emit(false); 363 }); 364 365 function onRgbInput() { 366 if (syncing) return; 367 var rgb = { 368 r: parseInt(rInput.value, 10) || 0, 369 g: parseInt(gInput.value, 10) || 0, 370 b: parseInt(bInput.value, 10) || 0 371 }; 372 setFromRgb(rgb, 'rgb', false); 373 scheduleRgbEmit(); 374 } 375 376 function onRgbBlur() { 377 if (rgbEmitTimer) { 378 clearTimeout(rgbEmitTimer); 379 rgbEmitTimer = null; 380 } 381 var rgb = { 382 r: clamp(parseInt(rInput.value, 10) || 0, 0, 255), 383 g: clamp(parseInt(gInput.value, 10) || 0, 0, 255), 384 b: clamp(parseInt(bInput.value, 10) || 0, 0, 255) 385 }; 386 setFromRgb(rgb, 'rgb', false); 387 emit(false); 388 } 389 390 rInput.addEventListener('input', onRgbInput); 391 gInput.addEventListener('input', onRgbInput); 392 bInput.addEventListener('input', onRgbInput); 393 rInput.addEventListener('blur', onRgbBlur); 394 gInput.addEventListener('blur', onRgbBlur); 395 bInput.addEventListener('blur', onRgbBlur); 396
397 tabs.forEach(function (tab) { 398 tab.addEventListener('click', function () { 399 setActiveTab(tab.dataset.tab); 400 }); 401 }); 402 403 bindGlobalListeners(); 404 api.setValue(cfg.value || '#ff4da6'); 405 406 return api; 407 } 408 409 function closeOpen() { 410 if (openPicker) openPicker.close(); 411 } 412 413 function isOpen() { 414 return !!openPicker; 415 } 416 417 global.PupwifeSettingsColorPicker = { 418 mount: mount, 419 closeOpen: closeOpen, 420 isOpen: isOpen, 421 normalizeHex: normalizeHex 422 }; 423})(window);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.