116 lines
4.2 KiB
JavaScript
116 lines
4.2 KiB
JavaScript
// 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.
|
||
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;
|
||
})();
|