@lobehub/ui
Version:
Lobe UI is an open-source UI component library for building AIGC web apps
208 lines (205 loc) • 5.62 kB
JavaScript
'use client';
import { useIsClient } from "../hooks/useIsClient.mjs";
import { registerDevSingleton } from "../utils/devSingleton.mjs";
import { ModalStackItem } from "./ModalStackItem.mjs";
import { RawModalStackItem } from "./RawModalStackItem.mjs";
import { memo, useEffect, useState, useSyncExternalStore } from "react";
import { jsx } from "react/jsx-runtime";
import { createPortal } from "react-dom";
//#region src/Modal/imperative.tsx
const MODAL_PORTAL_ATTR = "data-lobe-ui-modal-portal";
const containerMap = /* @__PURE__ */ new WeakMap();
let modalStack = [];
let modalSeed = 0;
const listeners = /* @__PURE__ */ new Set();
const rawDestroyTimers = /* @__PURE__ */ new Map();
const notify = () => {
listeners.forEach((listener) => listener());
};
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
const EMPTY_STACK = [];
const getSnapshot = () => modalStack;
const getServerSnapshot = () => EMPTY_STACK;
const getOrCreateContainer = (root) => {
const cached = containerMap.get(root);
if (cached && cached.isConnected) return cached;
const el = document.createElement("div");
el.setAttribute(MODAL_PORTAL_ATTR, "true");
root.append(el);
containerMap.set(root, el);
return el;
};
const resolveRoot = (root) => {
if (root) return root;
return document.body;
};
const ModalPortal = ({ children, root }) => {
const [container, setContainer] = useState(() => {
const resolved = resolveRoot(root);
if (!resolved) return null;
return getOrCreateContainer(resolved);
});
if (!container) setContainer(getOrCreateContainer(document.body));
if (!container) return null;
return createPortal(children, container);
};
const updateModal = (id, nextProps) => {
let changed = false;
modalStack = modalStack.map((item) => {
if (item.id !== id) return item;
if (item.kind !== "modal") return item;
changed = true;
return {
...item,
props: {
...item.props,
...nextProps
}
};
});
if (changed) notify();
};
const updateRawProps = (id, nextProps) => {
let changed = false;
modalStack = modalStack.map((item) => {
if (item.id !== id) return item;
if (item.kind !== "raw") return item;
changed = true;
return {
...item,
props: {
...item.props,
...nextProps
}
};
});
if (changed) notify();
};
const setRawOpen = (id, open) => {
let changed = false;
modalStack = modalStack.map((item) => {
if (item.id !== id) return item;
if (item.kind !== "raw") return item;
if (item.open === open) return item;
changed = true;
return {
...item,
open
};
});
if (open) {
const timer = rawDestroyTimers.get(id);
if (timer) {
clearTimeout(timer);
rawDestroyTimers.delete(id);
}
}
if (changed) notify();
};
const closeModal = (id) => {
const target = modalStack.find((item) => item.id === id);
if (!target) return;
if (target.kind === "modal") {
updateModal(id, { open: false });
return;
}
setRawOpen(id, false);
if (!(target.options?.destroyOnClose ?? true)) return;
const delay = target.options?.destroyDelay ?? 200;
const existing = rawDestroyTimers.get(id);
if (existing) clearTimeout(existing);
const timer = window.setTimeout(() => {
rawDestroyTimers.delete(id);
destroyModal(id);
}, delay);
rawDestroyTimers.set(id, timer);
};
const destroyModal = (id) => {
const timer = rawDestroyTimers.get(id);
if (timer) {
clearTimeout(timer);
rawDestroyTimers.delete(id);
}
const nextStack = modalStack.filter((item) => item.id !== id);
if (nextStack.length === modalStack.length) return;
modalStack = nextStack;
notify();
};
const ModalStack = memo(({ stack }) => {
if (!useIsClient()) return null;
return stack.map((item) => {
if (item.kind === "modal") return /* @__PURE__ */ jsx(ModalStackItem, {
id: item.id,
onClose: closeModal,
onDestroy: destroyModal,
onUpdate: updateModal,
props: item.props
}, item.id);
return /* @__PURE__ */ jsx(RawModalStackItem, {
component: item.component,
id: item.id,
onClose: closeModal,
onUpdate: updateRawProps,
open: item.open,
options: item.options,
props: item.props
}, item.id);
});
});
ModalStack.displayName = "ModalStack";
const ModalHost = ({ root }) => {
const stack = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
const isClient = useIsClient();
useEffect(() => {
if (!isClient) return;
return registerDevSingleton("ModalHost", root ?? document.body);
}, [isClient, root]);
if (!isClient) return null;
if (stack.length === 0) return null;
return /* @__PURE__ */ jsx(ModalPortal, {
root,
children: /* @__PURE__ */ jsx(ModalStack, { stack })
});
};
const createModal = (props) => {
const id = `modal-${Date.now()}-${modalSeed++}`;
modalStack = [...modalStack, {
id,
kind: "modal",
props: {
...props,
open: props.open ?? true
}
}];
notify();
return {
close: () => closeModal(id),
destroy: () => destroyModal(id),
setCanDismissByClickOutside: (value) => updateModal(id, { maskClosable: value }),
update: (nextProps) => updateModal(id, nextProps)
};
};
function createRawModal(component, props, options) {
const id = `modal-${Date.now()}-${modalSeed++}`;
modalStack = [...modalStack, {
component,
id,
kind: "raw",
open: true,
options,
props
}];
notify();
return {
close: () => closeModal(id),
destroy: () => destroyModal(id),
setCanDismissByClickOutside: (value) => updateRawProps(id, { maskClosable: value }),
update: (nextProps) => updateRawProps(id, nextProps)
};
}
//#endregion
export { ModalHost, createModal, createRawModal };
//# sourceMappingURL=imperative.mjs.map