rojects/source/components/confirm-dialog/confirm-dialog.ts

177 lines
6.1 KiB
TypeScript
Raw Permalink Normal View History

export interface ConfirmDialogOptions {
icon?: string;
title: string;
message: string;
confirmLabel?: string;
cancelLabel?: string;
danger?: boolean;
}
class ConfirmDialog extends HTMLElement {
private resolver: ((value: boolean) => void) | null = null;
connectedCallback(): void {
this.style.display = 'none';
this.innerHTML = `
<div class="cd-backdrop"></div>
<div class="cd-box" role="dialog" aria-modal="true">
<div class="cd-titlebar">
<span class="cd-icon"></span>
<span class="cd-title"></span>
<button class="cd-close" title="Close"></button>
</div>
<div class="cd-body">
<p class="cd-message"></p>
</div>
<div class="cd-footer"></div>
</div>
`;
this.querySelector('.cd-backdrop')!.addEventListener('click', () => this.close(false));
this.querySelector('.cd-close')!.addEventListener('click', () => this.close(false));
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape' && this.style.display !== 'none') this.close(false);
});
}
show(options: ConfirmDialogOptions): Promise<boolean> {
console.log( "Show dialog" );
this.querySelector('.cd-icon')!.textContent = options.icon ?? '';
this.querySelector('.cd-title')!.textContent = options.title;
this.querySelector('.cd-message')!.textContent = options.message;
const footer = this.querySelector('.cd-footer')!;
footer.innerHTML = '';
if (options.cancelLabel) {
const cancel = document.createElement('button');
cancel.className = 'cd-btn cd-btn-cancel';
cancel.textContent = options.cancelLabel;
cancel.addEventListener('click', () => this.close(false));
footer.appendChild(cancel);
}
const confirm = document.createElement('button');
confirm.className = `cd-btn cd-btn-confirm${options.danger ? ' cd-btn-danger' : ''}`;
confirm.textContent = options.confirmLabel ?? 'OK';
confirm.addEventListener('click', () => this.close(true));
footer.appendChild(confirm);
this.style.display = '';
confirm.focus();
return new Promise<boolean>(resolve => { this.resolver = resolve; });
}
private close(value: boolean): void {
this.style.display = 'none';
console.log( "Closing:", value );
if (this.resolver) { this.resolver(value); this.resolver = null; }
}
}
customElements.define('confirm-dialog', ConfirmDialog);
export function showConfirmDialog(options: ConfirmDialogOptions): Promise<boolean> {
let dialog = document.querySelector('confirm-dialog') as ConfirmDialog | null;
if (!dialog) {
dialog = document.createElement('confirm-dialog') as ConfirmDialog;
document.body.appendChild(dialog);
}
return dialog.show(options);
}
// ── Input dialog ──────────────────────────────────────────────────────────────
export interface InputDialogOptions {
icon?: string;
title: string;
label: string;
defaultValue?: string;
confirmLabel?: string;
cancelLabel?: string;
}
class InputDialog extends HTMLElement {
private resolver: (( value: string | null ) => void) | null = null;
connectedCallback(): void {
this.style.display = 'none';
this.innerHTML = `
<div class="cd-backdrop"></div>
<div class="cd-box" role="dialog" aria-modal="true">
<div class="cd-titlebar">
<span class="cd-icon"></span>
<span class="cd-title"></span>
<button class="cd-close" title="Close"></button>
</div>
<div class="cd-body">
<label class="cd-input-label"></label>
<input class="cd-input-field" type="text" autocomplete="off" spellcheck="false">
</div>
<div class="cd-footer"></div>
</div>
`;
this.querySelector( '.cd-backdrop' )!.addEventListener( 'click', () => this._close( null ) );
this.querySelector( '.cd-close' )!.addEventListener( 'click', () => this._close( null ) );
document.addEventListener( 'keydown', ( e: KeyboardEvent ) => {
if ( e.key === 'Escape' && this.style.display !== 'none' ) this._close( null );
if ( e.key === 'Enter' && this.style.display !== 'none' ) this._submit();
} );
}
show( options: InputDialogOptions ): Promise<string | null> {
this.querySelector( '.cd-icon' )!.textContent = options.icon ?? '';
this.querySelector( '.cd-title' )!.textContent = options.title;
( this.querySelector( '.cd-input-label' ) as HTMLElement ).textContent = options.label;
const input = this.querySelector( '.cd-input-field' ) as HTMLInputElement;
input.value = options.defaultValue ?? '';
const footer = this.querySelector( '.cd-footer' )!;
footer.innerHTML = '';
const cancel = document.createElement( 'button' );
cancel.className = 'cd-btn cd-btn-cancel';
cancel.textContent = options.cancelLabel ?? 'Cancel';
cancel.addEventListener( 'click', () => this._close( null ) );
footer.appendChild( cancel );
const confirm = document.createElement( 'button' );
confirm.className = 'cd-btn cd-btn-confirm';
confirm.textContent = options.confirmLabel ?? 'OK';
confirm.addEventListener( 'click', () => this._submit() );
footer.appendChild( confirm );
this.style.display = '';
input.focus();
input.select();
return new Promise<string | null>( resolve => { this.resolver = resolve; } );
}
private _submit(): void {
const value = ( this.querySelector( '.cd-input-field' ) as HTMLInputElement ).value.trim();
this._close( value || null );
}
private _close( value: string | null ): void {
this.style.display = 'none';
if ( this.resolver ) { this.resolver( value ); this.resolver = null; }
}
}
customElements.define( 'input-dialog', InputDialog );
export function showInputDialog( options: InputDialogOptions ): Promise<string | null> {
let dialog = document.querySelector( 'input-dialog' ) as InputDialog | null;
if ( !dialog ) {
dialog = document.createElement( 'input-dialog' ) as InputDialog;
document.body.appendChild( dialog );
}
return dialog.show( options );
}