Protect editor sessions from unsaved link navigation and unload
Quality / quality (ubuntu-latest) (push) Failing after 10m2s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-09-05 16:15:01 +05:30
1 parent 88f2e456ef
commit a1704877e8
4 files changed
+70 -1

No files matched your search

+1
View File
@@ -1,2 +1,3 @@
export * from "./state.ts";
export * from "./runtime.ts";
export * from "./unsaved.ts";
+40
View File
@@ -0,0 +1,40 @@
export function mountUnsavedChangesGuard(
root: HTMLElement,
options: { hasUnsavedChanges(): boolean; confirmLeave(): boolean },
): { destroy(): void } {
const document = root.ownerDocument;
const view = document.defaultView!;
let destroyed = false;
const dirty = () => !destroyed && root.isConnected && options.hasUnsavedChanges();
const beforeUnload = (event: BeforeUnloadEvent) => {
if (!dirty()) return;
event.preventDefault();
event.returnValue = "";
};
const click = (event: MouseEvent) => {
if (event.defaultPrevented || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
const target = event.target as Element | null;
const link = target?.closest?.("a[href]") as HTMLAnchorElement | null;
if (!link || link.hasAttribute("download") || (link.target && link.target !== "_self")) return;
const url = new URL(link.href, document.baseURI);
if (!["http:", "https:"].includes(url.protocol)) return;
const current = new URL(view.location.href);
if (url.origin === current.origin && url.pathname === current.pathname && url.search === current.search && url.hash) return;
if (dirty() && !options.confirmLeave()) {
event.preventDefault();
event.stopImmediatePropagation();
}
};
const observer = new view.MutationObserver(() => { if (!root.isConnected) destroy(); });
function destroy() {
if (destroyed) return;
destroyed = true;
view.removeEventListener("beforeunload", beforeUnload);
document.removeEventListener("click", click, true);
observer.disconnect();
}
view.addEventListener("beforeunload", beforeUnload);
document.addEventListener("click", click, true);
observer.observe(document.documentElement, { childList: true, subtree: true });
return { destroy };
}