1/* Shared by the static portal, public readers and their same-origin embeds. */ 2(function (root, factory) { 3 var policy = factory(); 4 if (typeof module === 'object' && module.exports) module.exports = policy; 5 if (!root || !root.document || root.YunfuPortalTheme) return; 6 var doc = root.document, key = 'yunfu:portal-theme', mode = 'auto', theme = '', controls = [], active = null; 7 var previousColorScheme = '', ownsTheme = false; 8 function enabled() { return /^\/official(?:\/|$)/.test(root.location.pathname) || doc.documentElement.hasAttribute('data-public-portal-theme'); } 9 try { mode = policy.normalize(root.localStorage.getItem(key)); } catch (_) {} 10 var icons = { 11 light: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1.5 1.5m11 11L19 19M5 19l1.5-1.5m11-11L19 5"/>', 12 dark: '<path d="M20.5 13.1A8.6 8.6 0 0 1 10.9 3.5 8.7 8.7 0 1 0 20.5 13.1Z"/>', 13 auto: '<circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 1 0 18Z" fill="currentColor" stroke="none"/>' 14 }; 15 var labels = { auto: 'èªå¨', light: '亮è²', dark: 'æè²' }; 16 function svg(value) { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + icons[value] + '</svg>'; } 17 function broadcast() { 18 doc.querySelectorAll('iframe').forEach(function (frame) { 19 try { frame.contentWindow.postMessage({ type: 'yunfu-portal-theme', mode: mode }, root.location.origin); } catch (_) {} 20 }); 21 } 22 function renderControls() { 23 controls = controls.filter(function (item) { return item.host.isConnected; }); 24 controls.forEach(function (item) { 25 item.trigger.innerHTML = svg(mode) + '<span class="portal-theme-label">' + labels[mode] + '</span>'; 26 item.trigger.setAttribute('aria-label', 'é è²æ¨¡å¼ï¼' + labels[mode]); 27 item.trigger.title = mode === 'auto' ? 'èªå¨é è² Â· å½å' + labels[theme] + 'ï¼07:00â19:00 亮è²' : 'å½å' + labels[theme] + 'ï¼ç¹å»è°æ´'; 28 }); 29 if (active) active.panel.querySelectorAll('[data-mode]').forEach(function (button) { button.setAttribute('aria-checked', String(button.dataset.mode === mode)); }); 30 } 31 function apply(force) { 32 if (!enabled()) { 33 close(false); theme = ''; 34 if (ownsTheme) { 35 doc.documentElement.removeAttribute('data-portal-theme'); doc.documentElement.removeAttribute('data-portal-theme-mode'); 36 doc.documentElement.style.colorScheme = previousColorScheme; ownsTheme = false; 37 } 38 return; 39 } 40 if (!ownsTheme) { previousColorScheme = doc.documentElement.style.colorScheme; ownsTheme = true; } 41 var next = policy.resolve(mode, new Date()); 42 doc.documentElement.setAttribute('data-portal-theme', next); 43 doc.documentElement.setAttribute('data-portal-theme-mode', mode); 44 // Scoped to public pages only; no system-wide admin preference is changed. 45 doc.documentElement.style.colorScheme = next; 46 if (theme !== next || force) { 47 theme = next; renderControls(); broadcast(); 48 root.dispatchEvent(new CustomEvent('portal-theme-change', { detail: { mode: mode, theme: theme } })); 49 } 50 } 51 function setMode(value) { 52 mode = policy.normalize(value); 53 try { root.localStorage.setItem(key, mode); } catch (_) {} 54 apply(true); 55 } 56 function close(restore) { 57 if (!active) return; 58 var previous = active; 59 active = null; previous.panel.remove(); previous.trigger.setAttribute('aria-expanded', 'false'); 60 if (restore && previous.trigger.isConnected) previous.trigger.focus(); 61 } 62 function position() { 63 if (!active) return; 64 var rect = active.trigger.getBoundingClientRect(), width = Math.min(248, root.innerWidth - 24); 65 active.panel.style.width = width + 'px'; 66 active.panel.style.left = Math.max(12, Math.min(rect.right - width, root.innerWidth - width - 12)) + 'px'; 67 active.panel.style.top = Math.min(rect.bottom + 10, Math.max(12, root.innerHeight - 262)) + 'px'; 68 } 69 function open(item) { 70 if (active && active.trigger === item.trigger) { close(false); return; } 71 close(false); 72 var panel = doc.createElement('div'); 73 panel.className = 'portal-theme-menu'; panel.id = item.trigger.getAttribute('aria-controls'); 74 panel.setAttribute('role', 'menu'); panel.setAttribute('aria-label', 'éæ©å®ç½é è²'); 75 panel.innerHTML = '<p class="portal-theme-heading">å¤è§é è²</p>' + ['light', 'dark', 'auto'].map(function (value) { 76 return '<button type="button" role="menuitemradio" data-mode="' + value + '" aria-checked="' + (mode === value) + '">' + svg(value) + '<span>' + labels[value] + '</span><i aria-hidden="true">â</i></button>'; 77 }).join('') + '<p class="portal-theme-note">èªå¨æ¨¡å¼æè®¾å¤å½å°æ¶é´åæ¢<br>07:00â19:00 亮è²ï¼å ¶ä½æ¶é´æè²</p>'; 78 active = { panel: panel, trigger: item.trigger }; 79 panel.addEventListener('click', function (event) { var button = event.target.closest('[data-mode]'); if (button) { setMode(button.dataset.mode); close(true); } }); 80 panel.addEventListener('keydown', function (event) { 81 var buttons = Array.from(panel.querySelectorAll('[data-mode]')), index = buttons.indexOf(doc.activeElement); 82 if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Home' || event.key === 'End') { 83 event.preventDefault(); var next = event.key === 'Home' ? 0 : event.key === 'End' ? 2 : (index + (event.key === 'ArrowDown' ? 1 : -1) + 3) % 3; 84 buttons[next].focus(); 85 } 86 if (event.key === 'Tab') close(true); 87 }); 88 doc.body.appendChild(panel); item.trigger.setAttribute('aria-expanded', 'true'); position(); 89 panel.querySelector('[data-mode="' + mode + '"]').focus(); 90 } 91 function mount() { 92 if (!enabled()) return; 93 doc.querySelectorAll('[data-portal-theme-control]').forEach(function (host) {
94 if (controls.some(function (item) { return item.host === host && item.trigger.isConnected; })) return; 95 var trigger = doc.createElement('button'); 96 trigger.type = 'button'; trigger.className = 'portal-theme-trigger'; trigger.setAttribute('aria-haspopup', 'menu'); trigger.setAttribute('aria-expanded', 'false'); 97 trigger.setAttribute('aria-controls', 'portal-theme-menu-' + (controls.length + 1)); 98 var item = { host: host, trigger: trigger }; controls.push(item); 99 trigger.addEventListener('click', function () { open(item); }); 100 trigger.addEventListener('keydown', function (event) { if (event.key === 'ArrowDown') { event.preventDefault(); open(item); } }); 101 host.replaceChildren(trigger); 102 }); 103 renderControls(); 104 } 105 root.YunfuPortalTheme = { getMode: function () { return mode; }, getTheme: function () { return theme; }, setMode: setMode, refresh: function () { apply(true); mount(); }, mount: mount }; 106 apply(true); 107 if (doc.readyState === 'loading') doc.addEventListener('DOMContentLoaded', mount, { once: true }); else mount(); 108 doc.addEventListener('click', function (event) { if (active && !active.panel.contains(event.target) && !active.trigger.contains(event.target)) close(false); }); 109 doc.addEventListener('keydown', function (event) { if (active && event.key === 'Escape') { event.preventDefault(); close(true); } }); 110 doc.addEventListener('visibilitychange', function () { if (!doc.hidden) apply(); }); 111 root.addEventListener('pageshow', function () { apply(); mount(); }); 112 root.addEventListener('focus', function () { apply(); }); 113 root.addEventListener('resize', position); 114 root.addEventListener('scroll', function () { close(false); }, { passive: true }); 115 root.addEventListener('storage', function (event) { if (event.key === key || event.key === null) { mode = policy.normalize(event.newValue); apply(true); } }); 116 root.addEventListener('message', function (event) { 117 if (root.parent !== root && event.source === root.parent && event.origin === root.location.origin && event.data && event.data.type === 'yunfu-portal-theme') { mode = policy.normalize(event.data.mode); apply(true); } 118 }); 119 doc.addEventListener('load', function (event) { if (event.target && event.target.tagName === 'IFRAME') broadcast(); }, true); 120 // Minute alignment checks also catch a changed device clock/time zone while open. 121 root.setInterval(function () { apply(); }, 30000); 122})(typeof window === 'undefined' ? null : window, function () { 123 function normalize(value) { return value === 'light' || value === 'dark' ? value : 'auto'; } 124 function resolve(mode, date) { mode = normalize(mode); if (mode !== 'auto') return mode; var hour = date.getHours(); return hour >= 7 && hour < 19 ? 'light' : 'dark'; } 125 return { normalize: normalize, resolve: resolve }; 126});
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.