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.