Files
datascape/assets/overlay.js
T
2026-07-03 11:08:27 +02:00

116 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Full-viewport Overlay component (mobile). Distinct from the .modal dialog:
// this hosts a whole rail (the folder tree or the right-rail widgets/TOC) at
// full screen with its own scroll and a top-right close control. It reuses the
// low-level focus/Escape/bfcache patterns from modal.js rather than that
// component's panel chrome.
//
// openOverlay(node, opts) MOVES `node` (an existing rail <aside>) into the
// overlay body and restores it to its original DOM position on close, so the
// rail keeps its state (expanded tree folders, rendered TOC). Only one overlay
// is open at a time.
(function () {
var overlay = null;
var bodyEl = null;
var closeBtn = null;
var hosted = null; // the moved node
var placeholder = null; // marks where to restore it
var prevFocus = null;
var onKeydown = null;
var onCloseCb = null; // opts.onClose of the currently hosted node
function build() {
overlay = document.createElement('div');
overlay.className = 'overlay';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
var bar = document.createElement('div');
bar.className = 'overlay-bar';
closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'btn btn-fab overlay-close';
closeBtn.title = 'Close';
closeBtn.setAttribute('aria-label', 'Close');
closeBtn.textContent = '×';
closeBtn.addEventListener('click', close);
bar.appendChild(closeBtn);
bodyEl = document.createElement('div');
bodyEl.className = 'overlay-body';
// Any navigation or anchor activation inside a rail dismisses the
// overlay: tree/calendar links navigate away, TOC links scroll the
// page underneath — either way the user should see the result, and
// closing synchronously (before navigation) avoids a bfcache snapshot
// that would restore the overlay open on back-nav.
bodyEl.addEventListener('click', function (e) {
if (e.target.closest('a')) close();
});
overlay.appendChild(bar);
overlay.appendChild(bodyEl);
}
function open(node, opts) {
if (overlay && overlay.parentNode) close();
if (!overlay) build();
opts = opts || {};
prevFocus = document.activeElement;
onCloseCb = typeof opts.onClose === 'function' ? opts.onClose : null;
hosted = node;
placeholder = document.createComment('overlay-slot');
node.parentNode.insertBefore(placeholder, node);
bodyEl.appendChild(node);
document.body.appendChild(overlay);
overlay.classList.add('is-open');
onKeydown = function (e) {
if (e.key === 'Escape') { e.preventDefault(); close(); }
};
document.addEventListener('keydown', onKeydown);
setTimeout(function () { closeBtn.focus(); }, 0);
if (typeof opts.onOpen === 'function') opts.onOpen();
return { close: close };
}
function close() {
if (!overlay || !overlay.parentNode) return;
if (onKeydown) {
document.removeEventListener('keydown', onKeydown);
onKeydown = null;
}
// Restore the hosted rail to its original spot in the shell.
if (hosted && placeholder && placeholder.parentNode) {
placeholder.parentNode.insertBefore(hosted, placeholder);
placeholder.parentNode.removeChild(placeholder);
}
hosted = null;
placeholder = null;
overlay.classList.remove('is-open');
overlay.parentNode.removeChild(overlay);
var toRestore = prevFocus;
prevFocus = null;
if (toRestore && toRestore.focus) {
try { toRestore.focus(); } catch (e) {}
}
// Fires after the node is back in its original spot, so the callback
// sees the final DOM (e.g. the todo rail flushing an autosave).
var cb = onCloseCb;
onCloseCb = null;
if (cb) cb();
}
// Belt-and-suspenders bfcache guard: force the overlay closed (restoring
// the rail) if the page is being hidden while it is still open.
window.addEventListener('pagehide', close);
window.openOverlay = open;
window.closeOverlay = close;
})();