1class BFMobile { 2 constructor() { 3 this._pwaPrompt = null; 4 this._DESKTOP_DESIGN_W = 930; 5 6 // beforeinstallprompt fyrer TIDLIGT â ofte før pwa() (der kører efter 7 // hello-roundtrippet). Fang eventet allerede her ved instansiering 8 // (boyfriend.js:56, script-load), sÃ¥ det ikke gÃ¥r tabt; pwa() henter 9 // det gemte prompt bagefter. 10 window.addEventListener('beforeinstallprompt', (e) => { 11 e.preventDefault(); 12 this._pwaPrompt = e; 13 document.querySelectorAll('[data-action="pwa-install"]').forEach((el) => { 14 const standalone = window.matchMedia('(display-mode: standalone)').matches 15 || window.navigator.standalone === true; 16 if (!standalone) el.hidden = false; 17 }); 18 }); 19 } 20 21 22 // Luk qsort-sheetet (klasse + dataset-state pÃ¥ body) â var kopieret 23 // 5 steder. 24 closeQsortSheet() { 25 document.body.classList.remove('bf-qsort-open'); 26 delete document.body.dataset.bfQsortOnly; 27 delete document.body.dataset.bfQsortPane; 28 } 29 30 boot() { 31 this.pwa(); 32 this.topbarCounters(); 33 34 const uaWantsDesktop = 35 (navigator.maxTouchPoints > 1 36 && !!navigator.userAgentData 37 && navigator.userAgentData.mobile === false 38 && !/Android/.test(navigator.userAgent)) 39 || (navigator.maxTouchPoints > 1 40 && !navigator.userAgentData 41 && /Macintosh/.test(navigator.userAgent) 42 && Math.min(screen.width, screen.height) < 700); 43 44 // Mus som primær input = rigtig computer (ogsÃ¥ touch-laptops) â altid 45 // desktop-layout uanset vinduesbredde: browser-zoom krymper CSS-px, sÃ¥ 46 // zoom+reload gav ellers mobilsite pÃ¥ pc. Devtools-emulering spoofer 47 // pointer:coarse og rammes ikke. 48 const mouseIsPrimary = !!(window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches); 49 50 // px-anker (ikke 64em): menu-registry.js bruger (max-width: 1023px) â 51 // ved root-font â 16px ville em/px-grænserne divergere. 52 const isMobileViewport = !uaWantsDesktop && !mouseIsPrimary && window.matchMedia('(max-width: 1023px)').matches; 53 const hasExistingSwiper = !!document.getElementById('bf_mobile_swiper'); 54 55 this._lockDesktopViewport(isMobileViewport); 56 57 const hideBootSplash = () => { 58 const el = document.getElementById('bf_boot_splash'); 59 if (!el || el.dataset.closing === '1') return; 60 el.dataset.closing = '1'; 61 const minVisible = 280; 62 const elapsed = performance.now() - _api.splash; 63 const wait = Math.max(0, minVisible - elapsed); 64 setTimeout(() => { 65 el.classList.remove('is-open'); 66 el.classList.add('is-closing'); 67 setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 220); 68 }, wait); 69 }; 70 requestAnimationFrame(() => requestAnimationFrame(hideBootSplash)); 71 72 if (!isMobileViewport || hasExistingSwiper) { 73 document.body.classList.toggle('is-mobile-layout', isMobileViewport && hasExistingSwiper); 74 document.documentElement.classList.toggle('is-mobile-layout', isMobileViewport && hasExistingSwiper); 75 return; 76 } 77 78 const $menu = document.querySelector('#toprowcontainer #menu'); 79 const $content = document.querySelector('#content_container .content'); 80 const $status = document.getElementById('status'); 81 const $contentContainer = document.getElementById('content_container'); 82 83 function wrap(el, paneClass) { 84 const w = document.createElement('div'); 85 w.className = 'bf-pane ' + paneClass; 86 w.appendChild(el); 87 return w; 88 } 89 90 const bindMailPill = (pill) => { 91 if (!pill) return; 92 const link = pill.closest('a'); 93 const base = link ? link.textContent.trim() : ''; 94 pill.setAttribute('aria-hidden', 'true'); 95 const apply = (n) => { 96 n = parseInt(n, 10) || 0; 97 pill.textContent = n; 98 pill.classList.toggle('hidden', n === 0); 99 pill.style.display = ''; 100 if (link) { 101 if (n > 0) { 102 link.setAttribute('aria-label', base + ', ' + n + (n === 1 ? ' ulæst' : ' ulæste')); 103 } else { 104 link.removeAttribute('aria-label'); 105 } 106 } 107 }; 108 if (typeof _mail.unread_count === 'number') { 109 apply(_mail.unread_count); 110 } else { 111 pill.classList.add('hidden'); 112 } 113 document.addEventListener('bf:mail-unread-count', (e) => apply((e.detail && e.detail.count))); 114 }; 115 116 const swiper = document.createElement('div'); 117 swiper.id = 'bf_mobile_swiper'; 118 document.body.classList.add('is-mobile-layout'); 119 document.documentElement.classList.add('is-mobile-layout'); 120 121 const menuPane = _template.loadById('template_drawer_mobile')[0]; 122 const menuSplit = menuPane.querySelector('.bf-menu-split'); 123 const menuLeft = menuPane.querySelector('.bf-menu-left'); 124 const menuRight = menuPane.querySelector('.bf-menu-right'); 125 const footer = menuPane.querySelector('.bf-menu-footer-block'); 126 const bottomRow = menuPane.querySelector('.bf-menu-footer'); 127 const subSlot = menuPane.querySelector('#bf_menu_sub_slot'); 128 const installBox = menuPane.querySelector('.bf-menu-install-box'); 129 const infoBox = menuPane.querySelector('.bf-menu-info-tile'); 130 const gdprBox = menuPane.querySelector('.bf-menu-gdpr-tile'); 131 const scrollToContent = () => setTimeout( 132 () => swiper.scrollTo({ left: 0, behavior: 'smooth' }), 300); 133 const $topmenuRight = $menu.querySelector('#topmenu_right'); 134 if ($topmenuRight) menuRight.insertBefore($topmenuRight, subSlot); 135 136 menuLeft.insertBefore($menu, footer); 137 138 const $newsticker = document.getElementById('newsticker'); 139 if ($newsticker) { 140 footer.appendChild($newsticker); 141 $newsticker.addEventListener('click', (e) => { 142 if (!e.target.closest('a')) return; 143 scrollToContent(); 144 }, true); 145 } 146
147 const $statusOnlineBox = document.querySelector('.status_online'); 148 const $profilesBox = $statusOnlineBox ? $statusOnlineBox.querySelector('.profiles_container') : null; 149 if ($profilesBox) { 150 const _disc = $profilesBox.querySelector('.disclaimer'); 151 if (_disc) _disc.remove(); 152 } 153 if ($statusOnlineBox) { 154 const visits = $statusOnlineBox.querySelector('.visits_container'); 155 const online = $statusOnlineBox.querySelector('.online_container'); 156 if (visits) { 157 visits.style.cursor = 'pointer'; 158 visits.addEventListener('click', () => { 159 document.location.href = '/#/s/lists_own/guests'; 160 scrollToContent(); 161 }); 162 bottomRow.insertBefore(visits, infoBox); 163 } 164 if (online) { 165 online.style.cursor = 'pointer'; 166 online.addEventListener('click', () => { 167 document.location.href = '/#/s/lists/online'; 168 scrollToContent(); 169 }); 170 bottomRow.insertBefore(online, infoBox); 171 } 172 } 173 const $itemNavn = $status.querySelector('#item_navn'); 174 175 infoBox.addEventListener('click', () => { 176 document.location.href = '/#/s/info/info'; 177 scrollToContent(); 178 }); 179 gdprBox.addEventListener('click', () => { 180 document.location.href = '/#/s/info/persondata'; 181 scrollToContent(); 182 }); 183 184 swiper.appendChild(menuPane); 185 186 const menuBackdrop = document.createElement('div'); 187 menuBackdrop.id = 'bf_menu_backdrop'; 188 document.body.appendChild(menuBackdrop); 189 190 const closeMenu = () => document.body.classList.remove('bf-menu-open'); 191 const toggleMenu = () => document.body.classList.toggle('bf-menu-open'); 192 193 menuBackdrop.addEventListener('click', closeMenu); 194 195 menuPane.addEventListener('click', (e) => { 196 const topA = e.target.closest('#menu > ul > li > a'); 197 if (topA) { 198 const li = topA.parentElement; 199 if (li.querySelector(':scope > ul') && !li.classList.contains('bf-menu-open')) { 200 return; 201 } 202 } 203 204 if (e.target.closest('a, .visits_container, .online_container, .bf-menu-info-tile, .bf-menu-gdpr-tile')) { 205 setTimeout(closeMenu, 250); 206 scrollToContent(); 207 } 208 }, true); 209 210 swiper.appendChild(wrap($content, 'bf-pane--content')); 211 212 const hjemLi = $menu.querySelector('#menu_logged_in > li:first-child'); 213 if (hjemLi) { 214 const subItems = hjemLi.querySelectorAll(':scope > ul > li'); 215 const ul = hjemLi.parentElement; 216 subItems.forEach(item => { 217 const _a = item.querySelector('a'); 218 const href = (_a ? _a.getAttribute('href') : '') || ''; 219 if (href.includes('/news/')) { 220 item.remove(); 221 return; 222 } 223 item.classList.add('bf-flat-from-hjem'); 224 ul.appendChild(item); 225 }); 226 hjemLi.remove(); 227 } 228 229 const _infoA = $menu.querySelector('#menu_logged_in > li > a[href="#/s/info/info"]'); 230 const infoLi = _infoA ? _infoA.parentElement : null; 231 if (infoLi) infoLi.remove(); 232 233 const chatA = $menu.querySelector('#menu_logged_in > li > a.chat'); 234 if (chatA) { 235 chatA.setAttribute('href', '#/s/chat/fri'); 236 const _chatUl = chatA.parentElement.querySelector(':scope > ul'); 237 if (_chatUl) _chatUl.remove(); 238 chatA.addEventListener('click', (e) => { 239 if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) { 240 return; 241 } 242 e.preventDefault(); 243 e.stopPropagation(); 244 e.stopImmediatePropagation(); 245 const room = (_chat.last_room === 'scor' || _chat.last_room === 'fri') 246 ? _chat.last_room : 'fri'; 247 _nav.load('/s/chat/' + room); 248 }, true); 249 } 250 const debatA = $menu.querySelector('#menu_logged_in > li > a[href="#/s/debat/debat"]'); 251 if (debatA) { 252 const _debatUl = debatA.parentElement.querySelector(':scope > ul'); 253 if (_debatUl) _debatUl.remove(); 254 } 255
256 const kontrolpanelLi = $menu.querySelector('#menu_logged_in > li.bf-flat-from-hjem'); 257 const _logafA = $menu.querySelector('#menu_logged_in > li > a[data-action="profile-logout"]'); 258 const logafLi = _logafA ? _logafA.parentElement : null; 259 const userBlock = document.createElement('div'); 260 userBlock.className = 'bf-user-block'; 261 if ($itemNavn) userBlock.appendChild($itemNavn); 262 if (kontrolpanelLi || logafLi) { 263 const userActions = document.createElement('div'); 264 userActions.className = 'bf-user-actions'; 265 const tipsLi = document.createElement('li'); 266 tipsLi.className = 'bf-user-action-tips'; 267 const tipsA = document.createElement('a'); 268 tipsA.href = '#'; 269 tipsA.setAttribute('data-action', 'show-tips'); 270 tipsA.textContent = 'tips'; 271 tipsLi.appendChild(tipsA); 272 userActions.appendChild(tipsLi); 273 if (kontrolpanelLi) userActions.appendChild(kontrolpanelLi); 274 if (logafLi) userActions.appendChild(logafLi); 275 userActions.addEventListener('click', (e) => { 276 if (!e.target.closest('a')) return; 277 setTimeout(() => { 278 swiper.scrollTo({ left: 0, behavior: 'smooth' }); 279 }, 300); 280 }, true); 281 userBlock.appendChild(userActions); 282 } 283 if (userBlock.children.length) { 284 menuLeft.insertBefore(userBlock, menuLeft.firstChild); 285 } 286 287 const _postA = $menu.querySelector('#menu_logged_in > li > a[href="#/s/mail/inbox"]'); 288 const postLi = _postA ? _postA.parentElement : null; 289 if (postLi) { 290 const pill = document.createElement('span'); 291 pill.className = 'bf-menu-pill'; 292 const _postLink = postLi.querySelector('a'); 293 if (_postLink) _postLink.appendChild(pill); 294 bindMailPill(pill); 295 } 296 297 menuLeft.addEventListener('click', (e) => { 298 const a = e.target.closest('#menu > ul > li > a'); 299 if (!a) return; 300 const li = a.parentElement; 301 if (!li.querySelector(':scope > ul')) return; 302 if (li.classList.contains('bf-menu-open')) return; 303 e.preventDefault(); 304 e.stopPropagation(); 305 e.stopImmediatePropagation(); 306 li.parentElement.querySelectorAll(':scope > li.bf-menu-open').forEach(other => { 307 other.classList.remove('bf-menu-open'); 308 }); 309 li.classList.add('bf-menu-open'); 310 }, true); 311 312 menuLeft.addEventListener('click', (e) => { 313 const a = e.target.closest('#menu a, #newsticker #link a'); 314 if (!a) return; 315 const li = a.parentElement; 316 if ( 317 li && 318 li.matches('#menu > ul > li') && 319 li.querySelector(':scope > ul') && 320 !li.classList.contains('bf-menu-open') 321 ) { 322 return; 323 } 324 const href = a.getAttribute('href') || ''; 325 if ( 326 href === '#/s/mail/inbox' || 327 href === '#/s/chat/scor' || 328 href === '#/s/chat/fri' || 329 href.startsWith('#/s/mail/show/') 330 ) { 331 return; 332 } 333 setTimeout(() => { 334 swiper.scrollTo({ left: 0, behavior: 'smooth' }); 335 }, 300); 336 }); 337 338 const $logo = document.querySelector('#toprowcontainer #logo'); 339 if ($logo) { 340 $logo.addEventListener('click', (e) => { 341 _elements.constructor.display_target = null; 342 if (_profile.user && _profile.user.id) { 343 e.preventDefault(); 344 // stopImmediatePropagation (ikke stopPropagation): elements.js 345 // binder OGSà click pÃ¥ SAMME element (bubble). Uden dette nÃ¥r 346 // dens location.href at fyre â dobbelt render ved logo-tap. 347 e.stopImmediatePropagation(); 348 _nav.load('/s/profile/profile'); 349 } 350 setTimeout(() => { 351 swiper.scrollTo({ left: 0, behavior: 'smooth' });
352 }, 300); 353 }, true); 354 } 355 356 const $bell = document.querySelector('#bf-topbar-counters a[data-counter="notifications"]'); 357 if ($bell) { 358 $bell.addEventListener('click', () => { 359 setTimeout(() => { 360 swiper.scrollTo({ left: 0, behavior: 'smooth' }); 361 }, 300); 362 }); 363 } 364 365 const statusPane = document.createElement('div'); 366 statusPane.className = 'bf-pane bf-pane--status'; 367 368 const tabBody = document.createElement('div'); 369 tabBody.id = 'bf_status_tab_body'; 370 statusPane.appendChild(tabBody); 371 372 // Status-panens faner (post/scor/fri/privat) er ALDRIG paginerede 373 // lister; kun content-panen hoster infiload. Uden denne gate sideloader 374 // scroll i scor/mail den anden panes liste, fordi infiload_call er global 375 // â og guarden manglede infiload_max, sÃ¥ den fyrede efter sidste side. 376 tabBody.addEventListener('scroll', () => { 377 if (!_lists.infiload_call || _lists.infiload_loading || _lists.infiload_max) return; 378 if (tabBody.dataset.bfHostsList !== '1') return; 379 const margin = _lists.infiload_margin || 250; 380 const remaining = tabBody.scrollHeight - tabBody.scrollTop - tabBody.clientHeight; 381 if (remaining < margin) { 382 _lists.infiload_call(); 383 } 384 }, { passive: true }); 385 386 const statusLegacy = document.createElement('div'); 387 statusLegacy.id = 'bf_status_legacy'; 388 statusLegacy.appendChild($status); 389 statusPane.appendChild(statusLegacy); 390 391 swiper.appendChild(statusPane); 392 393 const statusBottomNav = _template.loadById('template_status_bottomnav')[0]; 394 statusBottomNav.addEventListener('click', (e) => { 395 const a = e.target.closest('a[data-bn]'); 396 if (!a) return; 397 e.preventDefault(); 398 e.stopPropagation(); 399 const tab = a.dataset.tab; 400 if (tab) activateTab(tab); 401 }); 402 const statusBottomBar = document.createElement('div'); 403 statusBottomBar.id = 'bf_status_bottombar'; 404 statusBottomBar.appendChild(statusBottomNav); 405 406 const statusAd = document.createElement('div'); 407 statusAd.id = 'if_ad_status'; 408 statusBottomBar.insertBefore(statusAd, statusBottomNav); 409 410 statusPane.appendChild(statusBottomBar); 411 // Klasse-spejl af :has(> #bf_status_bottombar) (site.css) â :has() dør pÃ¥ 412 // iOS 15.0-15.3; bottombaren bygges altid, sÃ¥ spejlet er altid korrekt. 413 statusPane.classList.add('bf-has-bottombar'); 414 415 bindMailPill(statusBottomNav.querySelector('a[data-tab="post"] .bf-bn-pill')); 416 417 const tabRoute = { 418 post: () => section('mail', 'inbox'), 419 scor: () => section('chat', 'scor'), 420 fri: () => section('chat', 'fri'), 421 private: () => section('chat', 'private'), 422 }; 423 424 function activateTab(name) { 425 statusBottomNav.querySelectorAll('a[data-tab]').forEach((b) => { 426 b.classList.toggle('is-active', b.dataset.tab === name); 427 }); 428 _elements.constructor.display_target = '#bf_status_tab_body'; 429 430 const _histBefore = location.hash; 431 const $ifpm = document.getElementById('if_profile_menu'); 432 const $ifpc = document.getElementById('if_profile_content'); 433 // Status-fanernes ruter (section('mail'/'chat')) kalder _menu.show, 434 // som renderer i #if_menu â et element der FYSISK ligger i content- 435 // panen. Uden snapshot ville content-panens egen sektionsmenu blive 436 // erstattet af "post/svar"/"scor/fri/privat" nÃ¥r man swiper tilbage. 437 const $ifmenu = document.getElementById('if_menu'); 438 const menuSnapshot = $ifmenu ? $ifmenu.innerHTML : null; 439 const wasMenuVisible = $ifpm && $ifpm.style.display !== 'none' && $ifpm.children.length > 0;
440 const wasProfileVisible = $ifpc && $ifpc.style.display !== 'none' && $ifpc.children.length > 0; 441 const railEl = wasProfileVisible 442 ? document.getElementById('bf_profile_action_rail') 443 : null; 444 if (railEl) railEl.remove(); 445 try { 446 tabRoute[name](); 447 } catch (err) { 448 tabBody.textContent = 'Kunne ikke indlæse fanen: ' + err.message; 449 } 450 if ($ifmenu && menuSnapshot !== null) $ifmenu.innerHTML = menuSnapshot; 451 if (wasMenuVisible && $ifpm) $ifpm.style.display = ''; 452 if (wasProfileVisible && $ifpc) $ifpc.style.display = ''; 453 if (wasProfileVisible && railEl) { 454 const _oldRail = document.getElementById('bf_profile_action_rail'); 455 if (_oldRail) _oldRail.remove(); 456 document.body.appendChild(railEl); 457 } 458 if (location.hash !== _histBefore) { 459 history.replaceState(history.state, '', _histBefore || location.pathname); 460 } 461 } 462 463 function gotoStatusTab(name) { 464 statusInit = true; 465 activateTab(name); 466 swiper.scrollTo({ 467 left: swiper.scrollWidth - swiper.clientWidth, 468 behavior: 'smooth', 469 }); 470 } 471 472 swiper.addEventListener('scroll', () => { 473 requestAnimationFrame(() => { 474 const max = swiper.scrollWidth - swiper.clientWidth; 475 const paaStatus = max > 0 && swiper.scrollLeft > max - (swiper.clientWidth / 2); 476 document.body.classList.toggle('bf-status-pane-active', paaStatus); 477 }); 478 }, { passive: true }); 479 480 const _origNavLoad = _nav.load.bind(_nav); 481 const setDisplayTarget = (val) => { 482 _elements.constructor.display_target = val; 483 }; 484 _nav.load = function(loc, noregister) { 485 if (!_nav._mob_guarding && typeof _dialog === 'object' && _dialog.guardNavigation && _dialog.hasDirtyGuard()) { 486 _dialog.guardNavigation(() => { 487 _nav._mob_guarding = true; 488 try { 489 _nav.load(loc, noregister); 490 } finally { 491 // Uden finally forbliver guarding=true hvis load kaster â 492 // dirty-guarden er omgÃ¥et resten af sessionen. 493 _nav._mob_guarding = false; 494 } 495 }); 496 return; 497 } 498 499 if (typeof loc === 'string' && loc.length) { 500 if (loc === '/s/mail/inbox') { 501 gotoStatusTab('post'); 502 return; 503 } 504 const showMatch = loc.match(/^\/s\/mail\/show\/(.+)$/); 505 if (showMatch) { 506 statusInit = true; 507 setDisplayTarget('#bf_status_tab_body'); 508 statusBottomNav.querySelectorAll('a[data-tab]').forEach((b) => { 509 b.classList.toggle('is-active', b.dataset.tab === 'post'); 510 }); 511 const _histBefore = location.hash; 512 _mail.messenger(showMatch[1]); 513 if (location.hash !== _histBefore) { 514 history.replaceState(history.state, '', _histBefore || location.pathname); 515 } 516 swiper.scrollTo({ 517 left: swiper.scrollWidth - swiper.clientWidth, 518 behavior: 'smooth', 519 }); 520 return; 521 } 522 if (loc === '/s/chat/scor') { 523 gotoStatusTab('scor'); 524 return; 525 } 526 if (loc === '/s/chat/fri') { 527 gotoStatusTab('fri'); 528 return; 529 } 530 if (loc === '/s/chat/private') { 531 gotoStatusTab('private'); 532 return; 533 } 534 } 535 setDisplayTarget(null); 536 return _origNavLoad(loc, noregister); 537 }; 538 539 _nav.showmail = function(mailId) { 540 _nav.load('/s/mail/show/' + mailId); 541 }; 542 543 setTimeout(() => { 544 const hash = location.hash || ''; 545 if (!hash.startsWith('#/s/')) return; 546 const route = hash.substring(1); 547 if (route === '/s/mail/inbox' || 548 route === '/s/chat/scor' || 549 route === '/s/chat/fri' || 550 route === '/s/chat/private' || 551 /^\/s\/mail\/show\/.+/.test(route)) { 552 const mainContent = document.getElementById('if_content'); 553 if (mainContent) mainContent.innerHTML = ''; 554 _nav.load(route); 555 } 556 }, 0); 557 558 let statusInit = false; 559 const statusObserver = new IntersectionObserver((entries) => { 560 for (const ent of entries) { 561 if (ent.isIntersecting && !statusInit) { 562 statusInit = true; 563 statusObserver.disconnect(); 564 activateTab('post'); 565 } 566 } 567 }, { root: swiper, threshold: 0.5 }); 568 statusObserver.observe(statusPane); 569 570 $contentContainer.parentNode.insertBefore(swiper, $contentContainer); 571 $contentContainer.remove(); 572 573 const contentPane = swiper.querySelector('.bf-pane--content'); 574 // #content_container (role="main") demonteres i mobil-layoutet â 575 // landmarket følger med over pÃ¥ indholds-panen. 576 contentPane.setAttribute('role', 'main'); 577 const contentScroll = document.createElement('div'); 578 contentScroll.className = 'bf-content-scroll'; 579 while (contentPane.firstChild) contentScroll.appendChild(contentPane.firstChild); 580 contentPane.appendChild(contentScroll); 581 582 583 const renderMobileQsortDock = () => { 584 document.querySelectorAll('.bf-qsort-filter-btn').forEach((btn) => { 585 const paneEl = btn.closest('.bf-pane--content, .bf-pane--status'); 586 const scope = paneEl || document; 587 const qs = scope.querySelector('#qsort_filter_display') 588 || document.getElementById('qsort_filter_display'); 589 if (!qs) return; 590 const cells = btn.querySelectorAll('[data-helper]'); 591
592 cells.forEach((cell) => { 593 const id = cell.getAttribute('data-helper'); 594 const src = qs.querySelector('#' + id); 595 const txt = src ? (src.textContent || '').trim() : ''; 596 const v = cell.querySelector('.bf-qsort-val'); 597 if (v) v.textContent = txt || 'â'; 598 }); 599 600 const _sortInp = scope.querySelector('input[name=sort]'); 601 const cur = String((_sortInp ? _sortInp.value : '') || ''); 602 cells.forEach((cell) => { 603 const up = cell.getAttribute('data-sort-up'); 604 const dn = cell.getAttribute('data-sort-down'); 605 const badge = cell.querySelector('.bf-qsort-sort'); 606 if (!badge) return; 607 badge.classList.remove('is-active', 'is-up', 'is-down'); 608 if (cur === up) badge.classList.add('is-active', 'is-up'); 609 else if (cur === dn) badge.classList.add('is-active', 'is-down'); 610 }); 611 }); 612 }; 613 document.addEventListener('bf:qsort-updated', renderMobileQsortDock); 614 615 const wireQsort = () => { 616 document.querySelectorAll('#qsort_filter_display:not([data-bf-mobile-wired])').forEach((qs) => { 617 qs.dataset.bfMobileWired = '1'; 618 619 const qsInStatus = !!qs.closest('.bf-pane--status'); 620 const barSel = qsInStatus 621 ? '#bf_status_bottombar > #qsort_filter_display' 622 : '#bf_content_bottombar > #qsort_filter_display'; 623 document.querySelectorAll(barSel) 624 .forEach((old) => { if (old !== qs) old.remove(); }); 625 626 const filterWrap = qs.closest('#qsort_filter') || qs.parentNode; 627 if (!filterWrap) { return; } 628 const inner = filterWrap.parentNode; 629 const h1 = inner ? inner.querySelector('h1') : null; 630 631 const visBox = qs.querySelector('[data-template="qsort_mode_display_container"]') 632 || qs.querySelector('#qsort_mode_display_container'); 633 if (visBox) visBox.classList.add('bf-qsort-vis'); 634 635 const btn = _template.loadById('template_qsort_dock_btn')[0]; 636 btn.addEventListener('click', (ev) => { 637 const cell = ev.target.closest('[data-helper]'); 638 const map = { 639 qsort_age_display: 'age', 640 qsort_gender_display: 'gender', 641 qsort_area_display: 'area', 642 qsort_target_display: 'target', 643 }; 644 const which = cell ? map[cell.getAttribute('data-helper')] : null; 645 if (which) { 646 const qsortPane = btn.closest('.bf-pane--status') ? 'status' : 'content'; 647 if (document.body.classList.contains('bf-qsort-open') 648 && document.body.dataset.bfQsortOnly === which 649 && document.body.dataset.bfQsortPane === qsortPane) { 650 _mobile.closeQsortSheet(); 651 return; 652 } 653 document.body.dataset.bfQsortOnly = which; 654 document.body.dataset.bfQsortPane = qsortPane; 655 document.body.classList.add('bf-qsort-open'); 656 if (which === 'age') ensureAgeSlider(qsortPane); 657 } 658 }); 659 660 if (h1 && !h1.parentNode.classList.contains('bf-list-head')) { 661 const head = document.createElement('div'); 662 head.className = 'bf-list-head'; 663 h1.parentNode.insertBefore(head, h1); 664 head.appendChild(h1); 665 } 666 667 const pane = filterWrap.closest('.bf-pane--content, .bf-pane--status') 668 || document.querySelector('.bf-pane--content'); 669 if (pane) { 670 const isStatus = pane.classList.contains('bf-pane--status'); 671 const barId = isStatus ? 'bf_status_bottombar' : 'bf_content_bottombar'; 672 const navId = isStatus ? 'bf_status_bottomnav' : 'bf_content_bottomnav'; 673 const bar = pane.querySelector('#' + barId) || pane; 674 let dock = bar.querySelector('.bf-qsort-filter-dock'); 675 if (!dock) { 676 dock = document.createElement('div'); 677 dock.className = 'bf-qsort-filter-dock';
678 const nav = bar.querySelector('#' + navId); 679 if (nav) bar.insertBefore(dock, nav); 680 else bar.appendChild(dock); 681 } else { 682 dock.innerHTML = ''; 683 } 684 dock.appendChild(btn); 685 686 // Reparent qs ind i bottombar over dock â sÃ¥ samme flex-stak. 687 // VIGTIGT: nÃ¥r qs forlader <form name="qsort"> mister inputs 688 // deres form-association og form.serializeArray() ser ikke 689 // checkboxene (serveren tolker som "alle valgt"). Giv form 690 // et id og sæt HTML5 [form]-attribut pÃ¥ alle felter sÃ¥ de 691 // forbliver i form.elements efter reparent. 692 // Find DETTE filters egen form. Den ligger INDE i filterWrap 693 // (#qsort_filter) â ikke som ancestor â sÃ¥ closest() er forkert 694 // (den fandt før den GLOBALT første form = listens, hvorved 695 // scor's felter blev [form]-associeret til listens form og aldrig 696 // submittet). Giv hver form et UNIKT id pr. sub. 697 // Fallback Mà IKKE være et opdigtet navn: 'list' findes ikke i 698 // serverens QSORT_LISTS-whitelist, sÃ¥ save_qsort afviste 699 // gemningen TAVST â filteret sÃ¥ ud til at virke og var væk ved 700 // næste indlæsning. Fald tilbage pÃ¥ formens eget active-felt 701 // (den kender sit sub), og ellers _qsort_active. 702 const ownSub = (filterWrap && filterWrap.getAttribute('data-qsort-sub')) 703 || (() => { 704 const f = filterWrap && filterWrap.querySelector('form[name="qsort"] input[name=active]'); 705 return (f && f.value) || _lists._qsort_active || ''; 706 })(); 707 708 // Søgeresultater: kriterierne bor i søgeformen, og serveren 709 // skipper qsort-FILTRENE for search (skip_filter) â docken 710 // mÃ¥ kun tilbyde sortering, som pÃ¥ desktop 711 if (ownSub === 'search') { 712 btn.classList.add('bf-qsort-sortonly'); 713 qs.classList.add('bf-qsort-sortonly'); 714 } 715 716 // Docken deles af alle qsort-filtre â kun scorechattens mÃ¥ 717 // rammes af skjul-valget (body.bf-chat-qsort-hidden) 718 dock.classList.toggle('bf-qsort-dock-chat', ownSub === 'scor'); 719 const qsortForm = (filterWrap && filterWrap.querySelector('form[name="qsort"]')) 720 || qs.closest('form[name="qsort"]') 721 || filterWrap.closest('form[name="qsort"]'); 722 if (qsortForm) { 723 if (!qsortForm.id) qsortForm.id = 'qsort_form_' + ownSub; 724 qs.querySelectorAll('input, select, textarea').forEach((el) => { 725 el.setAttribute('form', qsortForm.id); 726 }); 727 } 728 if (qs.parentNode !== bar) { 729 bar.insertBefore(qs, dock); 730 } 731 732 const filterNoMode = filterWrap && filterWrap.getAttribute('data-qsort-nomode') === '1'; 733 if (!isStatus && !filterNoMode) { 734 const visCell = _template.loadById('template_qsort_vis_cell')[0]; 735 const renderVis = () => { 736 const isGalleri = _lists._qsortForm(ownSub).find("input[name=qsort_mode]").val() === '1'; 737 const ic = visCell.querySelector('.bf-qsort-ic'); 738 ic.innerHTML = '<span data-auto="icon" data-icon="' 739 + (isGalleri ? 'mob-vis-list' : 'mob-vis-grid') 740 + '"></span>'; 741 visCell.title = isGalleri ? 'Vis som liste' : 'Vis som galleri'; 742 }; 743 visCell.addEventListener('click', (ev) => { 744 ev.stopPropagation(); 745 const isGalleri = _lists._qsortForm(ownSub).find("input[name=qsort_mode]").val() === '1'; 746 _lists.qsort_mode(isGalleri ? 'oversigt' : 'galleri', ownSub); 747 _lists.qsort_submit(ownSub); 748 renderVis(); 749 }); 750 renderVis(); 751 dock.appendChild(visCell); 752 753 // bf:qsort-updated (qsort_update) er det præcise signal â 754 // den gamle MutationObserver kørte renderVis ved HVER 755 // mutation i filteret (inkl. ikon-loads) og blev aldrig 756 // disconnectet. Selvoprydning nÃ¥r cellen forsvinder. 757 const onQsortUpdated = () => { 758 if (!visCell.isConnected) { 759 document.removeEventListener('bf:qsort-updated', onQsortUpdated); 760 return; 761 } 762 renderVis(); 763 }; 764 document.addEventListener('bf:qsort-updated', onQsortUpdated); 765 } 766 767 const resetCell = _template.loadById('template_qsort_reset_cell')[0]; 768 resetCell.addEventListener('click', (ev) => { 769 ev.stopPropagation(); 770 _lists.qsort_reset(ownSub); 771 }); 772 dock.appendChild(resetCell); 773 } 774 775 const wiredSub = (filterWrap && filterWrap.getAttribute('data-qsort-sub')) 776 || (qs.closest('#qsort_filter') && qs.closest('#qsort_filter').getAttribute('data-qsort-sub')); 777 if (wiredSub && _lists.qsort_update) { 778 _lists.qsort_update(wiredSub); 779 } 780 781 renderMobileQsortDock(); 782 783 const inStatusPane = !!btn.closest('.bf-pane--status'); 784 const btnPaneEl = btn.closest('.bf-pane--content, .bf-pane--status'); 785 const ownFilter = btnPaneEl && btnPaneEl.querySelector('#qsort_filter'); 786 const btnNoMode = !!ownFilter && ownFilter.getAttribute('data-qsort-nomode') === '1'; 787 const noSort = inStatusPane || btnNoMode; 788 if (!noSort) { 789 btn.querySelectorAll('[data-helper]').forEach((cell) => { 790 cell.addEventListener('contextmenu', (ev) => ev.preventDefault()); 791 }); 792 const fireSort = (cell) => { 793 const up = cell.getAttribute('data-sort-up'); 794 const dn = cell.getAttribute('data-sort-down'); 795 const sortSub = ownFilter ? ownFilter.getAttribute('data-qsort-sub') : null; 796 const $sort = _lists._qsortForm(sortSub).find('input[name=sort]'); 797 const cur = String($sort.val() || ''); 798 const next = (cur === up) ? dn : up; 799 $sort.val(next); 800 renderMobileQsortDock(); 801 renderSortBar(); 802 _lists.qsort_submit(sortSub); 803 }; 804 this._wireLongPress(btn, { 805 selector: '[data-helper]', 806 onLongPress: (cell) =>
806 fireSort(cell), 807 }); 808 809 const sortBar = document.createElement('div'); 810 sortBar.className = 'bf-qsort-sortbar'; 811 qs.appendChild(sortBar); 812 813 const CELL_LBL = { 814 qsort_age_display: 'alder', 815 qsort_gender_display: 'køn', 816 qsort_area_display: 'område', 817 qsort_target_display: 'søger', 818 }; 819 function renderSortBar() { 820 const map = { 821 age: 'qsort_age_display', 822 gender: 'qsort_gender_display', 823 area: 'qsort_area_display', 824 target: 'qsort_target_display', 825 }; 826 const helper = map[document.body.dataset.bfQsortOnly]; 827 const cell = helper ? btn.querySelector('[data-helper="' + helper + '"]') : null; 828 if (!cell) return; 829 const sortSub = ownFilter ? ownFilter.getAttribute('data-qsort-sub') : null; 830 const cur = String(_lists._qsortForm(sortSub).find('input[name=sort]').val() || ''); 831 sortBar.innerHTML = ''; 832 const lbl = document.createElement('span'); 833 lbl.className = 'bf-qsort-sortbar-lbl'; 834 lbl.textContent = 'Sortér:'; 835 sortBar.appendChild(lbl); 836 [['up', cell.getAttribute('data-sort-up')], 837 ['down', cell.getAttribute('data-sort-down')]].forEach(([dir, val]) => { 838 const chip = document.createElement('button'); 839 chip.type = 'button'; 840 chip.className = 'bf-qsort-sortchip' + (cur === val ? ' is-active' : ''); 841 chip.innerHTML = '<span data-auto="icon" data-icon="sort-' + dir + '"></span><span>' 842 + CELL_LBL[helper] + '</span>'; 843 chip.querySelectorAll('[data-auto="icon"]').forEach((el) => _elements.auto_load(el)); 844 chip.addEventListener('click', (ev) => { 845 ev.stopPropagation(); 846 if (cur === val) return; 847 _lists._qsortForm(sortSub).find('input[name=sort]').val(val); 848 renderMobileQsortDock(); 849 renderSortBar(); 850 _lists.qsort_submit(sortSub); 851 }); 852 sortBar.appendChild(chip); 853 }); 854 } 855 btn.addEventListener('click', () => renderSortBar()); 856 } 857 }); 858 }; 859 let submitTimer = null; 860 const scheduleSubmit = () => { 861 if (submitTimer) clearTimeout(submitTimer); 862 submitTimer = setTimeout(() => { 863 submitTimer = null; 864 _lists.qsort_submit(); 865 }, 200); 866 }; 867 document.addEventListener('change', (e) => { 868 if (!document.body.classList.contains('bf-qsort-open')) return; 869 if (!e.target.closest('#qsort_filter_display')) return; 870 if (e.target.closest('[data-change]')) return; 871 scheduleSubmit(); 872 }); 873 874 const ensureAgeSlider = (paneName) => { 875 const paneSel = (paneName === 'status') ? '.bf-pane--status' : '.bf-pane--content'; 876 const paneEl = document.querySelector(paneSel) || document; 877 const display = paneEl.querySelector('#qsort_filter_display') 878 || document.getElementById('qsort_filter_display'); 879 if (!display) return; 880 const ageBox = display.querySelector('.qsort_optionbox:first-child .qsort_optionbox_options');
881 if (!ageBox || ageBox.querySelector('.bf-age-slider')) return; 882 const fromEl = paneEl.querySelector('input[name=qsort_age_from]') || ageBox.querySelector('input[name=qsort_age_from]'); 883 const toEl = paneEl.querySelector('input[name=qsort_age_to]') || ageBox.querySelector('input[name=qsort_age_to]'); 884 const $from = $(fromEl); 885 const $to = $(toEl); 886 ageBox.insertAdjacentHTML('afterbegin', 887 _elements.age_slider_markup($from.val(), $to.val(), false)); 888 _elements.age_slider_bind(ageBox.querySelector('.bf-age-slider'), (from, to) => { 889 $from.val(from); 890 $to.val(to); 891 if (fromEl) fromEl.dispatchEvent(new Event('change', { bubbles: true })); 892 }); 893 }; 894 895 wireQsort(); 896 const syncDockVisibility = () => { 897 const pane = document.querySelector('.bf-pane--content'); 898 const hasList = !!contentScroll.querySelector('#qsort_filter'); 899 const dock = pane && pane.querySelector('.bf-qsort-filter-dock'); 900 const sheet = pane && pane.querySelector('#bf_content_bottombar > #qsort_filter_display'); 901 const profMenu = pane && pane.querySelector('#if_profile_menu'); 902 const profCont = pane && pane.querySelector('#if_profile_content'); 903 const profileVisible = (el) => el && el.offsetParent !== null 904 && getComputedStyle(el).display !== 'none'; 905 const overlaid = profileVisible(profMenu) || profileVisible(profCont); 906 if (!hasList && dock) { 907 dock.remove(); 908 } else if (dock) { 909 dock.style.display = overlaid ? 'none' : ''; 910 if (!overlaid && window._mobiletips) { 911 _mobiletips.qsortTip(); 912 } 913 } 914 if (!hasList && sheet) { 915 sheet.remove(); 916 if (document.body.classList.contains('bf-qsort-open') 917 && document.body.dataset.bfQsortPane !== 'status') { 918 _mobile.closeQsortSheet(); 919 } 920 } 921 const contentSheetOpen = !!(pane && pane.querySelector('#bf_content_bottombar > #qsort_filter_display')); 922 if (overlaid && contentSheetOpen && document.body.classList.contains('bf-qsort-open') 923 && document.body.dataset.bfQsortPane !== 'status') { 924 _mobile.closeQsortSheet(); 925 } 926 const mailTools = pane && pane.querySelector('#bf_content_bottombar > #mail_tools_mobile'); 927 if (mailTools) { 928 const hasInbox = !!contentScroll.querySelector('.section_mail_inbox, .section_mail_reply'); 929 if (!hasInbox) { 930 mailTools.remove(); 931 } else { 932 mailTools.style.display = overlaid ? 'none' : ''; 933 } 934 } 935 936 syncPagerDock(pane, overlaid); 937 938 const statusPaneEl = document.querySelector('.bf-pane--status'); 939 if (statusPaneEl) { 940 const sBody = statusPaneEl.querySelector('#bf_status_tab_body'); 941 const sBar = statusPaneEl.querySelector('#bf_status_bottombar'); 942 const hasChatFilter = !!(sBody && sBody.querySelector('#qsort_filter')); 943 const hasChatRoom = !!(sBody && sBody.querySelector('.section_chat')); 944 const hasMessenger = !!(sBody && sBody.querySelector('.section_mail_messenger')); 945 const sDock = sBar && sBar.querySelector('.bf-qsort-filter-dock'); 946 const sSheet = sBar && sBar.querySelector('#qsort_filter_display'); 947 const sEditor = sBar && sBar.querySelector('#chat_editor'); 948 if (!hasChatFilter && sDock) sDock.remove(); 949 if (!hasChatFilter && sSheet) { 950 sSheet.remove(); 951 if (document.body.classList.contains('bf-qsort-open') 952 && document.body.dataset.bfQsortPane !== 'content') { 953 _mobile.closeQsortSheet(); 954 } 955 } 956 if (!hasChatRoom && sEditor) sEditor.remove(); 957 if (!hasMessenger && sBar) { 958 sBar.querySelectorAll(':scope > .msgr_compose').forEach((el) => el.remove()); 959 }
960 const hasInboxS = !!(sBody && sBody.querySelector('.section_mail_inbox, .section_mail_reply')); 961 if (!hasInboxS && sBar) { 962 sBar.querySelectorAll(':scope > #mail_tools_mobile').forEach((el) => el.remove()); 963 } 964 } 965 }; 966 967 function syncPagerDock(pane, overlaid) { 968 if (!pane) return; 969 const profMenu = pane.querySelector('#if_profile_menu'); 970 if (!profMenu) return; 971 let dock = profMenu.querySelector(':scope > .bf-profile-pager-dock'); 972 const hasItems = Array.isArray(_lists.current_items) 973 && _lists.current_items.length 974 && _lists.current_name; 975 const memberId = _profile.member ? _profile.member.id : null; 976 const fromList = _nav.fromList === true; 977 if (!overlaid || !hasItems || !memberId || !fromList) { 978 if (dock) dock.remove(); 979 return; 980 } 981 const items = _lists.current_items; 982 const index = items.findIndex((x) => Number(x) === Number(memberId)); 983 const atStart = index <= 0; 984 const moreAvailable = (typeof _lists.infiload_call === 'function') && !_lists.infiload_max; 985 const atEnd = (index < 0) || ((index + 1) === items.length); 986 const nextDisabled = atEnd && !moreAvailable; 987 if (!dock) { 988 dock = document.createElement('div'); 989 dock.className = 'bf-profile-pager-dock'; 990 profMenu.insertBefore(dock, profMenu.firstChild); 991 } 992 // Signatur-guard: genopbyg kun innerHTML + rebind ved reelt skift. 993 // Ellers rev observeren docken ned og bandt lyttere igen på hver 994 // mutation, selv når intet var ændret. 995 const sig = memberId + '|' + _lists.current_name + '|' + atStart + '|' + nextDisabled; 996 if (dock.dataset.bfPagerSig === sig) return; 997 dock.dataset.bfPagerSig = sig; 998 dock.innerHTML = 999 '<div class="bf-pager-text">' 1000 + '<span>Du ser nu resultater for:</span>' 1001 + '<b></b>' 1002 + '</div>' 1003 + '<button type="button" class="bf-qsort-cell bf-qsort-iconcell bf-pager-return" title="Tilbage til listen">' 1004 + '<span class="bf-qsort-ic"><span data-auto="icon" data-icon="mob-list"></span></span>' 1005 + '<span class="bf-qsort-lbl">liste</span>' 1006 + '</button>' 1007 + '<button type="button" class="bf-qsort-cell bf-qsort-iconcell bf-pager-prev"' + (atStart ? ' disabled' : '') + ' title="Forrige">' 1008 + '<span class="bf-qsort-ic"><span data-auto="icon" data-icon="mob-chev-left"></span></span>' 1009 + '<span class="bf-qsort-lbl">forrige</span>' 1010 + '</button>' 1011 + '<button type="button" class="bf-qsort-cell bf-qsort-iconcell bf-pager-next"' + (nextDisabled ? ' disabled' : '') + ' title="N\u00e6ste">' 1012 + '<span class="bf-qsort-ic"><span data-auto="icon" data-icon="mob-chev-right"></span></span>' 1013 + '<span class="bf-qsort-lbl">n\u00e6ste</span>' 1014 + '</button>'; 1015 dock.querySelectorAll('[data-auto="icon"]').forEach((el) => _elements.auto_load(el)); 1016 dock.querySelector('.bf-pager-text b').textContent = _lists.current_name; 1017 dock.querySelector('.bf-pager-return').addEventListener('click', () => _nav.list_return()); 1018 if (!atStart) { 1019 dock.querySelector('.bf-pager-prev').addEventListener('click', () => _nav.list_back()); 1020 } 1021 if (!nextDisabled) { 1022 dock.querySelector('.bf-pager-next').addEventListener('click', () => _nav.list_next()); 1023 } 1024 } 1025 syncDockVisibility(); 1026 // rAF-coalescing: uden det fyrer wireQsort/syncDockVisibility (og for 1027 // tabBody decorateMailRows) på HVER eneste childList-mutation under en
1028 // render-storm. Batch til ét kald pr. frame (som mirrorObserver). 1029 const rafBatch = (fn) => { 1030 let queued = false; 1031 return () => { 1032 if (queued) return; 1033 queued = true; 1034 requestAnimationFrame(() => { queued = false; fn(); }); 1035 }; 1036 }; 1037 new MutationObserver(rafBatch(() => { 1038 wireQsort(); 1039 syncDockVisibility(); 1040 })).observe(contentScroll, { 1041 childList: true, subtree: true, 1042 }); 1043 const decorateMailRows = () => { 1044 document.querySelectorAll('#mail_lines > div[user]').forEach((row) => { 1045 if (row.querySelector(':scope > .bf-mail-row-actions')) return; 1046 const uid = Number(row.getAttribute('user')); 1047 if (!uid) return; 1048 const box = document.createElement('div'); 1049 box.className = 'bf-mail-row-actions'; 1050 [ 1051 { icon: 'mail-pdf', title: 'Gem post som PDF', act: () => _mail.pdf(uid) }, 1052 { icon: 'mail-delete', title: 'Slet samtale', act: () => _mail.delete_thread(uid) }, 1053 ].forEach((it) => { 1054 const b = document.createElement('button'); 1055 b.type = 'button'; 1056 b.title = it.title; 1057 b.innerHTML = '<span data-auto="icon" data-icon="' + it.icon + '"></span>'; 1058 b.querySelectorAll('[data-auto="icon"]').forEach((el) => _elements.auto_load(el)); 1059 b.addEventListener('click', (e) => { 1060 e.preventDefault(); 1061 e.stopPropagation(); 1062 it.act(); 1063 }); 1064 box.appendChild(b); 1065 }); 1066 row.appendChild(box); 1067 }); 1068 }; 1069 new MutationObserver(rafBatch(() => { 1070 wireQsort(); 1071 syncDockVisibility(); 1072 decorateMailRows(); 1073 if (window._mobiletips) _mobiletips.mailTip(); 1074 })).observe(tabBody, { 1075 childList: true, subtree: true, 1076 }); 1077 const attached = new WeakSet(); 1078 const tryAttach = () => { 1079 ['#if_profile_menu', '#if_profile_content'].forEach((sel) => { 1080 const el = contentPane.querySelector(sel); 1081 if (el && !attached.has(el)) { 1082 attached.add(el); 1083 new MutationObserver(() => syncDockVisibility()).observe(el, { 1084 attributes: true, attributeFilter: ['style', 'class'], 1085 }); 1086 syncDockVisibility(); 1087 } 1088 }); 1089 }; 1090 tryAttach(); 1091 new MutationObserver(tryAttach).observe(contentPane, { 1092 childList: true, subtree: true, 1093 }); 1094 document.addEventListener('click', (e) => { 1095 if (!document.body.classList.contains('bf-qsort-open')) return; 1096 if (!e.target.closest('#qsort_filter_display') && !e.target.closest('.bf-qsort-filter-btn')) { 1097 _mobile.closeQsortSheet(); 1098 } 1099 }); 1100 1101 const buildMainNav = (id) => { 1102 const nav = document.createElement('nav'); 1103 nav.id = id; 1104 nav.setAttribute('aria-label', 'Hovedmenu'); 1105 nav.innerHTML = ` 1106 <a href="#" data-bn="menu"> 1107 <span class="bf-bn-icon"> 1108 <span data-auto="icon" data-icon="mob-menu"></span> 1109 </span> 1110 <span>menu</span> 1111 </a> 1112 <a href="#/s/mail/inbox" data-bn="chat"> 1113 <span class="bf-bn-icon"> 1114 <span data-auto="icon" data-icon="mob-post"></span> 1115 <span class="bf-bn-pill hidden"></span> 1116 </span> 1117 <span>post/chat</span> 1118 </a> 1119 <a href="#/s/lounges/lounges" data-bn="lounges" data-bn-area="lounges">
1120 <span data-auto="icon" data-icon="mob-lounges"></span> 1121 <span>lounges</span> 1122 </a> 1123 <a href="#/s/debat/debat" data-bn="debat" data-bn-area="debat"> 1124 <span data-auto="icon" data-icon="mob-debat"></span> 1125 <span>debat</span> 1126 </a> 1127 <a href="#/s/lists/online" data-bn="lister" data-bn-area="lists"> 1128 <span data-auto="icon" data-icon="mob-list"></span> 1129 <span>lister</span> 1130 </a> 1131 <a href="#/s/search/search" data-bn="soeg" data-bn-area="search"> 1132 <span data-auto="icon" data-icon="mob-search"></span> 1133 <span>søg</span> 1134 </a> 1135 `; 1136 nav.querySelectorAll('[data-auto="icon"]').forEach((el) => _elements.auto_load(el)); 1137 return nav; 1138 }; 1139 const bottomNav = buildMainNav('bf_content_bottomnav'); 1140 bottomNav.addEventListener('click', (e) => { 1141 const a = e.target.closest('a[href]'); 1142 if (!a) return; 1143 if (a.dataset.bn !== 'menu' 1144 && (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2)) { 1145 return; 1146 } 1147 e.preventDefault(); 1148 e.stopPropagation(); 1149 const href = a.getAttribute('href'); 1150 if (a.dataset.bn === 'menu') { 1151 toggleMenu(); 1152 return; 1153 } 1154 if (a.dataset.bn === 'chat') { 1155 statusInit = true; 1156 if (!tabBody.firstChild) { 1157 const _histBefore = location.hash; 1158 _elements.constructor.display_target = '#bf_status_tab_body'; 1159 section('mail', 'inbox'); 1160 if (location.hash !== _histBefore) { 1161 history.replaceState(history.state, '', _histBefore || location.pathname); 1162 } 1163 } 1164 swiper.scrollTo({ 1165 left: swiper.scrollWidth - swiper.clientWidth, 1166 behavior: 'smooth', 1167 }); 1168 return; 1169 } 1170 _nav.load(href.charAt(0) === '#' ? href.slice(1) : href); 1171 if (a.dataset.bnArea) openLp(a); 1172 }); 1173 1174 const statusMainNav = buildMainNav('bf_status_mainnav');
1175 statusBottomBar.appendChild(statusMainNav); 1176 bindMailPill(statusMainNav.querySelector('a[data-bn="chat"] .bf-bn-pill')); 1177 statusMainNav.addEventListener('click', (e) => { 1178 const a = e.target.closest('a[href]'); 1179 if (!a) return; 1180 if (a.dataset.bn !== 'menu' 1181 && (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2)) { 1182 return; 1183 } 1184 e.preventDefault(); 1185 e.stopPropagation(); 1186 if (a.dataset.bn === 'menu') { 1187 toggleMenu(); 1188 return; 1189 } 1190 if (a.dataset.bn === 'chat') return; 1191 const href = a.getAttribute('href'); 1192 _nav.load(href.charAt(0) === '#' ? href.slice(1) : href); 1193 if (a.dataset.bnArea) openLp(a); 1194 setTimeout(() => { 1195 swiper.scrollTo({ left: 0, behavior: 'smooth' }); 1196 }, 300); 1197 }); 1198 const bottomBar = document.createElement('div'); 1199 bottomBar.id = 'bf_content_bottombar'; 1200 bottomBar.appendChild(bottomNav); 1201 const profMenuForBar = contentPane.querySelector('#if_profile_menu'); 1202 if (profMenuForBar) bottomBar.insertBefore(profMenuForBar, bottomNav); 1203 contentPane.appendChild(bottomBar); 1204 // Klasse-spejl af :has(> #bf_content_bottombar) (mobile.css) â se 1205 // status-panens spejl ovenfor; bottombaren bygges altid. 1206 contentPane.classList.add('bf-has-bottombar'); 1207 1208 const updateBottomNavVar = () => { 1209 const h = bottomBar.getBoundingClientRect().height; 1210 if (h) document.documentElement.style.setProperty('--bf-bottomnav-h', Math.round(h) + 'px'); 1211 const sh = statusBottomBar.getBoundingClientRect().height; 1212 if (sh) document.documentElement.style.setProperty('--bf-status-bottomnav-h', Math.round(sh) + 'px'); 1213 }; 1214 requestAnimationFrame(() => requestAnimationFrame(updateBottomNavVar)); 1215 if (typeof ResizeObserver !== 'undefined') { 1216 new ResizeObserver(updateBottomNavVar).observe(bottomBar); 1217 new ResizeObserver(updateBottomNavVar).observe(statusBottomBar); 1218 } 1219 window.addEventListener('orientationchange', updateBottomNavVar); 1220 window.addEventListener('resize', updateBottomNavVar); 1221 if (window.visualViewport) { 1222 window.visualViewport.addEventListener('resize', updateBottomNavVar); 1223 } 1224 1225 const KB_MIN = 150; 1226 let vpMax = window.visualViewport ? window.visualViewport.height : window.innerHeight; 1227 let kbFixTimers = []; 1228 let sidsteAnker = null; 1229 const BUND_TOL = 120;
1230 const kbFocusSel ='input:not([type=button]):not([type=submit]):not([type=checkbox]):not([type=radio]), textarea, [contenteditable=""], [contenteditable="true"]'; 1231 const syncKeyboardState = () => { 1232 const vv = window.visualViewport; 1233 if (!vv) return; 1234 if (vv.height > vpMax) vpMax = vv.height; 1235 const dækketVp = vpMax - vv.height; 1236 const dækketLayout = Math.max(0, document.documentElement.clientHeight - vv.height); 1237 const dækket = Math.max(dækketVp, dækketLayout); 1238 const skriverIFelt = !!(document.activeElement 1239 && document.activeElement.matches 1240 && document.activeElement.matches(kbFocusSel)); 1241 const åbent = skriverIFelt && dækket > KB_MIN; 1242 if (document.body.classList.contains('bf-keyboard-open') === åbent) { 1243 if (sidsteAnker) sidsteAnker(); 1244 return; 1245 } 1246 1247 const scrollere = []; 1248 document.querySelectorAll('#bf_mobile_swiper *').forEach((el) => { 1249 if (el.scrollHeight <= el.clientHeight + 1 || el.clientHeight < 50) return; 1250 const oy = getComputedStyle(el).overflowY; 1251 if (oy !== 'auto' && oy !== 'scroll') return; 1252 if (el.offsetParent === null) return; 1253 scrollere.push({ 1254 el: el, 1255 fraBund: el.scrollHeight - el.scrollTop - el.clientHeight 1256 }); 1257 }); 1258 for (let i = scrollere.length - 1; i >= 0; i--) { 1259 if (scrollere.some((a, j) => j !== i && scrollere[i].el.contains(a.el))) { 1260 scrollere.splice(i, 1); 1261 } 1262 } 1263 const genskabScroll = () => { 1264 updateBottomNavVar(); 1265 scrollere.forEach((s) => { 1266 const maxTop = s.el.scrollHeight - s.el.clientHeight; 1267 const mål = (s.fraBund <= BUND_TOL) ? maxTop : (maxTop - s.fraBund); 1268 const clamped = Math.max(0, Math.min(mål, maxTop)); 1269 if (Math.abs(s.el.scrollTop - clamped) > 1) s.el.scrollTop = clamped; 1270 }); 1271 }; 1272 1273 document.body.classList.toggle('bf-keyboard-open', åbent); 1274 1275 kbFixTimers.forEach(clearTimeout); 1276 kbFixTimers = (åbent ? [60, 150, 300, 500] : [60]).map((ms) => 1277 setTimeout(genskabScroll, ms)); 1278 requestAnimationFrame(genskabScroll); 1279 1280 sidsteAnker = åbent ? genskabScroll : null; 1281 }; 1282 if (window.visualViewport) { 1283 window.visualViewport.addEventListener('resize', syncKeyboardState); 1284 window.visualViewport.addEventListener('scroll', syncKeyboardState); 1285 } 1286 document.addEventListener('focusin', () => setTimeout(syncKeyboardState, 250)); 1287 document.addEventListener('focusout', () => setTimeout(syncKeyboardState, 250)); 1288 window.addEventListener('orientationchange', () => { 1289 vpMax = window.visualViewport ? window.visualViewport.height : window.innerHeight; 1290 setTimeout(syncKeyboardState, 300); 1291 }); 1292 syncKeyboardState(); 1293 if (document.fonts && document.fonts.ready) { 1294 document.fonts.ready.then(updateBottomNavVar).catch(() => {}); 1295 } 1296 1297 const lpPanel = document.createElement('nav'); 1298 lpPanel.id = 'bf_lp_panel'; 1299 lpPanel.hidden = true; 1300 bottomBar.insertBefore(lpPanel, bottomBar.firstChild); 1301 1302 if (window._mobiletips) { 1303 _mobiletips.init(); 1304 } 1305 1306 const LP_ICON = { 1307 lists: { 1308 online: 'mob-lp-online', 1309 newest: 'mob-lp-newest', 1310 diary: 'mob-lp-diary', 1311 updated: 'mob-lp-updated', 1312 nearby: 'mob-lp-nearby', 1313 guests: 'mob-lp-guests', 1314 favorites: 'mob-lp-favorites', 1315 interested: 'msgr-reply', 1316 notes: 'mob-lp-notes', 1317 visited: 'mob-lp-visited', 1318 blocked: 'mob-lp-blocked', 1319 }, 1320 lounges: { 1321 lounges: 'mob-lounges', 1322 own: 'mob-lp-guests', 1323 suggest: 'mob-lp-suggest', 1324 }, 1325 debat: { 1326 debat: 'mob-debat', 1327 fokus: 'mob-lp-fokus', 1328 egne: 'mob-lp-guests', 1329 }, 1330 }; 1331 const FALLBACK_ICON_NAME = 'mob-lp-fallback'; 1332 1333 function buildLpPanel(area) { 1334 lpPanel.innerHTML = ''; 1335 if (typeof MENU_REGISTRY === 'undefined' || !MENU_REGISTRY[area]) return; 1336 1337 const { items } = resolveMenu(area, { userId: ((_profile.user && _profile.user.id) != null ? (_profile.user && _profile.user.id) : null) }); 1338 const map = LP_ICON[area] || {}; 1339 lpPanel.dataset.area = area;
1340 items.forEach((it) => { 1341 const row = _template.loadById('template_lp_panel_row')[0]; 1342 row.setAttribute('href', '#' + it.link); 1343 row.dataset.bnList = it.ref; 1344 row.querySelector('[data-auto="icon"]') 1345 .setAttribute('data-icon', map[it.ref] || FALLBACK_ICON_NAME); 1346 row.querySelector('.bf-lp-label').textContent = it.text; 1347 lpPanel.appendChild(row); 1348 }); 1349 1350 if (area === 'lists') { 1351 _api.call('lists', 'latest_counts', 0, (data) => { 1352 if (lpPanel.dataset.area !== 'lists') return; 1353 ['newest', 'diary', 'updated', 'favorites'].forEach((kind) => { 1354 const n = (data && data[kind]) || 0; 1355 if (!n) return; 1356 const icon = lpPanel.querySelector('a[data-bn-list="' + kind + '"] .bf-bn-icon'); 1357 if (!icon || icon.querySelector('.bf-bn-pill')) return; 1358 const pill = document.createElement('span'); 1359 pill.className = 'bf-bn-pill'; 1360 pill.textContent = n; 1361 icon.appendChild(pill); 1362 }); 1363 }); 1364 } 1365 } 1366 1367 function buildSearchLpPanel() { 1368 lpPanel.innerHTML = ''; 1369 lpPanel.dataset.area = 'search'; 1370 const rows = []; 1371 (_search._saved_searches || []).forEach((v) => { 1372 rows.push({ 1373 icon: 'mob-search', 1374 text: v.name, 1375 action: () => { 1376 section('search', 'search'); 1377 setTimeout(() => _search.stored(v.id), 60); 1378 }, 1379 }); 1380 }); 1381 if (_search.activeSearch()) { 1382 rows.push({ 1383 icon: 'mob-search', 1384 text: 'Aktuelle søgeresultater', 1385 action: () => _search.openActiveSearch(), 1386 }); 1387 } 1388 if (!rows.length) { 1389 rows.push({ 1390 icon: 'mob-search', 1391 text: 'Søg', 1392 action: () => section('search', 'search'), 1393 }); 1394 } 1395 rows.forEach((r) => { 1396 const row = _template.loadById('template_lp_panel_row')[0]; 1397 row.setAttribute('href', '#'); 1398 row.dataset.lpAction = 'search'; 1399 row.querySelector('[data-auto="icon"]').setAttribute('data-icon', r.icon); 1400 row.querySelector('.bf-lp-label').textContent = r.text; 1401 row._lpAction = r.action; 1402 lpPanel.appendChild(row); 1403 }); 1404 } 1405 1406 let lpOpenBtn = null; 1407 let lpOpenedAt = 0; 1408 const openLp = (btn) => { 1409 const area = btn.dataset.bnArea; 1410 if (!area) return; 1411 if (area === 'search') buildSearchLpPanel(); 1412 else buildLpPanel(area); 1413 lpOpenBtn = btn; 1414 lpOpenedAt = Date.now(); 1415 lpPanel.hidden = false; 1416 requestAnimationFrame(() => lpPanel.classList.add('is-open')); 1417 }; 1418 const closeLp = () => { 1419 lpPanel.classList.remove('is-open'); 1420 lpOpenBtn = null; 1421 setTimeout(() => { if (!lpPanel.classList.contains('is-open')) lpPanel.hidden = true; }, 200); 1422 }; 1423 1424 lpPanel.addEventListener('click', (e) => { 1425 const a = e.target.closest('a[href]'); 1426 if (!a) return; 1427 e.preventDefault(); 1428 e.stopPropagation(); 1429 const href = a.getAttribute('href').replace(/^#/, ''); 1430 closeLp(); 1431 _nav.load(href); 1432 }); 1433 1434 const LP_MS = 300; 1435 const LP_TOL = 10; 1436 bottomNav.querySelectorAll('a[data-bn-area]').forEach((btn) => { 1437 this._wireLongPress(btn, { 1438 ms: LP_MS, tol: LP_TOL, 1439 onLongPress: () => openLp(btn), 1440 }); 1441 btn.addEventListener('click', (e) => { 1442 if (lpPanel.classList.contains('is-open') && lpOpenBtn === btn) { 1443 e.preventDefault(); 1444 e.stopPropagation(); 1445 e.stopImmediatePropagation(); 1446 closeLp(); 1447 } 1448 }, true); 1449 }); 1450 1451 document.addEventListener('pointerdown', (e) => { 1452 if (!lpPanel.classList.contains('is-open')) return; 1453 if (lpPanel.contains(e.target)) return; 1454 if (lpOpenBtn && lpOpenBtn.contains(e.target)) return; 1455 closeLp(); 1456 }); 1457 if (contentScroll) { 1458 contentScroll.addEventListener('scroll', () => { 1459 if (!lpPanel.classList.contains('is-open')) return; 1460 if (Date.now() - lpOpenedAt < 1200) return; 1461 closeLp(); 1462 }, { passive: true });
1463 } 1464 1465 const PROFILE_TAB_LP = { 1466 diary: (ctx) => [ 1467 { icon: 'mob-lp-diary', text: 'Egne indlæg', href: '#/s/profile/diary/' + ctx.own }, 1468 { icon: 'note', text: 'Skriv nyt indlæg', action: () => _diary.write_new() }, 1469 ], 1470 pictures: (ctx) => [ 1471 { icon: 'mob-pm-pictures', text: 'Mine billeder', href: '#/s/pictures/pictures/member/' + ctx.own }, 1472 { icon: 'msgr-attach', text: 'Tilføj', action: () => _pictures.open_upload() }, 1473 { icon: 'folder', text: 'Mapper', action: () => _pictures.folders_manage() }, 1474 ], 1475 guestbook: (ctx) => { 1476 if (ctx.viewingOwn) { 1477 return [ 1478 { icon: 'mob-pm-guestbook', text: 'Min gæstebog', action: () => section('profile', 'guestbook', { member: ctx.own, which: 'other' }) }, 1479 { icon: 'reply', text: 'Mine indlæg hos andre', action: () => section('profile', 'guestbook', { member: ctx.own, which: 'own' }) }, 1480 ]; 1481 } 1482 const navn = ctx.viewedName || 'brugerens'; 1483 const genitiv = navn + (/s$/i.test(navn) ? "'" : 's'); 1484 return [ 1485 { icon: 'mob-pm-guestbook', text: genitiv + ' gæstebog', action: () => section('profile', 'guestbook', { member: ctx.viewedId, which: 'other' }) }, 1486 { icon: 'reply', text: genitiv + ' indlæg hos andre', action: () => section('profile', 'guestbook', { member: ctx.viewedId, which: 'own' }) }, 1487 { icon: 'note', text: 'Skriv i gæstebogen', action: () => _guestbook.write_new(ctx.viewedId) }, 1488 ]; 1489 }, 1490 }; 1491 1492 const OWN_ONLY_TABS = new Set(['diary', 'pictures']); 1493 1494 const viewedMemberId = () => { 1495 const li = document.querySelector('#if_profile_menu li[data-ref][data-link]'); 1496 const m = ((li ? li.getAttribute('data-link') : '') || '').match(/\/(\d+)(?:\/|$)/); 1497 return m ? Number(m[1]) : null; 1498 }; 1499 1500 const viewingOwnProfile = () => { 1501 const own = (_profile.user && _profile.user.id); 1502 if (!own) return false; 1503 const vid = viewedMemberId(); 1504 return vid !== null ? vid === Number(own) : false; 1505 }; 1506 1507 const tabHasLp = (ref) => { 1508 if (!PROFILE_TAB_LP[ref]) return false; 1509 if (OWN_ONLY_TABS.has(ref)) return viewingOwnProfile(); 1510 return true; 1511 }; 1512 1513 const openProfileTabLp = (tab, ref) => { 1514 const own = (_profile.user && _profile.user.id); 1515 const ctx = { 1516 own, 1517 viewingOwn: viewingOwnProfile(), 1518 viewedId: viewedMemberId(), 1519 viewedName: (_profile.member && _profile.member.username), 1520 }; 1521 const rows = (PROFILE_TAB_LP[ref] || (() => []))(ctx); 1522 if (!rows.length) return; 1523 lpPanel.innerHTML = ''; 1524 lpPanel.dataset.area = ref; 1525 rows.forEach((r, i) => { 1526 const row = _template.loadById('template_lp_panel_row')[0]; 1527 row.setAttribute('href', r.href || '#'); 1528 if (r.action) row.dataset.lpAction = ref + ':' + i; 1529 row.querySelector('[data-auto="icon"]').setAttribute('data-icon', r.icon); 1530 row.querySelector('.bf-lp-label').textContent = r.text; 1531 row._lpAction = r.action || null; 1532 lpPanel.appendChild(row); 1533 }); 1534 lpOpenBtn = tab; 1535 lpOpenedAt = Date.now(); 1536 lpPanel.hidden = false; 1537 requestAnimationFrame(() => lpPanel.classList.add('is-open')); 1538 }; 1539 1540 lpPanel.addEventListener('click', (e) => { 1541 const row = e.target.closest('a[data-lp-action]'); 1542 if (!row || typeof row._lpAction !== 'function') return; 1543 e.preventDefault(); 1544 e.stopPropagation(); 1545 const fn = row._lpAction; 1546 closeLp(); 1547 fn(); 1548 }, true); 1549 1550 this._wireLongPress(document, { 1551 selector: '#if_profile_menu li[data-ref]', 1552 dedupeKey: 'bfProfileTabLp', 1553 shouldStart: (tab) => tabHasLp(tab.getAttribute('data-ref')), 1554 onLongPress: (tab) => openProfileTabLp(tab, tab.getAttribute('data-ref')), 1555 }); 1556 1557 document.addEventListener('click', (e) => { 1558 const tab = e.target.closest('#if_profile_menu li[data-ref]'); 1559 if (!tab) return; 1560 const ref = tab.getAttribute('data-ref'); 1561 if (!tabHasLp(ref)) return; 1562 if (lpPanel.classList.contains('is-open') && lpOpenBtn === tab) { 1563 closeLp(); 1564 return; 1565 } 1566 openProfileTabLp(tab, ref); 1567 }); 1568 1569 bindMailPill(bottomNav.querySelector('a[data-bn="chat"] .bf-bn-pill')); 1570 1571 const mailSheet = document.createElement('nav'); 1572 mailSheet.id = 'bf_mail_actions'; 1573 mailSheet.hidden = true; 1574 const statusBar = document.querySelector('#bf_status_bottombar') || bottomBar; 1575 statusBar.insertBefore(mailSheet, statusBar.firstChild); 1576 1577 const closeMailSheet = () => { 1578 mailSheet.classList.remove('is-open'); 1579 mailSheet.classList.remove('bf-mail-sheet--insert'); 1580 setTimeout(() => { if (!mailSheet.classList.contains('is-open')) mailSheet.hidden = true; }, 200); 1581 }; 1582 1583 const renderSheetShell = (title, sheetClass) => { 1584 if (sheetClass) mailSheet.classList.add(sheetClass); 1585 else mailSheet.classList.remove('bf-mail-sheet--insert'); 1586 mailSheet.innerHTML = ''; 1587 _template.loadById('template_mail_sheet_shell').appendTo(mailSheet); 1588 const titleEl = mailSheet.querySelector('.bf-mail-actions-title'); 1589 if (title) titleEl.textContent = title; 1590 else titleEl.remove(); 1591 return mailSheet.querySelector('.bf-mail-actions-list'); 1592 }; 1593 1594 const openSheet = () => { 1595 mailSheet.hidden = false; 1596 requestAnimationFrame(() => mailSheet.classList.add('is-open')); 1597 }; 1598 1599 const appendActionRows = (listEl, items) => {
1600 items.forEach((it, i) => { 1601 const row = _template.loadById('template_mail_action_row')[0]; 1602 row.dataset.bfAct = String(i); 1603 if (it.disabled) row.classList.add('is-disabled'); 1604 row.querySelector('[data-auto="icon"]').setAttribute('data-icon', it.icon); 1605 row.querySelector('.bf-mail-action-label').textContent = it.label; 1606 if (it.count != null) { 1607 const pill = document.createElement('span'); 1608 pill.className = 'count'; 1609 pill.textContent = it.count; 1610 row.querySelector('.bf-pm-icon').appendChild(pill); 1611 } 1612 row.addEventListener('click', (e) => { 1613 e.preventDefault(); 1614 e.stopPropagation(); 1615 if (it.disabled) return; 1616 closeMailSheet(); 1617 if (typeof it.click === 'function') it.click(); 1618 }); 1619 listEl.appendChild(row); 1620 }); 1621 }; 1622 1623 const showActionSheet = (title, items) => { 1624 if (!items.length) return; 1625 appendActionRows(renderSheetShell(title), items); 1626 openSheet(); 1627 }; 1628 1629 this.mailToolsSheet = (title, items, ctxEl) => { 1630 const contentBar = document.getElementById('bf_content_bottombar'); 1631 const iContent = ctxEl && !ctxEl.closest('.bf-pane--status'); 1632 const host = (iContent && contentBar) ? contentBar : statusBar; 1633 1634 if (mailSheet.parentElement !== host) { 1635 host.insertBefore(mailSheet, host.firstChild); 1636 } 1637 1638 showActionSheet(title, items); 1639 }; 1640 1641 const openInboxRowSheet = (row) => { 1642 const uid = Number(row.getAttribute('user')); 1643 if (!uid) return; 1644 1645 const items = [ 1646 { label: 'Gem som PDF', icon: 'mail-pdf', click: () => _mail.pdf(uid) }, 1647 { label: 'Slet samtale', icon: 'mail-delete', click: () => _mail.delete_thread(uid) }, 1648 ]; 1649 1650 showActionSheet('Værktøjer', items); 1651 }; 1652 1653 const openMessageSheet = (msgRow) => { 1654 const mailEl = msgRow.querySelector('.mail_body[mail], .mail_attachment[mail]'); 1655 const mailId = mailEl ? Number(mailEl.getAttribute('mail')) : 0; 1656 const hasDelete = !!msgRow.querySelector('.mail_options .mail_opt_delete_single, .mail_options span[title*="Slet"]'); 1657 if (!hasDelete && !mailId) return; 1658 1659 const copyMessageText = () => { 1660 const body = msgRow.querySelector('.mail_body'); 1661 if (!body) return; 1662 const txt = (body.textContent || '').trim(); 1663 if (!txt) return; 1664 1665 const execFallback = () => { 1666 const ta = document.createElement('textarea'); 1667 ta.value = txt; 1668 ta.setAttribute('readonly', ''); 1669 ta.style.position = 'fixed'; 1670 ta.style.opacity = '0'; 1671 document.body.appendChild(ta); 1672 ta.select(); 1673 try { document.execCommand('copy'); } catch (_) {} 1674 document.body.removeChild(ta); 1675 }; 1676 if ((navigator.clipboard && navigator.clipboard.writeText)) { 1677 navigator.clipboard.writeText(txt).catch(execFallback); 1678 } else { 1679 execFallback(); 1680 } 1681 }; 1682 1683 const items = [{ label: 'Kopier tekst', icon: 'mail-copy', click: copyMessageText }]; 1684 if (mailId) { 1685 const own = msgRow.classList.contains('mail_from_me') || !!msgRow.closest('.mail_from_me'); 1686 items.push({ label: 'Slet besked', icon: 'mail-delete', click: () => _mail.delete_single(mailId, own) }); 1687 } 1688 showActionSheet('Værktøjer', items); 1689 }; 1690 1691 const openEmoteSheet = (textarea) => { 1692 if (!textarea) return; 1693 const listEl = renderSheetShell('Smileys', 'bf-mail-sheet--insert'); 1694 const grid = document.createElement('div'); 1695 grid.className = 'bf-emote-grid';
1696 _elements.emote_codes().forEach((code) => { 1697 const a = document.createElement('a'); 1698 a.href = '#'; 1699 a.innerHTML = `<img class="emote-svg" src="${_elements.emote_url(code)}" alt=":${code}:" />`; 1700 a.addEventListener('click', (e) => { 1701 e.preventDefault(); 1702 e.stopPropagation(); 1703 _elements.emote_insert(code, textarea); 1704 closeMailSheet(); 1705 }); 1706 grid.appendChild(a); 1707 }); 1708 listEl.appendChild(grid); 1709 openSheet(); 1710 }; 1711 1712 const openReplySheet = (textarea) => { 1713 if (!textarea) return; 1714 _api.call('mail', 'reply_list', 0, (data) => { 1715 if (data === 'error') { 1716 const listEl = renderSheetShell('Standardsvar', 'bf-mail-sheet--insert'); 1717 const empty = document.createElement('div'); 1718 empty.className = 'bf-reply-empty'; 1719 empty.textContent = 'Kræver abonnement'; 1720 listEl.appendChild(empty); 1721 openSheet(); 1722 return; 1723 } 1724 if (!data || typeof data !== 'object') return; 1725 const listEl = renderSheetShell('Standardsvar', 'bf-mail-sheet--insert'); 1726 const list = document.createElement('div'); 1727 list.className = 'bf-reply-list'; 1728 Object.values(data).forEach((v) => { 1729 const id = Number(v.id); 1730 if (!id) return; 1731 const a = document.createElement('a'); 1732 a.href = '#'; 1733 a.textContent = String(v.navn || ''); 1734 a.addEventListener('click', (e) => { 1735 e.preventDefault(); 1736 e.stopPropagation(); 1737 _mail.standard_reply_insert(id, textarea); 1738 closeMailSheet(); 1739 }); 1740 list.appendChild(a); 1741 }); 1742 listEl.appendChild(list); 1743 openSheet(); 1744 }); 1745 }; 1746 1747 const wireMailLpOn = (host, rowSelector, openFn, datasetKey) => { 1748 if (!host || host.dataset[datasetKey] === '1') return; 1749 this._wireLongPress(host, { 1750 selector: rowSelector, 1751 dedupeKey: datasetKey, 1752 onLongPress: (row) => openFn(row), 1753 }); 1754 host.addEventListener('scroll', () => { 1755 if (mailSheet.classList.contains('is-open')) closeMailSheet(); 1756 }, { passive: true }); 1757 }; 1758 wireMailLpOn(contentScroll, '#mail_lines > div', openInboxRowSheet, 'bfMailLpInbox'); 1759 wireMailLpOn(tabBody, '#mail_lines > div', openInboxRowSheet, 'bfMailLpInbox'); 1760 wireMailLpOn(tabBody, '#mail_read > .mail_msg', openMessageSheet, 'bfMailLpMessage'); 1761 1762 const openFormatSheet = (textarea) => { 1763 if (!textarea) return; 1764 showActionSheet('Formatering', [ 1765 { label: 'Fed: *tekst*', icon: 'format-fed', click: () => _elements.format_apply(textarea, '*') }, 1766 { label: 'Kursiv: /tekst/', icon: 'format-kursiv', click: () => _elements.format_apply(textarea, '/') }, 1767 { label: 'Understreget: _tekst_', icon: 'format-understreget', click: () => _elements.format_apply(textarea, '_') }, 1768 { label: 'Omtal profil: #profilnavn#', icon: 'format-navn', click: () => _elements.format_apply(textarea, '#') }, 1769 ]); 1770 }; 1771 1772 const wireMsgrInsertSheets = (host) => { 1773 if (!host || host.dataset.bfMsgrInsert === '1') return; 1774 host.dataset.bfMsgrInsert = '1'; 1775 host.addEventListener('click', (e) => { 1776 const btn = e.target.closest('.msgr_tool_smiley, .msgr_tool_reply, .msgr_tool_format'); 1777 if (!btn) return; 1778 const compose = host.querySelector(':scope > .msgr_compose'); 1779 if (!compose || !compose.contains(btn)) return; 1780 const textarea = compose.querySelector('#mailtxt'); 1781 if (!textarea) return; 1782 e.preventDefault(); 1783 e.stopPropagation(); 1784 e.stopImmediatePropagation(); 1785 if (btn.classList.contains('msgr_tool_smiley')) { 1786 openEmoteSheet(textarea); 1787 } else if (btn.classList.contains('msgr_tool_format')) { 1788 openFormatSheet(textarea); 1789 } else { 1790 openReplySheet(textarea); 1791 } 1792 }, true); 1793 }; 1794 wireMsgrInsertSheets(statusBar); 1795 1796 document.addEventListener('pointerdown', (e) => { 1797 if (!mailSheet.classList.contains('is-open')) return; 1798 if (mailSheet.contains(e.target)) return; 1799 closeMailSheet(); 1800 }); 1801 1802 const $guestSrc = document.getElementById('guest_container'); 1803 const $favSrc = document.getElementById('favorite_container'); 1804 1805 subSlot.innerHTML = ` 1806 <section class="bf-mirror" data-mirror="guests"> 1807 <a href="#/s/lists_own/guests" class="bf-mirror-heading">dine gæster<span class="left_arrow"></span></a> 1808 <div class="bf-mirror-list" id="bf_mirror_guests"></div> 1809 </section> 1810 <section class="bf-mirror" data-mirror="favorites"> 1811 <a href="#/s/lists_own/favorites" class="bf-mirror-heading">favoritter<span class="left_arrow"></span></a> 1812 <div class="bf-mirror-list" id="bf_mirror_favorites"></div> 1813 </section> 1814 <section class="bf-mirror" data-mirror="logins">
1815 <span class="bf-mirror-heading">gemte logins</span> 1816 <div class="bf-mirror-list" id="bf_mirror_logins"></div> 1817 </section> 1818 `; 1819 const $mirrorGuests = subSlot.querySelector('#bf_mirror_guests'); 1820 const $mirrorFavs = subSlot.querySelector('#bf_mirror_favorites'); 1821 const $mirrorLogins = subSlot.querySelector('#bf_mirror_logins'); 1822 1823 const $loginsSection = subSlot.querySelector('[data-mirror="logins"]'); 1824 if ($loginsSection && menuRight) { 1825 menuRight.insertBefore($loginsSection, menuRight.firstChild); 1826 } 1827 1828 const wireMirrorClicks = (host, prefix) => { 1829 host.addEventListener('click', (e) => { 1830 const row = e.target.closest('.link[data-mirror-of^="' + prefix + '_"]'); 1831 if (!row) return; 1832 e.preventDefault(); 1833 e.stopPropagation(); 1834 const id = parseInt(row.getAttribute('data-mirror-of').slice(prefix.length + 1), 10); 1835 if (!id) return; 1836 _nav.profile_display(id); 1837 document.body.classList.remove('bf-menu-open'); 1838 }); 1839 }; 1840 wireMirrorClicks($mirrorGuests, 'guest_container'); 1841 wireMirrorClicks($mirrorFavs, 'favorite_container'); 1842 1843 function syncMirror(src, dst) { 1844 if (!src) { dst.innerHTML = ''; return; } 1845 // Skjulte rækker = offline (favorite_hide slideUp'er dem) â spejl 1846 // kun de synlige, og alle sammen (gæste-kilden er selv cappet). 1847 const slice = Array.from(src.children).filter((el) => el.id && el.style.display !== 'none'); 1848 dst.innerHTML = ''; 1849 slice.forEach((row) => { 1850 const c = row.cloneNode(true); 1851 c.style.display = ''; 1852 c.setAttribute('data-mirror-of', row.id); 1853 c.removeAttribute('id'); 1854 c.querySelectorAll('[id]').forEach((el) => { 1855 if (!el.closest('svg')) el.removeAttribute('id'); 1856 }); 1857 dst.appendChild(c); 1858 }); 1859 if (!slice.length) { 1860 dst.innerHTML = '<div class="bf-mirror-empty">â ingen â</div>'; 1861 } 1862 } 1863 1864 const $loginsSrc = document.getElementById('logins_saved'); 1865 const syncLoginsMirror = () => { 1866 if (!$mirrorLogins) return; 1867 const rows = $loginsSrc 1868 ? Array.from($loginsSrc.querySelectorAll('li[data-login-saved]')) 1869 .filter((li) => li.style.display !== 'none') 1870 : []; 1871 $mirrorLogins.innerHTML = ''; 1872 rows.forEach((li) => { 1873 const id = parseInt(li.getAttribute('data-login-saved'), 10); 1874 const name = (li.querySelector('a') || { textContent: '' }).textContent; 1875 if (!id || !name) return; 1876 const row = document.createElement('div'); 1877 row.className = 'link'; 1878 const a = document.createElement('a'); 1879 a.href = '#'; 1880 a.textContent = name; 1881 row.appendChild(a); 1882 row.addEventListener('click', (e) => { 1883 e.preventDefault(); 1884 document.body.classList.remove('bf-menu-open'); 1885 _profile.login_saved(id); 1886 }); 1887 $mirrorLogins.appendChild(row); 1888 }); 1889 if ($loginsSection) $loginsSection.style.display = $mirrorLogins.children.length ? '' : 'none'; 1890 }; 1891 1892 const refreshMirrors = () => { 1893 syncMirror($guestSrc, $mirrorGuests); 1894 syncMirror($favSrc, $mirrorFavs); 1895 syncLoginsMirror(); 1896 }; 1897 refreshMirrors(); 1898 1899 let mirrorRaf = 0; 1900 const mirrorObserver = new MutationObserver(() => { 1901 if (mirrorRaf) return; 1902 mirrorRaf = requestAnimationFrame(() => { 1903 mirrorRaf = 0; 1904 refreshMirrors(); 1905 }); 1906 }); 1907 if ($guestSrc) mirrorObserver.observe($guestSrc, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] }); 1908 if ($favSrc) mirrorObserver.observe($favSrc, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] }); 1909 if ($loginsSrc) mirrorObserver.observe($loginsSrc, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] }); 1910 1911 requestAnimationFrame(() => { 1912 swiper.scrollLeft = 0; 1913 }); 1914 1915 let loggedOutFooter = null; 1916 let currentMode = null; 1917 1918 const $toprow = document.querySelector('#toprowcontainer .toprow'); 1919 let blivmedlemSlot = null; 1920 1921 function mountBlivmedlemInHeader() { 1922 if (!$toprow) return; 1923 if (!blivmedlemSlot) { 1924 blivmedlemSlot = document.createElement('div'); 1925 blivmedlemSlot.className = 'bf-header-blivmedlem'; 1926 } 1927 if (blivmedlemSlot.parentElement !== $toprow) { 1928 $toprow.appendChild(blivmedlemSlot); 1929 } 1930 const btn = document.querySelector('.section_login .blivmedlem a') 1931 || document.querySelector('.section_login .blivmedlem'); 1932 if (btn && btn.parentElement !== blivmedlemSlot) { 1933 blivmedlemSlot.appendChild(btn); 1934 } 1935 } 1936 1937 function unmountBlivmedlemFromHeader() { 1938 if (blivmedlemSlot && blivmedlemSlot.parentElement) { 1939 blivmedlemSlot.parentElement.removeChild(blivmedlemSlot); 1940 } 1941 } 1942 1943 function applyLoggedOutLayout() { 1944 if (currentMode === 'out') return; 1945 currentMode = 'out'; 1946 document.body.classList.add('is-logged-out'); 1947 mountBlivmedlemInHeader(); 1948 1949 if (!loggedOutFooter) { 1950 loggedOutFooter = document.createElement('div'); 1951 loggedOutFooter.className = 'bf-loggedout-footer'; 1952 }
1953 let topRow = loggedOutFooter.querySelector('.bf-loggedout-top'); 1954 if (!topRow) { 1955 topRow = document.createElement('div'); 1956 topRow.className = 'bf-loggedout-top'; 1957 loggedOutFooter.appendChild(topRow); 1958 } 1959 if (footer && footer.children.length) topRow.appendChild(footer); 1960 if (installBox) topRow.appendChild(installBox); 1961 1962 if (bottomRow) { 1963 const visits = bottomRow.querySelector('.visits_container'); 1964 if (visits) visits.style.display = 'none'; 1965 if ($profilesBox && $profilesBox.parentElement !== bottomRow) { 1966 bottomRow.insertBefore($profilesBox, bottomRow.firstChild); 1967 } 1968 if (bottomRow.children.length) loggedOutFooter.appendChild(bottomRow); 1969 } 1970 const footerHost = contentScroll || contentPane; 1971 if (footerHost) footerHost.appendChild(loggedOutFooter); 1972 } 1973 1974 function applyLoggedInLayout() { 1975 if (currentMode === 'in') return; 1976 currentMode = 'in'; 1977 document.body.classList.remove('is-logged-out'); 1978 unmountBlivmedlemFromHeader(); 1979 if (footer && menuLeft) menuLeft.appendChild(footer); 1980 if (bottomRow && menuSplit) menuSplit.appendChild(bottomRow); 1981 if (installBox && menuRight) menuRight.appendChild(installBox); 1982 if (bottomRow) { 1983 const visits = bottomRow.querySelector('.visits_container'); 1984 if (visits) visits.style.display = ''; 1985 } 1986 if ($profilesBox && $statusOnlineBox && $profilesBox.parentElement !== $statusOnlineBox) { 1987 $statusOnlineBox.appendChild($profilesBox); 1988 } 1989 if (loggedOutFooter && loggedOutFooter.parentElement) { 1990 loggedOutFooter.parentElement.removeChild(loggedOutFooter); 1991 } 1992 } 1993 1994 document.addEventListener('bf:login-change', (e) => { 1995 if (e.detail && e.detail.loggedIn) applyLoggedInLayout(); 1996 else applyLoggedOutLayout(); 1997 }); 1998 1999 document.addEventListener('bf:login-page-rendered', () => { 2000 if (currentMode === 'out') mountBlivmedlemInHeader(); 2001 }); 2002 2003 const $ifAd = document.getElementById('if_ad'); 2004 if ($ifAd && bottomBar) { 2005 bottomBar.insertBefore($ifAd, bottomBar.firstChild); 2006 } 2007 } 2008 2009 _wireLongPress(host, opts) { 2010 if (!host) return; 2011 const { selector = null, onLongPress, ms = 300, tol = 10, dedupeKey = null, shouldStart = null } = opts || {}; 2012 if (typeof onLongPress !== 'function') return; 2013 if (dedupeKey && host.dataset) { 2014 if (host.dataset[dedupeKey] === '1') return; 2015 host.dataset[dedupeKey] = '1'; 2016 } 2017 2018 let timer = 0, fired = false, x = 0, y = 0, target = null; 2019 const cancel = () => { if (timer) { clearTimeout(timer); timer = 0; } }; 2020 const resolve = (e) => selector ? (e.target.closest ? e.target.closest(selector) : null) : host; 2021 2022 if (!selector && host.classList) host.classList.add('bf-lp-noselect'); 2023 2024 host.addEventListener('pointerdown', (e) => { 2025 const t = resolve(e); 2026 if (!t) return; 2027 if (shouldStart && !shouldStart(t)) return; 2028 if (selector && t.classList) t.classList.add('bf-lp-noselect'); 2029 target = t; fired = false; x = e.clientX; y = e.clientY; 2030 cancel(); 2031 timer = setTimeout(() => { 2032 timer = 0; fired = true; 2033 if (navigator.vibrate) navigator.vibrate(10); 2034 onLongPress(target, e); 2035 }, ms); 2036 }); 2037 host.addEventListener('pointermove', (e) => { 2038 if (!timer) return; 2039 if (Math.hypot(e.clientX - x, e.clientY - y) > tol) cancel(); 2040 }); 2041 ['pointerup', 'pointercancel', 'pointerleave'].forEach((ev) => host.addEventListener(ev, cancel)); 2042 host.addEventListener('click', (e) => { 2043 if (!fired) return; 2044 const t = selector ? (e.target.closest ? e.target.closest(selector) : null) : host; 2045 if (t !== target) return; 2046 e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); 2047 fired = false; 2048 }, true); 2049 host.addEventListener('contextmenu', (e) => { 2050 if (!selector || e.target.closest(selector)) e.preventDefault(); 2051 }); 2052 } 2053 2054 2055 _lockDesktopViewport(isMobileViewport) { 2056 if (isMobileViewport) return; 2057 2058 const app = document.getElementById('boyfriend'); 2059 if (!app) return; 2060 2061 const DESIGN = this._DESKTOP_DESIGN_W; 2062 2063 const apply = () => { 2064 try { 2065 const portrait = window.matchMedia('(orientation: portrait)').matches; 2066 // LAYOUT-viewporten, IKKE window.innerWidth: pinch-zoom ændrer 2067 // innerWidth (og fyrer resize pÃ¥ iOS), sÃ¥ skaleringen ellers 2068 // regner ny scale af den zoomede bredde og slÃ¥s med brugerens 2069 // zoom â det er dét der fÃ¥r siden til at hoppe. 2070 const avail = document.documentElement.clientWidth; 2071 2072 if (portrait && avail < DESIGN) { 2073 const scale = avail / DESIGN; 2074 document.body.classList.add('bf-desktop-zoomed'); 2075 app.style.width = DESIGN + 'px'; 2076 app.style.maxWidth = 'none'; 2077 app.style.boxSizing = 'border-box'; 2078 app.style.transformOrigin = 'top left'; 2079 app.style.transform = 'scale(' + scale + ')'; 2080 app.style.marginLeft = '0'; 2081 app.style.marginRight = '0'; 2082 const h = app.offsetHeight; 2083 app.style.marginBottom = (-(h * (1 - scale))) + 'px'; 2084 } else { 2085 document.body.classList.remove('bf-desktop-zoomed'); 2086 app.style.width = ''; 2087 app.style.maxWidth = ''; 2088 app.style.boxSizing = ''; 2089 app.style.transformOrigin = ''; 2090 app.style.transform = ''; 2091 app.style.marginLeft = ''; 2092 app.style.marginRight = ''; 2093 app.style.marginBottom = ''; 2094 } 2095 } catch (e) { } 2096 }; 2097 2098 setTimeout(apply, 300); 2099 const mq = window.matchMedia('(orientation: portrait)'); 2100 if (mq.addEventListener) mq.addEventListener('change', () => setTimeout(apply, 100)); 2101 else if (mq.addListener) mq.addListener(() => setTimeout(apply, 100)); 2102 window.addEventListener('resize', () => setTimeout(apply, 100)); 2103 } 2104 2105 pwa() { 2106 const ua = window.navigator.userAgent; 2107 const isStandalone = () => window.matchMedia('(display-mode: standalone)').matches 2108 || window.navigator.standalone === true; 2109 const isIos = /iphone|ipad|ipod/i.test(ua); 2110 const isIosSafari = isIos && !/crios|fxios|edgios/i.test(ua); 2111 const isIosOther = isIos && !isIosSafari; 2112 2113 const setHidden = (selector, hidden) => { 2114 document.querySelectorAll(selector).forEach((el) => { el.hidden = hidden; }); 2115 }; 2116 2117 setHidden('[data-action="pwa-install"]', true); 2118 setHidden('[data-pwa-ios-hint]', true); 2119 setHidden('[data-pwa-ios-other]', true); 2120 2121 if (!isStandalone()) { 2122 if (isIosSafari) setHidden('[data-pwa-ios-hint]', false); 2123 else if (isIosOther) setHidden('[data-pwa-ios-other]', false); 2124 } 2125 2126 // beforeinstallprompt fanges nu i constructoren (før dette kald). 2127 // Hvis eventet allerede er kommet, vis knappen med det samme. 2128 if (this._pwaPrompt && !isStandalone()) { 2129 setHidden('[data-action="pwa-install"]', false); 2130 } 2131 2132 window.addEventListener('appinstalled', () => { 2133 this._pwaPrompt = null; 2134 setHidden('[data-action="pwa-install"]', true); 2135 setHidden('[data-pwa-ios-hint]', true); 2136 setHidden('[data-pwa-ios-other]', true); 2137 }); 2138 2139 document.addEventListener('click', (e) => { 2140 const btn = e.target.closest('[data-action="pwa-install"]'); 2141 if (!btn) return; 2142 e.preventDefault(); 2143 if (!this._pwaPrompt) return; 2144 const prompt = this._pwaPrompt; 2145 prompt.prompt(); 2146 const done = () => { 2147 this._pwaPrompt = null; 2148 setHidden('[data-action="pwa-install"]', true); 2149 }; 2150 prompt.userChoice.then(done, done); 2151 }); 2152 2153 if ('serviceWorker' in navigator) { 2154 // pwa() kører efter hello-roundtrippet; er 'load' allerede fyret, 2155 // ville en load-listener aldrig blive kaldt â SW aldrig registreret. 2156 // Registrér direkte hvis dokumentet er færdigloadet, ellers vent. 2157 const registerSw = () => navigator.serviceWorker.register('/sw.js', { scope: '/' }) 2158 .catch((err) => console.warn('SW registration failed:', err));
2159 2160 if (document.readyState === 'complete') { 2161 registerSw(); 2162 } else { 2163 window.addEventListener('load', registerSw, { once: true }); 2164 } 2165 } 2166 } 2167 2168 topbarCounters() { 2169 const root = document.getElementById('bf-topbar-counters'); 2170 const bell = root.querySelector('a[data-counter="notifications"]'); 2171 const pill = bell.querySelector('.bf-tc-pill'); 2172 pill.setAttribute('aria-hidden', 'true'); 2173 2174 const setPill = (n) => { 2175 n = Math.max(0, parseInt(n, 10) || 0); 2176 pill.textContent = n; 2177 pill.style.display = n > 0 ? '' : 'none'; 2178 bell.setAttribute('aria-label', n > 0 ? `notifikationer, ${n} ${n === 1 ? 'ny' : 'nye'}` : 'notifikationer'); 2179 }; 2180 const _nbadge = document.getElementById('notifications_badge'); 2181 setPill(_nbadge ? _nbadge.textContent : undefined); 2182 document.addEventListener('bf:notifications-unread-count', (e) => setPill((e.detail && e.detail.count))); 2183 2184 document.addEventListener('bf:login-change', (e) => { 2185 root.style.display = (e.detail && e.detail.loggedIn) ? '' : 'none'; 2186 }); 2187 if (((_profile && _profile.user) && (_profile && _profile.user).id)) root.style.display = ''; 2188 } 2189}
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.