195 lines
4.6 KiB
TypeScript
195 lines
4.6 KiB
TypeScript
|
import { IDisposable } from "@coder/disposable";
|
||
|
import { Emitter } from "@coder/emitter";
|
||
|
|
||
|
import "./dialog.scss";
|
||
|
|
||
|
/**
|
||
|
* Dialog options.
|
||
|
*/
|
||
|
export interface IDialogOptions {
|
||
|
message?: string;
|
||
|
detail?: string;
|
||
|
buttons?: string[];
|
||
|
input?: {
|
||
|
value: string;
|
||
|
selection?: {
|
||
|
start: number;
|
||
|
end: number;
|
||
|
};
|
||
|
};
|
||
|
}
|
||
|
|
||
|
export interface IDialogAction {
|
||
|
buttonIndex?: number;
|
||
|
key?: IKey;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Pressed keys.
|
||
|
*/
|
||
|
export enum IKey {
|
||
|
Enter = "Enter",
|
||
|
Escape = "Escape",
|
||
|
}
|
||
|
|
||
|
export class Dialog {
|
||
|
|
||
|
private options: IDialogOptions;
|
||
|
private overlay: HTMLElement;
|
||
|
private cachedActiveElement: HTMLElement;
|
||
|
private input: HTMLInputElement;
|
||
|
private actionEmitter: Emitter<IDialogAction>;
|
||
|
private errors: HTMLElement;
|
||
|
private buttons: HTMLElement[];
|
||
|
|
||
|
public constructor(options: IDialogOptions) {
|
||
|
this.options = options;
|
||
|
|
||
|
this.actionEmitter = new Emitter();
|
||
|
|
||
|
const msgBox = document.createElement("div");
|
||
|
msgBox.classList.add("msgbox");
|
||
|
|
||
|
if (this.options.message) {
|
||
|
const messageDiv = document.createElement("div");
|
||
|
messageDiv.classList.add("msg");
|
||
|
messageDiv.innerText = this.options.message;
|
||
|
msgBox.appendChild(messageDiv);
|
||
|
}
|
||
|
|
||
|
if (this.options.detail) {
|
||
|
const detailDiv = document.createElement("div");
|
||
|
detailDiv.classList.add("detail");
|
||
|
detailDiv.innerText = this.options.detail;
|
||
|
msgBox.appendChild(detailDiv);
|
||
|
}
|
||
|
|
||
|
if (this.options.input) {
|
||
|
msgBox.classList.add("input");
|
||
|
this.input = document.createElement("input");
|
||
|
this.input.classList.add("input");
|
||
|
this.input.value = this.options.input.value;
|
||
|
this.input.addEventListener("keydown", (event) => {
|
||
|
if (event.key === IKey.Enter) {
|
||
|
event.preventDefault();
|
||
|
this.actionEmitter.emit({
|
||
|
buttonIndex: undefined,
|
||
|
key: IKey.Enter,
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
msgBox.appendChild(this.input);
|
||
|
}
|
||
|
|
||
|
this.errors = document.createElement("div");
|
||
|
this.errors.classList.add("errors");
|
||
|
msgBox.appendChild(this.errors);
|
||
|
|
||
|
if (this.options.buttons && this.options.buttons.length > 0) {
|
||
|
this.buttons = this.options.buttons.map((buttonText, buttonIndex) => {
|
||
|
const button = document.createElement("button");
|
||
|
button.innerText = buttonText;
|
||
|
button.addEventListener("click", () => {
|
||
|
this.actionEmitter.emit({
|
||
|
buttonIndex,
|
||
|
key: undefined,
|
||
|
});
|
||
|
});
|
||
|
|
||
|
return button;
|
||
|
});
|
||
|
|
||
|
const buttonWrapper = document.createElement("div");
|
||
|
buttonWrapper.classList.add("button-wrapper");
|
||
|
this.buttons.forEach((b) => buttonWrapper.appendChild(b));
|
||
|
msgBox.appendChild(buttonWrapper);
|
||
|
}
|
||
|
|
||
|
|
||
|
this.overlay = document.createElement("div");
|
||
|
this.overlay.style.cssText = `display: flex; align-items: center; justify-content: center; top: 0; left: 0; right: 0; bottom: 0; z-index: 15; position: absolute; background: rgba(0, 0, 0, 0.4); opacity: 0; transition: 300ms opacity ease;`;
|
||
|
this.overlay.appendChild(msgBox);
|
||
|
|
||
|
setTimeout(() => {
|
||
|
this.overlay.style.opacity = "1";
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Register a function to be called when the user performs an action.
|
||
|
*/
|
||
|
public onAction(callback: (action: IDialogAction) => void): IDisposable {
|
||
|
return this.actionEmitter.event(callback);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Input value if this dialog has an input.
|
||
|
*/
|
||
|
public get inputValue(): string {
|
||
|
return this.input ? this.input.value : undefined;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Display or remove an error.
|
||
|
*/
|
||
|
public set error(error: string) {
|
||
|
while (this.errors.lastChild) {
|
||
|
this.errors.removeChild(this.errors.lastChild);
|
||
|
}
|
||
|
if (error) {
|
||
|
const errorDiv = document.createElement("error");
|
||
|
errorDiv.innerText = error;
|
||
|
this.errors.appendChild(errorDiv);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Show the dialog.
|
||
|
*/
|
||
|
public show(): void {
|
||
|
if (!this.cachedActiveElement) {
|
||
|
this.cachedActiveElement = document.activeElement as HTMLElement;
|
||
|
document.body.appendChild(this.overlay);
|
||
|
document.addEventListener("keydown", this.onKeydown);
|
||
|
if (this.input) {
|
||
|
this.input.focus();
|
||
|
if (this.options.input.selection) {
|
||
|
this.input.setSelectionRange(
|
||
|
this.options.input.selection.start,
|
||
|
this.options.input.selection.end
|
||
|
);
|
||
|
}
|
||
|
} else if (this.buttons) {
|
||
|
this.buttons[0].focus();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove the dialog and clean up.
|
||
|
*/
|
||
|
public hide(): void {
|
||
|
if (this.cachedActiveElement) {
|
||
|
this.overlay.remove();
|
||
|
document.removeEventListener("keydown", this.onKeydown);
|
||
|
this.cachedActiveElement.focus();
|
||
|
this.cachedActiveElement = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Capture escape.
|
||
|
*/
|
||
|
private onKeydown = (event: KeyboardEvent): void => {
|
||
|
if (event.key === "Escape") {
|
||
|
event.preventDefault();
|
||
|
event.stopPropagation();
|
||
|
this.actionEmitter.emit({
|
||
|
buttonIndex: undefined,
|
||
|
key: IKey.Escape,
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|