PageSourceSearch

https://www.telrobot.net/official/portal-theme.js

js telrobot.net collected 2026-10-06 18:42:06 UTC 8,695 bytes, 126 lines download raw bytes

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.