import * as React from "react"; import * as ReactDOM from "react-dom"; import {ReactElement} from "react"; import {Registry} from "tc-shared/events"; const cssStyle = require("./Modal.scss"); export type ModalType = "error" | "warning" | "info" | "none"; export interface ModalEvents { "open": {}, "close": {}, /* create is implicitly at object creation */ "destroy": {} } export enum ModalState { SHOWN, HIDDEN, DESTROYED } export class ModalController { readonly events: Registry; readonly modalInstance: InstanceType; private initializedPromise: Promise; private domElement: Element; private refModal: React.RefObject; private modalState_: ModalState = ModalState.HIDDEN; constructor(instance: InstanceType) { this.modalInstance = instance; instance["__modal_controller"] = this; this.events = new Registry(); this.initialize(); } private initialize() { this.refModal = React.createRef(); this.domElement = document.createElement("div"); const element = ; document.body.appendChild(this.domElement); this.initializedPromise = new Promise(resolve => { ReactDOM.render(element, this.domElement, () => setTimeout(resolve, 0)); }); this.modalInstance["onInitialize"](); } modalState() { return this.modalState_; } async show() { await this.initializedPromise; if(this.modalState_ === ModalState.DESTROYED) throw tr("modal has been destroyed"); else if(this.modalState_ === ModalState.SHOWN) return; this.refModal.current?.setState({ show: true }); this.modalState_ = ModalState.SHOWN; this.modalInstance["onOpen"](); this.events.fire("open"); } async hide() : Promise { await this.initializedPromise; if(this.modalState_ === ModalState.DESTROYED) throw tr("modal has been destroyed"); else if(this.modalState_ === ModalState.HIDDEN) return; this.refModal.current?.setState({ show: false }); this.modalState_ = ModalState.HIDDEN; this.modalInstance["onClose"](); this.events.fire("close"); return new Promise(resolve => setTimeout(resolve, 500)); } destroy() { if(this.modalState_ === ModalState.SHOWN) { this.hide().then(() => this.destroy()); return; } ReactDOM.unmountComponentAtNode(this.domElement); this.domElement.remove(); this.domElement = undefined; this.modalState_ = ModalState.DESTROYED; this.modalInstance["onDestroy"](); this.events.fire("destroy"); } } export abstract class Modal { private __modal_controller: ModalController; public constructor() {} type() : ModalType { return "none"; } abstract renderBody() : ReactElement; abstract title() : string; /** * Will only return a modal controller when the modal has not been destroyed */ modalController() : ModalController | undefined { return this.__modal_controller; } protected onInitialize() {} protected onDestroy() {} protected onClose() {} protected onOpen() {} } class ModalImpl extends React.PureComponent<{ controller: ModalController }, { show: boolean }> { private readonly refModal = React.createRef(); constructor(props) { super(props); this.state = { show: false }; } render() { const modal = this.props.controller.modalInstance; let modalExtraClass = ""; const type = modal.type(); if(typeof type === "string" && type !== "none") modalExtraClass = cssStyle["modal-type-" + type]; const showClass = this.state.show ? cssStyle.shown : ""; return (
this.onBackdropClick(event)} ref={this.refModal}>
{tr("Modal
{modal.title()}
this.props.controller.destroy() }>
{modal.renderBody()}
); } private onBackdropClick(event: React.MouseEvent) { if(event.target !== this.refModal.current || event.isDefaultPrevented()) return; this.props.controller.destroy(); } } export function spawnReactModal(modalClass: new (T) => ModalClass, properties?: T) : ModalController { return new ModalController(new modalClass(properties)); }