Files
next-tjwater-frontend/src/shared/ui/floating-window-layer.ts
T
jiang 0ae0a41e61
Generic Container CI/CD / test-build-publish (push) Successful in 2m7s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m8s
feat(charts): manage investigation charts in a scrolling workspace
2026-11-10 17:56:23 +08:00

68 lines
2.4 KiB
TypeScript

let order: string[] = [];
const listeners = new Set<() => void>();
let host: HTMLDivElement | undefined;
export type FloatingWindowEntry = { id: string; title: string; focus: () => void; close: () => void };
const registered = new Map<string, {
entry: FloatingWindowEntry;
actions: Pick<FloatingWindowEntry, "focus" | "close">;
}>();
let entries: readonly FloatingWindowEntry[] = [];
function publishWindows() {
entries = order.flatMap((id) => {
const record = registered.get(id);
return record ? [record.entry] : [];
});
listeners.forEach((listener) => listener());
}
export const floatingWindowOrder = () => order;
/** Stable between changes, ordered from the back window to the foreground. */
export const floatingWindowEntries = () => entries;
export const subscribeFloatingWindows = (listener: () => void) => {
listeners.add(listener);
return () => { listeners.delete(listener); };
};
export function registerFloatingWindow(entry: FloatingWindowEntry) {
const previous = registered.get(entry.id);
if (previous) {
// Callbacks can change without invalidating the subscribed presentation.
previous.actions.focus = entry.focus;
previous.actions.close = entry.close;
if (previous.entry.title === entry.title) return;
previous.entry = { ...previous.entry, title: entry.title };
} else {
const actions = { focus: entry.focus, close: entry.close };
registered.set(entry.id, { actions, entry: { id: entry.id, title: entry.title,
focus: () => { if (registered.get(entry.id)?.actions === actions) actions.focus(); },
close: () => { if (registered.get(entry.id)?.actions === actions) actions.close(); } } });
if (!order.includes(entry.id)) order = [...order, entry.id];
}
publishWindows();
}
export function raiseFloatingWindow(id: string) {
if (order.at(-1) === id) return;
order = [...order.filter((item) => item !== id), id];
publishWindows();
}
export function removeFloatingWindow(id: string) {
if (!order.includes(id) && !registered.has(id)) return;
registered.delete(id);
order = order.filter((item) => item !== id);
publishWindows();
}
export function floatingWindowHost() {
if (!host?.isConnected) {
host = document.createElement("div");
// One stacking context keeps windows below dialogs and newly opened menus.
host.className = "pointer-events-none fixed inset-0 z-50";
host.dataset.floatingWindowLayer = "";
document.body.appendChild(host);
}
return host;
}