react-material-overlay
Version:
A fully featured Material UI implementation of overlays like modals, alert dialogs, lightboxes, and bottom sheets featuring easy stack management and browser history integration
189 lines (149 loc) • 4.82 kB
text/typescript
import React from 'react';
import { isNull, omitBy } from 'lodash';
import type { Id, Notify } from '../../types';
import enhancedMerge from '../../utils/enhancedMerge';
import mergeClasses from '../../utils/mergeClasses';
import { canBeRendered, isFn, isStr } from '../../utils/propValidator';
import RmoStack from '../RmoStack';
import {
IModal,
IModalContentProps,
IModalDefaultOptions,
IModalProps,
INotValidatedModalProps,
ModalContent
} from './types';
export type ContainerObserver = ReturnType<typeof createContainerObserver>;
export function createContainerObserver(containerId: Id, containerDefaultOptions: IModalDefaultOptions) {
let modalCount = 0;
let activeModals: Id[] = [];
let snapshot: IModal[] = [];
let defaultOptions = containerDefaultOptions;
const modals = new Map<Id, IModal>();
const listeners = new Set<Notify>();
const observe = (notify: Notify) => {
listeners.add(notify);
return () => listeners.delete(notify);
};
const notify = () => {
snapshot = Array.from(modals.values());
listeners.forEach((cb) => cb());
};
function shouldIgnorePop(modalId: Id) {
const lastActiveId = activeModals.at(-1);
return lastActiveId !== modalId;
}
const popModal = () => {
activeModals = activeModals.slice(0, -1);
notify();
};
const pushModal = (modal: IModal) => {
const { modalId, onOpen } = modal.props;
modals.set(modalId, modal);
activeModals = [...activeModals, modalId];
notify();
if (isFn(onOpen)) {
onOpen();
}
};
const buildModal = (content: ModalContent, options: INotValidatedModalProps) => {
if (modals.has(options.modalId)) {
throw new Error('modal is duplicated!');
}
const { modalId } = options;
const closeModal = async () => {
if (shouldIgnorePop(modalId)) {
return;
}
popModal();
await RmoStack.pop({ id: modalId, preventEventTriggering: true });
};
modalCount++;
const {
classes: defaultClasses,
closeButton: defaultCloseButton,
header: defaultHeader,
closeButtonIcon: defaultCloseButtonIcon,
contentWrapper: defaultContentWrapper,
..._defaultOptions
} = defaultOptions;
const { classes, closeButton, header, closeButtonIcon, contentWrapper, ...modalOptions } = options;
const modalProps = {
...enhancedMerge(_defaultOptions, omitBy(modalOptions, isNull)),
modalId,
containerId,
closeModal,
deleteModal() {
const modalToRemove = modals.get(modalId)!;
const { onClose } = modalToRemove.props;
if (isFn(onClose)) {
onClose();
}
modals.delete(modalId);
modalCount--;
if (modalCount < 0) {
modalCount = 0;
}
notify();
}
} as IModalProps;
if (isFn(classes)) {
modalProps.classes = classes(defaultClasses);
} else if (defaultClasses && classes) {
modalProps.classes = mergeClasses(defaultClasses, classes);
} else if (defaultClasses) {
modalProps.classes = defaultClasses;
} else {
modalProps.classes = classes;
}
modalProps.closeButton = defaultCloseButton;
if (closeButton === false || canBeRendered(closeButton)) {
modalProps.closeButton = closeButton;
} else if (closeButton === true) {
modalProps.closeButton = canBeRendered(defaultCloseButton) ? defaultCloseButton : true;
}
modalProps.closeButtonIcon = closeButtonIcon || defaultCloseButtonIcon;
modalProps.header = defaultHeader;
if (header === false || canBeRendered(header)) {
modalProps.header = header;
} else if (header === true) {
modalProps.header = canBeRendered(defaultHeader) ? defaultHeader : true;
}
modalProps.contentWrapper = defaultContentWrapper;
if (contentWrapper === false || canBeRendered(contentWrapper)) {
modalProps.contentWrapper = contentWrapper;
} else if (contentWrapper === true) {
modalProps.contentWrapper = canBeRendered(defaultContentWrapper) ? defaultContentWrapper : true;
}
let modalContent = content;
if (React.isValidElement(content) && !isStr(content.type)) {
modalContent = React.cloneElement(content as React.ReactElement<IModalContentProps>, {
closeModal,
modalProps
});
} else if (isFn(content)) {
modalContent = content({ closeModal, modalProps });
}
const activeModal: IModal = {
content: modalContent as React.ReactNode,
props: modalProps
};
return activeModal;
};
return {
id: containerId,
defaultOptions,
observe,
popModal,
pushModal,
get modalCount() {
return modalCount;
},
buildModal,
setDefaultOptions(d: IModalDefaultOptions) {
defaultOptions = d;
},
isModalActive: (id: Id) => activeModals.some((v) => v === id),
getSnapshot: () => snapshot
};
}