1const products = [ 2 { 3 name: 'moss Free', priceNote: 'Kostenlos · Echter Kreditrahmen bis 2,5 Mio. ⬠· 60 Tage Zahlungsziel', base: 0, cat: 'Fintech', highlight: true, 4 suitable: ['GmbH & UG','AG','GbR','eK','Gründer'], 5 cardType: 'Kreditkarte', rating: 4.7, 6 cashbackRate: 0, cashbackLabel: 'â', 7 limit: 'bis 2,5 Mio. â¬', 8 employeeCards: 'unbegrenzt', employeeCardsSort: 999, 9 datev: true, belegerfassung: true, 10 affiliateUrl: 'https://getmoss.com/?ref=firmendo', 11 logo: '<img class="provider-logo-img u-logo-media-fit" src="../images/mini-logos/moss.svg" alt="moss Logo" loading="lazy" decoding="async">' 12 }, 13 { 14 name: 'Finom Solo', priceNote: 'Kostenlos · Visa Debitkarte · bis 3% Cashback', base: 0, cat: 'Fintech', highlight: false, 15 suitable: ['Freiberufler','Einzelunternehmen','Gründer'], 16 cardType: 'Debitkarte', rating: 4.5, 17 cashbackRate: 3, cashbackLabel: 'bis 3 %', 18 limit: 'Debit', 19 employeeCards: 'solo', employeeCardsSort: 1, 20 datev: true, belegerfassung: true, 21 affiliateUrl: 'https://finom.co/?ref=firmendo', 22 logo: '<img class="provider-logo-img u-logo-media-fit" src="../images/mini-logos/finom.svg" alt="Finom Logo" loading="lazy" decoding="async">' 23 }, 24 { 25 name: 'Vivid Business', priceNote: 'Kostenlos · Visa Debitkarte · bis 4% Cashback', base: 0, cat: 'Online-Bank', highlight: false, 26 suitable: ['Freiberufler','Einzelunternehmen','GmbH & UG','Gründer'], 27 cardType: 'Debitkarte', rating: 4.8, 28 cashbackRate: 4, cashbackLabel: 'bis 4 %', 29 limit: 'Debit', 30 employeeCards: 'bis 50', employeeCardsSort: 50, 31 datev: false, belegerfassung: true, 32 affiliateUrl: 'https://vivid.money/de-de/business/?ref=firmendo', 33 logo: '<img class="provider-logo-img u-logo-media-fit" src="../images/mini-logos/vivid-money.png" alt="Vivid Money Logo" loading="lazy" decoding="async">' 34 }, 35 { 36 name: 'Revolut Business Free', priceNote: 'Kostenlos · Mastercard Debitkarte · Multi-Währung', base: 0, cat: 'Fintech', highlight: false, 37 suitable: ['Freiberufler','Einzelunternehmen','GmbH & UG','Gründer'], 38 cardType: 'Debitkarte', rating: 4.4, 39 cashbackRate: 0, cashbackLabel: 'â', 40 limit: 'Debit', 41 employeeCards: 'ja', employeeCardsSort: 10, 42 datev: false, belegerfassung: true, 43 affiliateUrl: 'https://www.revolut.com/business/?ref=firmendo', 44 logo: '<img class="provider-logo-img u-logo-media-fit" src="../images/mini-logos/revolut.svg" alt="Revolut Logo" loading="lazy" decoding="async">' 45 }, 46 { 47 name: 'Qonto Business', priceNote: 'Ab 9 â¬/Monat · Mastercard Debitkarte · DATEV-Export', base: 9, cat: 'Fintech', highlight: false, 48 suitable: ['Freiberufler','Einzelunternehmen','GmbH & UG','AG','GbR','Gründer'], 49 cardType: 'Debitkarte', rating: 4.6, 50 cashbackRate: 0, cashbackLabel: 'â', 51 limit: 'Debit', 52 employeeCards: 'bis 30', employeeCardsSort: 30, 53 datev: true, belegerfassung: true, 54 affiliateUrl: 'https://qonto.com/de?ref=firmendo', 55 logo: '<img class="provider-logo-img u-logo-media-fit" src="../images/mini-logos/qonto.png" alt="Qonto Logo" loading="lazy" decoding="async">' 56 }, 57 { 58 name: 'American Express Business Gold', priceNote: '216 â¬/Jahr · Charge-Karte · Membership Rewards', base: 216, cat: 'Kreditkarten-Anbieter', highlight: false, 59 suitable: ['Freiberufler','Einzelunternehmen','GmbH & UG','AG','GbR'], 60 cardType: 'Kreditkarte', rating: 4.6, 61 cashbackRate: 0, cashbackLabel: 'Membership Rewards', 62 limit: 'variabel', 63 employeeCards: 'Corporate Cards', employeeCardsSort: 999, 64 datev: false, belegerfassung: false, 65 affiliateUrl: 'https://americanexpress.com/de/?ref=firmendo', 66 logo: '<img class="provider-logo-img u-logo-media-fit" src="../images/mini-logos/american-express.svg" alt="American Express Logo" loading="lazy" decoding="async">' 67 }, 68 { 69 name: 'N26 Business', priceNote: 'Kostenlos · Mastercard Debitkarte · 0,1% Cashback', base: 0, cat: 'Online-Bank', highlight: false, 70 suitable: ['Freiberufler','Einzelunternehmen'], 71 cardType: 'Debitkarte', rating: 4.2, 72 cashbackRate: 0.1, cashbackLabel: '0,1 %', 73 limit: 'Debit', 74 employeeCards: 'solo', employeeCardsSort: 1, 75 datev: false, belegerfassung: false, 76 affiliateUrl: 'https://n26.com/de-de/business?ref=firmendo', 77 logo: '<img class="provider-logo-img u-logo-media-fit" src="../images/mini-logos/n26.svg" alt="N26 Logo" loading="lazy" decoding="async">' 78 }, 79 {
80 name: 'Kontist Premium', priceNote: '9 â¬/Monat · Visa Debitkarte · Steuerautomatisierung', base: 9, cat: 'Fintech', highlight: false, 81 suitable: ['Freiberufler','Einzelunternehmen'], 82 cardType: 'Debitkarte', rating: 4.3, 83 cashbackRate: 0, cashbackLabel: 'â', 84 limit: 'Debit', 85 employeeCards: 'solo', employeeCardsSort: 1, 86 datev: true, belegerfassung: true, 87 affiliateUrl: 'https://kontist.com/?ref=firmendo', 88 logo: '<img class="provider-logo-img u-logo-media-fit" src="../images/mini-logos/kontist.png" alt="Kontist Logo" loading="lazy" decoding="async">' 89 } 90]; 91 92const YES = `<svg class="icon-yes" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>`; 93const NO = `<svg class="icon-no" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`; 94const YES_SMALL = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--green)" stroke-width="3" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>`; 95const NO_SMALL = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`; 96 97let activeFilters = { cashback: false, employees: false, datev: false, kreditrahmen: false }; 98let sortKey = 'base', sortAsc = true; 99 100/* ========== CUSTOM DROPDOWN ========== */ 101function fdGetValue(id) { 102 const el = document.getElementById(id); 103 return el ? (el.dataset.value || '') : ''; 104} 105function fdSetValue(id, value, label) { 106 const el = document.getElementById(id); 107 if (!el) return; 108 el.dataset.value = value; 109 const txt = el.querySelector('.fd-trigger-text'); 110 if (txt && label !== undefined) txt.textContent = label; 111 // Update selected state in the dropdown menu. 112 el.querySelectorAll('.fd-option').forEach(o => { 113 o.classList.toggle('selected', o.dataset.value === value); 114 }); 115 // Highlight triggers for filters only. 116 if (id === 'fd-sort') { 117 el.classList.remove('has-value'); 118 } else { 119 el.classList.toggle('has-value', value !== ''); 120 } 121} 122function fdInit() { 123 document.querySelectorAll('.fd').forEach(fd => { 124 const trigger = fd.querySelector('.fd-trigger'); 125 const menu = fd.querySelector('.fd-menu'); 126 if (!trigger || !menu) return; 127 trigger.addEventListener('click', (e) => { 128 e.stopPropagation(); 129 // close others 130 document.querySelectorAll('.fd.open').forEach(o => { 131 if (o !== fd) o.classList.remove('open'); 132 }); 133 fd.classList.toggle('open'); 134 trigger.setAttribute('aria-expanded', fd.classList.contains('open') ? 'true' : 'false'); 135 }); 136 menu.querySelectorAll('.fd-option').forEach(opt => { 137 opt.addEventListener('click', (e) => { 138 e.stopPropagation(); 139 const val = opt.dataset.value || ''; 140 const label = opt.textContent.trim(); 141 fdSetValue(fd.id, val, label); 142 fd.classList.remove('open'); 143 trigger.setAttribute('aria-expanded', 'false'); 144 // Trigger appropriate handler 145 if (fd.id === 'fd-sort') applySort(); 146 else applyFilters(); 147 }); 148 }); 149 }); 150 // Close on outside click 151 document.addEventListener('click', () => { 152 document.querySelectorAll('.fd.open').forEach(o => { 153 o.classList.remove('open'); 154 const t = o.querySelector('.fd-trigger'); 155 if (t) t.setAttribute('aria-expanded', 'false'); 156 }); 157 }); 158 // Close on Escape 159 document.addEventListener('keydown', (e) => { 160 if (e.key === 'Escape') { 161 document.querySelectorAll('.fd.open').forEach(o => o.classList.remove('open')); 162 } 163 }); 164} 165 166function toggleFilter(key, btn) { 167 activeFilters[key] = !activeFilters[key]; 168 btn.classList.toggle('active', activeFilters[key]); 169 renderTable(); 170} 171 172function applyFilters() { renderTable(); } 173 174function applySort() { 175 const val = fdGetValue('fd-sort') || 'base-asc'; 176 const [key, dir] = val.split('-'); 177 sortKey = key; 178 sortAsc = dir === 'asc'; 179 document.querySelectorAll('th.sortable').forEach(t => { 180 t.classList.remove('asc','desc'); 181 if (t.dataset.key === key) t.classList.add(sortAsc ? 'asc' : 'desc'); 182 }); 183 renderTable(); 184} 185 186function resetFilters() { 187 activeFilters = { cashback: false, employees: false, datev: false, kreditrahmen: false }; 188 document.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active')); 189 fdSetValue('fd-suit', '', 'Alle Rechtsformen'); 190 fdSetValue('fd-type', '', 'Alle Typen'); 191 fdSetValue('fd-budget', '', 'Alle Preise'); 192 fdSetValue('fd-sort', 'base-asc', 'Preis: aufsteigend'); 193 sortKey = 'base'; sortAsc = true; 194 document.querySelectorAll('th.sortable').forEach(t => t.classList.remove('asc','desc')); 195 const defaultTh = document.querySelector('th.sortable[data-key="base"]'); 196 if (defaultTh) defaultTh.classList.add('asc'); 197 renderTable(); 198} 199 200function updateFilterCount() { 201 const suit = fdGetValue('fd-suit'); 202 const type = fdGetValue('fd-type'); 203 const budget = fdGetValue('fd-budget'); 204 let count = 0; 205 if (suit) count++; 206 if (type) count++; 207 if (budget) count++;
208 Object.values(activeFilters).forEach(v => { if (v) count++; }); 209 const badge = document.getElementById('filterCountBadge'); 210 const cnt = document.getElementById('filterCount'); 211 if (count > 0) { 212 badge.classList.remove('hidden'); 213 cnt.textContent = count; 214 } else { 215 badge.classList.add('hidden'); 216 } 217} 218 219function renderTable() { 220 const suit = fdGetValue('fd-suit'); 221 const type = fdGetValue('fd-type'); 222 const budget = fdGetValue('fd-budget'); 223 224 let data = products.filter(p => { 225 if (suit && !p.suitable.includes(suit)) return false; 226 if (type === 'Debitkarte' && p.cardType !== 'Debitkarte') return false; 227 if (type === 'Kreditkarte' && p.cardType !== 'Kreditkarte') return false; 228 if (type === 'Prepaid' && p.cardType !== 'Prepaid') return false; 229 if (budget === 'free' && p.base > 0) return false; 230 if (budget === 'low' && (p.base <= 0 || p.base > 10)) return false; 231 if (budget === 'mid' && (p.base <= 10 || p.base > 50)) return false; 232 if (budget === 'premium' && p.base < 50) return false; 233 if (activeFilters.cashback && !(p.cashbackRate > 0)) return false; 234 if (activeFilters.employees && (p.employeeCardsSort <= 1 || p.employeeCards === 'solo')) return false; 235 if (activeFilters.datev && !p.datev) return false; 236 if (activeFilters.kreditrahmen && p.cardType !== 'Kreditkarte') return false; 237 return true; 238 }); 239 240 if (sortKey) { 241 data.sort((a, b) => { 242 let av = a[sortKey]; 243 let bv = b[sortKey]; 244 if (typeof av === 'string') av = av.toLowerCase(); 245 if (typeof bv === 'string') bv = bv.toLowerCase(); 246 if (av === bv) return 0; 247 return sortAsc ? (av > bv ? 1 : -1) : (av < bv ? 1 : -1); 248 }); 249 } 250 251 document.getElementById('cnt').textContent = data.length; 252 const totalEl = document.getElementById('cntTotal'); 253 if (totalEl) totalEl.textContent = products.length; 254 255 updateFilterCount(); 256 renderDesktopTable(data); 257 renderMobileCards(data); 258} 259 260function renderDesktopTable(data) { 261 const tbody = document.getElementById('tbody'); 262 if (data.length === 0) { 263 tbody.innerHTML = `<tr><td colspan="8"><div class="empty-state"> 264 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg> 265 <p>Keine Anbieter gefunden. Bitte Filter anpassen.</p></div></td></tr>`; 266 return; 267 } 268 269 tbody.innerHTML = data.map(p => { 270 const priceStr = p.base === 0 271 ? `<span class="td-price-amount free">0 €</span>` 272 : `<span class="td-price-amount">${p.base % 1 === 0 ? p.base : p.base.toFixed(2).replace('.',',')} €</span><span class="td-price-period">/ ${p.base >= 100 ? 'Jahr' : 'Monat'}</span>`; 273 const cardBadgeClass = p.cardType === 'Kreditkarte' ? 'kredit' : p.cardType === 'Prepaid' ? 'prepaid' : 'debit'; 274 // Cashback: numeric uses green, Rewards (Amex) uses purple, none uses gray. 275 const cashbackCell = p.cashbackRate > 0 276 ? `<span class="cashback-val">${p.cashbackLabel}</span>` 277 : (p.cashbackLabel && p.cashbackLabel !== 'â' 278 ? `<span class="cashback-val rewards">${p.cashbackLabel}</span>` 279 : `<span class="cashback-none">—</span>`); 280 // Kreditrahmen: debit uses gray, concrete limits use orange, variable uses purple. 281 const limitCell = p.limit === 'Debit' 282 ? `<span class="td-limit-value debit">Debit</span>` 283 : p.limit === 'â' 284 ? `<span class="td-limit-value debit">—</span>` 285 : p.limit === 'variabel' 286 ? `<span class="td-limit-value variabel">variabel</span>` 287 : `<span class="td-limit-value credit">${p.limit}</span>`; 288 // Employee cards: unlimited/corporate uses orange, numeric values use blue. 289 const empCell = p.employeeCards === 'solo' 290 ? `<span class="td-employees-value none" title="Nur Einzelkonto, keine Mitarbeiterkarten">Einzelkonto</span>` 291 : p.employeeCardsSort >= 999 292 ? `<span class="td-employees-value premium">${p.employeeCards}</span>` 293 : (p.employeeCardsSort > 0 294 ? `<span class="td-employees-value">${p.employeeCards}</span>` 295 : `<span class="td-employees-value none">—</span>`); 296 const buchhaltungCell = `<div class="buchhaltung-stack">
297 <span class="buchhaltung-item ${p.datev ? '' : 'off'}">DATEV</span> 298 <span class="buchhaltung-item ${p.belegerfassung ? '' : 'off'}">Belege</span> 299 </div>`; 300 301 return ` 302 <tr class="${p.highlight ? 'highlight' : ''}"> 303 <td> 304 <div class="td-anbieter u-table-provider-row"> 305 <div class="td-logo u-logo-dynamic-bg u-bg-white">${p.logo}</div> 306 <div class="u-stack-start-gap-3 u-min-0"> 307 ${p.highlight ? '<span class="highlight-badge">Empfehlung</span>' : ''} 308 <div class="td-name">${p.name}</div> 309 <div class="td-cat">${p.cat}</div> 310 </div> 311 </div> 312 </td> 313 <td class="td-price"> 314 <div class="price-wrap">${priceStr}${p.priceNote ? `<div class="price-tooltip">${p.priceNote}</div>` : ''}</div> 315 </td> 316 <td class="td-center"><span class="card-badge ${cardBadgeClass}">${p.cardType}</span></td> 317 <td class="td-center">${cashbackCell}</td> 318 <td class="td-center">${limitCell}</td> 319 <td class="td-center">${empCell}</td> 320 <td class="td-center">${buchhaltungCell}</td> 321 <td class="td-cta"> 322 <div class="td-cta-stack"> 323 <a href="${p.affiliateUrl}" target="_blank" rel="noopener sponsored" class="btn-table${p.highlight ? ' highlighted' : ''}"> 324 Zum Anbieter
325 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg> 326 </a> 327 </div> 328 </td> 329 </tr>`; 330 }).join(''); 331} 332 333function renderMobileCards(data) { 334 const mc = document.getElementById('mobileCards'); 335 if (!mc) return; 336 if (data.length === 0) { 337 mc.innerHTML = `<div class="mobile-empty"> 338 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg> 339 <p>Keine Anbieter gefunden. Bitte Filter anpassen.</p></div>`; 340 return; 341 } 342 343 mc.innerHTML = data.map(p => { 344 const priceStr = p.base === 0 345 ? `<div class="mc-price-amount free">0 €</div><div class="mc-price-period">Grundgebühr</div>` 346 : `<div class="mc-price-amount">${p.base % 1 === 0 ? p.base : p.base.toFixed(2).replace('.',',')} €</div><div class="mc-price-period">/ ${p.base >= 100 ? 'Jahr' : 'Monat'}</div>`; 347 const cardBadgeClass = p.cardType === 'Kreditkarte' ? 'kredit' : p.cardType === 'Prepaid' ? 'prepaid' : 'debit'; 348 const cashbackVal = p.cashbackRate > 0 349 ? `<span class="mc-spec-value green">${p.cashbackLabel}</span>` 350 : (p.cashbackLabel && p.cashbackLabel !== 'â' 351 ? `<span class="mc-spec-value purple">${p.cashbackLabel}</span>` 352 : `<span class="mc-spec-value muted">—</span>`); 353 const limitVal = p.limit === 'Debit' 354 ? `<span class="mc-spec-value muted">Debit</span>` 355 : p.limit === 'â' 356 ? `<span class="mc-spec-value muted">—</span>` 357 : p.limit === 'variabel' 358 ? `<span class="mc-spec-value purple">variabel</span>` 359 : `<span class="mc-spec-value orange">${p.limit}</span>`; 360 const empVal = p.employeeCards === 'solo' 361 ? `<span class="mc-spec-value muted">Einzelkonto</span>` 362 : p.employeeCardsSort >= 999 363 ? `<span class="mc-spec-value orange">${p.employeeCards}</span>` 364 : (p.employeeCardsSort > 0 365 ? `<span class="mc-spec-value blue">${p.employeeCards}</span>` 366 : `<span class="mc-spec-value muted">—</span>`); 367 const datevVal = p.datev 368 ? `<span class="mc-spec-value green">${YES_SMALL} DATEV</span>` 369 : `<span class="mc-spec-value muted">${NO_SMALL} DATEV</span>`; 370 371 return ` 372 <div class="mc-card ${p.highlight ? 'highlight' : ''}"> 373 <div class="mc-head"> 374 <div class="mc-logo">${p.logo}</div> 375 <div class="mc-head-text"> 376 <div class="mc-name">${p.name}</div> 377 <div class="mc-meta"> 378 <span class="mc-cat">${p.cat}</span> 379 <span class="card-badge ${cardBadgeClass}">${p.cardType}</span> 380 </div> 381 ${p.highlight ? '<span class="mc-badge">Empfehlung</span>' : ''} 382 </div> 383 <div class="mc-price-main">${priceStr}</div> 384 </div> 385 386 <div class="mc-specs"> 387 <div class="mc-spec"> 388 <div class="mc-spec-label">Cashback</div> 389 ${cashbackVal} 390 </div> 391 <div class="mc-spec"> 392 <div class="mc-spec-label">Kreditrahmen</div> 393 ${limitVal} 394 </div> 395 <div class="mc-spec"> 396 <div class="mc-spec-label">Mitarbeiterkarten</div> 397 ${empVal} 398 </div> 399 <div class="mc-spec"> 400 <div class="mc-spec-label">Buchhaltung</div> 401 ${datevVal} 402 </div> 403 </div> 404 405 <div class="mc-footer"> 406 <div class="mc-footer-note">${p.priceNote || ''}</div> 407 <a href="${p.affiliateUrl}" target="_blank" rel="noopener sponsored" class="mc-cta"> 408 Zum Anbieter 409 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg> 410 </a> 411 </div> 412 </div>`; 413 }).join(''); 414} 415 416document.addEventListener('DOMContentLoaded', () => { 417 fdInit(); 418 document.querySelectorAll('th.sortable').forEach(th => { 419 th.addEventListener('click', () => {
420 const key = th.dataset.key; 421 if (sortKey === key) sortAsc = !sortAsc; 422 else { sortKey = key; sortAsc = true; } 423 document.querySelectorAll('th.sortable').forEach(t => { t.classList.remove('asc','desc'); }); 424 th.classList.add(sortAsc ? 'asc' : 'desc'); 425 // Sync fd-sort dropdown 426 const matchingVal = `${key}-${sortAsc ? 'asc' : 'desc'}`; 427 const fdSort = document.getElementById('fd-sort'); 428 if (fdSort) { 429 const matchingOpt = fdSort.querySelector(`.fd-option[data-value="${matchingVal}"]`); 430 if (matchingOpt) { 431 fdSetValue('fd-sort', matchingVal, matchingOpt.textContent.trim()); 432 } 433 } 434 renderTable(); 435 }); 436 }); 437 // Set default sort indicator 438 const defaultTh = document.querySelector('th.sortable[data-key="base"]'); 439 if (defaultTh) defaultTh.classList.add('asc'); 440 renderTable(); 441 442 // Scroll detection inside DOMContentLoaded for reliable measurements. 443 const _tw = document.getElementById('tableWrap'); 444 const _sw = document.getElementById('tableScrollWrap'); 445 const _hint = document.getElementById('scrollHint'); 446 if (_tw && _sw) { 447 const checkScroll = () => { 448 const hasScroll = _tw.scrollWidth > _tw.clientWidth + 2; 449 const atStart = _tw.scrollLeft <= 2; 450 const atEnd = _tw.scrollLeft + _tw.clientWidth >= _tw.scrollWidth - 8; 451 _sw.classList.toggle('scrollable', hasScroll); 452 _sw.classList.toggle('at-start', atStart); 453 _sw.classList.toggle('at-end', atEnd); 454 if (_hint) _hint.classList.toggle('visible', hasScroll && !atEnd); 455 }; 456 _tw.addEventListener('scroll', checkScroll, { passive: true }); 457 window.addEventListener('resize', checkScroll); 458 setTimeout(checkScroll, 50); 459 setTimeout(checkScroll, 300); 460 setTimeout(checkScroll, 800); 461 checkScroll(); 462 } 463});
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.