mirror of
git://f0xx.org/android_cast
synced 2026-07-29 07:20:00 +03:00
Centralize copyright in examples/platform/footer.config.php, render via footer.php in crashes/builder/login layouts, and serve hub through index.php. Co-authored-by: Cursor <cursoragent@cursor.com>
1094 lines
44 KiB
PHP
1094 lines
44 KiB
PHP
<?php declare(strict_types=1);
|
|
require_once dirname(__DIR__) . '/platform/footer.php';
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en" data-theme="dark" data-clock-font="mixed">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Android Cast — Project hub</title>
|
|
<script>
|
|
(function () {
|
|
var t = localStorage.getItem('crash_console_theme');
|
|
if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
|
|
})();
|
|
</script>
|
|
<link rel="stylesheet" href="/app/androidcast_project/crashes/assets/css/app.css">
|
|
<link rel="stylesheet" href="hub.css?v=20260532globe">
|
|
<link rel="stylesheet" href="hub-logo-layers.css?v=20260532globe">
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js"
|
|
}
|
|
}
|
|
</script>
|
|
<script src="assets/analytics.config.js"></script>
|
|
<script src="/app/androidcast_project/crashes/assets/js/analytics.js" defer></script>
|
|
</head>
|
|
<body class="hub-page">
|
|
<div id="lcd-alphabet-host" hidden aria-hidden="true"></div>
|
|
<div id="overtime-alphabet-host" hidden aria-hidden="true"></div>
|
|
<div id="quartz-alphabet-host" hidden aria-hidden="true"></div>
|
|
<div id="hub-logo-stage" class="hub-logo-stage" hidden aria-hidden="true"></div>
|
|
<section class="hub-clock-widget" id="hub-clock-widget" aria-label="World clock widget">
|
|
<div class="cw-clock-inner">
|
|
<div class="cw-primary-block">
|
|
<div class="cw-line cw-row cw-row-clock cw-line-time">
|
|
<strong class="cw-time-main">
|
|
<span id="cw-hm-lcd" class="ot-lcd-line ot-lcd-bold cw-col-hm" aria-label="Hours and minutes"></span>
|
|
<span id="cw-colon-lcd" class="ot-lcd-line ot-lcd-colon cw-colon-blink" aria-label="Separator"></span>
|
|
<span id="cw-ss-lcd" class="ot-lcd-line ot-lcd-sec cw-col-ss" aria-label="Seconds"></span>
|
|
<span id="cw-tz-text" class="ot-lcd-line ot-lcd-tz cw-col-tz" aria-label="Timezone offset"></span>
|
|
</strong>
|
|
</div>
|
|
<div class="cw-line cw-row cw-row-tz cw-line-region ot-lcd-subline" id="cw-region">EUROPE/WARSAW</div>
|
|
<div class="cw-line cw-row cw-row-date cw-line-date ot-lcd-subline" id="cw-date-line">WED 27 MAY 2026</div>
|
|
</div>
|
|
<div class="cw-separator" aria-hidden="true"></div>
|
|
<div class="cw-world-section" id="cw-world-row"></div>
|
|
</div>
|
|
</section>
|
|
<div id="hub-docs-modal" class="hub-docs-modal" hidden aria-hidden="true">
|
|
<div class="hub-docs-backdrop" data-close-docs></div>
|
|
<section class="hub-docs-panel card card--lift" role="dialog" aria-modal="true" aria-labelledby="hub-docs-title">
|
|
<header class="hub-docs-header">
|
|
<h2 id="hub-docs-title">Deployment model</h2>
|
|
<button type="button" class="hub-docs-close" data-close-docs aria-label="Close">x</button>
|
|
</header>
|
|
<div class="hub-docs-layout">
|
|
<aside class="hub-docs-toc detail-tree" id="hub-docs-toc" aria-label="Table of contents">
|
|
<p class="muted hub-docs-toc-title">Table of contents</p>
|
|
</aside>
|
|
<div class="hub-docs-panels" id="hub-docs-panels"></div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
<div class="shell hub-shell">
|
|
<main class="main-pane hub-main">
|
|
<header class="hub-header">
|
|
<div class="hub-brand">
|
|
<img class="hub-logo-mark" src="assets/hub-logo-transparent.svg" alt="" width="112" height="84" decoding="async">
|
|
<div class="hub-title-with-thunder">
|
|
<h1 class="hub-title">Android Cast</h1>
|
|
<img class="hub-thunder" src="assets/hub-thunder.svg?v=20260527z" alt="" decoding="async">
|
|
</div>
|
|
</div>
|
|
<p class="muted hub-subtitle">Project hub</p>
|
|
<label class="toolbar-select hub-theme">
|
|
<span>Theme</span>
|
|
<select id="theme-select" aria-label="UI theme">
|
|
<option value="dark">Dark</option>
|
|
<option value="light">Light</option>
|
|
</select>
|
|
</label>
|
|
</header>
|
|
</main>
|
|
</div>
|
|
<nav class="hub-nav-dock" id="hub-nav-dock" aria-label="Android Cast apps">
|
|
<a class="hub-card card card--lift" href="git/">
|
|
<span class="hub-card-icon" aria-hidden="true">
|
|
<span class="nav-icon nav-icon--git"></span>
|
|
</span>
|
|
<span class="hub-card-label">AndroidCast git</span>
|
|
</a>
|
|
<a class="hub-card card card--lift" href="crashes/?view=tickets">
|
|
<span class="hub-card-icon" aria-hidden="true">
|
|
<span class="nav-icon nav-icon--tickets"></span>
|
|
</span>
|
|
<span class="hub-card-label">AndroidCast tickets</span>
|
|
</a>
|
|
<a class="hub-card card card--lift" href="crashes/?view=reports">
|
|
<span class="hub-card-icon" aria-hidden="true">
|
|
<span class="nav-icon nav-icon--reports"></span>
|
|
</span>
|
|
<span class="hub-card-label">AndroidCast crashes</span>
|
|
</a>
|
|
<a class="hub-card card card--lift" href="graphs/">
|
|
<span class="hub-card-icon" aria-hidden="true">
|
|
<span class="nav-icon nav-icon--graphs"></span>
|
|
</span>
|
|
<span class="hub-card-label">AndroidCast graphs</span>
|
|
</a>
|
|
<a class="hub-card card card--lift" href="build/">
|
|
<span class="hub-card-icon" aria-hidden="true">
|
|
<span class="nav-icon nav-icon--builder"></span>
|
|
</span>
|
|
<span class="hub-card-label">AndroidCast builder</span>
|
|
</a>
|
|
</nav>
|
|
<?php platform_render_footer(['use_i18n' => false]); ?>
|
|
<script>
|
|
(function () {
|
|
var GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 330 };
|
|
function globeCoverRect() {
|
|
var vw = window.innerWidth;
|
|
var vh = window.innerHeight;
|
|
var scale = Math.max(vw / GLOBE_VIEW.imgW, vh / GLOBE_VIEW.imgH);
|
|
var drawW = GLOBE_VIEW.imgW * scale;
|
|
var drawH = GLOBE_VIEW.imgH * scale;
|
|
var offX = (vw - drawW) / 2;
|
|
var offY = (vh - drawH) / 2;
|
|
var left = offX + (GLOBE_VIEW.cx - GLOBE_VIEW.r) * scale;
|
|
var top = offY + (GLOBE_VIEW.cy - GLOBE_VIEW.r) * scale;
|
|
var bottom = offY + (GLOBE_VIEW.cy + GLOBE_VIEW.r) * scale;
|
|
var right = offX + (GLOBE_VIEW.cx + GLOBE_VIEW.r) * scale;
|
|
return { left: left, top: top, bottom: bottom, right: right, width: right - left, height: bottom - top };
|
|
}
|
|
function layoutHubNavDock() {
|
|
var dock = document.getElementById('hub-nav-dock');
|
|
if (!dock) return;
|
|
var globe = globeCoverRect();
|
|
var dockW = Math.min(300, Math.max(210, globe.width * 0.38));
|
|
dock.style.width = dockW + 'px';
|
|
dock.style.left = globe.left + 'px';
|
|
dock.style.top = globe.top + 'px';
|
|
dock.style.height = globe.height + 'px';
|
|
dock.style.display = 'flex';
|
|
dock.style.flexDirection = 'column';
|
|
dock.style.flexWrap = 'nowrap';
|
|
dock.style.gap = '5px';
|
|
dock.style.justifyContent = 'flex-end';
|
|
}
|
|
layoutHubNavDock();
|
|
window.addEventListener('resize', layoutHubNavDock);
|
|
})();
|
|
</script>
|
|
<script>
|
|
(function () {
|
|
var sel = document.getElementById('theme-select');
|
|
if (sel) {
|
|
var cur = document.documentElement.getAttribute('data-theme') || 'dark';
|
|
sel.value = cur;
|
|
sel.addEventListener('change', function () {
|
|
var v = sel.value;
|
|
document.documentElement.setAttribute('data-theme', v);
|
|
try { localStorage.setItem('crash_console_theme', v); } catch (e) {}
|
|
});
|
|
}
|
|
|
|
var stage = document.getElementById('hub-logo-stage');
|
|
|
|
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
var supportsSvg = !!(svg && svg.createSVGRect);
|
|
var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover'));
|
|
var logoEnabled = !!(stage && supportsSvg && supportsLayout);
|
|
|
|
var logoBuild = '20260532globe';
|
|
|
|
function hubAsset(rel) {
|
|
var link = document.querySelector('link[href*="hub.css"]');
|
|
if (link) {
|
|
var href = link.getAttribute('href') || '';
|
|
return href.replace(/[^/]*$/, '') + rel;
|
|
}
|
|
return rel;
|
|
}
|
|
|
|
var LCD_FONT_CATALOG = {
|
|
'dseg14-classic': {
|
|
url: 'assets/dseg14-classic-alphabet.svg',
|
|
prefix: 'd14c',
|
|
css: '"DSEG14 Classic", "Consolas", monospace'
|
|
},
|
|
'dseg14-modern': {
|
|
url: 'assets/dseg14-modern-alphabet.svg',
|
|
prefix: 'd14m',
|
|
css: '"DSEG14 Modern", "Consolas", monospace'
|
|
},
|
|
'ds-digital': {
|
|
url: 'assets/ds-digital-alphabet.svg',
|
|
prefix: 'dsd',
|
|
css: '"DS-Digital", "Consolas", monospace'
|
|
},
|
|
'digital-7-mono-italic': {
|
|
url: 'assets/digital-7-mono-italic-alphabet.svg',
|
|
prefix: 'd7mi',
|
|
css: '"Digital-7 Mono Italic", "Consolas", monospace'
|
|
}
|
|
};
|
|
|
|
var clockFontParams = new URLSearchParams(window.location.search);
|
|
var clockFontKey = (clockFontParams.get('clockFont') || 'digital-7-mono-italic').toLowerCase();
|
|
if (!LCD_FONT_CATALOG[clockFontKey]) clockFontKey = 'digital-7-mono-italic';
|
|
var lcdFontReady = false;
|
|
var lcdSymbolIds = {};
|
|
var lcdFontPrefix = LCD_FONT_CATALOG[clockFontKey].prefix;
|
|
var fragments = {
|
|
space: 'assets/hub-logo-fragment-space.svg',
|
|
globe: 'assets/hub-logo-fragment-globe.svg',
|
|
continents: 'assets/hub-logo-continents-mercator-globe.svg',
|
|
gridOverlay: 'assets/hub-logo-fragment-globe-grid-overlay.svg',
|
|
compass: 'assets/hub-logo-fragment-compass.svg'
|
|
};
|
|
var parts = {
|
|
full: ['space', 'globe', 'continents', 'gridOverlay', 'compass'],
|
|
space: ['space'],
|
|
globe: ['globe', 'continents', 'gridOverlay', 'compass']
|
|
};
|
|
|
|
var params = new URLSearchParams(window.location.search);
|
|
var part = (params.get('logoPart') || 'full').toLowerCase();
|
|
var grid = (params.get('grid') || '3').toLowerCase();
|
|
var list = parts[part] || parts.full;
|
|
|
|
function fragmentSrc(name) {
|
|
var base = fragments[name];
|
|
if (name === 'globe' && grid === '4') {
|
|
base = 'assets/hub-logo-fragment-globe-4x4.svg';
|
|
}
|
|
return base + '?v=' + logoBuild;
|
|
}
|
|
|
|
var useGlobe3d = logoEnabled && (params.get('globe3d') || '1') !== '0';
|
|
var globe3dLayers = { globe: true, continents: true, gridOverlay: true };
|
|
|
|
function appendStaticLayers() {
|
|
list.forEach(function (name) {
|
|
if (useGlobe3d && globe3dLayers[name]) return;
|
|
var img = document.createElement('img');
|
|
img.className = 'hub-logo-layer hub-logo-layer--' + name;
|
|
img.src = fragmentSrc(name);
|
|
img.alt = '';
|
|
img.decoding = 'async';
|
|
img.loading = 'lazy';
|
|
if (name === 'compass') {
|
|
img.id = 'hub-compass-layer';
|
|
img.setAttribute('role', 'button');
|
|
img.setAttribute('tabindex', '0');
|
|
img.setAttribute('aria-label', 'Open deployment model');
|
|
}
|
|
stage.appendChild(img);
|
|
});
|
|
}
|
|
|
|
if (logoEnabled) {
|
|
appendStaticLayers();
|
|
|
|
var logoOpacity = params.get('logoOpacity');
|
|
if (logoOpacity) {
|
|
stage.style.opacity = logoOpacity;
|
|
}
|
|
|
|
stage.hidden = false;
|
|
stage.setAttribute('aria-hidden', 'true');
|
|
|
|
if (useGlobe3d) {
|
|
var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
var globeYawDeg = parseFloat(params.get('globeYawDeg'));
|
|
var globeOpts = {
|
|
build: logoBuild,
|
|
reducedMotion: reducedMotion
|
|
};
|
|
if (isFinite(globeYawDeg)) {
|
|
globeOpts.initialRotationY = globeYawDeg * Math.PI / 180;
|
|
}
|
|
var globeModuleUrl = new URL('assets/hub-globe.js', window.location.href);
|
|
globeModuleUrl.searchParams.set('v', logoBuild);
|
|
import(globeModuleUrl.href).then(function (mod) {
|
|
return mod.mountHubGlobe(stage, globeOpts);
|
|
}).then(function (handle) {
|
|
if (handle) {
|
|
stage.dataset.globe3d = 'active';
|
|
}
|
|
if (!handle) {
|
|
['globe', 'continents', 'gridOverlay'].forEach(function (name) {
|
|
if (list.indexOf(name) === -1) return;
|
|
var img = document.createElement('img');
|
|
img.className = 'hub-logo-layer hub-logo-layer--' + name;
|
|
img.src = fragmentSrc(name);
|
|
img.alt = '';
|
|
img.decoding = 'async';
|
|
stage.appendChild(img);
|
|
});
|
|
}
|
|
}).catch(function (err) {
|
|
console.warn('HubGlobe 3D unavailable, using flat layers', err);
|
|
['globe', 'continents', 'gridOverlay'].forEach(function (name) {
|
|
if (list.indexOf(name) === -1) return;
|
|
var img = document.createElement('img');
|
|
img.className = 'hub-logo-layer hub-logo-layer--' + name;
|
|
img.src = fragmentSrc(name);
|
|
img.alt = '';
|
|
stage.appendChild(img);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/* Nav dock overlaps globe left hemisphere; stack top → bottom. */
|
|
var GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 330 };
|
|
function globeCoverRect() {
|
|
var vw = window.innerWidth;
|
|
var vh = window.innerHeight;
|
|
var scale = Math.max(vw / GLOBE_VIEW.imgW, vh / GLOBE_VIEW.imgH);
|
|
var drawW = GLOBE_VIEW.imgW * scale;
|
|
var drawH = GLOBE_VIEW.imgH * scale;
|
|
var offX = (vw - drawW) / 2;
|
|
var offY = (vh - drawH) / 2;
|
|
var left = offX + (GLOBE_VIEW.cx - GLOBE_VIEW.r) * scale;
|
|
var top = offY + (GLOBE_VIEW.cy - GLOBE_VIEW.r) * scale;
|
|
var bottom = offY + (GLOBE_VIEW.cy + GLOBE_VIEW.r) * scale;
|
|
var right = offX + (GLOBE_VIEW.cx + GLOBE_VIEW.r) * scale;
|
|
return { left: left, top: top, bottom: bottom, right: right, width: right - left, height: bottom - top };
|
|
}
|
|
function layoutHubNavDock() {
|
|
var dock = document.getElementById('hub-nav-dock');
|
|
if (!dock) return;
|
|
var globe = globeCoverRect();
|
|
var dockW = Math.min(300, Math.max(210, globe.width * 0.38));
|
|
dock.style.width = dockW + 'px';
|
|
dock.style.left = globe.left + 'px';
|
|
dock.style.top = globe.top + 'px';
|
|
dock.style.height = globe.height + 'px';
|
|
dock.style.right = 'auto';
|
|
dock.style.bottom = 'auto';
|
|
dock.style.display = 'flex';
|
|
dock.style.flexDirection = 'column';
|
|
dock.style.flexWrap = 'nowrap';
|
|
dock.style.alignItems = 'stretch';
|
|
dock.style.gap = '5px';
|
|
dock.style.justifyContent = 'flex-end';
|
|
}
|
|
layoutHubNavDock();
|
|
window.addEventListener('resize', layoutHubNavDock);
|
|
|
|
var docsModal = document.getElementById('hub-docs-modal');
|
|
var docsToc = document.getElementById('hub-docs-toc');
|
|
var docsPanels = document.getElementById('hub-docs-panels');
|
|
var compassLayer = document.getElementById('hub-compass-layer');
|
|
var docsApi = '/app/androidcast_project/crashes/api/hub_deploy_docs.php';
|
|
var docsQueue = [];
|
|
var docsLoading = false;
|
|
var docsCache = {};
|
|
var docsPanelsById = {};
|
|
|
|
function closeDocs() {
|
|
docsModal.hidden = true;
|
|
docsModal.setAttribute('aria-hidden', 'true');
|
|
}
|
|
|
|
function openDocs() {
|
|
docsModal.hidden = false;
|
|
docsModal.setAttribute('aria-hidden', 'false');
|
|
}
|
|
|
|
function xhrJson(url, onOk, onErr) {
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open('GET', url, true);
|
|
xhr.setRequestHeader('Accept', 'application/json');
|
|
xhr.onreadystatechange = function () {
|
|
if (xhr.readyState !== 4) return;
|
|
if (xhr.status < 200 || xhr.status >= 300) {
|
|
if (onErr) onErr();
|
|
return;
|
|
}
|
|
try {
|
|
onOk(JSON.parse(xhr.responseText));
|
|
} catch (e) {
|
|
if (onErr) onErr();
|
|
}
|
|
};
|
|
xhr.send();
|
|
}
|
|
|
|
function ensureDocsPanel(sectionId, title) {
|
|
if (docsPanelsById[sectionId]) return docsPanelsById[sectionId];
|
|
var item = document.createElement('div');
|
|
item.className = 'detail-tree-item hub-docs-section';
|
|
item.dataset.section = sectionId;
|
|
var panelId = 'hub-doc-panel-' + sectionId;
|
|
item.innerHTML =
|
|
'<div class="detail-tree-row report-row" role="button" tabindex="0" data-tree-toggle data-controls="' + panelId + '">' +
|
|
'<span class="report-tree-cell">' +
|
|
'<button type="button" class="report-tree-toggle" aria-expanded="false" aria-controls="' + panelId + '" tabindex="-1">' +
|
|
'<span class="report-tree-arrow" aria-hidden="true"></span>' +
|
|
'</button>' +
|
|
'</span>' +
|
|
'<span class="detail-tree-caption">' + title + '</span>' +
|
|
'</div>' +
|
|
'<div class="detail-tree-body" id="' + panelId + '" hidden><p class="muted">Loading...</p></div>';
|
|
docsPanels.appendChild(item);
|
|
docsPanelsById[sectionId] = item;
|
|
bindDocsTree(item);
|
|
return item;
|
|
}
|
|
|
|
function bindDocsTree(root) {
|
|
root.querySelectorAll('[data-tree-toggle]').forEach(function (row) {
|
|
var panelId = row.getAttribute('data-controls');
|
|
var panel = panelId ? document.getElementById(panelId) : null;
|
|
var btn = row.querySelector('.report-tree-toggle');
|
|
if (!panel) return;
|
|
var toggle = function () {
|
|
var open = panel.hidden;
|
|
panel.hidden = !open;
|
|
if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
row.classList.toggle('report-row--open', open);
|
|
};
|
|
if (btn) {
|
|
btn.addEventListener('click', function (e) {
|
|
e.stopPropagation();
|
|
toggle();
|
|
});
|
|
}
|
|
row.addEventListener('click', toggle);
|
|
row.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
toggle();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderSectionHtml(sectionId, html) {
|
|
var item = docsPanelsById[sectionId];
|
|
if (!item) return;
|
|
var body = item.querySelector('.detail-tree-body');
|
|
var row = item.querySelector('.detail-tree-row');
|
|
var btn = item.querySelector('.report-tree-toggle');
|
|
if (body) body.innerHTML = html || '<p class="muted">No content.</p>';
|
|
if (body && row && body.hidden) {
|
|
body.hidden = false;
|
|
row.classList.add('report-row--open');
|
|
if (btn) btn.setAttribute('aria-expanded', 'true');
|
|
}
|
|
}
|
|
|
|
function pumpDocsQueue() {
|
|
if (docsLoading || docsQueue.length === 0) return;
|
|
docsLoading = true;
|
|
var sectionId = docsQueue.shift();
|
|
if (docsCache[sectionId]) {
|
|
renderSectionHtml(sectionId, docsCache[sectionId]);
|
|
docsLoading = false;
|
|
pumpDocsQueue();
|
|
return;
|
|
}
|
|
xhrJson(docsApi + '?section=' + encodeURIComponent(sectionId), function (payload) {
|
|
if (payload && payload.ok) {
|
|
docsCache[sectionId] = payload.html || '';
|
|
renderSectionHtml(sectionId, docsCache[sectionId]);
|
|
} else {
|
|
renderSectionHtml(sectionId, '<p class="muted">Failed to load section.</p>');
|
|
}
|
|
docsLoading = false;
|
|
pumpDocsQueue();
|
|
}, function () {
|
|
renderSectionHtml(sectionId, '<p class="muted">Network error while loading section.</p>');
|
|
docsLoading = false;
|
|
pumpDocsQueue();
|
|
});
|
|
}
|
|
|
|
function queueSection(sectionId, front) {
|
|
if (!sectionId) return;
|
|
if (front) docsQueue.unshift(sectionId);
|
|
else docsQueue.push(sectionId);
|
|
pumpDocsQueue();
|
|
}
|
|
|
|
function buildTocNode(node, parentEl) {
|
|
var children = node.children || [];
|
|
if (children.length) {
|
|
children.forEach(function (child) {
|
|
ensureDocsPanel(child.id, child.title);
|
|
queueSection(child.id, false);
|
|
});
|
|
return;
|
|
}
|
|
ensureDocsPanel(node.id, node.title);
|
|
queueSection(node.id, false);
|
|
}
|
|
|
|
function openDeploymentModel(prioritySection) {
|
|
openDocs();
|
|
docsPanels.innerHTML = '';
|
|
docsPanelsById = {};
|
|
docsCache = {};
|
|
docsQueue = [];
|
|
docsLoading = false;
|
|
docsToc.innerHTML = '<p class="muted hub-docs-toc-title">Table of contents</p>';
|
|
|
|
xhrJson(docsApi + '?action=toc', function (payload) {
|
|
if (!payload || !payload.ok || !payload.sections) {
|
|
docsPanels.innerHTML = '<p class="muted">Failed to load table of contents.</p>';
|
|
return;
|
|
}
|
|
payload.sections.forEach(function (node) {
|
|
var group = document.createElement('div');
|
|
group.className = 'hub-docs-toc-group';
|
|
var head = document.createElement('button');
|
|
head.type = 'button';
|
|
head.className = 'hub-docs-toc-head';
|
|
head.textContent = node.title;
|
|
head.dataset.section = node.id;
|
|
head.addEventListener('click', function () {
|
|
var kids = node.children || [];
|
|
if (kids.length) queueSection(kids[0].id, true);
|
|
else queueSection(node.id, true);
|
|
});
|
|
group.appendChild(head);
|
|
(node.children || []).forEach(function (child) {
|
|
var leaf = document.createElement('button');
|
|
leaf.type = 'button';
|
|
leaf.className = 'hub-docs-toc-leaf';
|
|
leaf.textContent = child.title;
|
|
leaf.dataset.section = child.id;
|
|
leaf.addEventListener('click', function () { queueSection(child.id, true); });
|
|
group.appendChild(leaf);
|
|
});
|
|
docsToc.appendChild(group);
|
|
buildTocNode(node, docsPanels);
|
|
});
|
|
if (prioritySection) queueSection(prioritySection, true);
|
|
else if (payload.default_section) queueSection(payload.default_section, true);
|
|
}, function () {
|
|
docsPanels.innerHTML = '<p class="muted">Network error while loading deployment model.</p>';
|
|
});
|
|
}
|
|
|
|
function bindCompassOpen() {
|
|
if (!logoEnabled || !compassLayer) return;
|
|
var open = function () { openDeploymentModel(); };
|
|
compassLayer.addEventListener('click', open);
|
|
compassLayer.addEventListener('keydown', function (ev) {
|
|
if (ev.key === 'Enter' || ev.key === ' ') {
|
|
ev.preventDefault();
|
|
open();
|
|
}
|
|
});
|
|
}
|
|
bindCompassOpen();
|
|
|
|
docsModal.addEventListener('click', function (ev) {
|
|
var target = ev.target;
|
|
if (!(target instanceof Element)) return;
|
|
if (target.hasAttribute('data-close-docs')) closeDocs();
|
|
});
|
|
document.addEventListener('keydown', function (ev) {
|
|
if (ev.key === 'Escape' && !docsModal.hidden) closeDocs();
|
|
});
|
|
|
|
var cityLabels = {
|
|
'America/New_York': 'NEW YORK',
|
|
'Europe/Moscow': 'MOSCOW',
|
|
'Asia/Shanghai': 'BEIJING',
|
|
'Africa/Johannesburg': 'CAPETOWN'
|
|
};
|
|
var roman = ['XII', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI'];
|
|
|
|
function pad2(v) { return String(v).padStart(2, '0'); }
|
|
function systemZone() { return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; }
|
|
|
|
function secondaryZones(primary) {
|
|
var preferred = ['America/New_York', 'Europe/Moscow', 'Asia/Shanghai'];
|
|
var list = preferred.filter(function (z) { return z !== primary; });
|
|
while (list.length < 3) {
|
|
if (!list.includes('Africa/Johannesburg') && primary !== 'Africa/Johannesburg') list.push('Africa/Johannesburg');
|
|
else list.push('UTC');
|
|
}
|
|
return list.slice(0, 3);
|
|
}
|
|
|
|
function timeParts(date, zone) {
|
|
var parts = new Intl.DateTimeFormat('en-US', {
|
|
timeZone: zone, hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
|
|
}).formatToParts(date);
|
|
var map = {};
|
|
parts.forEach(function (p) { if (p.type !== 'literal') map[p.type] = p.value; });
|
|
return { hh: map.hour || '00', mm: map.minute || '00', ss: map.second || '00' };
|
|
}
|
|
|
|
function formatDate(date, zone) {
|
|
var parts = new Intl.DateTimeFormat('en-US', {
|
|
timeZone: zone, weekday: 'short', day: '2-digit', month: 'short', year: 'numeric'
|
|
}).formatToParts(date);
|
|
var map = {};
|
|
parts.forEach(function (p) { if (p.type !== 'literal') map[p.type] = p.value; });
|
|
return (map.weekday || '---').slice(0, 3).toUpperCase() + ' '
|
|
+ (map.day || '00') + ' '
|
|
+ (map.month || '---').slice(0, 3).toUpperCase() + ' '
|
|
+ (map.year || '0000');
|
|
}
|
|
|
|
function buildWorldClock(tz) {
|
|
var cell = document.createElement('div');
|
|
cell.className = 'cw-cell cw-world-cell';
|
|
var stack = document.createElement('div');
|
|
stack.className = 'cw-world-stack';
|
|
|
|
var label = document.createElement('div');
|
|
label.className = 'cw-world-label';
|
|
label.textContent = cityLabels[tz] || tz.toUpperCase();
|
|
stack.appendChild(label);
|
|
|
|
var dial = document.createElement('div');
|
|
dial.className = 'cw-dial';
|
|
for (var i = 0; i < 12; i++) {
|
|
var n = document.createElement('div');
|
|
n.className = 'cw-roman';
|
|
if (i === 0 || i === 3 || i === 6 || i === 9) n.className += ' cw-roman-major';
|
|
else n.className += ' cw-roman-minor';
|
|
n.style.setProperty('--i', String(i));
|
|
n.textContent = roman[i];
|
|
dial.appendChild(n);
|
|
}
|
|
|
|
var hour = document.createElement('div');
|
|
hour.className = 'cw-hand cw-hour';
|
|
var minute = document.createElement('div');
|
|
minute.className = 'cw-hand cw-minute';
|
|
var second = document.createElement('div');
|
|
second.className = 'cw-hand cw-second';
|
|
var pin = document.createElement('div');
|
|
pin.className = 'cw-pin';
|
|
dial.appendChild(hour);
|
|
dial.appendChild(minute);
|
|
dial.appendChild(second);
|
|
dial.appendChild(pin);
|
|
stack.appendChild(dial);
|
|
cell.appendChild(stack);
|
|
|
|
return { tz: tz, hour: hour, minute: minute, second: second, root: cell };
|
|
}
|
|
|
|
var overtimeReady = false;
|
|
var overtimeSymbolIds = {};
|
|
var quartzReady = false;
|
|
var quartzSymbolIds = {};
|
|
|
|
function overtimeSymbolId(ch) {
|
|
if (ch >= '0' && ch <= '9') return 'ot-' + ch;
|
|
if (ch === ':') return 'ot-colon';
|
|
if (ch === '+') return 'ot-plus';
|
|
if (ch === '-') return 'ot-minus';
|
|
if (ch === '/') return 'ot-slash';
|
|
if (ch === ' ') return 'ot-space';
|
|
if (ch >= 'A' && ch <= 'Z') return 'ot-' + ch;
|
|
return null;
|
|
}
|
|
|
|
function loadOvertimeAlphabet(done) {
|
|
var host = document.getElementById('overtime-alphabet-host');
|
|
if (!host) { done(false); return; }
|
|
fetch('assets/overtime-01-alphabet.svg?v=' + logoBuild)
|
|
.then(function (res) { return res.text(); })
|
|
.then(function (svgText) {
|
|
host.innerHTML = svgText;
|
|
host.querySelectorAll('symbol').forEach(function (sym) {
|
|
if (sym.id) overtimeSymbolIds[sym.id] = sym;
|
|
});
|
|
overtimeReady = true;
|
|
done(true);
|
|
})
|
|
.catch(function () { done(false); });
|
|
}
|
|
|
|
function quartzSymbolId(ch) {
|
|
if (ch >= '0' && ch <= '9') return 'qz-' + ch;
|
|
if (ch === ':') return 'qz-colon';
|
|
if (ch === '+') return 'qz-plus';
|
|
if (ch === '-') return 'qz-minus';
|
|
if (ch === '/') return 'qz-slash';
|
|
if (ch === ' ') return 'qz-space';
|
|
if (ch >= 'A' && ch <= 'Z') return 'qz-' + ch;
|
|
return null;
|
|
}
|
|
|
|
function lcdSymbolId(ch) {
|
|
var p = lcdFontPrefix;
|
|
if (ch >= '0' && ch <= '9') return p + '-' + ch;
|
|
if (ch === ':') return p + '-colon';
|
|
if (ch === '+') return p + '-plus';
|
|
if (ch === '-') return p + '-minus';
|
|
if (ch === '/') return p + '-slash';
|
|
if (ch === '.') return p + '-dot';
|
|
if (ch === ' ') return p + '-space';
|
|
if (ch >= 'A' && ch <= 'Z') return p + '-' + ch;
|
|
if (ch >= 'a' && ch <= 'z') return p + '-' + ch.toUpperCase();
|
|
return null;
|
|
}
|
|
|
|
function loadLcdAlphabet(done) {
|
|
var host = document.getElementById('lcd-alphabet-host');
|
|
var spec = LCD_FONT_CATALOG[clockFontKey];
|
|
if (!host || !spec) { done(false); return; }
|
|
fetch(hubAsset(spec.url) + '?v=' + logoBuild)
|
|
.then(function (res) {
|
|
if (!res.ok) throw new Error('alphabet fetch failed');
|
|
return res.text();
|
|
})
|
|
.then(function (svgText) {
|
|
host.replaceChildren();
|
|
var parser = new DOMParser();
|
|
var doc = parser.parseFromString(svgText, 'image/svg+xml');
|
|
var sprite = doc.documentElement;
|
|
if (sprite.querySelector('parsererror')) throw new Error('alphabet parse failed');
|
|
sprite.setAttribute('aria-hidden', 'true');
|
|
sprite.style.cssText = 'position:absolute;width:0;height:0;overflow:hidden;pointer-events:none';
|
|
host.appendChild(sprite);
|
|
lcdSymbolIds = {};
|
|
sprite.querySelectorAll('symbol').forEach(function (sym) {
|
|
if (sym.id) lcdSymbolIds[sym.id] = sym;
|
|
});
|
|
lcdFontReady = Object.keys(lcdSymbolIds).length > 0;
|
|
document.documentElement.setAttribute('data-clock-font', 'mixed');
|
|
var widget = document.getElementById('hub-clock-widget');
|
|
if (widget) widget.setAttribute('data-lcd-mode', lcdFontReady ? 'svg' : 'text');
|
|
done(lcdFontReady);
|
|
})
|
|
.catch(function () {
|
|
lcdFontReady = false;
|
|
var widget = document.getElementById('hub-clock-widget');
|
|
if (widget) widget.setAttribute('data-lcd-mode', 'text');
|
|
done(false);
|
|
});
|
|
}
|
|
|
|
function cloneLcdSymbolContent(svg, sym) {
|
|
sym.childNodes.forEach(function (node) {
|
|
if (node.nodeType !== 1) return;
|
|
svg.appendChild(node.cloneNode(true));
|
|
});
|
|
}
|
|
|
|
function renderLcdText(el, text, opts) {
|
|
opts = opts || {};
|
|
if (!el) return;
|
|
el.textContent = text;
|
|
el.classList.add('cw-lcd-text');
|
|
if (opts.blinkColon) el.classList.add('ot-colon-blink');
|
|
}
|
|
|
|
function loadQuartzAlphabet(done) {
|
|
var host = document.getElementById('quartz-alphabet-host');
|
|
if (!host) { done(false); return; }
|
|
fetch('assets/quartz-alphabet.svg?v=' + logoBuild)
|
|
.then(function (res) { return res.text(); })
|
|
.then(function (svgText) {
|
|
host.innerHTML = svgText;
|
|
host.querySelectorAll('symbol').forEach(function (sym) {
|
|
if (sym.id) quartzSymbolIds[sym.id] = sym;
|
|
});
|
|
quartzReady = true;
|
|
done(true);
|
|
})
|
|
.catch(function () { done(false); });
|
|
}
|
|
|
|
var OVERTIME_SEG_GRID = [
|
|
[3, 1, 14, 3], [1, 4, 3, 11], [16, 4, 3, 11],
|
|
[3, 15, 14, 3], [1, 17, 3, 11], [16, 17, 3, 11],
|
|
[3, 28, 14, 3]
|
|
];
|
|
|
|
function parseOvertimeRects(sym) {
|
|
var rects = [];
|
|
sym.querySelectorAll('rect').forEach(function (node) {
|
|
rects.push({
|
|
x: parseFloat(node.getAttribute('x') || 0),
|
|
y: parseFloat(node.getAttribute('y') || 0),
|
|
w: parseFloat(node.getAttribute('width') || 0),
|
|
h: parseFloat(node.getAttribute('height') || 0)
|
|
});
|
|
});
|
|
return rects;
|
|
}
|
|
|
|
function overtimeSegMatchesRect(seg, rect) {
|
|
var eps = 1.25;
|
|
return Math.abs(rect.x - seg[0]) <= eps
|
|
&& Math.abs(rect.y - seg[1]) <= eps
|
|
&& Math.abs(rect.w - seg[2]) <= eps
|
|
&& Math.abs(rect.h - seg[3]) <= eps;
|
|
}
|
|
|
|
function overtimeSegIsLit(seg, activeRects) {
|
|
for (var i = 0; i < activeRects.length; i++) {
|
|
if (overtimeSegMatchesRect(seg, activeRects[i])) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function buildOvertimeInactiveGrid(svg, sym, glyphW) {
|
|
if (glyphW === 12 || glyphW === 8 || glyphW === 16) return;
|
|
if (glyphW !== 20 && glyphW < 22) return;
|
|
var activeRects = parseOvertimeRects(sym);
|
|
var seg = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
seg.setAttribute('class', 'ot-grid');
|
|
OVERTIME_SEG_GRID.forEach(function (d) {
|
|
if (overtimeSegIsLit(d, activeRects)) return;
|
|
var r = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
r.setAttribute('x', String(d[0]));
|
|
r.setAttribute('y', String(d[1]));
|
|
r.setAttribute('width', String(d[2]));
|
|
r.setAttribute('height', String(d[3]));
|
|
r.setAttribute('rx', '1');
|
|
seg.appendChild(r);
|
|
});
|
|
if (seg.childNodes.length) svg.appendChild(seg);
|
|
}
|
|
|
|
function cloneOvertimeSymbol(svg, sym) {
|
|
sym.querySelectorAll('rect').forEach(function (node) {
|
|
var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
['x', 'y', 'width', 'height', 'rx', 'transform'].forEach(function (attr) {
|
|
var val = node.getAttribute(attr);
|
|
if (val != null) rect.setAttribute(attr, val);
|
|
});
|
|
rect.setAttribute('class', 'ot-active');
|
|
svg.appendChild(rect);
|
|
});
|
|
}
|
|
|
|
function renderOvertimeText(el, text, opts) {
|
|
opts = opts || {};
|
|
if (!el) return;
|
|
el.innerHTML = '';
|
|
if (!overtimeReady) {
|
|
el.textContent = text;
|
|
return;
|
|
}
|
|
String(text).toUpperCase().split('').forEach(function (ch) {
|
|
if (opts.forceFallback) {
|
|
var forced = document.createElement('span');
|
|
forced.className = 'ot-fallback-char' + (opts.bold ? ' ot-fallback-char--bold' : '');
|
|
forced.textContent = ch === ' ' ? '\u00A0' : ch;
|
|
el.appendChild(forced);
|
|
return;
|
|
}
|
|
var sid = overtimeSymbolId(ch);
|
|
if (!sid || !overtimeSymbolIds[sid]) {
|
|
var fb = document.createElement('span');
|
|
fb.className = 'ot-fallback-char' + (opts.bold ? ' ot-fallback-char--bold' : '');
|
|
fb.textContent = ch === ' ' ? '\u00A0' : ch;
|
|
el.appendChild(fb);
|
|
return;
|
|
}
|
|
var wrap = document.createElement('span');
|
|
wrap.className = 'ot-glyph-wrap' + (opts.bold ? ' ot-glyph-wrap--bold' : '');
|
|
if (opts.blinkColon && ch === ':') wrap.className += ' ot-colon-blink';
|
|
var sym = overtimeSymbolIds[sid];
|
|
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
var viewBox = sym.getAttribute('viewBox') || '0 0 20 32';
|
|
svg.setAttribute('viewBox', viewBox);
|
|
svg.setAttribute('class', 'ot-glyph');
|
|
svg.setAttribute('aria-hidden', 'true');
|
|
var vb = viewBox.split(/\s+/).map(function (v) { return Number(v) || 0; });
|
|
var glyphW = vb[2] || 20;
|
|
if (opts.ghost !== false) buildOvertimeInactiveGrid(svg, sym, glyphW);
|
|
cloneOvertimeSymbol(svg, sym);
|
|
wrap.appendChild(svg);
|
|
el.appendChild(wrap);
|
|
});
|
|
}
|
|
|
|
/* Clock digits: original Overtime path (<use> only, no ghost grid). */
|
|
function renderOvertimeDigits(el, text, blinkColon, bold) {
|
|
if (!el) return;
|
|
el.innerHTML = '';
|
|
if (!overtimeReady) {
|
|
el.textContent = text;
|
|
return;
|
|
}
|
|
text.split('').forEach(function (ch) {
|
|
var sid = overtimeSymbolId(ch);
|
|
if (!sid || !overtimeSymbolIds[sid]) {
|
|
el.appendChild(document.createTextNode(ch));
|
|
return;
|
|
}
|
|
var wrap = document.createElement('span');
|
|
wrap.className = 'ot-glyph-wrap' + (bold ? ' ot-glyph-wrap--bold' : '');
|
|
if (blinkColon && ch === ':') wrap.className += ' ot-colon-blink';
|
|
var sym = overtimeSymbolIds[sid];
|
|
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
svg.setAttribute('viewBox', sym.getAttribute('viewBox') || '0 0 20 32');
|
|
svg.setAttribute('class', 'ot-glyph');
|
|
var use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
|
use.setAttribute('href', '#' + sid);
|
|
svg.appendChild(use);
|
|
wrap.appendChild(svg);
|
|
el.appendChild(wrap);
|
|
});
|
|
}
|
|
|
|
function renderQuartzText(el, text, opts) {
|
|
opts = opts || {};
|
|
if (!el) return;
|
|
el.innerHTML = '';
|
|
if (!quartzReady) {
|
|
el.textContent = text;
|
|
return;
|
|
}
|
|
String(text).toUpperCase().split('').forEach(function (ch) {
|
|
var sid = quartzSymbolId(ch);
|
|
if (!sid || !quartzSymbolIds[sid]) {
|
|
var fb = document.createElement('span');
|
|
fb.className = 'qz-fallback-char' + (opts.bold ? ' qz-fallback-char--bold' : '');
|
|
fb.textContent = ch === ' ' ? '\u00A0' : ch;
|
|
el.appendChild(fb);
|
|
return;
|
|
}
|
|
var wrap = document.createElement('span');
|
|
wrap.className = 'qz-glyph-wrap' + (opts.bold ? ' qz-glyph-wrap--bold' : '');
|
|
if (opts.blinkColon && ch === ':') wrap.className += ' qz-colon-blink';
|
|
var sym = quartzSymbolIds[sid];
|
|
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
var viewBox = sym.getAttribute('viewBox') || '0 0 20 32';
|
|
svg.setAttribute('viewBox', viewBox);
|
|
svg.setAttribute('class', 'qz-glyph');
|
|
var vb = viewBox.split(/\s+/).map(function (v) { return Number(v) || 0; });
|
|
var use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
|
use.setAttribute('href', '#' + sid);
|
|
use.setAttribute('class', 'qz-active');
|
|
svg.appendChild(use);
|
|
wrap.appendChild(svg);
|
|
el.appendChild(wrap);
|
|
});
|
|
}
|
|
|
|
function renderQuartzDigits(el, text, blinkColon, bold) {
|
|
renderQuartzText(el, text, { blinkColon: blinkColon, bold: bold });
|
|
}
|
|
|
|
function regionLabel(tz) {
|
|
var parts = tz.split('/');
|
|
if (parts.length >= 2) {
|
|
return parts[0].replace(/_/g, ' ').toUpperCase() + '/' + parts[parts.length - 1].replace(/_/g, ' ').toUpperCase();
|
|
}
|
|
return tz.toUpperCase();
|
|
}
|
|
|
|
function gmtOffsetLabel(date, tz) {
|
|
try {
|
|
var parts = new Intl.DateTimeFormat('en-US', { timeZone: tz, timeZoneName: 'shortOffset' }).formatToParts(date);
|
|
var name = parts.find(function (p) { return p.type === 'timeZoneName'; });
|
|
if (name && name.value) {
|
|
return name.value.replace('GMT', 'GMT').replace('UTC', 'GMT').toUpperCase();
|
|
}
|
|
} catch (e) {}
|
|
return 'GMT+0';
|
|
}
|
|
|
|
function formatDateLine(date, zone) {
|
|
var parts = new Intl.DateTimeFormat('en-US', {
|
|
timeZone: zone, weekday: 'short', day: '2-digit', month: 'short', year: 'numeric'
|
|
}).formatToParts(date);
|
|
var map = {};
|
|
parts.forEach(function (p) { if (p.type !== 'literal') map[p.type] = p.value; });
|
|
return (map.weekday || '---').slice(0, 3).toUpperCase() + ' '
|
|
+ String(Number(map.day || '0')).padStart(2, '0') + ' '
|
|
+ (map.month || '---').slice(0, 3).toUpperCase() + ' '
|
|
+ (map.year || '0000');
|
|
}
|
|
|
|
var primaryTz = systemZone();
|
|
var worldRow = document.getElementById('cw-world-row');
|
|
var secondary = secondaryZones(primaryTz).map(function (tz) {
|
|
var model = buildWorldClock(tz);
|
|
worldRow.appendChild(model.root);
|
|
return model;
|
|
});
|
|
|
|
function setClockopiaText(el, text) {
|
|
renderLcdText(el, text, {});
|
|
}
|
|
|
|
function updateClockWidget() {
|
|
var now = new Date();
|
|
var p = timeParts(now, primaryTz);
|
|
renderLcdText(document.getElementById('cw-hm-lcd'), p.hh + ':' + p.mm, { bold: true });
|
|
renderLcdText(document.getElementById('cw-colon-lcd'), ':', { blinkColon: true });
|
|
renderLcdText(document.getElementById('cw-ss-lcd'), p.ss, {});
|
|
renderLcdText(document.getElementById('cw-tz-text'), gmtOffsetLabel(now, primaryTz), {});
|
|
renderLcdText(document.getElementById('cw-region'), regionLabel(primaryTz), {});
|
|
renderLcdText(document.getElementById('cw-date-line'), formatDateLine(now, primaryTz), {});
|
|
|
|
secondary.forEach(function (clock) {
|
|
var t = timeParts(now, clock.tz);
|
|
var h = Number(t.hh), m = Number(t.mm), s = Number(t.ss);
|
|
var hDeg = ((h % 12) + (m / 60) + (s / 3600)) * 30;
|
|
var mDeg = (m + (s / 60)) * 6;
|
|
var sDeg = s * 6;
|
|
clock.hour.style.transform = 'translate(-50%, -100%) rotate(' + hDeg + 'deg)';
|
|
clock.minute.style.transform = 'translate(-50%, -100%) rotate(' + mDeg + 'deg)';
|
|
clock.second.style.transform = 'translate(-50%, -100%) rotate(' + sDeg + 'deg)';
|
|
});
|
|
}
|
|
|
|
loadLcdAlphabet(function () {
|
|
updateClockWidget();
|
|
setInterval(updateClockWidget, 1000);
|
|
});
|
|
|
|
/* Resize handles: hidden by default (CSS). Skip red debug boxes entirely. */
|
|
(function makeClockWidgetInteractive() {
|
|
var widget = document.getElementById('hub-clock-widget');
|
|
if (!widget) return;
|
|
var drag = null;
|
|
widget.addEventListener('pointerdown', function (ev) {
|
|
var target = ev.target;
|
|
if (!(target instanceof Element)) return;
|
|
var box = widget.getBoundingClientRect();
|
|
var handle = target.closest('[data-resize]');
|
|
var resizeDir = handle ? handle.getAttribute('data-resize') : null;
|
|
if (resizeDir) {
|
|
drag = {
|
|
mode: 'resize',
|
|
dir: resizeDir,
|
|
x: ev.clientX, y: ev.clientY,
|
|
left: box.left, top: box.top, width: box.width, height: box.height
|
|
};
|
|
} else {
|
|
drag = { mode: 'move', dx: ev.clientX - box.left, dy: ev.clientY - box.top };
|
|
}
|
|
widget.setPointerCapture(ev.pointerId);
|
|
widget.classList.add('is-dragging');
|
|
ev.preventDefault();
|
|
});
|
|
|
|
widget.addEventListener('pointermove', function (ev) {
|
|
if (!drag) return;
|
|
if (drag.mode === 'move') {
|
|
widget.style.left = Math.max(0, ev.clientX - drag.dx) + 'px';
|
|
widget.style.top = Math.max(0, ev.clientY - drag.dy) + 'px';
|
|
} else {
|
|
var minW = 280, minH = 210;
|
|
var dx = ev.clientX - drag.x;
|
|
var dy = ev.clientY - drag.y;
|
|
var left = drag.left, top = drag.top, width = drag.width, height = drag.height;
|
|
if (drag.dir.indexOf('e') !== -1) width = Math.max(minW, drag.width + dx);
|
|
if (drag.dir.indexOf('s') !== -1) height = Math.max(minH, drag.height + dy);
|
|
if (drag.dir.indexOf('w') !== -1) {
|
|
width = Math.max(minW, drag.width - dx);
|
|
left = drag.left + (drag.width - width);
|
|
}
|
|
if (drag.dir.indexOf('n') !== -1) {
|
|
height = Math.max(minH, drag.height - dy);
|
|
top = drag.top + (drag.height - height);
|
|
}
|
|
widget.style.left = Math.max(0, left) + 'px';
|
|
widget.style.top = Math.max(0, top) + 'px';
|
|
widget.style.width = width + 'px';
|
|
widget.style.minWidth = width + 'px';
|
|
widget.style.transform = 'none';
|
|
}
|
|
});
|
|
|
|
function endDrag(ev) {
|
|
if (!drag) return;
|
|
drag = null;
|
|
widget.classList.remove('is-dragging');
|
|
widget.releasePointerCapture(ev.pointerId);
|
|
}
|
|
|
|
widget.addEventListener('pointerup', endDrag);
|
|
widget.addEventListener('pointercancel', endDrag);
|
|
})();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|