132 lines
5.7 KiB
JavaScript
132 lines
5.7 KiB
JavaScript
(function (root, factory) {
|
|
if (typeof module === 'object' && module.exports) module.exports = factory;
|
|
else {
|
|
root.createSignOutReview = factory;
|
|
root.createSignOutReview.mount = boundary => {
|
|
const document = root.document;
|
|
const review = factory({
|
|
sheet: document.getElementById('sign-out-review-sheet'),
|
|
heading: document.getElementById('sign-out-review-heading'),
|
|
summary: document.getElementById('sign-out-review-summary'),
|
|
warning: document.getElementById('sign-out-review-warning'),
|
|
cancelButton: document.getElementById('cancel-sign-out'),
|
|
confirmButton: document.getElementById('confirm-sign-out'),
|
|
escapeTarget: document,
|
|
historyTarget: root,
|
|
history: root.history,
|
|
backgroundTargets: Array.from(document.querySelectorAll('body > :not(#sign-out-review-sheet)')),
|
|
getActiveElement: () => document.activeElement,
|
|
localStorage: root.localStorage,
|
|
sessionStorage: root.sessionStorage,
|
|
privateDatabases: root.stackchainPrivateDatabases,
|
|
inspectPrivateDatabases: root.inspectStackchainPrivateDatabases,
|
|
onConfirm: mode => mode === 'all'
|
|
? boundary.signOutAllDevices({ reviewed: true })
|
|
: boundary.signOut(),
|
|
});
|
|
review.start();
|
|
return review;
|
|
};
|
|
}
|
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function createSignOutReview(options) {
|
|
let mode = 'current';
|
|
let launcher = null;
|
|
let open = false;
|
|
let historyEntry = false;
|
|
|
|
function ownedItemCount(storage) {
|
|
if (!storage) return 0;
|
|
let count = 0;
|
|
for (let index = 0; index < storage.length; index += 1) {
|
|
if (storage.key(index)?.startsWith('stackchain.')) count += 1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
async function show(nextMode, source) {
|
|
mode = nextMode;
|
|
launcher = source;
|
|
const itemCount = ownedItemCount(options.localStorage)
|
|
+ (options.sessionStorage === options.localStorage ? 0 : ownedItemCount(options.sessionStorage));
|
|
let inventory = { recordCount: 0, unavailable: true };
|
|
try {
|
|
inventory = await options.inspectPrivateDatabases?.(options.privateDatabases) || inventory;
|
|
} catch (_error) { /* Unknown private work must use the guarded confirmation path. */ }
|
|
const recordCount = Math.max(0, Number(inventory.recordCount) || 0);
|
|
options.heading.textContent = mode === 'all' ? 'Review sign out on every device' : 'Review sign out';
|
|
options.summary.textContent = inventory.unavailable
|
|
? `This device has ${itemCount} private browser item${itemCount === 1 ? '' : 's'}; private work status is unknown.`
|
|
: `This device has ${itemCount} private browser item${itemCount === 1 ? '' : 's'} and ${recordCount} private work record${recordCount === 1 ? '' : 's'}.`;
|
|
const atRisk = itemCount > 0 || recordCount > 0 || inventory.unavailable;
|
|
if (mode === 'all') {
|
|
options.warning.textContent = atRisk
|
|
? 'Every device will need to sign in again. Private drafts or queued work on this device may not be synced and will be erased.'
|
|
: 'Every device will need to sign in again. This device will be cleared after its session is revoked.';
|
|
} else {
|
|
options.warning.textContent = atRisk
|
|
? 'Private drafts or queued work may not be synced. Signing out erases them from this device.'
|
|
: 'Signing out clears this device after the session is revoked.';
|
|
}
|
|
options.confirmButton.textContent = atRisk
|
|
? 'Sign out and erase private work'
|
|
: (mode === 'all' ? 'Sign out all devices' : 'Sign out');
|
|
options.sheet.hidden = false;
|
|
(options.backgroundTargets || []).forEach(target => { target.inert = true; });
|
|
open = true;
|
|
options.history?.pushState?.({ stackchainSignOutReview: true }, '');
|
|
historyEntry = Boolean(options.history?.back);
|
|
options.cancelButton.focus();
|
|
}
|
|
|
|
function close({ restoreFocus = true } = {}) {
|
|
if (!open) return;
|
|
options.sheet.hidden = true;
|
|
(options.backgroundTargets || []).forEach(target => { target.inert = false; });
|
|
open = false;
|
|
if (restoreFocus) launcher?.focus?.();
|
|
}
|
|
|
|
function dismiss() {
|
|
if (historyEntry) {
|
|
historyEntry = false;
|
|
options.history.back();
|
|
} else close();
|
|
}
|
|
|
|
async function confirm() {
|
|
options.confirmButton.disabled = true;
|
|
try {
|
|
await options.onConfirm(mode);
|
|
close({ restoreFocus: false });
|
|
} catch (_error) {
|
|
options.warning.textContent = 'Sign out could not finish clearing private work. Close other Stackchain tabs, then retry.';
|
|
} finally {
|
|
options.confirmButton.disabled = false;
|
|
}
|
|
}
|
|
|
|
function start() {
|
|
options.launcher?.addEventListener('click', event => show('current', event.currentTarget || options.launcher));
|
|
options.allLauncher?.addEventListener('click', event => show('all', event.currentTarget || options.allLauncher));
|
|
options.cancelButton?.addEventListener('click', dismiss);
|
|
options.confirmButton?.addEventListener('click', confirm);
|
|
options.escapeTarget?.addEventListener('keydown', event => {
|
|
if (!open) return;
|
|
if (event.key === 'Escape') { event.preventDefault(); dismiss(); return; }
|
|
if (event.key !== 'Tab') return;
|
|
const focusable = Array.from(options.sheet.querySelectorAll('button:not([disabled])'));
|
|
const first = focusable[0];
|
|
const last = focusable[focusable.length - 1];
|
|
const active = options.getActiveElement?.();
|
|
if (!event.shiftKey && active === last) { event.preventDefault(); first?.focus(); }
|
|
if (event.shiftKey && active === first) { event.preventDefault(); last?.focus(); }
|
|
});
|
|
options.historyTarget?.addEventListener('popstate', () => {
|
|
historyEntry = false;
|
|
close();
|
|
});
|
|
}
|
|
|
|
return { start, show, close };
|
|
});
|