UNPKG

@lobehub/ui

Version:

Lobe UI is an open-source UI component library for building AIGC web apps

208 lines (205 loc) 5.62 kB
'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