1(function () { 2 'use strict'; 3 4 var STORAGE_KEY = 'spacewaves_favorites'; 5 var HOME_SLUG = 'spacewaves-home'; 6 var HOME_THUMB = 'https://cdn.spacewaves2.net/images/spacewaves.webp'; 7 var menu = document.getElementById('game-favorites-menu'); 8 var trigger = document.getElementById('game-favorites-trigger'); 9 var dropdown = document.getElementById('game-favorites-dropdown'); 10 var badge = document.getElementById('game-favorites-badge'); 11 var closeTimer = null; 12 var toastTimer = null; 13 var toastEl = null; 14 var reorderBusy = false; 15 var hoverCloseTimer = null; 16 17 function read() { 18 try { 19 var raw = localStorage.getItem(STORAGE_KEY); 20 if (!raw) return []; 21 var data = JSON.parse(raw); 22 return Array.isArray(data) ? data : []; 23 } catch (e) { 24 return []; 25 } 26 } 27 28 function write(list) { 29 try { 30 localStorage.setItem(STORAGE_KEY, JSON.stringify(list)); 31 } catch (e) { /* ignore */ } 32 syncTriggerBadge(); 33 document.dispatchEvent(new CustomEvent('spacewaves-favorites-change')); 34 } 35 36 function normalizeItem(item) { 37 if (!item || !item.slug) return null; 38 return { 39 slug: String(item.slug), 40 name: String(item.name || item.slug), 41 url: String(item.url || '/'), 42 image: item.image ? String(item.image) : '', 43 addedAt: item.addedAt || Date.now() 44 }; 45 } 46 47 function isFavorite(slug) { 48 if (!slug) return false; 49 return read().some(function (g) { return g.slug === slug; }); 50 } 51 52 function toggle(item) { 53 var norm = normalizeItem(item); 54 if (!norm) return false; 55 var list = read(); 56 var idx = list.findIndex(function (g) { return g.slug === norm.slug; }); 57 if (idx >= 0) { 58 list.splice(idx, 1); 59 write(list); 60 showToast(menu && menu.getAttribute('data-removed-label')); 61 return false; 62 } 63 list.push(norm); 64 write(list); 65 showToast(menu && menu.getAttribute('data-added-label')); 66 return true; 67 } 68 69 function remove(slug, silent) { 70 if (!slug) return; 71 write(read().filter(function (g) { return g.slug !== slug; })); 72 if (!silent) { 73 showToast(menu && menu.getAttribute('data-removed-label')); 74 } 75 } 76 77 function move(slug, delta) { 78 var list = read(); 79 var idx = list.findIndex(function (g) { return g.slug === slug; }); 80 if (idx < 0) return false; 81 var next = idx + delta; 82 if (next < 0 || next >= list.length) return false; 83 var tmp = list[idx]; 84 list[idx] = list[next]; 85 list[next] = tmp; 86 write(list); 87 return true; 88 } 89 90 function syncMoveButtons(listWrap) { 91 if (!listWrap) return; 92 var rows = listWrap.querySelectorAll('.game-favorites__item'); 93 rows.forEach(function (row, index) { 94 var upBtn = row.querySelector('.fav-list__act--up'); 95 if (upBtn) upBtn.disabled = index === 0; 96 }); 97 } 98 99 /** Swap two rows with FLIP so the panel does not look like a full refresh. */ 100 function animateReorder(slug, delta) { 101 if (reorderBusy || !dropdown || delta !== -1) { 102 if (move(slug, delta) && isDropdownOpen()) renderDropdown({ skipEnter: true }); 103 return; 104 } 105 var listWrap = dropdown.querySelector('.game-favorites__list'); 106 if (!listWrap) { 107 if (move(slug, delta)) renderDropdown({ skipEnter: true }); 108 return; 109 } 110 listWrap.classList.remove('is-entering'); 111 112 var rows = Array.prototype.slice.call(listWrap.children); 113 var idx = -1; 114 for (var i = 0; i < rows.length; i++) { 115 if (rows[i].getAttribute('data-slug') === slug) { 116 idx = i; 117 break; 118 } 119 } 120 if (idx <= 0) return; 121 122 var moving = rows[idx]; 123 var above = rows[idx - 1]; 124 var firstMoving = moving.getBoundingClientRect(); 125 var firstAbove = above.getBoundingClientRect(); 126 127 if (!move(slug, delta)) return; 128 129 listWrap.insertBefore(moving, above); 130 syncMoveButtons(listWrap); 131 132 var lastMoving = moving.getBoundingClientRect(); 133 var lastAbove = above.getBoundingClientRect(); 134 var dyMoving = firstMoving.top - lastMoving.top; 135 var dyAbove = firstAbove.top - lastAbove.top; 136 137 if (Math.abs(dyMoving) < 1 && Math.abs(dyAbove) < 1) return; 138 139 reorderBusy = true; 140 listWrap.classList.add('game-favorites__list--reordering'); 141 moving.classList.add('is-swapping'); 142 above.classList.add('is-swapping'); 143
144 moving.style.transition = 'none'; 145 above.style.transition = 'none'; 146 moving.style.transform = 'translate3d(0,' + dyMoving + 'px,0)'; 147 above.style.transform = 'translate3d(0,' + dyAbove + 'px,0)'; 148 149 requestAnimationFrame(function () { 150 requestAnimationFrame(function () { 151 var ease = 'transform .28s cubic-bezier(.22,.7,.25,1)'; 152 moving.style.transition = ease; 153 above.style.transition = ease; 154 moving.style.transform = 'translate3d(0,0,0)'; 155 above.style.transform = 'translate3d(0,0,0)'; 156 157 var done = false; 158 function finish() { 159 if (done) return; 160 done = true; 161 moving.style.transition = 'none'; 162 above.style.transition = 'none'; 163 moving.style.transform = ''; 164 above.style.transform = ''; 165 moving.classList.remove('is-swapping'); 166 above.classList.remove('is-swapping'); 167 listWrap.classList.remove('game-favorites__list--reordering'); 168 /* Force reflow once with overflow still stable before unlocking. */ 169 void listWrap.offsetHeight; 170 reorderBusy = false; 171 } 172 moving.addEventListener('transitionend', function onEnd(e) { 173 if (e.propertyName && e.propertyName !== 'transform') return; 174 moving.removeEventListener('transitionend', onEnd); 175 finish(); 176 }); 177 setTimeout(finish, 360); 178 }); 179 }); 180 } 181 182 function thumbFor(item) { 183 if (!item) return ''; 184 if (item.slug === HOME_SLUG) { 185 var homeThumb = (menu && menu.getAttribute('data-home-thumb')) || item.image || HOME_THUMB; 186 if (!homeThumb || /\/images\/ico\.webp(?:\?|$)/.test(homeThumb)) { 187 return HOME_THUMB; 188 } 189 return homeThumb; 190 } 191 if (item.image) return item.image; 192 if (item.url && /\/geometry-games\//.test(item.url)) { 193 return '/geometry-games/' + encodeURIComponent(item.slug) + '/200x200.webp'; 194 } 195 if (item.slug) return '/wave-games/' + encodeURIComponent(item.slug) + '/200x200.webp'; 196 return ''; 197 } 198 199 function syncTriggerBadge() { 200 if (!badge) return; 201 var n = read().length; 202 if (n > 0) { 203 badge.textContent = n > 99 ? '99+' : String(n); 204 badge.hidden = false; 205 badge.setAttribute('aria-hidden', 'false'); 206 if (trigger) trigger.classList.add('game-favorites__trigger--has-items'); 207 } else { 208 badge.hidden = true; 209 badge.setAttribute('aria-hidden', 'true'); 210 if (trigger) trigger.classList.remove('game-favorites__trigger--has-items'); 211 } 212 } 213 214 function isDropdownOpen() { 215 return !!(trigger && trigger.classList.contains('game-favorites__trigger--open')); 216 } 217 218 function positionDropdownMobile() { 219 if (!dropdown) return; 220 if (window.innerWidth > 900) { 221 dropdown.style.top = ''; 222 dropdown.style.left = ''; 223 dropdown.style.right = ''; 224 dropdown.style.width = ''; 225 return; 226 } 227 var header = document.querySelector('.site-header, header'); 228 var top = 64; 229 if (header) { 230 top = Math.round(header.getBoundingClientRect().bottom + 8); 231 } 232 dropdown.style.top = top + 'px'; 233 dropdown.style.left = '12px'; 234 dropdown.style.right = '12px'; 235 dropdown.style.width = 'auto'; 236 } 237 238 function isHoverDevice() { 239 try { 240 return window.matchMedia('(hover: hover) and (pointer: fine)').matches; 241 } catch (e) { 242 return true; 243 } 244 } 245 246 function clearHoverClose() { 247 if (hoverCloseTimer) { 248 clearTimeout(hoverCloseTimer); 249 hoverCloseTimer = null; 250 } 251 } 252 253 function scheduleHoverClose() { 254 clearHoverClose(); 255 hoverCloseTimer = setTimeout(function () { 256 hoverCloseTimer = null; 257 if (isHoverDevice()) closeDropdown(); 258 }, 160); 259 } 260 261 function setDropdownOpen(open) { 262 if (!dropdown || !trigger) return; 263 if (closeTimer) { 264 clearTimeout(closeTimer); 265 closeTimer = null; 266 } 267 if (open) { 268 clearHoverClose(); 269 if (window.SpaceWavesSearchClose) window.SpaceWavesSearchClose(); 270 dropdown.removeAttribute('hidden'); 271 trigger.classList.add('game-favorites__trigger--open'); 272 trigger.setAttribute('aria-expanded', 'true'); 273 renderDropdown(); 274 positionDropdownMobile(); 275 requestAnimationFrame(function () { 276 requestAnimationFrame(function () { 277 dropdown.classList.add('game-favorites__dropdown--open'); 278 }); 279 }); 280 } else { 281 dropdown.classList.remove('game-favorites__dropdown--open'); 282 trigger.classList.remove('game-favorites__trigger--open'); 283 trigger.setAttribute('aria-expanded', 'false'); 284 closeTimer = setTimeout(function () { 285 if (!isDropdownOpen()) { 286 dropdown.setAttribute('hidden', ''); 287 } 288 closeTimer = null; 289 }, 240); 290 } 291 } 292 293 function closeDropdown() { 294 setDropdownOpen(false); 295 } 296 297 function svgHeartOutline() { 298 var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 299 svg.setAttribute('viewBox', '0 0 24 24'); 300 svg.setAttribute('aria-hidden', 'true'); 301 var path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 302 path.setAttribute('fill', 'none'); 303 path.setAttribute('stroke', 'currentColor'); 304 path.setAttribute('stroke-width', '2'); 305 path.setAttribute('d', 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'); 306 svg.appendChild(path); 307 return svg; 308 } 309 310 function svgIconUp() { 311 var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 312 svg.setAttribute('viewBox', '0 0 24 24'); 313 svg.setAttribute('width', '16'); 314 svg.setAttribute('height', '16'); 315 svg.setAttribute('aria-hidden', 'true'); 316 var path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 317 path.setAttribute('fill', 'currentColor');
318 path.setAttribute('d', 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'); 319 svg.appendChild(path); 320 return svg; 321 } 322 323 function svgIconTrash() { 324 var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 325 svg.setAttribute('viewBox', '0 0 24 24'); 326 svg.setAttribute('width', '15'); 327 svg.setAttribute('height', '15'); 328 svg.setAttribute('fill', 'none'); 329 svg.setAttribute('stroke', 'currentColor'); 330 svg.setAttribute('stroke-width', '2'); 331 svg.setAttribute('stroke-linecap', 'round'); 332 svg.setAttribute('stroke-linejoin', 'round'); 333 svg.setAttribute('aria-hidden', 'true'); 334 function line(x1, y1, x2, y2) { 335 var el = document.createElementNS('http://www.w3.org/2000/svg', 'line'); 336 el.setAttribute('x1', x1); 337 el.setAttribute('y1', y1); 338 el.setAttribute('x2', x2); 339 el.setAttribute('y2', y2); 340 return el; 341 } 342 var path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 343 path.setAttribute('d', 'M3 6h18'); 344 var path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 345 path2.setAttribute('d', 'M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2'); 346 var path3 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 347 path3.setAttribute('d', 'M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6'); 348 svg.appendChild(path); 349 svg.appendChild(path2); 350 svg.appendChild(path3); 351 svg.appendChild(line(10, 11, 10, 17)); 352 svg.appendChild(line(14, 11, 14, 17)); 353 return svg; 354 } 355 356 function labelAttr(name, fallback) { 357 var value = menu && menu.getAttribute(name); 358 return (value && value.trim()) || fallback; 359 } 360 361 function siteHost() { 362 return (location.hostname || '').replace(/^www\./i, '') || 'spacewaves2.net'; 363 } 364 365 /* Inline SVG on purpose: no icon font or icon-only stylesheet on this site. */ 366 function installDesktopSvg() { 367 return ( 368 '<svg class="fav-menu-actions__icon fav-menu-actions__icon--install" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false">' + 369 '<path fill="currentColor" d="M20 17H4V5h8V3H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h4v2h8v-2h4a2 2 0 0 0 2-2v-5h-2v5zM17 14l5-5-1.41-1.41L18 10.17V3h-2v7.17l-2.59-2.58L12 9l5 5z"/>' + 370 '</svg>' 371 ); 372 } 373 374 function googleGSvg() { 375 return ( 376 '<svg class="fav-menu-actions__icon fav-menu-actions__icon--google" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false">' + 377 '<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>' + 378 '<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>' + 379 '<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>' + 380 '<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>' + 381 '</svg>' 382 ); 383 } 384 385 /* Only the installable check â never the banner's 30-day dismiss key. */ 386 function canShowInstallAction() { 387 if (typeof window.SpaceWavesPwaCanInstall === 'function') return window.SpaceWavesPwaCanInstall(); 388 /* Before the idle PWA script loads: honor the early-captured BIP from the bootstrap */ 389 try { 390 if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true) { 391 return false; 392 } 393 } catch (e) {} 394 return !!window.__SPACEWAVES_PWA_DEFERRED; 395 } 396 397 function syncInstallActionVisibility() { 398 var installBtn = document.getElementById('fav-action-install'); 399 if (installBtn) installBtn.hidden = !canShowInstallAction(); 400 } 401 402 var installActionBound = false; 403 function bindInstallActionEvents() { 404 if (installActionBound) return; 405 installActionBound = true; 406 document.addEventListener('spacewaves-pwa-installable', syncInstallActionVisibility); 407 document.addEventListener('spacewaves-pwa-installed', syncInstallActionVisibility); 408 syncInstallActionVisibility(); 409 } 410 411 /* The install script itself loads at idle, so listen after it has had a chance 412 to run instead of hooking beforeinstallprompt here. */ 413 function scheduleInstallActionBind() { 414 if ('requestIdleCallback' in window) { 415 requestIdleCallback(bindInstallActionEvents, { timeout: 4000 }); 416 } else { 417 setTimeout(bindInstallActionEvents, 2000); 418 } 419 } 420 421 function triggerPwaInstall() {
422 if (!canShowInstallAction()) return; 423 if (typeof window.SpaceWavesPwaInstall === 'function') { 424 window.SpaceWavesPwaInstall(); 425 closeDropdown(); 426 return; 427 } 428 /* PWA script not ready yet â wait briefly then retry */ 429 var tries = 0; 430 var timer = setInterval(function () { 431 tries += 1; 432 if (typeof window.SpaceWavesPwaInstall === 'function') { 433 clearInterval(timer); 434 window.SpaceWavesPwaInstall(); 435 closeDropdown(); 436 } else if (tries >= 20) { 437 clearInterval(timer); 438 } 439 }, 100); 440 } 441 442 /* A temporary anchor is the only reliable way to keep rel on a new tab. */ 443 function openGooglePreferred() { 444 var a = document.createElement('a'); 445 a.href = 'https://www.google.com/preferences/source?q=' + encodeURIComponent(siteHost()); 446 a.target = '_blank'; 447 a.rel = 'nofollow noreferrer noopener'; 448 a.style.display = 'none'; 449 document.body.appendChild(a); 450 a.click(); 451 a.remove(); 452 closeDropdown(); 453 } 454 455 function buildActionsFooter() { 456 var footer = document.createElement('div'); 457 footer.className = 'fav-menu-actions'; 458 footer.setAttribute('role', 'group'); 459 footer.setAttribute('aria-label', labelAttr('data-quick-actions-label', 'Quick actions')); 460 461 var installBtn = document.createElement('button'); 462 installBtn.type = 'button'; 463 installBtn.id = 'fav-action-install'; 464 installBtn.className = 'fav-menu-actions__btn'; 465 installBtn.innerHTML = installDesktopSvg() + '<span class="fav-menu-actions__label"></span>'; 466 installBtn.querySelector('.fav-menu-actions__label').textContent = labelAttr('data-install-app-label', 'Install to Desktop'); 467 installBtn.hidden = true; 468 installBtn.addEventListener('click', function (e) { 469 e.preventDefault(); 470 e.stopPropagation(); 471 triggerPwaInstall(); 472 }); 473 474 var googleBtn = document.createElement('button'); 475 googleBtn.type = 'button'; 476 googleBtn.id = 'fav-action-google'; 477 googleBtn.className = 'fav-menu-actions__btn'; 478 googleBtn.innerHTML = googleGSvg() + '<span class="fav-menu-actions__label"></span>'; 479 googleBtn.querySelector('.fav-menu-actions__label').textContent = labelAttr('data-add-preferred-google-label', 'Add preferred on Google'); 480 googleBtn.addEventListener('click', function (e) { 481 e.preventDefault(); 482 e.stopPropagation(); 483 openGooglePreferred(); 484 }); 485 486 footer.appendChild(installBtn); 487 footer.appendChild(googleBtn); 488 return footer; 489 } 490 491 /* The footer must survive every re-render, so drop only the rendered rows 492 instead of wiping the whole dropdown. */ 493 function clearRenderedList() { 494 if (!dropdown) return; 495 var stale = dropdown.querySelectorAll('.game-favorites__panel-head,.game-favorites__list,.game-favorites__empty-state'); 496 Array.prototype.forEach.call(stale, function (node) { node.remove(); }); 497 } 498 499 function ensureActionsFooter() { 500 if (!dropdown) return null; 501 var footer = dropdown.querySelector('.fav-menu-actions'); 502 if (!footer) { 503 footer = buildActionsFooter(); 504 dropdown.appendChild(footer); 505 syncInstallActionVisibility(); 506 } 507 return footer; 508 } 509 510 function buildPanelHead(count) { 511 var head = document.createElement('div'); 512 head.className = 'game-favorites__panel-head'; 513 var title = document.createElement('span'); 514 title.className = 'game-favorites__panel-title'; 515 title.textContent = (menu && menu.getAttribute('data-panel-label')) || 'My favorites'; 516 var countEl = document.createElement('span'); 517 countEl.className = 'game-favorites__panel-count'; 518 countEl.textContent = String(count); 519 head.appendChild(title); 520 head.appendChild(countEl); 521 return head; 522 } 523 524 function buildEmptyState() { 525 var wrap = document.createElement('div'); 526 wrap.className = 'game-favorites__empty-state'; 527 wrap.setAttribute('role', 'status'); 528 var iconWrap = document.createElement('div'); 529 iconWrap.className = 'game-favorites__empty-icon'; 530 iconWrap.appendChild(svgHeartOutline()); 531 var title = document.createElement('p'); 532 title.className = 'game-favorites__empty-title'; 533 title.textContent = (menu && menu.getAttribute('data-empty-label')) || 'No favorites yet'; 534 var hint = document.createElement('p'); 535 hint.className = 'game-favorites__empty-hint';
536 hint.textContent = (menu && menu.getAttribute('data-empty-hint')) || 'Tap the heart under any game player to save it here.'; 537 wrap.appendChild(iconWrap); 538 wrap.appendChild(title); 539 wrap.appendChild(hint); 540 return wrap; 541 } 542 543 function renderDropdown(opts) { 544 if (!dropdown || !menu) return; 545 opts = opts || {}; 546 var list = read(); 547 var removeLabel = menu.getAttribute('data-remove-label') || 'Remove'; 548 var moveUpLabel = menu.getAttribute('data-move-up-label') || 'Move up'; 549 var withEnter = !opts.skipEnter; 550 var footer = ensureActionsFooter(); 551 clearRenderedList(); 552 553 if (!list.length) { 554 dropdown.insertBefore(buildEmptyState(), footer); 555 return; 556 } 557 558 dropdown.insertBefore(buildPanelHead(list.length), footer); 559 var listWrap = document.createElement('div'); 560 listWrap.className = 'game-favorites__list' + (withEnter ? ' is-entering' : ''); 561 var frag = document.createDocumentFragment(); 562 563 list.forEach(function (item, index) { 564 var row = document.createElement('div'); 565 row.className = 'game-favorites__item fav-list__item'; 566 row.setAttribute('data-slug', item.slug); 567 row.setAttribute('role', 'option'); 568 if (withEnter) { 569 row.style.animationDelay = (index * 0.04) + 's'; 570 } 571 572 var link = document.createElement('a'); 573 link.className = 'game-favorites__item-link nav-search__opt'; 574 link.href = item.url; 575 link.title = item.name; 576 577 var img = document.createElement('img'); 578 img.src = thumbFor(item); 579 img.alt = ''; 580 img.width = 28; 581 img.height = 28; 582 img.decoding = 'async'; 583 img.onerror = function () { 584 if (item.slug === HOME_SLUG && img.getAttribute('src') !== HOME_THUMB) { 585 img.src = HOME_THUMB; 586 return; 587 } 588 img.removeAttribute('src'); 589 img.style.display = 'none'; 590 }; 591 592 var title = document.createElement('span'); 593 title.textContent = item.name; 594 link.appendChild(img); 595 link.appendChild(title); 596 link.addEventListener('click', function () { closeDropdown(); }); 597 598 var actions = document.createElement('div'); 599 actions.className = 'fav-list__actions'; 600 601 var upBtn = document.createElement('button'); 602 upBtn.type = 'button'; 603 upBtn.className = 'fav-list__act fav-list__act--up'; 604 upBtn.setAttribute('aria-label', moveUpLabel); 605 upBtn.disabled = index === 0; 606 upBtn.appendChild(svgIconUp()); 607 upBtn.addEventListener('click', function (e) { 608 e.preventDefault(); 609 e.stopPropagation(); 610 upBtn.blur(); 611 animateReorder(item.slug, -1); 612 }); 613 614 var rmBtn = document.createElement('button'); 615 rmBtn.type = 'button'; 616 rmBtn.className = 'fav-list__act fav-list__act--remove'; 617 rmBtn.setAttribute('aria-label', removeLabel); 618 rmBtn.appendChild(svgIconTrash()); 619 rmBtn.addEventListener('click', function (e) { 620 e.preventDefault(); 621 e.stopPropagation(); 622 if (reorderBusy) return; 623 row.classList.add('is-removing'); 624 setTimeout(function () { 625 remove(item.slug, false); 626 renderDropdown({ skipEnter: true }); 627 syncFavoriteButtons(); 628 }, 200); 629 }); 630 631 actions.appendChild(upBtn); 632 actions.appendChild(rmBtn); 633 row.appendChild(link); 634 row.appendChild(actions); 635 frag.appendChild(row); 636 }); 637 638 listWrap.appendChild(frag); 639 dropdown.insertBefore(listWrap, footer); 640 641 if (withEnter) { 642 var enterMs = 280 + Math.max(0, list.length - 1) * 40 + 40; 643 setTimeout(function () { 644 if (listWrap.parentNode) listWrap.classList.remove('is-entering'); 645 }, enterMs); 646 } 647 } 648 649 function showToast(message) { 650 if (!message) return; 651 if (!toastEl) { 652 toastEl = document.createElement('div'); 653 toastEl.className = 'game-favorites-toast'; 654 toastEl.setAttribute('role', 'status'); 655 toastEl.setAttribute('aria-live', 'polite'); 656 var icon = document.createElement('span'); 657 icon.className = 'game-favorites-toast__icon'; 658 icon.appendChild(svgHeartOutline()); 659 var text = document.createElement('span'); 660 text.className = 'game-favorites-toast__text'; 661 toastEl.appendChild(icon); 662 toastEl.appendChild(text); 663 document.body.appendChild(toastEl); 664 } 665 var textEl = toastEl.querySelector('.game-favorites-toast__text'); 666 if (textEl) textEl.textContent = message; 667 toastEl.classList.add('game-favorites-toast--show'); 668 if (toastTimer) clearTimeout(toastTimer); 669 toastTimer = setTimeout(function () { 670 if (toastEl) toastEl.classList.remove('game-favorites-toast--show'); 671 toastTimer = null; 672 }, 2200); 673 } 674 675 function syncFavoriteButtons() { 676 document.querySelectorAll('.game-favorite-btn[data-favorite-slug]').forEach(function (btn) { 677 var slug = btn.getAttribute('data-favorite-slug'); 678 var on = isFavorite(slug); 679 btn.classList.toggle('is-favorited', on); 680 btn.setAttribute('aria-pressed', on ? 'true' : 'false'); 681 var label = btn.querySelector('.game-favorite-btn__label'); 682 if (label) { 683 label.textContent = on 684 ? (btn.getAttribute('data-label-remove') || 'favorited') 685 : (btn.getAttribute('data-label-add') || 'favorite'); 686 } 687 var hint = on 688 ? (btn.getAttribute('data-label-remove-hint') || btn.getAttribute('data-label-remove') || '') 689 : (btn.getAttribute('data-label-add-hint') || btn.getAttribute('data-label-add') || ''); 690 if (hint) { 691 btn.setAttribute('title', hint); 692 btn.setAttribute('aria-label', hint); 693 } 694 }); 695 } 696 697 function bindFavoriteButtons() { 698 document.querySelectorAll('.game-favorite-btn[data-favorite-slug]').forEach(function (btn) { 699 if (btn._swFavBound) return; 700 btn._swFavBound = true; 701 btn.addEventListener('click', function () { 702 var item = { 703 slug: btn.getAttribute('data-favorite-slug'), 704 name: btn.getAttribute('data-favorite-name'), 705 url: btn.getAttribute('data-favorite-url'), 706 image: btn.getAttribute('data-favorite-image') || '' 707 }; 708 toggle(item); 709 syncFavoriteButtons(); 710 if (isDropdownOpen()) renderDropdown(); 711 }); 712 }); 713 syncFavoriteButtons(); 714 } 715 716 if (trigger && dropdown && menu) { 717 /* Desktop: open on hover. Mobile: open on tap. */ 718 menu.addEventListener('mouseenter', function () {
719 if (!isHoverDevice()) return; 720 clearHoverClose(); 721 setDropdownOpen(true); 722 }); 723 menu.addEventListener('mouseleave', function () { 724 if (!isHoverDevice()) return; 725 scheduleHoverClose(); 726 }); 727 728 trigger.addEventListener('click', function (e) { 729 e.stopPropagation(); 730 if (isHoverDevice()) { 731 /* Already opened by hover; keep open, don't toggle shut. */ 732 clearHoverClose(); 733 if (!isDropdownOpen()) setDropdownOpen(true); 734 return; 735 } 736 setDropdownOpen(!isDropdownOpen()); 737 }); 738 739 document.addEventListener('mousedown', function (e) { 740 if (!menu || menu.contains(e.target)) return; 741 closeDropdown(); 742 }); 743 document.addEventListener('touchstart', function (e) { 744 if (!menu || menu.contains(e.target)) return; 745 closeDropdown(); 746 }, { passive: true }); 747 var menuToggle = document.getElementById('nav-toggle'); 748 if (menuToggle) { 749 menuToggle.addEventListener('click', function () { 750 closeDropdown(); 751 }); 752 } 753 document.addEventListener('keydown', function (e) { 754 if (e.key === 'Escape') closeDropdown(); 755 }); 756 window.addEventListener('resize', function () { 757 if (isDropdownOpen()) positionDropdownMobile(); 758 }); 759 } 760 761 document.addEventListener('spacewaves-favorites-change', syncFavoriteButtons); 762 763 function init() { 764 syncTriggerBadge(); 765 bindFavoriteButtons(); 766 scheduleInstallActionBind(); 767 } 768 769 if (document.readyState === 'loading') { 770 document.addEventListener('DOMContentLoaded', init); 771 } else { 772 init(); 773 } 774 775 window.SpaceWavesFavorites = { 776 read: read, 777 toggle: toggle, 778 remove: remove, 779 move: move, 780 isFavorite: isFavorite, 781 closeDropdown: closeDropdown 782 }; 783})();
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.