PageSourceSearch

https://pupwife.com/scripts/desktop/citrusWindowLoader.js

js pupwife.com collected 2026-09-30 15:05:55 UTC 37,378 bytes, 943 lines download raw bytes

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.