PageSourceSearch

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

js pupwife.com collected 2026-09-30 15:06:19 UTC 11,105 bytes, 298 lines download raw bytes

1/**
2 * Bubbly custom dropdown for custom cursor selection (settings menu).
3 */
4(function (global) {
5    'use strict';
6
7    var NEW_CURSOR_VALUES = {
8        'rough-paw': true,
9        'rough-pencil': true,
10        'rough-heart': true,
11        'twojs-recursive': true,
12        'cat-paw-arm': true
13    };
14
15    var CUSTOM_CURSOR_GROUPS = [
16        {
17            label: 'default',
18            options: [
19                { value: 'none', label: 'default' }
20            ]
21        },
22        {
23            label: 'image cursors',
24            options: [
25                { value: 'asriel', label: 'Asriel' },
26                { value: 'axolotl', label: 'Axolotl' }
27            ]
28        },
29        {
30            label: 'Rough.js sketch',
31            options: [
32                { value: 'rough-drawn', label: 'Drawn' },
33                { value: 'rough-paw', label: 'Sketch paw' },
34                { value: 'rough-pencil', label: 'Pencil nib' },
35                { value: 'rough-heart', label: 'Heart pointer' }
36            ]
37        },
38        {
39            label: 'Two.js animated',
40            options: [
41                { value: 'twojs-geometric', label: 'Animated Geometric' },
42                { value: 'twojs-particles', label: 'Interactive Particles' },
43                { value: 'twojs-morphing', label: 'Morphing Shape' },
44                { value: 'twojs-physics', label: 'Physics-Based' },
45                { value: 'twojs-reactive', label: 'Reactive' },
46                { value: 'twojs-recursive', label: 'Recursive' }
47            ]
48        },
49        {
50            label: 'stretchy',
51            options: [
52                { value: 'cat-paw-arm', label: 'Stretchy paw' }
53            ]
54        }
55    ];
56
57    var state = {
58        root: null,
59        hidden: null,
60        trigger: null,
61        triggerLabel: null,
62        panel: null,
63        listbox: null,
64        floatAnchor: null,
65        onChange: null,
66        value: 'none',
67        disabled: false,
68        docListenersBound: false,
69        triggerBadge: null
70    };
71
72    function decorateOptions() {
73        CUSTOM_CURSOR_GROUPS.forEach(function (group) {
74            group.options.forEach(function (opt) {
75                opt.isNew = !!NEW_CURSOR_VALUES[opt.value];
76            });
77        });
78    }
79
80    decorateOptions();
81
82    function findOption(value) {
83        for (var g = 0; g < CUSTOM_CURSOR_GROUPS.length; g++) {
84            var group = CUSTOM_CURSOR_GROUPS[g];
85            for (var i = 0; i < group.options.length; i++) {
86                if (group.options[i].value === value) {
87                    return group.options[i];
88                }
89            }
90        }
91        return { value: value, label: value, isNew: false };
92    }
93
94    function updateTriggerBadge(meta) {
95        if (!state.trigger) return;
96        if (meta.isNew) {
97            if (!state.triggerBadge) {
98                state.triggerBadge = document.createElement('span');
99                state.triggerBadge.className = 'settings-bubble-select__new settings-bubble-select__new--trigger';
100                state.triggerBadge.textContent = 'NEW!';
101                var chevron = state.trigger.querySelector('.settings-bubble-select__chevron');
102                if (chevron) {
103                    state.trigger.insertBefore(state.triggerBadge, chevron);
104                } else {
105                    state.trigger.appendChild(state.triggerBadge);
106                }
107            }
108        } else if (state.triggerBadge) {
109            state.triggerBadge.remove();
110            state.triggerBadge = null;
111        }
112    }
113
114    function setValue(value) {
115        var meta = findOption(value);
116        state.value = meta.value;
117        if (state.hidden) {
118            state.hidden.value = meta.value;
119        }
120        if (state.triggerLabel) {
121            state.triggerLabel.textContent = meta.label;
122        }
123        updateTriggerBadge(meta);
124        if (state.listbox) {
125            state.listbox.querySelectorAll('.settings-bubble-select__option').forEach(function (btn) {
126                var selected = btn.dataset.value === meta.value;
127                btn.classList.toggle('is-selected', selected);
128                btn.setAttribute('aria-selected', selected ? 'true' : 'false');
129            });
130        }
131    }
132
133    function close() {
134        if (!state.panel || !state.trigger) return;
135        if (state.floatAnchor && typeof state.floatAnchor.close === 'function') {
136            state.floatAnchor.close();
137        }
138        state.panel.hidden = true;
139        state.trigger.setAttribute('aria-expanded', 'false');
140        if (state.root) {
141            state.root.classList.remove('is-open');
142        }
143    }
144
145    function open() {
146        if (!state.panel || !state.trigger || state.disabled) return;
147        if (global.PupwifeTrailPicker && typeof global.PupwifeTrailPicker.close === 'function') {
148            global.PupwifeTrailPicker.close();
149        }
150        if (global.PupwifeSettingsBubbleSelect && typeof global.PupwifeSettingsBubbleSelect.closeOpen === 'function') {
151            global.PupwifeSettingsBubbleSelect.closeOpen();
152        }
153        state.panel.hidden = false;
154        state.trigger.setAttribute('aria-expanded', 'true');
155        if (state.root) {
156            state.root.classList.add('is-open');
157        }
158        if (state.floatAnchor && typeof state.floatAnchor.open === 'function') {
159            state.floatAnchor.open();
160        }
161    }
162
163    function setDisabled(disabled) {
164        state.disabled = !!disabled;
165        if (state.trigger) {
166            state.trigger.disabled = state.disabled;
167        }
168        if (state.root) {
169            state.root.classList.toggle('is-disabled', state.disabled);
170        }
171        if (state.disabled) {
172            close();
173        }
174    }
175
176    function bindDocumentListeners() {
177        if (state.docListenersBound) return;
178        state.docListenersBound = true;
179
180        document.addEventListener('click', function (e) {
181            if (!state.root || state.panel.hidden) return;
182            if (state.root.contains(e.target)) return;
183            if (state.floatAnchor && typeof state.floatAnchor.contains === 'function' && state.floatAnchor.contains(e.target)) {
184                return;
185            }
186            close();
187        });
188
189        document.addEventListener('keydown', function (e) {
190            if (e.key === 'Escape' && state.panel && !state.panel.hidden) {
191                close();
192            }
193        });
194    }
195
196    function mount(root, options) {
197        options = options || {};
198        state.root = root;
199        state.onChange = typeof options.onChange === 'function' ? options.onChange : null;
200        root.classList.add('settings-bubble-select');
201
202        root.innerHTML =
203            '<input type="hidden" id="setting-customCursor" value="none">' +
204            '<button type="button" class="settings-bubble-select__trigger" id="setting-customCursor-trigger"' +
205            ' aria-haspopup="listbox" aria-expanded="false" aria-controls="setting-customCursor-listbox">' +
206            '<span class="settings-bubble-select__trigger-text" id="setting-customCursor-trigger-label">default</span>' +
207            '<span class="settings-bubble-select__chevron" aria-hidden="true">' +
208            '<i class="fa-solid fa-chevron-down fa-width-auto"></i></span>' +
209            '</button>' +
210            '<div class="settings-bubble-select__panel" id="setting-customCursor-panel" hidden>' +
211            '<div class="settings-bubble-select__scroll" id="setting-customCursor-listbox" role="listbox" aria-label="Custom cursor style"></div>' +
212            '</div>';
213
214        state.hidden = root.querySelector('#setting-customCursor');
215        state.trigger = root.querySelector('#setting-customCursor-trigger');
216        state.triggerLabel = root.querySelector('#setting-customCursor-trigger-label');
217        state.panel = root.querySelector('#setting-customCursor-panel');
218        state.listbox = root.querySelector('#setting-customCursor-listbox');
219
220        if (global.PupwifeSettingsBubbleSelect && typeof global.PupwifeSettingsBubbleSelect.createFloatingDropdown === 'function') {
221            state.floatAnchor = global.PupwifeSettingsBubbleSelect.createFloatingDropdown({
222                root: root,
223                trigger: state.trigger,
224                panel: state.panel,
225                scrollEl: state.listbox,
226                floatingClass: 'settings-bubble-select__panel--floating',
227                preferAbove: true
228            });
229        }
230
231        CUSTOM_CURSOR_GROUPS.forEach(function (group) {
232            var groupEl = document.createElement('div');
233            groupEl.className = 'settings-bubble-select__group';
234
235            var title = document.createElement('p');
236            title.className = 'settings-bubble-select__group-label';
237            title.textContent = group.label;
238            groupEl.appendChild(title);
239
240            group.options.forEach(function (opt) {
241                var btn = document.createElement('button');
242                btn.type = 'button';
243                btn.className = 'settings-bubble-select__option';
244                btn.dataset.value = opt.value;
245                btn.setAttribute('role', 'option');
246                btn.setAttribute('aria-selected', 'false');
247
248                var labelSpan = document.createElement('span');
249                labelSpan.className = 'settings-bubble-select__option-label';
250                labelSpan.textContent = opt.label;
251                btn.appendChild(labelSpan);
252
253                if (opt.isNew) {
254                    var badge = document.createElement('span');
255                    badge.className = 'settings-bubble-select__new';
256                    badge.textContent = 'NEW!';
257                    btn.appendChild(badge);
258                }
259
260                btn.addEventListener('click', function () {
261                    if (state.disabled) return;
262                    setValue(opt.value);
263                    close();
264                    if (state.onChange) {
265                        state.onChange(opt.value);
266                    }
267                });
268
269                groupEl.appendChild(btn);
270            });
271
272            state.listbox.appendChild(groupEl);
273        });
274
275        state.trigger.addEventListener('click', function () {
276            if (state.disabled) return;
277            if (state.panel.hidden) {
278                open();
279            } else {
280                close();
281            }
282        });
283
284        bindDocumentListeners();
285        setValue(options.value || 'none');
286        setDisabled(!!options.disabled);
287    }
288
289    global.PupwifeCursorPicker = {
290        mount: mount,
291        setValue: setValue,
292        setDisabled: setDisabled,
293        close: close,
294        findOption: findOption,
295        GROUPS: CUSTOM_CURSOR_GROUPS,
296        NEW_CURSOR_VALUES: NEW_CURSOR_VALUES
297    };
298})(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.