1(function() { 2 'use strict'; 3 4 // Theme toggle 5 var themeToggle = document.getElementById('themeToggle'); 6 var iconLight = document.getElementById('themeIconLight'); 7 var iconDark = document.getElementById('themeIconDark'); 8 var html = document.documentElement; 9 10 function getEffectiveTheme() { 11 var theme = html.getAttribute('data-theme'); 12 if (theme === 'dark') return 'dark'; 13 if (theme === 'light') return 'light'; 14 // auto â check system preference 15 return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 16 } 17 18 function updateThemeIcons() { 19 var effective = getEffectiveTheme(); 20 // Show sun icon when dark (click to go light), moon when light (click to go dark) 21 iconLight.style.display = effective === 'dark' ? 'inline' : 'none'; 22 iconDark.style.display = effective === 'light' ? 'inline' : 'none'; 23 } 24 25 // Restore saved preference 26 var saved = localStorage.getItem('mkt_theme'); 27 if (saved) { 28 html.setAttribute('data-theme', saved); 29 } 30 updateThemeIcons(); 31 32 if (themeToggle) { 33 themeToggle.addEventListener('click', function() { 34 var effective = getEffectiveTheme(); 35 var next = effective === 'dark' ? 'light' : 'dark'; 36 html.setAttribute('data-theme', next); 37 localStorage.setItem('mkt_theme', next); 38 updateThemeIcons(); 39 }); 40 } 41 42 // Listen for system theme changes when in auto mode 43 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() { 44 if (html.getAttribute('data-theme') === 'auto') { 45 updateThemeIcons(); 46 } 47 }); 48 49 // Favorite / Save toggle â persists to the server (no login required; anon 50 // saves are attributed to a first-party cookie the endpoint sets). 51 var favBtn = document.getElementById('favoriteBtn'); 52 if (favBtn) { 53 var favListingId = parseInt(favBtn.getAttribute('data-listing-id'), 10) || 0; 54 55 var paintFav = function(saved) { 56 favBtn.innerHTML = '<i class="fa' + (saved ? 's' : 'r') + ' fa-heart"></i> ' + (saved ? 'Saved' : 'Save'); 57 }; 58 59 favBtn.addEventListener('click', function() { 60 if (!favListingId || favBtn.dataset.busy === '1') return; 61 favBtn.dataset.busy = '1'; 62 var wasSaved = favBtn.querySelector('i').classList.contains('fas'); 63 paintFav(!wasSaved); // optimistic 64 65 fetch('/api/marketplace/favorite', { 66 method: 'POST', 67 headers: { 'Content-Type': 'application/json' }, 68 credentials: 'same-origin', 69 body: JSON.stringify({ listing_id: favListingId }) 70 }) 71 .then(function(r) { return r.json(); }) 72 .then(function(d) { 73 if (!d || !d.success) { paintFav(wasSaved); return; } // revert on failure 74 paintFav(d.saved); 75 }) 76 .catch(function() { paintFav(wasSaved); }) 77 .finally(function() { favBtn.dataset.busy = '0'; }); 78 }); 79 } 80 81 // Alert subscription 82 var alertBtn = document.getElementById('alertBtn'); 83 if (alertBtn) { 84 alertBtn.addEventListener('click', function() { 85 // TODO: POST to /api/marketplace/price_alert.php 86 alert('Price alert subscription coming soon.'); 87 }); 88 } 89 90 // Buy Now â starts Stripe Checkout. Guest-friendly: no login required. 91 // If the buyer is on a parked seller domain (non-canonical host), that 92 // host doesn't serve the /api endpoints, so we redirect to the canonical 93 // listing URL with autobuy=1 and auto-fire the flow once it loads. 94 function startBuyNowCheckout(triggerBtn, channel) { 95 if (!priceCard) return; 96 97 if (priceCard.dataset.buyNowEnabled !== '1') { 98 alert('Buy Now is not available â the seller has not completed payout setup.'); 99 return; 100 } 101 102 var canonicalUrl = priceCard.dataset.canonicalUrl; 103 try { 104 if (canonicalUrl && new URL(canonicalUrl).host !== window.location.host) { 105 window.location = canonicalUrl + (canonicalUrl.indexOf('?') === -1 ? '?' : '&') + 'autobuy=1'; 106 return; 107 } 108 } catch (e) { /* ignore parse errors */ } 109 110 var listingId = parseInt(priceCard.dataset.listingId, 10); 111 if (!listingId) return; 112 113 var origLabel = triggerBtn ? triggerBtn.innerHTML : ''; 114 if (triggerBtn) { 115 triggerBtn.disabled = true; 116 triggerBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Redirecting...'; 117 } 118 119 fetch('/api/marketplace/checkout', { 120 method: 'POST', 121 headers: { 'Content-Type': 'application/json' }, 122 body: JSON.stringify({ listing_id: listingId, channel: channel || 'card' }) 123 }).then(function(r) { 124 return r.json().then(function(data) { return { ok: r.ok, data: data }; }); 125 }).then(function(res) { 126 if (res.ok && res.data.success && res.data.checkout_url) { 127 window.location = res.data.checkout_url; 128 return; 129 } 130 alert(res.data && res.data.error ? res.data.error : 'Could not start checkout. Please try again.'); 131 if (triggerBtn) { 132 triggerBtn.disabled = false;
133 triggerBtn.innerHTML = origLabel; 134 } 135 }).catch(function() { 136 alert('Network error. Please try again.'); 137 if (triggerBtn) { 138 triggerBtn.disabled = false; 139 triggerBtn.innerHTML = origLabel; 140 } 141 }); 142 } 143 144 var buyBtn = document.getElementById('buyNowBtn'); 145 if (buyBtn && !buyBtn.disabled) { 146 buyBtn.addEventListener('click', function() { startBuyNowCheckout(buyBtn, 'card'); }); 147 } 148 149 // Alipay (zh pages): familiar rail for Chinese buyers 150 var alipayBtn = document.getElementById('payAlipayBtn'); 151 if (alipayBtn && !alipayBtn.disabled) { 152 alipayBtn.addEventListener('click', function() { startBuyNowCheckout(alipayBtn, 'alipay'); }); 153 } 154 155 // Crypto/stablecoins: instant, no surcharge 156 var cryptoBtn = document.getElementById('payCryptoBtn'); 157 if (cryptoBtn && !cryptoBtn.disabled) { 158 cryptoBtn.addEventListener('click', function() { startBuyNowCheckout(cryptoBtn, 'crypto'); }); 159 } 160 161 // Bitcoin (BTCPay on our side): our own invoice page. Login required - a verified email is the only 162 // identity we ask of a Bitcoin buyer - so a signed-out click goes to login and back to this listing. 163 function startBtcCheckout(triggerBtn) { 164 if (!priceCard) return; 165 var canonicalUrl = priceCard.dataset.canonicalUrl; 166 try { 167 if (canonicalUrl && new URL(canonicalUrl).host !== window.location.host) { 168 window.location = canonicalUrl + (canonicalUrl.indexOf('?') === -1 ? '?' : '&') + 'autobtc=1'; 169 return; 170 } 171 } catch (e) { /* ignore */ } 172 var listingId = parseInt(priceCard.dataset.listingId, 10); 173 if (!listingId) return; 174 var origLabel = triggerBtn ? triggerBtn.innerHTML : ''; 175 if (triggerBtn) { triggerBtn.disabled = true; triggerBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating invoice...'; } 176 fetch('/api/marketplace/btc-checkout', { 177 method: 'POST', 178 headers: { 'Content-Type': 'application/json' }, 179 body: JSON.stringify({ listing_id: listingId }) 180 }).then(function(r) { 181 return r.json().then(function(data) { return { ok: r.ok, data: data }; }); 182 }).then(function(res) { 183 if (res.ok && res.data.success && res.data.pay_url) { window.location = res.data.pay_url; return; } 184 if (res.data && res.data.login_required) { 185 window.location = 'https://appraise.net/login?redirect=' + encodeURIComponent(window.location.pathname + '?autobtc=1'); 186 return; 187 } 188 alert(res.data && res.data.error ? res.data.error : 'Could not start Bitcoin checkout. Please try again.'); 189 if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; } 190 }).catch(function() { 191 alert('Network error. Please try again.'); 192 if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; } 193 }); 194 } 195 var btcBtn = document.getElementById('payBtcBtn'); 196 if (btcBtn && !btcBtn.disabled) { 197 btcBtn.addEventListener('click', function() { startBtcCheckout(btcBtn); }); 198 if (/[?&]autobtc=1/.test(window.location.search)) { startBtcCheckout(btcBtn); } 199 } 200 201 // US bank debit (ACH): no card surcharge; settles in a few business days 202 var bankBtn = document.getElementById('payBankBtn'); 203 if (bankBtn && !bankBtn.disabled) { 204 bankBtn.addEventListener('click', function() { startBuyNowCheckout(bankBtn, 'bank'); }); 205 } 206 207 // ---- Buy via Escrow.com ---- 208 // Delegates to the shared modal in /js/marketplace-escrow-modal.js, which 209 // collects the buyer's Escrow.com email (with pre-fill / localStorage), 210 // then POSTs to escrow-checkout.php and redirects to the agreement URL. 211 function startEscrowCheckout(triggerBtn) { 212 if (!priceCard) return; 213 214 // Non-canonical host redirect â same pattern as Stripe Buy Now. 215 var canonicalUrl = priceCard.dataset.canonicalUrl; 216 try { 217 if (canonicalUrl && new URL(canonicalUrl).host !== window.location.host) { 218 window.location = canonicalUrl + (canonicalUrl.indexOf('?') === -1 ? '?' : '&') + 'autoescrow=1'; 219 return; 220 } 221 } catch (e) { /* ignore */ } 222 223 var listingId = parseInt(priceCard.dataset.listingId, 10); 224 if (!listingId) return; 225 226 if (typeof window.openEscrowModal === 'function') { 227 window.openEscrowModal({ 228 mode: 'listing', 229 listingId: listingId, 230 triggerBtn: triggerBtn 231 }); 232 } else { 233 // Defensive fallback if the modal module wasn't loaded for some 234 // reason â should never hit in production. 235 alert('Escrow.com checkout is temporarily unavailable. Please try again or use card payment.'); 236 } 237 } 238 239 var escrowBtn = document.getElementById('buyEscrowBtn'); 240 if (escrowBtn && !escrowBtn.disabled) { 241 escrowBtn.addEventListener('click', function() { startEscrowCheckout(escrowBtn); }); 242 } 243 244 // ---- LTO Start Lease (mirrors Buy Now but hits lto-checkout.php) ---- 245 function startLtoCheckout(triggerBtn) { 246 if (!priceCard) return; 247 248 var canonicalUrl = priceCard.dataset.canonicalUrl; 249 try { 250 if (canonicalUrl && new URL(canonicalUrl).host !== window.location.host) { 251 window.location = canonicalUrl + (canonicalUrl.indexOf('?') === -1 ? '?' : '&') + 'autolto=1'; 252 return; 253 } 254 } catch (e) { /* ignore */ } 255 256 var listingId = parseInt(priceCard.dataset.listingId, 10); 257 if (!listingId) return; 258 259 var origLabel = triggerBtn ? triggerBtn.innerHTML : ''; 260 if (triggerBtn) { 261 triggerBtn.disabled = true; 262 triggerBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Redirecting...'; 263 } 264 265 fetch('/api/marketplace/lto-checkout', { 266 method: 'POST', 267 headers: { 'Content-Type': 'application/json' }, 268 body: JSON.stringify({ listing_id: listingId }) 269 }).then(function(r) { 270 return r.json().then(function(data) { return { ok: r.ok, data: data }; }); 271 }).then(function(res) { 272 if (res.ok && res.data.success && res.data.checkout_url) { 273 window.location = res.data.checkout_url; 274 return; 275 } 276 alert(res.data && res.data.error ? res.data.error : 'Could not start lease. Please try again.'); 277 if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; } 278 }).catch(function() { 279 alert('Network error. Please try again.'); 280 if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; } 281 }); 282 } 283 284 var ltoBtn = document.getElementById('ltoStartBtn'); 285 if (ltoBtn && !ltoBtn.disabled) { 286 ltoBtn.addEventListener('click', function() { startLtoCheckout(ltoBtn); }); 287 } 288 289 // Auto-fire flows when landed with ?autobuy=1 / ?autolto=1 (from a parked-domain handoff). 290 function stripParam(name) { 291 try { 292 var pattern = new RegExp('([?&])' + name + '=1&?'); 293 var cleanUrl = window.location.pathname + window.location.search.replace(pattern, '$1').replace(/[?&]$/, '') + window.location.hash; 294 window.history.replaceState({}, '', cleanUrl); 295 } catch (e) { /* ignore */ } 296 } 297 if (buyBtn && !buyBtn.disabled && /[?&]autobuy=1\b/.test(window.location.search)) {
298 stripParam('autobuy'); 299 startBuyNowCheckout(buyBtn); 300 } 301 if (ltoBtn && !ltoBtn.disabled && /[?&]autolto=1\b/.test(window.location.search)) { 302 stripParam('autolto'); 303 startLtoCheckout(ltoBtn); 304 } 305 306 // ---- Make Offer / Inquiry modal ---- 307 var priceCard = document.querySelector('[data-card="price"]'); 308 var offerModal = document.getElementById('offerModal'); 309 310 function openOfferModal(mode) { 311 if (!priceCard || !offerModal) return; 312 313 // If served on a non-canonical host (seller's parked domain via catchall), 314 // show a handoff interstitial before bouncing to appraise.net so the 315 // origin change isn't a surprise to the buyer. 316 var canonicalUrl = priceCard.dataset.canonicalUrl; 317 if (canonicalUrl) { 318 try { 319 var canonicalHost = new URL(canonicalUrl).host; 320 if (canonicalHost && canonicalHost !== window.location.host) { 321 openHandoffModal(canonicalUrl, mode); 322 return; 323 } 324 } catch (e) { /* ignore parse errors */ } 325 } 326 327 if (priceCard.dataset.isLoggedIn !== '1') { 328 var returnTo = (canonicalUrl || window.location.href); 329 window.location = 'https://appraise.net/login?redirect=' + encodeURIComponent(returnTo); 330 return; 331 } 332 333 var isInquiry = mode === 'inquiry'; 334 var domain = priceCard.dataset.domain || 'this domain'; 335 var amountField = document.getElementById('offerAmountField'); 336 var msgInput = document.getElementById('offerMessageInput'); 337 var amountInput = document.getElementById('offerAmountInput'); 338 339 document.getElementById('offerModalTitle').textContent = isInquiry ? 'Inquire About Price' : 'Make an Offer'; 340 document.getElementById('offerModalSub').textContent = isInquiry 341 ? 'Send a question to the seller of ' + domain + '.' 342 : 'Submit an offer for ' + domain + '. You choose how long it stays open.'; 343 document.getElementById('offerSubmitLabel').textContent = isInquiry ? 'Send Inquiry' : 'Send Offer'; 344 345 amountField.hidden = isInquiry; 346 var expiryField = document.getElementById('offerExpiryField'); 347 if (expiryField) expiryField.hidden = isInquiry; 348 amountInput.value = ''; 349 msgInput.value = ''; 350 if (isInquiry) { 351 msgInput.placeholder = 'What would you like to ask the seller?'; 352 } else { 353 msgInput.placeholder = 'Add a note for the seller...'; 354 } 355 356 var err = document.getElementById('offerModalError'); 357 err.hidden = true; err.textContent = ''; 358 359 offerModal.hidden = false; 360 offerModal.dataset.mode = mode; 361 setTimeout(function() { 362 (isInquiry ? msgInput : amountInput).focus(); 363 }, 50); 364 } 365 366 function closeOfferModal() { 367 if (offerModal) offerModal.hidden = true; 368 } 369 370 function submitOffer() { 371 if (!offerModal || !priceCard) return; 372 var mode = offerModal.dataset.mode; 373 var isInquiry = mode === 'inquiry'; 374 var listingId = parseInt(priceCard.dataset.listingId, 10); 375 var amountInput = document.getElementById('offerAmountInput'); 376 var msgInput = document.getElementById('offerMessageInput'); 377 var submitBtn = document.getElementById('offerSubmitBtn'); 378 var err = document.getElementById('offerModalError'); 379 err.hidden = true; err.textContent = ''; 380 381 var amount = isInquiry ? 0 : parseFloat(amountInput.value); 382 var message = msgInput.value.trim(); 383 384 if (!isInquiry && (!amount || amount <= 0)) { 385 err.textContent = 'Enter a valid offer amount.'; 386 err.hidden = false; 387 return; 388 } 389 var minOffer = parseFloat(priceCard.dataset.minOfferPrice) || 0; 390 if (!isInquiry && minOffer > 0 && amount < minOffer) { 391 err.textContent = 'Minimum offer is $' + minOffer.toLocaleString(); 392 err.hidden = false; 393 return; 394 } 395 if (isInquiry && !message) { 396 err.textContent = 'Please type your question for the seller.'; 397 err.hidden = false;
398 return; 399 } 400 401 submitBtn.disabled = true; 402 var origLabel = document.getElementById('offerSubmitLabel').textContent; 403 document.getElementById('offerSubmitLabel').textContent = 'Sending...'; 404 405 fetch('/api/marketplace/deal', { 406 method: 'POST', 407 headers: { 'Content-Type': 'application/json' }, 408 body: JSON.stringify({ 409 action: 'create_offer', 410 listing_id: listingId, 411 amount: amount, 412 message: message, 413 inquiry: isInquiry, 414 expiry_days: parseInt((document.getElementById('offerExpiryInput') || {}).value, 10) || 14 415 }) 416 }).then(function(r) { return r.json(); }).then(function(data) { 417 if (data.success && data.thread_token) { 418 window.location = '/marketplace/deal/' + data.thread_token; 419 } else { 420 err.textContent = data.error || 'Could not submit. Please try again.'; 421 err.hidden = false; 422 submitBtn.disabled = false; 423 document.getElementById('offerSubmitLabel').textContent = origLabel; 424 } 425 }).catch(function() { 426 err.textContent = 'Network error. Please try again.'; 427 err.hidden = false; 428 submitBtn.disabled = false; 429 document.getElementById('offerSubmitLabel').textContent = origLabel; 430 }); 431 } 432 433 // Delegated handlers â survive artist-tool rebuilds of priceCardContent 434 document.addEventListener('click', function(e) { 435 if (!e.target.closest) return; 436 if (e.target.closest('#makeOfferBtn')) { openOfferModal('offer'); return; } 437 if (e.target.closest('#inquireBtn')) { openOfferModal('inquiry'); return; } 438 if (e.target.closest('.mobile-cta .btn-cta') && priceCard) { 439 var lt = priceCard.dataset.listingType; 440 // buy_now_or_offer's mobile CTA shows "Buy Now" as the primary action, 441 // so route it through the BIN flow just like a buy_now listing. 442 if (lt === 'buy_now' || lt === 'buy_now_or_offer') { 443 var bn = document.getElementById('buyNowBtn'); 444 if (bn && !bn.disabled) { startBuyNowCheckout(bn); } 445 else if (lt === 'buy_now_or_offer') { openOfferModal('offer'); } 446 return; 447 } 448 if (lt === 'make_offer') { openOfferModal('offer'); return; } 449 if (lt === 'inquiry') { openOfferModal('inquiry'); return; } 450 if (lt === 'lease_to_own') { var lto = document.getElementById('ltoStartBtn'); if (lto) lto.click(); } 451 } 452 }); 453 454 if (offerModal) { 455 offerModal.querySelectorAll('[data-offer-close]').forEach(function(el) { 456 el.addEventListener('click', closeOfferModal); 457 }); 458 document.getElementById('offerSubmitBtn').addEventListener('click', submitOffer); 459 document.addEventListener('keydown', function(e) { 460 if (e.key === 'Escape' && !offerModal.hidden) closeOfferModal(); 461 }); 462 } 463 464 // Handoff modal (non-canonical hosts) 465 var handoffModal = document.getElementById('handoffModal'); 466 467 function openHandoffModal(canonicalUrl, mode) { 468 if (!handoffModal) { window.location = canonicalUrl; return; } 469 var isInquiry = mode === 'inquiry'; 470 document.getElementById('handoffTitle').textContent = isInquiry ? 'Continue on Appraise.net' : 'Continue on Appraise.net'; 471 document.getElementById('handoffSub').innerHTML = isInquiry 472 ? "Inquiries are handled on <strong>Appraise.net Marketplace</strong>, where you can message the seller securely. You'll be taken there now and can return any time." 473 : "Offers are negotiated and paid securely on <strong>Appraise.net Marketplace</strong>. You'll be taken there to complete your offer â and you can return here any time."; 474 document.getElementById('handoffContinueBtn').setAttribute('href', canonicalUrl); 475 handoffModal.hidden = false; 476 } 477 478 function closeHandoffModal() { 479 if (handoffModal) handoffModal.hidden = true; 480 } 481 482 if (handoffModal) { 483 handoffModal.querySelectorAll('[data-handoff-close]').forEach(function(el) { 484 el.addEventListener('click', closeHandoffModal); 485 }); 486 document.addEventListener('keydown', function(e) { 487 if (e.key === 'Escape' && !handoffModal.hidden) closeHandoffModal(); 488 }); 489 } 490 491 // Seller: Toggle publish 492 var publishBtn = document.getElementById('togglePublishBtn'); 493 if (publishBtn) { 494 publishBtn.addEventListener('click', function() { 495 // TODO: POST to toggle status 496 alert('Publish toggle coming soon.'); 497 }); 498 } 499 500 // Seller: Delete
501 var deleteBtn = document.getElementById('deleteListingBtn'); 502 if (deleteBtn) { 503 deleteBtn.addEventListener('click', function() { 504 if (confirm('Are you sure you want to delete this listing? This cannot be undone.')) { 505 // TODO: POST to delete 506 alert('Delete coming soon.'); 507 } 508 }); 509 } 510 511 // Left sidebar visibility on 1280px+ 512 var leftSidebar = document.querySelector('.sticky-sidebar'); 513 if (leftSidebar && window.innerWidth >= 1280) { 514 leftSidebar.style.display = 'block'; 515 } 516 window.addEventListener('resize', function() { 517 if (leftSidebar && window.innerWidth >= 1280) { 518 leftSidebar.style.display = 'block'; 519 } 520 }); 521 // Certificate modal 522 var certModal = document.getElementById('certModal'); 523 if (certModal) { 524 var certIframe = document.getElementById('certIframe'); 525 var certBackdrop = certModal.querySelector('.cert-modal-backdrop'); 526 var certClose = certModal.querySelector('.cert-modal-close'); 527
528 var certLoading = certModal.querySelector('.cert-modal-loading'); 529 530 document.querySelectorAll('.cert-link').forEach(function(link) { 531 link.addEventListener('click', function(e) { 532 e.preventDefault(); 533 var certId = this.dataset.certId; 534 if (certId) { 535 if (certLoading) certLoading.style.display = 'flex'; 536 certIframe.style.opacity = '0'; 537 certIframe.src = 'https://appraise.net/certificate?id=' + encodeURIComponent(certId); 538 certModal.classList.add('open'); 539 } 540 }); 541 }); 542 543 certIframe.addEventListener('load', function() { 544 if (certLoading) certLoading.style.display = 'none'; 545 certIframe.style.opacity = '1'; 546 }); 547 548 function closeCertModal() { 549 certModal.classList.remove('open'); 550 certIframe.src = ''; 551 certIframe.style.opacity = '0'; 552 } 553 554 certBackdrop.addEventListener('click', closeCertModal); 555 certClose.addEventListener('click', closeCertModal); 556 document.addEventListener('keydown', function(e) { 557 if (e.key === 'Escape' && certModal.classList.contains('open')) closeCertModal(); 558 }); 559 } 560})(); 561 562/* ---- Nav brand-logo lightbox: the badge shows a cropped strip of the logo; click it to 563 see the full logo in an overlay (X, backdrop, the logo itself, or Esc all close). ---- */ 564(function() { 565 var badge = document.querySelector('.nav-brand-logo.logo-zoomable'); 566 var box = document.getElementById('logo-lightbox'); 567 if (!badge || !box) return; 568 var img = box.querySelector('.logo-lightbox-img'); 569 570 function openLogo() { 571 img.src = badge.getAttribute('data-logo-src') || ''; 572 img.alt = badge.getAttribute('data-logo-alt') || ''; 573 box.hidden = false; 574 document.addEventListener('keydown', onKey); 575 } 576 function closeLogo() { 577 box.hidden = true; 578 img.removeAttribute('src'); 579 document.removeEventListener('keydown', onKey); 580 } 581 function onKey(e) { if (e.key === 'Escape') closeLogo(); } 582 583 badge.addEventListener('click', function(e) { e.preventDefault(); openLogo(); }); 584 badge.addEventListener('keydown', function(e) { 585 if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openLogo(); } 586 }); 587 // Any click on the overlay (backdrop, close button, or the enlarged logo) dismisses it. 588 box.addEventListener('click', closeLogo); 589})();
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.