PageSourceSearch

https://pupwife.com/scripts/effects/settingsColorPicker.js

js pupwife.com collected 2026-09-30 15:06:24 UTC 16,679 bytes, 423 lines download raw bytes

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.