|
@@ -0,0 +1,560 @@
|
|
|
|
+// hoist the class declaration because either rollup or babel is being a hoe
|
|
|
|
+import { addGlobalStyle } from "./dom.js";
|
|
|
|
+import { NanoEmitter } from "./NanoEmitter.js";
|
|
|
|
+
|
|
|
|
+export const defaultDialogCss = `\
|
|
|
|
+.uu-no-select {
|
|
|
|
+ user-select: none;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-bg {
|
|
|
|
+ --uu-dialog-bg: #333333;
|
|
|
|
+ --uu-dialog-bg-highlight: #252525;
|
|
|
|
+ --uu-scroll-indicator-bg: rgba(10, 10, 10, 0.7);
|
|
|
|
+ --uu-dialog-separator-color: #797979;
|
|
|
|
+ --uu-dialog-border-radius: 10px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-bg {
|
|
|
|
+ display: block;
|
|
|
|
+ position: fixed;
|
|
|
|
+ width: 100%;
|
|
|
|
+ height: 100%;
|
|
|
|
+ top: 0;
|
|
|
|
+ left: 0;
|
|
|
|
+ z-index: 5;
|
|
|
|
+ background-color: rgba(0, 0, 0, 0.6);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog {
|
|
|
|
+ --uu-calc-dialog-height: calc(min(100vh - 40px, var(--uu-dialog-height-max)));
|
|
|
|
+ position: absolute;
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: column;
|
|
|
|
+ width: calc(min(100% - 60px, var(--uu-dialog-width-max)));
|
|
|
|
+ border-radius: var(--uu-dialog-border-radius);
|
|
|
|
+ height: auto;
|
|
|
|
+ max-height: var(--uu-calc-dialog-height);
|
|
|
|
+ left: 50%;
|
|
|
|
+ top: 50%;
|
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
|
+ z-index: 6;
|
|
|
|
+ color: #fff;
|
|
|
|
+ background-color: var(--uu-dialog-bg);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog.align-top {
|
|
|
|
+ top: 0;
|
|
|
|
+ transform: translate(-50%, 40px);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog.align-bottom {
|
|
|
|
+ top: 100%;
|
|
|
|
+ transform: translate(-50%, -100%);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-body {
|
|
|
|
+ font-size: 1.5rem;
|
|
|
|
+ padding: 20px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-body.small {
|
|
|
|
+ padding: 15px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+#uu-dialog-opts {
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: column;
|
|
|
|
+ position: relative;
|
|
|
|
+ padding: 30px 0px;
|
|
|
|
+ overflow-y: auto;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-header {
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: space-between;
|
|
|
|
+ align-items: center;
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
+ padding: 15px 20px 15px 20px;
|
|
|
|
+ background-color: var(--uu-dialog-bg);
|
|
|
|
+ border: 2px solid var(--uu-dialog-separator-color);
|
|
|
|
+ border-style: none none solid none !important;
|
|
|
|
+ border-radius: var(--uu-dialog-border-radius) var(--uu-dialog-border-radius) 0px 0px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-header.small {
|
|
|
|
+ padding: 10px 15px;
|
|
|
|
+ border-style: none none solid none !important;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-header-pad {
|
|
|
|
+ content: " ";
|
|
|
|
+ min-height: 32px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-header-pad.small {
|
|
|
|
+ min-height: 24px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-titlecont {
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-titlecont-no-title {
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
+ align-items: center;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-title {
|
|
|
|
+ position: relative;
|
|
|
|
+ display: inline-block;
|
|
|
|
+ font-size: 22px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-close {
|
|
|
|
+ cursor: pointer;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-header-img,
|
|
|
|
+.uu-dialog-close
|
|
|
|
+{
|
|
|
|
+ width: 32px;
|
|
|
|
+ height: 32px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-header-img.small,
|
|
|
|
+.uu-dialog-close.small
|
|
|
|
+{
|
|
|
|
+ width: 24px;
|
|
|
|
+ height: 24px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-footer {
|
|
|
|
+ font-size: 17px;
|
|
|
|
+ text-decoration: underline;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-footer.hidden {
|
|
|
|
+ display: none;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-footer-cont {
|
|
|
|
+ margin-top: 6px;
|
|
|
|
+ padding: 15px 20px;
|
|
|
|
+ background: var(--uu-dialog-bg);
|
|
|
|
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, var(--uu-dialog-bg) 30%, var(--uu-dialog-bg) 100%);
|
|
|
|
+ border: 2px solid var(--uu-dialog-separator-color);
|
|
|
|
+ border-style: solid none none none !important;
|
|
|
|
+ border-radius: 0px 0px var(--uu-dialog-border-radius) var(--uu-dialog-border-radius);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.uu-dialog-footer-buttons-cont button:not(:last-of-type) {
|
|
|
|
+ margin-right: 15px;
|
|
|
|
+}`;
|
|
|
|
+
|
|
|
|
+/** ID of the last opened (top-most) dialog */
|
|
|
|
+export let currentDialogId: string | null = null;
|
|
|
|
+/** IDs of all currently open dialogs, top-most first */
|
|
|
|
+export const openDialogs: string[] = [];
|
|
|
|
+
|
|
|
|
+export const defaultStrings = {
|
|
|
|
+ closeDialogTooltip: "Click to close the dialog",
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+/** Options passed to the Dialog constructor */
|
|
|
|
+export interface DialogOptions {
|
|
|
|
+ /** ID that gets added to child element IDs - has to be unique and conform to HTML ID naming rules! */
|
|
|
|
+ id: string;
|
|
|
|
+ /** Target and max width of the dialog in pixels */
|
|
|
|
+ width: number;
|
|
|
|
+ /** Target and max height of the dialog in pixels */
|
|
|
|
+ height: number;
|
|
|
|
+ /** Whether the dialog should close when the background is clicked - defaults to true */
|
|
|
|
+ closeOnBgClick?: boolean;
|
|
|
|
+ /** Whether the dialog should close when the escape key is pressed - defaults to true */
|
|
|
|
+ closeOnEscPress?: boolean;
|
|
|
|
+ /** Whether the dialog should be destroyed when it's closed - defaults to false */
|
|
|
|
+ destroyOnClose?: boolean;
|
|
|
|
+ /** Whether the dialog should be unmounted when it's closed - defaults to true - superseded by destroyOnClose */
|
|
|
|
+ unmountOnClose?: boolean;
|
|
|
|
+ /** Whether all listeners should be removed when the dialog is destroyed - defaults to true */
|
|
|
|
+ removeListenersOnDestroy?: boolean;
|
|
|
|
+ /** Whether the dialog should have a smaller overall appearance - defaults to false */
|
|
|
|
+ small?: boolean;
|
|
|
|
+ /** Where to align or anchor the dialog vertically - defaults to "center" */
|
|
|
|
+ verticalAlign?: "top" | "center" | "bottom";
|
|
|
|
+ /** Strings used in the dialog (used for translations) - defaults to the default English strings */
|
|
|
|
+ strings?: Partial<typeof defaultStrings>;
|
|
|
|
+ /** CSS to apply to the dialog - defaults to the {@linkcode defaultDialogCss} */
|
|
|
|
+ dialogCss?: string;
|
|
|
|
+ /** Called to render the body of the dialog */
|
|
|
|
+ renderBody: () => HTMLElement | Promise<HTMLElement>;
|
|
|
|
+ /** Called to render the header of the dialog - leave undefined for a blank header */
|
|
|
|
+ renderHeader?: () => HTMLElement | Promise<HTMLElement>;
|
|
|
|
+ /** Called to render the footer of the dialog - leave undefined for no footer */
|
|
|
|
+ renderFooter?: () => HTMLElement | Promise<HTMLElement>;
|
|
|
|
+ /** Called to render the close button of the dialog - leave undefined for no close button */
|
|
|
|
+ renderCloseBtn?: () => HTMLElement | Promise<HTMLElement>;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/** Creates and manages a modal dialog element */
|
|
|
|
+export class Dialog extends NanoEmitter<{
|
|
|
|
+ /** Emitted just **after** the dialog is closed */
|
|
|
|
+ close: () => void;
|
|
|
|
+ /** Emitted just **after** the dialog is opened */
|
|
|
|
+ open: () => void;
|
|
|
|
+ /** Emitted just **after** the dialog contents are rendered */
|
|
|
|
+ render: () => void;
|
|
|
|
+ /** Emitted just **after** the dialog contents are cleared */
|
|
|
|
+ clear: () => void;
|
|
|
|
+ /** Emitted just **after** the dialog is destroyed and **before** all listeners are removed */
|
|
|
|
+ destroy: () => void;
|
|
|
|
+}> {
|
|
|
|
+ /** Options passed to the dialog in the constructor */
|
|
|
|
+ public readonly options;
|
|
|
|
+ /** ID that gets added to child element IDs - has to be unique and conform to HTML ID naming rules! */
|
|
|
|
+ public readonly id;
|
|
|
|
+ /** Strings used in the dialog (used for translations) */
|
|
|
|
+ public strings;
|
|
|
|
+
|
|
|
|
+ protected dialogOpen = false;
|
|
|
|
+ protected dialogMounted = false;
|
|
|
|
+
|
|
|
|
+ constructor(options: DialogOptions) {
|
|
|
|
+ super();
|
|
|
|
+
|
|
|
|
+ const { strings, ...opts } = options;
|
|
|
|
+
|
|
|
|
+ this.strings = {
|
|
|
|
+ ...defaultStrings,
|
|
|
|
+ ...(strings ?? {}),
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ this.options = {
|
|
|
|
+ closeOnBgClick: true,
|
|
|
|
+ closeOnEscPress: true,
|
|
|
|
+ destroyOnClose: false,
|
|
|
|
+ unmountOnClose: true,
|
|
|
|
+ removeListenersOnDestroy: true,
|
|
|
|
+ smallHeader: false,
|
|
|
|
+ verticalAlign: "center",
|
|
|
|
+ dialogCss: defaultDialogCss,
|
|
|
|
+ ...opts,
|
|
|
|
+ };
|
|
|
|
+ this.id = opts.id;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //#region public
|
|
|
|
+
|
|
|
|
+ /** Call after DOMContentLoaded to pre-render the dialog and invisibly mount it in the DOM */
|
|
|
|
+ public async mount() {
|
|
|
|
+ if(this.dialogMounted)
|
|
|
|
+ return;
|
|
|
|
+ this.dialogMounted = true;
|
|
|
|
+
|
|
|
|
+ if(!document.querySelector("style.uu-dialog-css"))
|
|
|
|
+ addGlobalStyle(this.options.dialogCss).classList.add("uu-dialog-css");
|
|
|
|
+
|
|
|
|
+ const bgElem = document.createElement("div");
|
|
|
|
+ bgElem.id = `uu-${this.id}-dialog-bg`;
|
|
|
|
+ bgElem.classList.add("uu-dialog-bg");
|
|
|
|
+ if(this.options.closeOnBgClick)
|
|
|
|
+ bgElem.ariaLabel = bgElem.title = this.getString("closeDialogTooltip");
|
|
|
|
+
|
|
|
|
+ bgElem.style.setProperty("--uu-dialog-width-max", `${this.options.width}px`);
|
|
|
|
+ bgElem.style.setProperty("--uu-dialog-height-max", `${this.options.height}px`);
|
|
|
|
+
|
|
|
|
+ bgElem.style.visibility = "hidden";
|
|
|
|
+ bgElem.style.display = "none";
|
|
|
|
+ bgElem.inert = true;
|
|
|
|
+
|
|
|
|
+ bgElem.appendChild(await this.getDialogContent());
|
|
|
|
+ document.body.appendChild(bgElem);
|
|
|
|
+
|
|
|
|
+ this.attachListeners(bgElem);
|
|
|
|
+
|
|
|
|
+ this.events.emit("render");
|
|
|
|
+ return bgElem;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /** Closes the dialog and clears all its contents (unmounts elements from the DOM) in preparation for a new rendering call */
|
|
|
|
+ public unmount() {
|
|
|
|
+ this.close();
|
|
|
|
+
|
|
|
|
+ this.dialogMounted = false;
|
|
|
|
+
|
|
|
|
+ const clearSelectors = [
|
|
|
|
+ `#uu-${this.id}-dialog-bg`,
|
|
|
|
+ `#uu-style-dialog-${this.id}`,
|
|
|
|
+ ];
|
|
|
|
+
|
|
|
|
+ for(const sel of clearSelectors)
|
|
|
|
+ document.querySelector(sel)?.remove();
|
|
|
|
+
|
|
|
|
+ this.events.emit("clear");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /** Clears the DOM of the dialog and then renders it again */
|
|
|
|
+ public async remount() {
|
|
|
|
+ this.unmount();
|
|
|
|
+ await this.mount();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Opens the dialog - also mounts it if it hasn't been mounted yet
|
|
|
|
+ * Prevents default action and immediate propagation of the passed event
|
|
|
|
+ */
|
|
|
|
+ public async open(e?: MouseEvent | KeyboardEvent) {
|
|
|
|
+ e?.preventDefault();
|
|
|
|
+ e?.stopImmediatePropagation();
|
|
|
|
+
|
|
|
|
+ if(this.isOpen())
|
|
|
|
+ return;
|
|
|
|
+ this.dialogOpen = true;
|
|
|
|
+
|
|
|
|
+ if(openDialogs.includes(this.id))
|
|
|
|
+ throw new Error(`A dialog with the same ID of '${this.id}' already exists and is open!`);
|
|
|
|
+
|
|
|
|
+ if(!this.isMounted())
|
|
|
|
+ await this.mount();
|
|
|
|
+
|
|
|
|
+ const dialogBg = document.querySelector<HTMLElement>(`#uu-${this.id}-dialog-bg`);
|
|
|
|
+
|
|
|
|
+ if(!dialogBg)
|
|
|
|
+ return console.warn(`Couldn't find background element for dialog with ID '${this.id}'`);
|
|
|
|
+
|
|
|
|
+ dialogBg.style.visibility = "visible";
|
|
|
|
+ dialogBg.style.display = "block";
|
|
|
|
+ dialogBg.inert = false;
|
|
|
|
+
|
|
|
|
+ currentDialogId = this.id;
|
|
|
|
+ openDialogs.unshift(this.id);
|
|
|
|
+
|
|
|
|
+ // make sure all other dialogs are inert
|
|
|
|
+ for(const dialogId of openDialogs)
|
|
|
|
+ if(dialogId !== this.id)
|
|
|
|
+ document.querySelector(`#uu-${dialogId}-dialog-bg`)?.setAttribute("inert", "true");
|
|
|
|
+
|
|
|
|
+ // make sure body is inert and scroll is locked
|
|
|
|
+ document.body.classList.remove("uu-no-select");
|
|
|
|
+ document.body.setAttribute("inert", "true");
|
|
|
|
+
|
|
|
|
+ this.events.emit("open");
|
|
|
|
+
|
|
|
|
+ return dialogBg;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /** Closes the dialog - prevents default action and immediate propagation of the passed event */
|
|
|
|
+ public close(e?: MouseEvent | KeyboardEvent) {
|
|
|
|
+ e?.preventDefault();
|
|
|
|
+ e?.stopImmediatePropagation();
|
|
|
|
+
|
|
|
|
+ if(!this.isOpen())
|
|
|
|
+ return;
|
|
|
|
+ this.dialogOpen = false;
|
|
|
|
+
|
|
|
|
+ const dialogBg = document.querySelector<HTMLElement>(`#uu-${this.id}-dialog-bg`);
|
|
|
|
+
|
|
|
|
+ if(!dialogBg)
|
|
|
|
+ return console.warn(`Couldn't find background element for dialog with ID '${this.id}'`);
|
|
|
|
+
|
|
|
|
+ dialogBg.style.visibility = "hidden";
|
|
|
|
+ dialogBg.style.display = "none";
|
|
|
|
+ dialogBg.inert = true;
|
|
|
|
+
|
|
|
|
+ openDialogs.splice(openDialogs.indexOf(this.id), 1);
|
|
|
|
+ currentDialogId = openDialogs[0] ?? null;
|
|
|
|
+
|
|
|
|
+ // make sure the new top-most dialog is not inert
|
|
|
|
+ if(currentDialogId)
|
|
|
|
+ document.querySelector(`#uu-${currentDialogId}-dialog-bg`)?.removeAttribute("inert");
|
|
|
|
+
|
|
|
|
+ // remove the scroll lock and inert attribute on the body if no dialogs are open
|
|
|
|
+ if(openDialogs.length === 0) {
|
|
|
|
+ document.body.classList.add("uu-no-select");
|
|
|
|
+ document.body.removeAttribute("inert");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ this.events.emit("close");
|
|
|
|
+
|
|
|
|
+ if(this.options.destroyOnClose)
|
|
|
|
+ this.destroy();
|
|
|
|
+ // don't destroy *and* unmount at the same time
|
|
|
|
+ else if(this.options.unmountOnClose)
|
|
|
|
+ this.unmount();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /** Returns true if the dialog is currently open */
|
|
|
|
+ public isOpen() {
|
|
|
|
+ return this.dialogOpen;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /** Returns true if the dialog is currently mounted */
|
|
|
|
+ public isMounted() {
|
|
|
|
+ return this.dialogMounted;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /** Clears the DOM of the dialog and removes all event listeners */
|
|
|
|
+ public destroy() {
|
|
|
|
+ this.unmount();
|
|
|
|
+ this.events.emit("destroy");
|
|
|
|
+ this.options.removeListenersOnDestroy && this.unsubscribeAll();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //#region static
|
|
|
|
+
|
|
|
|
+ /** Returns the ID of the top-most dialog (the dialog that has been opened last) */
|
|
|
|
+ public static getCurrentDialogId() {
|
|
|
|
+ return currentDialogId;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /** Returns the IDs of all currently open dialogs, top-most first */
|
|
|
|
+ public static getOpenDialogs() {
|
|
|
|
+ return openDialogs;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //#region protected
|
|
|
|
+
|
|
|
|
+ protected getString(key: keyof typeof defaultStrings) {
|
|
|
|
+ return this.strings[key] ?? defaultStrings[key];
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /** Called once to attach all generic event listeners */
|
|
|
|
+ protected attachListeners(bgElem: HTMLElement) {
|
|
|
|
+ if(this.options.closeOnBgClick) {
|
|
|
|
+ bgElem.addEventListener("click", (e) => {
|
|
|
|
+ if(this.isOpen() && (e.target as HTMLElement)?.id === `uu-${this.id}-dialog-bg`)
|
|
|
|
+ this.close(e);
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if(this.options.closeOnEscPress) {
|
|
|
|
+ document.body.addEventListener("keydown", (e) => {
|
|
|
|
+ if(e.key === "Escape" && this.isOpen() && Dialog.getCurrentDialogId() === this.id)
|
|
|
|
+ this.close(e);
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //#region protected
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Adds generic, accessible interaction listeners to the passed element.
|
|
|
|
+ * All listeners have the default behavior prevented and stop propagation (for keyboard events only as long as the captured key is valid).
|
|
|
|
+ * @param listenerOptions Provide a {@linkcode listenerOptions} object to configure the listeners
|
|
|
|
+ */
|
|
|
|
+ protected onInteraction<
|
|
|
|
+ TElem extends HTMLElement
|
|
|
|
+ > (
|
|
|
|
+ elem: TElem,
|
|
|
|
+ listener: (evt: MouseEvent | KeyboardEvent) => void,
|
|
|
|
+ listenerOptions?: AddEventListenerOptions & {
|
|
|
|
+ preventDefault?: boolean;
|
|
|
|
+ stopPropagation?: boolean;
|
|
|
|
+ },
|
|
|
|
+ ) {
|
|
|
|
+ const { preventDefault = true, stopPropagation = true, ...listenerOpts } = listenerOptions ?? {};
|
|
|
|
+
|
|
|
|
+ const interactionKeys = ["Enter", " ", "Space"];
|
|
|
|
+
|
|
|
|
+ const proxListener = (e: MouseEvent | KeyboardEvent) => {
|
|
|
|
+ if(e instanceof KeyboardEvent) {
|
|
|
|
+ if(interactionKeys.includes(e.key)) {
|
|
|
|
+ preventDefault && e.preventDefault();
|
|
|
|
+ stopPropagation && e.stopPropagation();
|
|
|
|
+ }
|
|
|
|
+ else return;
|
|
|
|
+ }
|
|
|
|
+ else if(e instanceof MouseEvent) {
|
|
|
|
+ preventDefault && e.preventDefault();
|
|
|
|
+ stopPropagation && e.stopPropagation();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // clean up the other listener that isn't automatically removed if `once` is set
|
|
|
|
+ listenerOpts?.once && e.type === "keydown" && elem.removeEventListener("click", proxListener, listenerOpts);
|
|
|
|
+ listenerOpts?.once && e.type === "click" && elem.removeEventListener("keydown", proxListener, listenerOpts);
|
|
|
|
+ listener(e);
|
|
|
|
+ };
|
|
|
|
+ elem.addEventListener("click", proxListener, listenerOpts);
|
|
|
|
+ elem.addEventListener("keydown", proxListener, listenerOpts);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /** Returns the dialog content element and all its children */
|
|
|
|
+ protected async getDialogContent() {
|
|
|
|
+ const header = this.options.renderHeader?.();
|
|
|
|
+ const footer = this.options.renderFooter?.();
|
|
|
|
+
|
|
|
|
+ const dialogWrapperEl = document.createElement("div");
|
|
|
|
+ dialogWrapperEl.id = `uu-${this.id}-dialog`;
|
|
|
|
+ dialogWrapperEl.classList.add("uu-dialog");
|
|
|
|
+ dialogWrapperEl.ariaLabel = dialogWrapperEl.title = "";
|
|
|
|
+ dialogWrapperEl.role = "dialog";
|
|
|
|
+ dialogWrapperEl.setAttribute("aria-labelledby", `uu-${this.id}-dialog-title`);
|
|
|
|
+ dialogWrapperEl.setAttribute("aria-describedby", `uu-${this.id}-dialog-body`);
|
|
|
|
+
|
|
|
|
+ if(this.options.verticalAlign !== "center")
|
|
|
|
+ dialogWrapperEl.classList.add(`align-${this.options.verticalAlign}`);
|
|
|
|
+
|
|
|
|
+ //#region header
|
|
|
|
+
|
|
|
|
+ const headerWrapperEl = document.createElement("div");
|
|
|
|
+ headerWrapperEl.classList.add("uu-dialog-header");
|
|
|
|
+ this.options.small && headerWrapperEl.classList.add("small");
|
|
|
|
+
|
|
|
|
+ if(header) {
|
|
|
|
+ const headerTitleWrapperEl = document.createElement("div");
|
|
|
|
+ headerTitleWrapperEl.id = `uu-${this.id}-dialog-title`;
|
|
|
|
+ headerTitleWrapperEl.classList.add("uu-dialog-title-wrapper");
|
|
|
|
+ headerTitleWrapperEl.role = "heading";
|
|
|
|
+ headerTitleWrapperEl.ariaLevel = "1";
|
|
|
|
+
|
|
|
|
+ headerTitleWrapperEl.appendChild(header instanceof Promise ? await header : header);
|
|
|
|
+ headerWrapperEl.appendChild(headerTitleWrapperEl);
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ // insert element to pad the header height
|
|
|
|
+ const padEl = document.createElement("div");
|
|
|
|
+ padEl.classList.add("uu-dialog-header-pad", this.options.small ? "small" : "");
|
|
|
|
+ headerWrapperEl.appendChild(padEl);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if(this.options.renderCloseBtn) {
|
|
|
|
+ const closeBtnEl = await this.options.renderCloseBtn();
|
|
|
|
+ closeBtnEl.classList.add("uu-dialog-close");
|
|
|
|
+ this.options.small && closeBtnEl.classList.add("small");
|
|
|
|
+ closeBtnEl.tabIndex = 0;
|
|
|
|
+ if(closeBtnEl.hasAttribute("alt"))
|
|
|
|
+ closeBtnEl.setAttribute("alt", this.getString("closeDialogTooltip"));
|
|
|
|
+ closeBtnEl.title = closeBtnEl.ariaLabel = this.getString("closeDialogTooltip");
|
|
|
|
+ this.onInteraction(closeBtnEl, () => this.close());
|
|
|
|
+ headerWrapperEl.appendChild(closeBtnEl);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ dialogWrapperEl.appendChild(headerWrapperEl);
|
|
|
|
+
|
|
|
|
+ //#region body
|
|
|
|
+
|
|
|
|
+ const dialogBodyElem = document.createElement("div");
|
|
|
|
+ dialogBodyElem.id = `uu-${this.id}-dialog-body`;
|
|
|
|
+ dialogBodyElem.classList.add("uu-dialog-body");
|
|
|
|
+ this.options.small && dialogBodyElem.classList.add("small");
|
|
|
|
+
|
|
|
|
+ const body = this.options.renderBody();
|
|
|
|
+
|
|
|
|
+ dialogBodyElem.appendChild(body instanceof Promise ? await body : body);
|
|
|
|
+ dialogWrapperEl.appendChild(dialogBodyElem);
|
|
|
|
+
|
|
|
|
+ //#region footer
|
|
|
|
+
|
|
|
|
+ if(footer) {
|
|
|
|
+ const footerWrapper = document.createElement("div");
|
|
|
|
+ footerWrapper.classList.add("uu-dialog-footer-cont");
|
|
|
|
+ dialogWrapperEl.appendChild(footerWrapper);
|
|
|
|
+ footerWrapper.appendChild(footer instanceof Promise ? await footer : footer);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return dialogWrapperEl;
|
|
|
|
+ }
|
|
|
|
+}
|