import Bind from "../../core/Bind.js"; import { BindableProperty } from "../../core/BindableProperty.js"; import XNode from "../../core/XNode.js"; import sleep from "../../core/sleep.js"; import { CancelToken, IClassOf, IDisposable, IRect } from "../../core/types.js"; import { AtomControl } from "../controls/AtomControl.js"; import { ChildEnumerator } from "../core/AtomUI.js"; import type PopupService from "./PopupService.js"; import type { IDialogOptions } from "./PopupService.js"; import "./PopupWindow.global.css"; let popupService: typeof PopupService; const loadPopupService = async () => { if (popupService) { return popupService; } return popupService = (await (import("./PopupService.js"))).default as any as typeof PopupService; }; const focus = (popup: PopupWindow) => { const element = popup.element; if (!element) { return; } const host = element.querySelector(`[data-window-element="title"]`) ?? element.querySelector(`[data-window-element="action-bar"]`); if(host) { // @ts-expect-error popup.setupDragging(host as HTMLElement); } // this.element may become null if it was immediately // closed, very rare case, but possible if // supplied cancelToken was cancelled const anyAutofocus = element.querySelector(`*[autofocus]`); if (!anyAutofocus) { // const windowContent = element.querySelector("[data-window-content]"); // if (windowContent) { // const firstInput = windowContent.querySelector("input,button,a") as HTMLInputElement; // if (firstInput) { // firstInput.focus(); // return; // } // } const cb = element.querySelector(".popup-close-button") as HTMLButtonElement; if (cb) { cb.focus(); } return; } (anyAutofocus as HTMLElement).focus?.(); }; export default class PopupWindow extends AtomControl { public static async showWindow(options?: IDialogOptions): Promise; public static async showWindow(window: IClassOf, options?: IDialogOptions): Promise; public static async showWindow( window: IClassOf | IDialogOptions, options?: IDialogOptions): Promise { if (arguments.length <= 1) { options = arguments[0]; window = this; } options ??= {}; // this will force lastTarget to be set await sleep(1); const PS = await loadPopupService(); return PS.showWindow(PS.lastTarget, window as any, options); } public static async showModal(options?: IDialogOptions): Promise; public static async showModal(window: IClassOf, options?: IDialogOptions): Promise; public static async showModal( window: IClassOf | IDialogOptions, options?: IDialogOptions): Promise { if (arguments.length <= 1) { options = arguments[0]; window = this; } options ??= {}; options.modal ??= true; // this will force lastTarget to be set await sleep(1); const PS = await loadPopupService(); return PS.showWindow(PS.lastTarget, window as any, options); } @BindableProperty public title?: string; public viewModelTitle?: string; public close: (r?) => void; public cancel: (r?) => void; @BindableProperty public titleRenderer: () => XNode; @BindableProperty public closeButtonRenderer: () => XNode; @BindableProperty public footerRenderer: () => XNode; @BindableProperty public headerRenderer: () => XNode; @BindableProperty public iconRenderer: () => XNode; @BindableProperty public actionBarRenderer: () => XNode; @BindableProperty public closeWarning: string; protected readonly cancelToken: CancelToken; private initialized = false; public onPropertyChanged(name) { super.onPropertyChanged(name); switch (name as keyof PopupWindow) { case "iconRenderer": this.recreate(name, "icon"); break; case "actionBarRenderer": this.recreate(name, "action-bar"); break; case "footerRenderer": this.recreate(name, "footer"); break; case "titleRenderer": this.recreate(name, "title"); break; case "headerRenderer": this.recreate(name, "header"); break; case "closeButtonRenderer": this.recreate(name, "close"); break; } } protected init(): any { // do nothing... } protected async requestCancel() { if (this.closeWarning) { if (!await ConfirmPopup.confirm({ message : this.closeWarning })) { return; } } this.cancel(); } protected recreate(renderer, name): HTMLElement { const node = this[renderer]?.() ?? undefined; for (const e of ChildEnumerator.enumerate(this.element)) { if (e.getAttribute("data-window-element") === name) { this.dispose(e); e.remove(); break; } } if (node) { const na = node.attributes ??= {}; na["data-window-element"] = name; super.render(
{node}
); return this.element.querySelector(`[data-window-element="${name}"]`); } return null; } /** * This is because if someone changes renderer, entire content will * vanish, so we need to update allow update of only content element * @returns */ protected rendererChanged() { for (const content of ChildEnumerator.where(this.element, (e) => e.getAttribute("data-window-element") === "content")) { this.dispose(content); content.remove(); } const r = this.renderer; if (!r) { return; } delete this.render; this.render(r); } protected preCreate(): void { this.title = null; this.viewModelTitle = null; this.element.setAttribute("not-ready", "1"); const c = new CancelToken(); // @ts-expect-error this.cancelToken = c; this.registerDisposable({ dispose() { c.cancel(); } }); const handler = (e: KeyboardEvent) => { if (e.key === "Escape") { this.app.runAsync(() => this.requestCancel()); e.preventDefault(); return; } }; this.bindEvent(this.element, "keydown", handler); // document.body.addEventListener("keydown", handler); // this.registerDisposable({ // dispose() { // document.body.removeEventListener("keydown", handler); // } // }); this.element.setAttribute("data-popup-window", "popup-window"); // init will be called if parameters are set... // this.runAfterInit(() => this.app.runAsync(() => this.init?.())); const p = this.element; this.bindEvent(this.element, "popupReady", (e) => { if (e.defaultPrevented) { return; } p.setAttribute("data-ready", "true"); const parent = (p.offsetParent ?? document.body) as HTMLElement; const left = `${(parent.offsetWidth - p.offsetWidth) / 2}px`; const top = `${(parent.offsetHeight - p.offsetHeight) / 2}px`; p.style.left = left; p.style.top = top; p.removeAttribute("not-ready"); }); } protected render(node: XNode, e?: any, creator?: any): void { // this is because nested render will call `this.` instead of `super.` if (e || node?.attributes?.["data-window-element"]) { super.render(node, e, creator); return; } this.render = super.render; this.titleRenderer ??= () =>
this.title || this.viewModelTitle)}/>; this.closeButtonRenderer ??= () =>
; this.closeButtonRenderer = () =>
; } protected requestCancel(): Promise { this.cancel(); return Promise.resolve(); } // protected render(node: XNode, e?: any, creator?: any) { // this.render = super.render; // this.element.dataset.confirmPopup = "confirm-popup"; // this.closeButtonRenderer = () =>
; // const extracted = this.extractControlProperties(node); // const na = node.attributes ??= {}; // na["data-element"] = "message"; // super.render(
// { node } //
//
//
); // } }