1/** 2 * Load SPA route content into CitrusOS windows; full-page apps without routes use iframe. 3 * Each window keeps its own DOM and stays painted when unfocused; route destroy runs on close. 4 */ 5(function (global) { 6 'use strict'; 7 8 /** @type {WeakMap<HTMLElement, { routeId: string, route: object, doc: Document, loaded: boolean }>} */ 9 var instances = new WeakMap(); 10 /** @type {HTMLElement|null} */ 11 var focusedContainer = null; 12 /** @type {WeakMap<HTMLElement, boolean>} */ 13 var loadingByContainer = new WeakMap(); 14 /** @type {WeakMap<HTMLElement, Promise<void>>} */ 15 var initInFlight = new WeakMap(); 16 17 function shouldSkipScriptLoad(src) { 18 if (!src) return false; 19 if (src.indexOf('firebase-app-compat') !== -1 && global.firebase) { 20 return true; 21 } 22 if (src.indexOf('firebase-database-compat') !== -1 && 23 global.firebase && typeof global.firebase.database === 'function') { 24 return true; 25 } 26 if (src.indexOf('firebaseConfig.js') !== -1) { 27 if (typeof global.ensureFirebaseApp === 'function') { 28 global.ensureFirebaseApp(); 29 } 30 if (global.firebase && global.firebase.apps && global.firebase.apps.length) { 31 return true; 32 } 33 } 34 return false; 35 } 36 37 function ensureFirebaseReady() { 38 if (typeof global.ensureFirebaseApp === 'function') { 39 global.ensureFirebaseApp(); 40 } 41 return Promise.resolve(); 42 } 43 44 function loadScript(src, opts) { 45 opts = opts || {}; 46 if (shouldSkipScriptLoad(src)) { 47 return Promise.resolve(); 48 } 49 if (global.PupwifeScriptLoad && typeof global.PupwifeScriptLoad.once === 'function') { 50 return global.PupwifeScriptLoad.once(src, opts); 51 } 52 return new Promise(function (resolve, reject) { 53 var typeAttr = opts.type === 'module' ? 'module' : ''; 54 var existing = document.querySelector('script[src="' + src + '"]'); 55 if (existing) { 56 var existingType = existing.getAttribute('type') || ''; 57 if (existingType !== typeAttr) existing = null; 58 } 59 if (existing) { 60 resolve(); 61 return; 62 } 63 var s = document.createElement('script'); 64 s.src = src; 65 s.async = false; 66 if (typeAttr) s.type = typeAttr; 67 s.onload = function () { resolve(); }; 68 s.onerror = function () { reject(new Error('Failed to load ' + src)); }; 69 document.body.appendChild(s); 70 }); 71 } 72 73 function loadRouteScripts(list, type) { 74 if (!list || !list.length) return Promise.resolve(); 75 var chain = Promise.resolve(); 76 for (var i = 0; i < list.length; i++) { 77 (function (src) { 78 chain = chain.then(function () { 79 return loadScript(src, type === 'module' ? { type: 'module' } : {}); 80 }); 81 })(list[i]); 82 } 83 return chain; 84 } 85 86 function ensureRouteScripts(route) { 87 if (!route) return Promise.resolve(); 88 var chain = ensureFirebaseReady(); 89 if (route.scripts && route.scripts.length) { 90 chain = chain.then(function () { return loadRouteScripts(route.scripts); }); 91 } 92 if (route.modules && route.modules.length) { 93 chain = chain.then(function () { return loadRouteScripts(route.modules, 'module'); }); 94 } 95 return chain; 96 } 97 98 function ensureStylesheet(href) { 99 var base = (href || '').split('?')[0].split('#')[0]; 100 if (base) { 101 var links = document.querySelectorAll('link[rel="stylesheet"]'); 102 for (var i = 0; i < links.length; i++) { 103 var existingHref = links[i].getAttribute('href') || ''; 104 if (existingHref.split('?')[0].split('#')[0] === base && existingHref !== href) { 105 links[i].parentNode.removeChild(links[i]); 106 } 107 } 108 } 109 if (document.querySelector('link[href="' + href + '"], link[data-spa-stylesheet="' + href + '"]')) return Promise.resolve(); 110 return new Promise(function (resolve) { 111 var link = document.createElement('link'); 112 link.rel = 'stylesheet'; 113 link.href = href; 114 link.setAttribute('data-spa-stylesheet', href); 115 link.onload = function () { resolve(); }; 116 link.onerror = function () { resolve(); }; 117 document.head.appendChild(link); 118 global.setTimeout(resolve, 2000); 119 }); 120 } 121 122 function ensureRouteStyles(route) { 123 if (global.CitrusStylesPolicy && typeof global.CitrusStylesPolicy.pruneDisallowedHostStyles === 'function') { 124 global.CitrusStylesPolicy.pruneDisallowedHostStyles(); 125 } 126 if (!route || !route.css || !route.css.length) return Promise.resolve(); 127 var cssList = route.css; 128 if (global.CitrusStylesPolicy && typeof global.CitrusStylesPolicy.filterRouteCss === 'function') { 129 cssList = global.CitrusStylesPolicy.filterRouteCss(cssList); 130 } 131 if (!cssList.length) return Promise.resolve(); 132 var chain = Promise.resolve(); 133 for (var i = 0; i < cssList.length; i++) { 134 (function (href) { 135 chain = chain.then(function () { return ensureStylesheet(href); }); 136 })(cssList[i]); 137 } 138 return chain; 139 } 140 141 function isAppShellNode(el) { 142 if (!el || !el.id) return false;
143 var id = el.id; 144 return id === 'page-background' || 145 id === 'aurora-overlay' || 146 id === 'webgl-overlay' || 147 id === 'site-theme-toggle' || 148 id === 'sidebar-menu' || 149 id === 'hamburger-menu-btn'; 150 } 151 152 function shouldSkipBodyChild(el, pageWrap) { 153 if (!el) return true; 154 if (el.tagName === 'SCRIPT' || el.tagName === 'STYLE') return true; 155 if (el === pageWrap) return true; 156 if (isAppShellNode(el)) return true; 157 if (el.id === 'footer-nav' || el.tagName === 'FOOTER') return true; 158 return false; 159 } 160 161 function appendImportedChild(shell, node) { 162 if (!node) return false; 163 shell.appendChild(document.importNode(node, true)); 164 return true; 165 } 166 167 function extractContentRoot(doc) { 168 var body = doc.body; 169 if (!body) return null; 170 171 var shell = document.createElement('div'); 172 shell.setAttribute('data-citrus-app-root', '1'); 173 var hasContent = false; 174 var pageWrap = doc.getElementById('page-wrap'); 175 176 /* Chatroom compact layout, theme tokens, and scroll all scope to #page-wrap â keep the node. */ 177 if (pageWrap && body.classList.contains('page-chatroom')) { 178 var chatRoot = document.importNode(pageWrap, true); 179 chatRoot.setAttribute('data-citrus-app-root', '1'); 180 return chatRoot; 181 } 182 183 if (pageWrap) { 184 var wrapKids = pageWrap.children; 185 for (var i = 0; i < wrapKids.length; i++) { 186 if (isAppShellNode(wrapKids[i])) continue; 187 if (appendImportedChild(shell, wrapKids[i])) hasContent = true; 188 } 189 } 190 191 var bodyKids = body.children; 192 for (var j = 0; j < bodyKids.length; j++) { 193 var kid = bodyKids[j]; 194 if (shouldSkipBodyChild(kid, pageWrap)) continue; 195 if (pageWrap) { 196 if (appendImportedChild(shell, kid)) hasContent = true; 197 } else { 198 if (appendImportedChild(shell, kid)) hasContent = true; 199 } 200 } 201 202 if (hasContent) return shell; 203 204 var source = doc.getElementById('content'); 205 if (!source) { 206 if (pageWrap) { 207 source = pageWrap.querySelector('main') || pageWrap; 208 } else { 209 source = doc.querySelector('main'); 210 } 211 } 212 if (!source) { 213 source = doc.querySelector( 214 '.weather-container, .uvmapping-page, .sort-app, ' + 215 '#normalMapContainer, .letter-app, .mememaker-app, #noise-app' 216 ); 217 } 218 if (!source && doc.getElementById('content')) { 219 source = doc.getElementById('content'); 220 } 221 if (!source) return null; 222 var root = document.importNode(source, true); 223 root.setAttribute('data-citrus-app-root', '1'); 224 return root; 225 } 226 227 function hrefToRouteId(href) { 228 var key = (href || '').replace(/^\//, '').split('?')[0].split('#')[0]; 229 if (!/\.html$/i.test(key)) key += '.html'; 230 return key; 231 } 232 233 function getRoute(routeId) { 234 if (global.PupwifeSpa && global.PupwifeSpa.routes && global.PupwifeSpa.routes[routeId]) { 235 return global.PupwifeSpa.routes[routeId]; 236 } 237 return null; 238 } 239 240 function isKnownDirectMountRoute(routeId) { 241 if (global.CitrusDirectMountRoutes && 242 typeof global.CitrusDirectMountRoutes.isDirectMountRoute === 'function' && 243 global.CitrusDirectMountRoutes.isDirectMountRoute(routeId)) { 244 return true; 245 } 246 var route = getRoute(routeId); 247 return !!(route && route.directMount); 248 } 249 250 function waitForRoute(routeId, maxMs) { 251 maxMs = maxMs || 8000; 252 var started = Date.now(); 253 return new Promise(function (resolve) { 254 function tick() { 255 var route = getRoute(routeId); 256 if (route) { 257 resolve(route); 258 return; 259 } 260 if (Date.now() - started >= maxMs) { 261 resolve(null); 262 return; 263 } 264 global.setTimeout(tick, 50); 265 } 266 tick(); 267 }); 268 } 269 270 var CHATROOM_PALETTE_THEMES = ['default', 'midnight', 'forest', 'sunset', 'banana', 'ocean', 'rose', 'sakura', 'purple']; 271 272 function normalizeChatroomPalette(value) { 273 return CHATROOM_PALETTE_THEMES.indexOf(value) !== -1 ? value : 'default'; 274 } 275 276 /** Citrus chat never runs chatroom.html head scripts â read sitewide theme before route JS boots. */ 277 function readCitrusChatroomThemeFromStorage() { 278 var theme = 'default'; 279 var colorMode = 'dark'; 280 var animations = true; 281 var fontSize = ''; 282 var emojiSize = ''; 283 try { 284 if (global.SiteThemePalettes && typeof global.SiteThemePalettes.readMergedThemeSettings === 'function') { 285 var merged = global.SiteThemePalettes.readMergedThemeSettings(); 286 theme = normalizeChatroomPalette(merged.palette); 287 colorMode = merged.colorMode === 'light' ? 'light' : 'dark'; 288 } 289 var chat = JSON.parse(global.localStorage.getItem('chatroom_settings') || '{}'); 290 if (chat.animations === false) animations = false;
291 if (chat.fontSize) fontSize = chat.fontSize; 292 if (chat.emojiSize) emojiSize = chat.emojiSize; 293 } catch (e) { /* ignore */ } 294 return { 295 theme: normalizeChatroomPalette(theme), 296 colorMode: colorMode === 'light' ? 'light' : 'dark', 297 animations: animations, 298 fontSize: fontSize, 299 emojiSize: emojiSize 300 }; 301 } 302 303 function applyChatroomThemeAttrsToElement(el, settings) { 304 if (!el || !settings) return; 305 el.setAttribute('data-chatroom-theme', settings.theme); 306 el.setAttribute('data-chatroom-color-mode', settings.colorMode); 307 if (settings.fontSize) { 308 el.setAttribute('data-chatroom-font-size', settings.fontSize); 309 } else { 310 el.removeAttribute('data-chatroom-font-size'); 311 } 312 if (settings.emojiSize) { 313 el.setAttribute('data-chatroom-emoji-size', settings.emojiSize); 314 } else { 315 el.removeAttribute('data-chatroom-emoji-size'); 316 } 317 if (settings.animations) { 318 el.classList.add('animations'); 319 } else { 320 el.classList.remove('animations'); 321 } 322 } 323 324 function syncCitrusChatCompactChrome(containerEl) { 325 if (!containerEl) return; 326 var body = containerEl.closest('.citrus-window__body'); 327 containerEl.classList.add('citrus-window__content--chat-compact'); 328 containerEl.classList.remove('citrus-window__content--chat-full'); 329 if (body) { 330 body.classList.add('citrus-window__body--chat-compact'); 331 } 332 if (document.body) { 333 document.body.classList.add('chatroom-citrus-compact'); 334 document.body.classList.remove('chatroom-citrus-full'); 335 } 336 } 337 338 function primeCitrusChatroomShell(containerEl, root) { 339 var settings = readCitrusChatroomThemeFromStorage(); 340 var wrap = null; 341 if (root) { 342 if (root.id === 'page-wrap') wrap = root; 343 else if (root.querySelector) wrap = root.querySelector('#page-wrap'); 344 } 345 if (!wrap && containerEl && containerEl.querySelector) { 346 wrap = containerEl.querySelector('#page-wrap'); 347 } 348 if (wrap) { 349 applyChatroomThemeAttrsToElement(wrap, settings); 350 } 351 if (document.body && document.body.classList.contains('page-citrusos')) { 352 applyChatroomThemeAttrsToElement(document.body, settings); 353 } 354 if (global.ChatroomSpa && typeof global.ChatroomSpa.applyChatroomTheme === 'function') { 355 global.ChatroomSpa.applyChatroomTheme(); 356 } 357 syncCitrusChatCompactChrome(containerEl); 358 } 359 360 function syncShellThemeClass() { 361 var isDark = document.documentElement.classList.contains('dark-mode'); 362 document.body.classList.toggle('dark-mode', isDark); 363 } 364 365 function stripBodyRouteClasses() { 366 var list = document.body.classList; 367 var remove = []; 368 for (var i = 0; i < list.length; i++) { 369 var cls = list[i]; 370 if (cls.indexOf('page-') === 0 && cls !== 'page-citrusos') remove.push(cls); 371 } 372 for (var j = 0; j < remove.length; j++) list.remove(remove[j]); 373 document.body.classList.remove('db-fit-viewport', 'citrusos-app-active'); 374 document.body.classList.remove('studyhall-ready', 'studyhall-loading', 'studyhall-quiz-open'); 375 document.body.removeAttribute('data-studyhall-view'); 376 } 377 378 function parseRouteClasses(route) { 379 if (!route || !route.bodyClass) return []; 380 var parts = route.bodyClass.split(/\s+/); 381 var out = []; 382 for (var i = 0; i < parts.length; i++) { 383 var cls = parts[i]; 384 if (cls && cls !== 'page-citrusos') out.push(cls); 385 } 386 return out; 387 } 388 389 function stripContainerRouteClasses(containerEl) { 390 if (!containerEl || !containerEl.classList) return; 391 var inst = instances.get(containerEl); 392 if (inst && inst.appliedClasses && inst.appliedClasses.length) { 393 for (var i = 0;
393 i < inst.appliedClasses.length; i++) { 394 containerEl.classList.remove(inst.appliedClasses[i]); 395 } 396 inst.appliedClasses = null; 397 } 398 containerEl.classList.remove('citrusos-app-active'); 399 containerEl.removeAttribute('data-studyhall-view'); 400 } 401 402 function applyRouteContainerClass(containerEl, route) { 403 if (!containerEl) return []; 404 stripContainerRouteClasses(containerEl); 405 var applied = parseRouteClasses(route); 406 for (var i = 0; i < applied.length; i++) { 407 containerEl.classList.add(applied[i]); 408 } 409 containerEl.classList.add('citrusos-app-active'); 410 var inst = instances.get(containerEl); 411 if (inst) inst.appliedClasses = applied.concat(['citrusos-app-active']); 412 return applied; 413 } 414 415 function applyShellBodyClass() { 416 stripBodyRouteClasses(); 417 document.body.classList.add('page-citrusos', 'dashboard'); 418 syncShellThemeClass(); 419 } 420 421 function fixAssets(node) { 422 if (!node) return; 423 if (typeof global.fixRelativeSiteAssets === 'function') { 424 global.fixRelativeSiteAssets(node); 425 return; 426 } 427 var rootRelative = /^(?:images|icons|cursors|audio|video|fonts|marsey)\//i; 428 function fixEl(el) { 429 if (!el || !el.getAttribute) return; 430 var src = el.getAttribute('src'); 431 if (!src || src.charAt(0) === '/' || /^(https?:)?\/\//i.test(src)) return; 432 if (rootRelative.test(src) || src.indexOf('./') === 0) { 433 el.setAttribute('src', '/' + src.replace(/^\.\//, '')); 434 } 435 } 436 fixEl(node); 437 if (!node.querySelectorAll) return; 438 var els = node.querySelectorAll('[src]'); 439 for (var i = 0; i < els.length; i++) fixEl(els[i]); 440 } 441 442 function markInitialized(containerEl, on) { 443 if (!containerEl) return; 444 var inst = instances.get(containerEl); 445 if (inst) inst.initialized = !!on; 446 } 447 448 function runDestroy(route, routeId, containerEl) { 449 if (routeId === 'chatroom.html' && global.__chatroomCitrusReparentActive) { 450 return Promise.resolve(); 451 } 452 var inst = containerEl ? instances.get(containerEl) : null; 453 if (!route || typeof route.destroy !== 'function') { 454 if (inst) inst.initialized = false; 455 return Promise.resolve(); 456 } 457 var destroyOptions = null; 458 if (routeId === 'chatroom.html' && 459 global.document.body && 460 global.document.body.classList.contains('page-citrusos')) { 461 destroyOptions = { citrusWindowClose: true }; 462 } 463 var chain; 464 try { 465 var result = route.destroy(containerEl || null, destroyOptions); 466 if (result && typeof result.then === 'function') { 467 chain = result.catch(function (e) { 468 console.warn('[CitrusWindowLoader] destroy failed', e); 469 }); 470 } else { 471 chain = Promise.resolve(); 472 } 473 } catch (e) { 474 console.warn('[CitrusWindowLoader] destroy failed', e); 475 chain = Promise.resolve(); 476 } 477 return chain.then(function () { 478 if (inst) inst.initialized = false; 479 }); 480 } 481 482 function setRouteScope(containerEl) { 483 if (global.CitrusRouteScope && typeof global.CitrusRouteScope.setActiveScope === 'function') { 484 global.CitrusRouteScope.setActiveScope(containerEl || null); 485 } else { 486 global.__citrusRouteScopeEl = containerEl || null; 487 } 488 } 489 490 function ensureCitrusToolDeps() { 491 var chain = Promise.resolve(); 492 if (!global.CitrusPrefs) { 493 chain = chain.then(function () { return loadScript('/scripts/desktop/citrusPrefs.js'); }); 494 } 495 if (!global.CitrusToolContract) { 496 chain = chain.then(function () { return loadScript('/scripts/desktop/citrusToolContract.js'); }); 497 } 498 if (!global.CitrusToolUi) { 499 chain = chain.then(function () { return loadScript('/scripts/desktop/citrusToolUi.js'); }); 500 } 501 return chain; 502 } 503 504 function runInit(route, doc, containerEl) { 505 if (!route || typeof route.init !== 'function') return Promise.resolve(); 506 if (containerEl && initInFlight.has(containerEl)) { 507 return initInFlight.get(containerEl); 508 } 509 setRouteScope(containerEl || null); 510 var chain = ensureCitrusToolDeps().then(function () { 511 try { 512 var result = route.init(containerEl || null); 513 if (result && typeof result.then === 'function') { 514 return result.catch(function (e) {
515 console.warn('[CitrusWindowLoader] init failed', e); 516 }); 517 } 518 } catch (e) { 519 console.warn('[CitrusWindowLoader] init failed', e); 520 } 521 return Promise.resolve(); 522 }); 523 if (containerEl) { 524 initInFlight.set(containerEl, chain); 525 chain = chain.finally(function () { 526 initInFlight.delete(containerEl); 527 }); 528 } 529 return chain; 530 } 531 532 function blurContainer(containerEl) { 533 if (!containerEl) return Promise.resolve(); 534 if (focusedContainer === containerEl) { 535 focusedContainer = null; 536 setRouteScope(null); 537 applyShellBodyClass(); 538 } 539 return Promise.resolve(); 540 } 541 542 function retargetContainer(fromEl, toEl) { 543 if (!fromEl || !toEl || fromEl === toEl) return; 544 var inst = instances.get(fromEl); 545 if (!inst) return; 546 instances.set(toEl, inst); 547 instances.delete(fromEl); 548 if (focusedContainer === fromEl) focusedContainer = toEl; 549 } 550 551 function focusContainer(containerEl) { 552 if (!containerEl) { 553 focusedContainer = null; 554 setRouteScope(null); 555 applyShellBodyClass(); 556 return Promise.resolve(); 557 } 558 559 var inst = instances.get(containerEl); 560 var needsInit = !!(inst && inst.loaded && inst.route && inst.mode !== 'iframe' && !inst.initialized); 561 562 if (focusedContainer === containerEl && !needsInit) { 563 return Promise.resolve(); 564 } 565 566 focusedContainer = containerEl; 567 setRouteScope(containerEl); 568 569 if (inst && inst.loaded && inst.route && inst.mode !== 'iframe') { 570 if (inst.initialized) return Promise.resolve(); 571 return runInit(inst.route, inst.doc, containerEl).then(function () { 572 markInitialized(containerEl, true); 573 }); 574 } 575 return Promise.resolve(); 576 } 577 578 function unloadContainer(containerEl) { 579 if (!containerEl) return Promise.resolve(); 580 var inst = instances.get(containerEl); 581 var chain = Promise.resolve(); 582 583 if (focusedContainer === containerEl) { 584 focusedContainer = null; 585 chain = blurContainer(containerEl).then(function () { 586 applyShellBodyClass(); 587 }); 588 } 589 590 return chain.then(function () { 591 if (inst) { 592 return runDestroy(inst.route, inst.routeId, containerEl).then(function () { 593 stripContainerRouteClasses(containerEl); 594 instances.delete(containerEl); 595 }); 596 } 597 }).then(function () { 598 clearContainerPresentation(containerEl); 599 containerEl.innerHTML = ''; 600 if (global.ChatroomSpa && typeof global.ChatroomSpa.hasMountedCitrusChat === 'function' && 601 !global.ChatroomSpa.hasMountedCitrusChat() && 602 typeof global.ChatroomSpa.clearChatroomTheme === 'function') { 603 global.ChatroomSpa.clearChatroomTheme(null, { force: true }); 604 } 605 }); 606 } 607 608 function destroyActive() { 609 if (!focusedContainer) return Promise.resolve(); 610 var containerEl = focusedContainer; 611 var inst = instances.get(containerEl); 612 focusedContainer = null; 613 setRouteScope(null); 614 applyShellBodyClass(); 615 if (!inst || !inst.loaded) return Promise.resolve(); 616 return runDestroy(inst.route, inst.routeId, containerEl); 617 } 618 619 function isContainerLoaded(containerEl, routeId) { 620 var inst = instances.get(containerEl); 621 return !!(inst && inst.loaded && inst.routeId === routeId); 622 } 623 624 function shouldNavigateAway(routeId) { 625 if (global.CitrusAppRegistry && typeof global.CitrusAppRegistry.isExternalLaunch === 'function') { 626 return global.CitrusAppRegistry.isExternalLaunch(routeId); 627 } 628 return false; 629 } 630 631 function clearContainerPresentation(containerEl) { 632 if (!containerEl || !containerEl.classList) return; 633 containerEl.classList.remove('citrus-window__content--iframe'); 634 } 635 636 function bindIframeImmersiveKeys(iframe, routeId) { 637 if (!iframe || routeId !== 'pupocalypse.html') return; 638 function onLoad() { 639 iframe.removeEventListener('load', onLoad); 640 var childWin; 641 try { childWin = iframe.contentWindow; } catch (e) { return; } 642 if (!childWin) return; 643 childWin.addEventListener('keydown', function (ev) { 644 if (ev.key !== 'F11') return; 645 ev.preventDefault(); 646 global.dispatchEvent(new CustomEvent('citrus:toggle-immersive', { 647 detail: { routeId: routeId } 648 })); 649 }); 650 } 651 if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') { 652 onLoad(); 653 } else { 654 iframe.addEventListener('load', onLoad); 655 } 656 } 657 658 function mountIframe(containerEl, routeId) { 659 clearContainerPresentation(containerEl); 660 containerEl.innerHTML = ''; 661 if (containerEl.classList.contains('citrus-fullscreen__content')) { 662 containerEl.style.display = 'flex'; 663 containerEl.style.flexDirection = 'column'; 664 } else { 665 containerEl.classList.add('citrus-window__content--iframe'); 666 } 667 668 var iframe = document.createElement('iframe'); 669 iframe.className = containerEl.classList.contains('citrus-fullscreen__content') 670 ? 'citrus-fullscreen__iframe' 671 : 'citrus-window__iframe'; 672 iframe.setAttribute('title', routeId.replace(/\.html$/i, '')); 673 iframe.src = '/' + routeId + (/\?/.test(routeId) ? '' : '?citrusEmbed=1&pupv=20260904c'); 674 containerEl.appendChild(iframe); 675 676 bindIframeImmersiveKeys(iframe, routeId); 677 678 containerEl.addEventListener('pointerdown', function () { 679 if (typeof iframe.focus === 'function') { 680 try { iframe.focus(); } catch (e) { /* ignore */ } 681 } 682 }); 683 684 instances.set(containerEl, { 685 routeId: routeId, 686 route: null, 687 doc: null, 688 loaded: true, 689 mode: 'iframe' 690 }); 691 return focusContainer(containerEl); 692 } 693 694 function createDirectMountRoot() { 695 var shell = document.createElement('div'); 696 shell.setAttribute('data-citrus-app-root', '1'); 697 var content = document.createElement('div'); 698 content.setAttribute('data-citrus-mount', '1'); 699 content.className = 'citrus-route-mount'; 700 shell.appendChild(content); 701 return shell; 702 } 703 704 function usesDirectMount(route) { 705 return !!(route && route.directMount); 706 } 707 708 function finalizeSpaMount(containerEl, routeId, route, root, doc, options, mou
708ntOpts) { 709 mountOpts = mountOpts || {}; 710 if (!mountOpts.skipMount) { 711 containerEl.innerHTML = ''; 712 containerEl.appendChild(root); 713 applyRouteContainerClass(containerEl, route); 714 } 715 if (routeId === 'chatroom.html') { 716 var chatWrap = containerEl.querySelector('#page-wrap'); 717 if (chatWrap) { 718 chatWrap.setAttribute('data-chatroom-layout', 'compact'); 719 } 720 primeCitrusChatroomShell(containerEl, root); 721 if (global.ChatroomCitrusMenus && typeof global.ChatroomCitrusMenus.applyLayoutMode === 'function') { 722 global.ChatroomCitrusMenus.applyLayoutMode(); 723 } 724 } 725 if (routeId === 'music.html') { 726 if (global.MusicPageCitrus && 727 typeof global.MusicPageCitrus.scheduleMusicWindowLayout === 'function') { 728 global.MusicPageCitrus.scheduleMusicWindowLayout(); 729 } 730 if (global.MusicPageResize && 731 typeof global.MusicPageResize.refresh === 'function') { 732 global.MusicPageResize.refresh(); 733 } 734 } 735 clearContainerPresentation(containerEl); 736 instances.set(containerEl, { 737 routeId: routeId, 738 route: route, 739 doc: doc || null, 740 loaded: true, 741 mode: 'spa', 742 appliedClasses: mountOpts.appliedClasses || null 743 }); 744 if (routeId === 'chatroom.html' && global.ChatroomSpa) { 745 if (typeof global.ChatroomSpa.scheduleChatroomThemeRecovery === 'function') { 746 global.ChatroomSpa.scheduleChatroomThemeRecovery(); 747 } else if (typeof global.ChatroomSpa.scheduleChatroomThemeApply === 'function') { 748 global.ChatroomSpa.scheduleChatroomThemeApply(); 749 } else if (typeof global.ChatroomSpa.applyChatroomTheme === 'function') { 750 global.ChatroomSpa.applyChatroomTheme(); 751 } 752 } 753 if (options.silentFocus) { 754 return runInit(route, doc || null, containerEl).then(function () { 755 markInitialized(containerEl, true); 756 if (global.MusicPageCitrus && typeof global.MusicPageCitrus.syncChromeFromWindowState === 'function') { 757 global.MusicPageCitrus.syncChromeFromWindowState(); 758 } 759 }); 760 } 761 return focusContainer(containerEl); 762 } 763 764 function loadDirectMountRoute(containerEl, routeId, route, options) { 765 var root = createDirectMountRoot(); 766 containerEl.innerHTML = ''; 767 containerEl.appendChild(root); 768 var appliedClasses = applyRouteContainerClass(containerEl, route); 769 return ensureRouteStyles(route).then(function () { 770 return ensureCitrusToolDeps(); 771 }).then(function () { 772 return ensureRouteScripts(route); 773 }).then(function () { 774 return finalizeSpaMount(containerEl, routeId, route, root, null, options, { 775 skipMount: true, 776 appliedClasses: appliedClasses.concat(['citrusos-app-active']) 777 }); 778 }); 779 } 780 781 function loadInto(containerEl, href, options) { 782 options = options || {}; 783 if (!containerEl) return Promise.resolve(false); 784 var routeId = hrefToRouteId(href); 785 if (shouldNavigateAway(routeId)) { 786 global.location.assign('/' + routeId); 787 return Promise.resolve(false); 788 } 789 if (loadingByContainer.get(containerEl)) return Promise.resolve(false); 790 791 if (isContainerLoaded(containerEl, routeId)) { 792 return focusContainer(containerEl).then(function () { return true; }); 793 } 794 795 var route = getRoute(routeId); 796 var prevInst = instances.get(containerEl); 797 var unloadPrev = prevInst 798 ? runDestroy(prevInst.route, prevInst.routeId, containerEl).then(function () { 799 stripContainerRouteClasses(containerEl); 800 }) 801 : Promise.resolve(); 802 803 if (!route) { 804 if (isKnownDirectMountRoute(routeId)) { 805 loadingByContainer.set(containerEl, true); 806 containerEl.innerHTML = '<div class="citrus-app-loading">Loadingâ¦</div>'; 807 return unloadPrev.then(function () { 808 return waitForRoute(routeId).then(function (resolved) { 809 loadingByContainer.delete(containerEl); 810 if (!resolved) { 811 instances.delete(containerEl); 812 clearContainerPresentation(containerEl); 813 containerEl.innerHTML = '<div class="citrus-app-error">Could not load this app. Refresh CitrusOS and try again.</div>'; 814 return false;
815 } 816 return loadDirectMountRoute(containerEl, routeId, resolved, options).then(function () { 817 return true; 818 }); 819 }); 820 }).catch(function (err) { 821 loadingByContainer.delete(containerEl); 822 instances.delete(containerEl); 823 console.warn('[CitrusWindowLoader]', err); 824 containerEl.innerHTML = '<div class="citrus-app-error">Could not open this app.</div>'; 825 return false; 826 }); 827 } 828 829 loadingByContainer.set(containerEl, true); 830 containerEl.innerHTML = '<div class="citrus-app-loading">Loadingâ¦</div>'; 831 return unloadPrev.then(function () { 832 return mountIframe(containerEl, routeId); 833 }).then(function () { 834 loadingByContainer.delete(containerEl); 835 return true; 836 }).catch(function (err) { 837 loadingByContainer.delete(containerEl); 838 instances.delete(containerEl); 839 clearContainerPresentation(containerEl); 840 console.warn('[CitrusWindowLoader]', err); 841 containerEl.innerHTML = '<div class="citrus-app-error">Could not open this app.</div>'; 842 return false; 843 }); 844 } 845 846 loadingByContainer.set(containerEl, true); 847 containerEl.innerHTML = '<div class="citrus-app-loading">Loadingâ¦</div>'; 848 849 if (usesDirectMount(route)) { 850 return unloadPrev.then(function () { 851 return loadDirectMountRoute(containerEl, routeId, route, options); 852 }).then(function () { 853 loadingByContainer.delete(containerEl); 854 return true; 855 }).catch(function (err) { 856 loadingByContainer.delete(containerEl); 857 instances.delete(containerEl); 858 console.warn('[CitrusWindowLoader]', err); 859 containerEl.innerHTML = '<div class="citrus-app-error">Could not open this app.</div>'; 860 return false; 861 }); 862 } 863 864 return unloadPrev.then(function () { 865 return fetch('/' + routeId, { credentials: 'same-origin', headers: { Accept: 'text/html' } }); 866 }).then(function (res) { 867 if (!res.ok) throw new Error('fetch ' + routeId); 868 return res.text(); 869 }).then(function (html) { 870 var parser = new DOMParser(); 871 var doc = parser.parseFromString(html, 'text/html'); 872 var root = extractContentRoot(doc); 873 if (!root) throw new Error('no content root in ' + routeId); 874 fixAssets(root); 875 return ensureRouteStyles(route).then(function () { 876 return ensureRouteScripts(route); 877 }).then(function () { 878 return { doc: doc, root: root }; 879 }); 880 }).then(function (payload) { 881 return finalizeSpaMount(containerEl, routeId, route, payload.root, payload.doc, options); 882 }).then(function () { 883 loadingByContainer.delete(containerEl); 884 return true; 885 }).catch(function (err) { 886 loadingByContainer.delete(containerEl); 887 instances.delete(containerEl); 888 console.warn('[CitrusWindowLoader]', err); 889 containerEl.innerHTML = '<div class="citrus-app-error">Could not open this app.</div>'; 890 return false; 891 }); 892 } 893 894 if (document.body && document.body.classList.contains('page-citrusos')) { 895 applyShellBodyClass(); 896 } 897 898 function hasRouteClass(className, containerEl) { 899 if (!className) return false; 900 if (global.CitrusRouteScope && typeof global.CitrusRouteScope.hasClass === 'function') { 901 return global.CitrusRouteScope.hasClass(className, containerEl); 902 } 903 if (containerEl && containerEl.classList && containerEl.classList.contains(className)) return true; 904 if (focusedContainer && focusedContainer.classList && focusedContainer.classList.contains(className)) { 905 return true; 906 } 907 return !!(document.body && document.body.classList.contains(className)); 908 } 909 910 function getRouteScope(containerEl) { 911 if (containerEl) return containerEl; 912 if (focusedContainer) return focusedContainer; 913 if (document.body && document.body.classList.contains('page-citrusos')) return null; 914 return document.body; 915 } 916 917 function getRouteIdForContainer(containerEl) { 918 if (!containerEl) return null; 919 var inst = instances.get(containerEl); 920 return inst ? inst.routeId : null; 921 } 922 923 global.CitrusWindowLoader = { 924 loadInto: loadInto,
925 focusContainer: focusContainer, 926 blurContainer: blurContainer, 927 retargetContainer: retargetContainer, 928 unloadContainer: unloadContainer, 929 destroyActive: destroyActive, 930 hasRouteClass: hasRouteClass, 931 getRouteScope: getRouteScope, 932 getRouteIdForContainer: getRouteIdForContainer, 933 getActiveRouteId: function () { 934 if (!focusedContainer) return null; 935 var inst = instances.get(focusedContainer); 936 return inst ? inst.routeId : null; 937 }, 938 getFocusedContainer: function () { return focusedContainer; }, 939 isLoaded: function (containerEl, href) { 940 return isContainerLoaded(containerEl, hrefToRouteId(href)); 941 } 942 }; 943})(typeof window !== 'undefined' ? window : this);
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.