Snapshot of main at 9f39a6e1e (2026-11-02)

This commit is contained in:
Ronald Huynen committed 2026-11-02 10:41:47 +01:00
commit 6ccf1792d7
2580 files changed
+1040202

No files matched your search

+198
View File
@@ -0,0 +1,198 @@
/**
* Full screen for the trading-network graph.
*
* What goes full screen is the graph's own box: its header (caption, subtext,
* spread and full-screen buttons), the canvas and the time-lapse panel - not
* the page around it. Esc leaves it; that is the browser's own handling.
*
* In full screen the header floats over the canvas (see the markup), and it
* and the playback panel fade out after IDLE_MS without activity, so the graph
* has the whole screen. Any pointer movement, tap, drag, wheel or key brings
* them back for another IDLE_MS.
*/
const IDLE_MS = 2000;
/**
* @param {object} opts
* @param {HTMLElement} opts.screenBox The box that goes full screen.
* @param {HTMLElement} opts.frameBox The graph's box; fills the screen (by CSS).
* @param {HTMLElement} opts.headerBox The header row that hides.
* @param {HTMLElement} opts.button The full-screen toggle.
* @param {Function} opts.panel -> the playback panel, or null.
* @param {AbortSignal} opts.signal Removes every listener on rebuild.
* @param {Function} opts.onResize (inFullscreen, headerHeight) after a switch.
*/
function setupFullscreen({ screenBox, frameBox, headerBox, button, panel, signal, onResize }) {
if (!screenBox || !frameBox || !button) {
return;
}
// Prefixed for older Safari; absent altogether on iPhone, where only video
// can go full screen - the button then goes rather than doing nothing.
const enter = screenBox.requestFullscreen || screenBox.webkitRequestFullscreen;
const leave = document.exitFullscreen || document.webkitExitFullscreen;
if (!enter || !leave) {
button.hidden = true;
return;
}
const active = () => document.fullscreenElement || document.webkitFullscreenElement || null;
const options = { capture: true, passive: true, signal };
let idleTimer = null;
// What the pointer is doing, for busy().
let pointerType = 'mouse';
let pressed = false;
let lastPoint = null;
// Whether the pointer's last known position lies on an element.
//
// By position rather than ":hover": hidden controls take no pointer
// events, so a mouse that wakes them by moving onto them and then rests
// there never makes them ":hover" - they hid from under it.
const under = (el) => {
if (!el || !lastPoint) {
return false;
}
const r = el.getBoundingClientRect();
return lastPoint.x >= r.left && lastPoint.x <= r.right
&& lastPoint.y >= r.top && lastPoint.y <= r.bottom;
};
const busy = () => {
if (pressed) {
// Mid-drag: a slider, or panning the graph.
return true;
}
// Kept up while the mouse rests on them. Mouse only: a finger lifted
// after a tap is not resting anywhere, and the last tap would
// otherwise keep them up for ever.
//
// Focus deliberately does NOT count. Clicking the full-screen button
// leaves it focused inside the header, and treating focus as "in use"
// meant the controls never hid at all.
return pointerType === 'mouse'
&& [headerBox, panel ? panel() : null].some(under);
};
const scheduleHide = () => {
clearTimeout(idleTimer);
idleTimer = setTimeout(() => {
if (active() !== screenBox) {
return;
}
if (busy()) {
scheduleHide();
return;
}
screenBox.classList.add('controls-hidden');
}, IDLE_MS);
};
const wake = () => {
if (active() !== screenBox) {
return;
}
screenBox.classList.remove('controls-hidden');
scheduleHide();
};
// Capture phase on the document: the canvas handles pointer events
// itself, and a drag on it must still count as movement. On the document
// rather than the screen box so that nothing inside the box - whatever
// element ends up under the pointer after a rebuild - can keep the
// controls from coming back; wake() itself ignores anything while the box
// is not full screen. mousemove as well, for browsers that leave out
// pointer events in some cases.
['pointermove', 'mousemove', 'pointerdown', 'touchstart', 'wheel', 'keydown'].forEach((type) => {
document.addEventListener(type, wake, options);
});
// A rebuild aborts these listeners and sets up afresh. A hide still
// pending from the old setup would otherwise fire later and hide the
// controls behind the new one's back.
if (signal) {
signal.addEventListener('abort', () => clearTimeout(idleTimer), { once: true });
}
screenBox.addEventListener('pointerdown', (event) => {
pressed = true;
pointerType = event.pointerType || 'mouse';
lastPoint = { x: event.clientX, y: event.clientY };
}, options);
screenBox.addEventListener('pointermove', (event) => {
pointerType = event.pointerType || pointerType;
lastPoint = { x: event.clientX, y: event.clientY };
}, options);
// Gone from the screen box altogether (another window, the taskbar).
screenBox.addEventListener('pointerleave', () => {
lastPoint = null;
}, { signal });
// On the document: a drag can end outside the box. Waking again on
// release restarts the countdown from the end of the drag, not its start.
['pointerup', 'pointercancel'].forEach((type) => {
document.addEventListener(type, () => {
if (pressed) {
pressed = false;
wake();
}
}, options);
});
button.addEventListener('click', (event) => {
event.preventDefault();
const result = active() === screenBox
? leave.call(document)
: enter.call(screenBox);
if (result && typeof result.catch === 'function') {
result.catch(() => {});
}
}, { signal });
const onChange = () => {
const on = active() === screenBox;
const label = on ? button.dataset.labelExit : button.dataset.labelEnter;
button.setAttribute('title', label);
button.setAttribute('aria-label', label);
button.querySelector('[data-fullscreen-enter]')?.classList.toggle('hidden', on);
button.querySelector('[data-fullscreen-exit]')?.classList.toggle('hidden', !on);
// The layout itself - the header floating over the canvas, the graph
// taking the whole screen - is pure CSS on :fullscreen (the
// group-fullscreen variant in the markup). Not a class or inline
// height set here: the time-lapse settings are wire:model.live, and
// the morph after each one reset both to the server's markup while
// the box stayed full screen, leaving the graph at its normal height.
screenBox.classList.remove('controls-hidden');
clearTimeout(idleTimer);
if (on) {
scheduleHide();
}
if (onResize) {
requestAnimationFrame(() => onResize(on, headerBox ? headerBox.offsetHeight : 0));
}
};
document.addEventListener('fullscreenchange', onChange, { signal });
document.addEventListener('webkitfullscreenchange', onChange, { signal });
}
export { setupFullscreen, IDLE_MS };