2020-05-20 20:46:59 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-06-10 18:13:56 +02:00
|
|
|
export class ModalController<InstanceType extends Modal = Modal> {
|
2020-05-20 20:46:59 +02:00
|
|
|
readonly events: Registry<ModalEvents>;
|
2020-06-10 18:13:56 +02:00
|
|
|
readonly modalInstance: InstanceType;
|
2020-05-20 20:46:59 +02:00
|
|
|
|
|
|
|
private initializedPromise: Promise<void>;
|
|
|
|
|
|
|
|
private domElement: Element;
|
|
|
|
private refModal: React.RefObject<ModalImpl>;
|
|
|
|
private modalState_: ModalState = ModalState.HIDDEN;
|
|
|
|
|
2020-06-10 18:13:56 +02:00
|
|
|
constructor(instance: InstanceType) {
|
2020-05-20 20:46:59 +02:00
|
|
|
this.modalInstance = instance;
|
|
|
|
instance["__modal_controller"] = this;
|
|
|
|
|
|
|
|
this.events = new Registry<ModalEvents>();
|
|
|
|
this.initialize();
|
|
|
|
}
|
|
|
|
|
|
|
|
private initialize() {
|
|
|
|
this.refModal = React.createRef();
|
|
|
|
this.domElement = document.createElement("div");
|
|
|
|
|
|
|
|
const element = <ModalImpl controller={this} ref={this.refModal} />;
|
|
|
|
document.body.appendChild(this.domElement);
|
|
|
|
this.initializedPromise = new Promise<void>(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<void> {
|
|
|
|
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<void>(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<HTMLDivElement>();
|
|
|
|
|
|
|
|
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 (
|
|
|
|
<div className={cssStyle.modal + " " + modalExtraClass + " " + showClass} tabIndex={-1} role={"dialog"} aria-hidden={true} onClick={event => this.onBackdropClick(event)} ref={this.refModal}>
|
|
|
|
<div className={cssStyle.dialog}>
|
|
|
|
<div className={cssStyle.content}>
|
|
|
|
<div className={cssStyle.header}>
|
|
|
|
<div className={cssStyle.icon}>
|
|
|
|
<img src="img/favicon/teacup.png" alt={tr("Modal - Icon")} />
|
|
|
|
</div>
|
|
|
|
<div className={cssStyle.title}>{modal.title()}</div>
|
|
|
|
<div className={cssStyle.buttonClose} onClick={() => this.props.controller.destroy() }>
|
|
|
|
<div className="icon_em client-close_button" />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className={cssStyle.body}>
|
|
|
|
{modal.renderBody()}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private onBackdropClick(event: React.MouseEvent) {
|
|
|
|
if(event.target !== this.refModal.current || event.isDefaultPrevented())
|
|
|
|
return;
|
|
|
|
|
|
|
|
this.props.controller.destroy();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-10 18:13:56 +02:00
|
|
|
export function spawnReactModal<ModalClass extends Modal, T>(modalClass: new (T) => ModalClass, properties?: T) : ModalController<ModalClass> {
|
|
|
|
return new ModalController(new modalClass(properties));
|
2020-05-20 20:46:59 +02:00
|
|
|
}
|