68 lines
2.4 KiB
TypeScript
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;
|
|
}
|