import {Component, createSignal, mergeProps, onCleanup, splitProps} from "solid-js"; import classNames from "./classnames"; import addEventListener from "dom-helpers/addEventListener"; import canUseDOM from "dom-helpers/canUseDOM"; import ownerDocument from "dom-helpers/ownerDocument"; import removeEventListener from "dom-helpers/removeEventListener"; import getScrollbarSize from "dom-helpers/scrollbarSize"; import transitionEnd from "dom-helpers/transitionEnd"; import { Modal as BaseModal, ModalHandle, ModalInstance, ModalProps as BaseModalProps, } from "solid-bootstrap-core"; import {getSharedManager} from "./BootstrapModalManager"; import Fade, {FadeProps} from "./Fade"; import ModalBody from "./ModalBody"; import ModalContext from "./ModalContext"; import ModalDialog from "./ModalDialog"; import ModalFooter from "./ModalFooter"; import ModalHeader from "./ModalHeader"; import ModalTitle from "./ModalTitle"; import {BsPrefixRefForwardingComponent} from "./helpers"; import {useBootstrapPrefix, useIsRTL} from "./ThemeProvider"; import {Dynamic, isServer} from "solid-js/web"; export interface ModalProps extends Omit< BaseModalProps, "role" | "renderBackdrop" | "renderDialog" | "transition" | "backdropTransition" | "children" > { size?: "sm" | "lg" | "xl"; fullscreen?: true | string | "sm-down" | "md-down" | "lg-down" | "xl-down" | "xxl-down"; bsPrefix?: string; centered?: boolean; backdropClass?: string; animation?: boolean; dialogClass?: string; contentClass?: string; dialogAs?: Component; scrollable?: boolean; [other: string]: any; } const defaultProps: Partial = { show: false, backdrop: true, keyboard: true, autoFocus: true, enforceFocus: true, restoreFocus: true, animation: true, dialogAs: ModalDialog, }; /* eslint-disable no-use-before-define, react/no-multi-comp */ function DialogTransition(props: FadeProps) { return ; } function BackdropTransition(props: FadeProps) { return ; } /* eslint-enable no-use-before-define */ const Modal: BsPrefixRefForwardingComponent<"div", ModalProps> = (p: ModalProps) => { const [local, props] = splitProps(mergeProps(defaultProps, p), [ "bsPrefix", "class", "style", "dialogClass", "contentClass", "children", "dialogAs", "aria-labelledby", /* BaseModal props */ "show", "animation", "backdrop", "keyboard", "onEscapeKeyDown", "onShow", "onHide", "container", "autoFocus", "enforceFocus", "restoreFocus", "restoreFocusOptions", "onEntered", "onExit", "onExiting", "onEnter", "onEntering", "onExited", "backdropClass", "manager", ]); const [modalStyle, setStyle] = createSignal({}); const [animateStaticModal, setAnimateStaticModal] = createSignal(false); let waitingForMouseUpRef = false; let ignoreBackdropClickRef = false; let removeStaticModalAnimationRef: (() => void) | null = null; let modal: ModalHandle; const isRTL = useIsRTL(); const mergedRef = (ref: ModalHandle) => { modal = ref; props.ref?.(ref); }; const bsPrefix = useBootstrapPrefix(local.bsPrefix, "modal"); const modalContext = { get onHide() { return local.onHide; }, }; function getModalManager() { if (local.manager) return local.manager; return getSharedManager({isRTL: isRTL()}); } function updateDialogStyle(node: Element) { if (!canUseDOM) return; const containerIsOverflowing = getModalManager().getScrollbarWidth() > 0; const modalIsOverflowing = node.scrollHeight > ownerDocument(node).documentElement.clientHeight; setStyle({ paddingRight: containerIsOverflowing && !modalIsOverflowing ? getScrollbarSize() : undefined, paddingLeft: !containerIsOverflowing && modalIsOverflowing ? getScrollbarSize() : undefined, }); } const handleWindowResize = () => { if (modal) { updateDialogStyle(modal.dialog!); } }; onCleanup(() => { if (!isServer) { removeEventListener(window as any, "resize", handleWindowResize); } removeStaticModalAnimationRef?.(); }); // We prevent the modal from closing during a drag by detecting where the // the click originates from. If it starts in the modal and then ends outside // don't close. const handleDialogMouseDown = () => { waitingForMouseUpRef = true; }; const handleMouseUp = (e: any) => { if (waitingForMouseUpRef && modal && e.target === modal.dialog) { ignoreBackdropClickRef = true; } waitingForMouseUpRef = false; }; const handleStaticModalAnimation = () => { setAnimateStaticModal(true); removeStaticModalAnimationRef = transitionEnd(modal!.dialog as any, () => { setAnimateStaticModal(false); }); }; const handleStaticBackdropClick = (e: any) => { if (e.target !== e.currentTarget) { return; } handleStaticModalAnimation(); }; const handleClick = (e: any) => { if (local.backdrop === "static") { handleStaticBackdropClick(e); return; } if (ignoreBackdropClickRef || e.target !== e.currentTarget) { ignoreBackdropClickRef = false; return; } local.onHide?.(); }; const handleEscapeKeyDown = (e: any) => { if (!local.keyboard && local.backdrop === "static") { // Call preventDefault to stop modal from closing in restart ui, // then play our animation. e.preventDefault(); handleStaticModalAnimation(); } else if (local.keyboard && local.onEscapeKeyDown) { local.onEscapeKeyDown(e); } }; const handleEnter: BaseModalProps["onEnter"] = (node?: any, ...args) => { if (node) { // @ts-ignore node.style.display = "block"; updateDialogStyle(node); } local.onEnter?.(node, ...args); }; const handleExit: BaseModalProps["onExit"] = (...args) => { removeStaticModalAnimationRef?.(); local.onExit?.(...args); }; const handleEntering: BaseModalProps["onEntering"] = (...args) => { local.onEntering?.(...args); if (!isServer) { // FIXME: This should work even when animation is disabled. addEventListener(window as any, "resize", handleWindowResize); } }; const handleExited: BaseModalProps["onExited"] = (node?: any) => { if (node) node.style.display = ""; // RHL removes it sometimes local.onExited?.(node); if (!isServer) { // FIXME: This should work even when animation is disabled. removeEventListener(window as any, "resize", handleWindowResize); } }; const renderBackdrop = (backdropProps: any) => (
); const baseModalStyle = () => { let s = {...local.style, ...modalStyle()}; // Sets `display` always block when `animation` is false if (!local.animation) { s.display = "block"; } return s; }; const renderDialog = (dialogProps: any) => ( ); return ( ); }; export default Object.assign(Modal, { Body: ModalBody, Header: ModalHeader, Title: ModalTitle, Footer: ModalFooter, Dialog: ModalDialog, TRANSITION_DURATION: 300, BACKDROP_TRANSITION_DURATION: 150, });