83 lines
5.0 KiB
PHP
83 lines
5.0 KiB
PHP
<?php
|
||
/** Общий inline runtime API/независимого PHP-преленда. Переменные задаёт вызывающий layout. */
|
||
$themeInitial = isset($themeInitial) && $themeInitial === 'dark' ? 'dark' : 'light';
|
||
$themeSurface = isset($themeSurface) && in_array($themeSurface, ['public', 'private', 'preland'], true) ? $themeSurface : 'public';
|
||
?>
|
||
<script>
|
||
(function (initial, surface) {
|
||
'use strict';
|
||
if (window.ZoomzillaTheme) { window.ZoomzillaTheme.refresh(); return; }
|
||
const key = 'theme_choice';
|
||
let shown = initial;
|
||
const canonical = value => typeof value === 'string' && value.length <= 128 && /^v1:(light|dark):(manual|preland):[A-Za-z0-9_-]{16,64}(?![\s\S])/.test(value) ? value : null;
|
||
const themeOf = value => value.split(':')[1];
|
||
function readCookie() {
|
||
try {
|
||
for (const part of document.cookie.split(';')) {
|
||
const cookie = part.trim();
|
||
if (!cookie.startsWith(key + '=')) continue;
|
||
const value = canonical(decodeURIComponent(cookie.slice(key.length + 1)));
|
||
if (value) return value;
|
||
}
|
||
} catch (_) {}
|
||
return null;
|
||
}
|
||
function read() {
|
||
const cookie = readCookie();
|
||
if (cookie) return cookie;
|
||
try { return canonical(window.localStorage.getItem(key)); } catch (_) { return null; }
|
||
}
|
||
function apply(theme) {
|
||
shown = theme;
|
||
document.documentElement.dataset.theme = theme;
|
||
document.documentElement.style.colorScheme = theme;
|
||
if (document.body) document.body.classList.toggle('theme-dark', theme === 'dark');
|
||
document.querySelectorAll('[data-theme-choice]').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.themeChoice === theme)));
|
||
}
|
||
function uuid() {
|
||
if (window.crypto && typeof window.crypto.randomUUID === 'function') return window.crypto.randomUUID();
|
||
const bytes = new Uint8Array(16);
|
||
if (window.crypto && window.crypto.getRandomValues) window.crypto.getRandomValues(bytes);
|
||
else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
||
bytes[6] = (bytes[6] & 15) | 64; bytes[8] = (bytes[8] & 63) | 128;
|
||
const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
|
||
return hex.slice(0, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16) + '-' + hex.slice(16, 20) + '-' + hex.slice(20);
|
||
}
|
||
function refresh() { const value = read(); apply(value ? themeOf(value) : shown); }
|
||
function choose(theme) {
|
||
if (theme !== 'light' && theme !== 'dark') return;
|
||
const from = shown;
|
||
const value = 'v1:' + theme + ':manual:' + uuid().replace(/-/g, '');
|
||
try { document.cookie = key + '=' + encodeURIComponent(value) + '; Expires=Tue, 19 Jan 2038 03:14:07 GMT; Path=/; SameSite=Lax' + (location.protocol === 'https:' ? '; Secure' : ''); } catch (_) {}
|
||
try { window.localStorage.setItem(key, value); } catch (_) {}
|
||
apply(theme);
|
||
if (from === theme) return;
|
||
const detail = {schema_version: 1, event_id: uuid(), from, to: theme, source: 'manual', surface, host: location.hostname, path: location.pathname, occurred_at: new Date().toISOString()};
|
||
try { document.dispatchEvent(new CustomEvent('theme_changed', {detail})); } catch (_) {}
|
||
}
|
||
window.ZoomzillaTheme = Object.freeze({current: () => shown, choose, refresh});
|
||
// Pointer-клик не сбрасывает ввод/selection; клавиатурная навигация остаётся штатной.
|
||
document.addEventListener('mousedown', event => {
|
||
const button = event.target instanceof Element ? event.target.closest('[data-theme-choice]') : null;
|
||
const active = document.activeElement;
|
||
if (button && active && (active.matches('input,textarea') || active.isContentEditable)) event.preventDefault();
|
||
});
|
||
document.addEventListener('click', event => {
|
||
const button = event.target instanceof Element ? event.target.closest('[data-theme-choice]') : null;
|
||
if (button && !button.disabled) choose(button.dataset.themeChoice);
|
||
});
|
||
window.addEventListener('storage', event => { if (event.key === key || event.key === null) refresh(); });
|
||
window.addEventListener('pageshow', refresh);
|
||
document.addEventListener('DOMContentLoaded', refresh);
|
||
document.addEventListener('pjax:end', refresh);
|
||
refresh();
|
||
// Только первый boot зеркалит валидную cookie и уведомляет другие вкладки
|
||
// о server import. Read-only refresh/reinit не создают цикл storage events.
|
||
const bootCookie = readCookie();
|
||
if (bootCookie) {
|
||
apply(themeOf(bootCookie));
|
||
try { if (window.localStorage.getItem(key) !== bootCookie) window.localStorage.setItem(key, bootCookie); } catch (_) {}
|
||
}
|
||
})(<?= json_encode($themeInitial, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>, <?= json_encode($themeSurface, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>);
|
||
</script>
|