import { INotification, Notification, NotificationAlign, NotificationCallProps, NotificationContent, NotificationRenderProps, NotificationType } from '@etsoo/notificationbase'; import { DomUtils } from '@etsoo/shared'; import { BSColor } from '../defs/BSColor'; import { SiteUtils } from '../site/SiteUtils'; /** * Notification Dom interface */ export interface INotificationDom extends INotification {} /** * Notification Dom call props */ export interface NotificationDomCallProps extends NotificationCallProps { /** * Full width */ fullWidth?: boolean; /** * Full screen */ fullScreen?: boolean; /** * Inputs layout */ inputs?: HTMLDivElement; /** * Max width */ maxWidth?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | false; /** * OK label */ okLabel?: string; /** * false to hide cancel button */ cancelButton?: boolean; /** * Cancel label */ cancelLabel?: string; /** * Window is closable */ closable?: boolean; /** * Type */ type?: string; /** * Primary button props */ primaryButton?: {}; /** * Buttons to override default buttons */ buttons?: ( notification: INotificationDom, callback: (event: MouseEvent, value?: any) => Promise ) => HTMLDivElement; } /** * Notification Dom */ export class NotificationDom extends Notification implements INotificationDom { private createButton(color: BSColor, value: string) { const button = document.createElement('button'); button.className = `btn btn-${color} w-25`; button.innerHTML = value; return button; } // Create confirm private createConfirm(_props: NotificationRenderProps, className?: string) { // Destruct const { type, title = SiteUtils.getLabel(NotificationType[type].toLowerCase(), '') } = this; const { buttons = ( _notification: INotificationDom, callback: (event: MouseEvent, value?: any) => Promise ) => { const div = document.createElement('div'); if (cancelButton && type === NotificationType.Confirm) { const cButton = this.createButton('secondary', cancelLabel); cButton.name = 'cancelButton'; cButton.addEventListener('click', async (event) => { SiteUtils.toggleButtonSpinner(cButton); await callback(event, false); SiteUtils.toggleButtonSpinner(cButton); }); div.append(cButton); } const okButton = this.createButton('primary', okLabel); okButton.name = 'okButton'; okButton.addEventListener('click', async (event) => { SiteUtils.toggleButtonSpinner(okButton); await callback(event, true); SiteUtils.toggleButtonSpinner(okButton); }); if (primaryButton) Object.assign(okButton, primaryButton); div.append(okButton); return div; }, okLabel = SiteUtils.getLabel('ok', 'OK'), cancelLabel = SiteUtils.getLabel('cancel', 'Cancel'), cancelButton = true, closable = true, inputs, primaryButton } = this.inputProps ?? {}; const callback = async (_event: MouseEvent, value: boolean) => { await this.returnValue(value); return true; }; // Modal const div = this.createModal(className); div.innerHTML = ``; const titleE = div.querySelector('.modal-title'); this.createContent(titleE, title); const bodyE = div.querySelector('.modal-body'); this.createContent(bodyE, this.content); if (bodyE) { if (inputs) { inputs.classList.add('mt-3'); bodyE.append(inputs); } const buttonsE = buttons(this, callback); buttonsE.classList.add('modal-footer'); bodyE.parentElement?.append(buttonsE); } // Setup callback this.doSetup(div); return div; } private getDialogStyle() { // Destruct const align = this.align; const { maxWidth = false, fullScreen } = this.inputProps ?? {}; const classNames: string[] = []; if (align === NotificationAlign.Center) classNames.push('modal-dialog-centered'); if (maxWidth !== false) classNames.push(`modal-${maxWidth}`); if (fullScreen) classNames.push('modal-fullscreen'); if (classNames.length === 0) return ''; return ' ' + classNames.join(' '); } private createContent( container: HTMLElement | null, content?: NotificationContent ) { if (container == null || content == null) return; if (typeof content === 'string') { container.innerHTML = content; } else { container.append(content); } } private createModal(className?: string) { // Div const div = document.createElement('div'); // data-bs-backdrop="static" data-bs-keyboard="false" div.dataset.bsBackdrop = 'static'; div.dataset.bsKeyboard = 'false'; div.className = 'modal fad'; if (className) div.classList.add(className); return div; } private doSetup(div: HTMLDivElement) { if (this.renderSetup) this.renderSetup(div); } // Create loading private createLoading(_props: NotificationRenderProps, className?: string) { // Modal const div = this.createModal(className); // Content let content = this.content; if (content === '@') content = SiteUtils.getLabel('loading', 'Loading...'); div.innerHTML = ``; const titleE = div.querySelector('.spinner-title'); this.createContent(titleE, content); // Setup callback this.doSetup(div); return div; } private createPrompt(_props: NotificationRenderProps, className?: string) { // Destruct const { buttons = ( _notification: INotificationDom, callback: (event: MouseEvent) => Promise ) => { const div = document.createElement('div'); if (cancelButton) { const cButton = this.createButton('secondary', cancelLabel); cButton.name = 'cancelButton'; cButton.addEventListener('click', async (event) => { SiteUtils.toggleButtonSpinner(cButton); if (this.onReturn) await this.onReturn(undefined); SiteUtils.toggleButtonSpinner(cButton); this.dismiss(); }); div.append(cButton); } const okButton = this.createButton('primary', okLabel); okButton.name = 'okButton'; okButton.addEventListener('click', async (event) => { SiteUtils.toggleButtonSpinner(okButton); await callback(event); SiteUtils.toggleButtonSpinner(okButton); }); if (primaryButton) Object.assign(okButton, primaryButton); div.append(okButton); return div; }, okLabel = SiteUtils.getLabel('ok', 'OK'), cancelLabel = SiteUtils.getLabel('cancel', 'Cancel'), cancelButton = true, inputs, primaryButton, type, inputProps } = this.inputProps ?? {}; let input: HTMLInputElement | null = null; let errorDiv: HTMLDivElement | null = null; const setError = (error: string) => { if (errorDiv) errorDiv.innerHTML = error; }; const handleSubmit = async (event: MouseEvent) => { // Form const button = event.currentTarget as HTMLButtonElement | null; if (button == null) return false; // Result let result: | boolean | string | void | PromiseLike = undefined; if (this.onReturn) { // Inputs case, no HTMLForm set to value, set the current form if (inputs) { result = this.onReturn(button.form); } else if (input) { if (type === 'switch') { result = this.onReturn(input.checked); } else { const inputValue = DomUtils.getInputValue(input); if ( (inputValue == null || inputValue === '') && input.required ) { input.focus(); return false; } result = this.onReturn(inputValue); } } } // Get the value // returns false to prevent default dismiss const v = await result; if (v === false) { input?.focus(); return false; } if (typeof v === 'string') { setError(v); input?.focus(); return false; } this.dismiss(); return true; }; // Modal const div = this.createModal(className); div.innerHTML = `
`; const form = div.querySelector('form'); form?.addEventListener('submit', (event) => { event.preventDefault(); (form.elements.namedItem('okButton') as HTMLButtonElement)?.click(); return false; }); const titleE = div.querySelector('.modal-title'); this.createContent(titleE, this.title); const bodyE = div.querySelector('.modal-body'); this.createContent(bodyE, this.content); if (bodyE) { if (inputs) { inputs.classList.add('mt-3'); bodyE.append(inputs); } else { if (type === 'switch') { const switchDiv = document.createElement('div'); switchDiv.className = 'form-check form-switch mt-3'; switchDiv.innerHTML = ``; input = switchDiv.querySelector('input'); bodyE.append(switchDiv); } else if (type === 'slider') { const sdiv = document.createElement('div'); sdiv.className = 'input-group input-group-sm align-items-center mt-3'; sdiv.innerHTML = ``; const sInput = sdiv.querySelector('input'); if (sInput?.nextElementSibling) sInput.nextElementSibling.innerHTML = sInput.value; input = sInput; bodyE.append(sdiv); } else { const myInput = document.createElement('input'); myInput.type = type ?? 'text'; myInput.required = true; myInput.className = 'form-control mt-3'; myInput.addEventListener('change', () => setError('')); input = myInput; bodyE.append(myInput); } if (input) Object.assign(input, inputProps); } // Error display errorDiv = document.createElement('div'); errorDiv.className = 'text-danger mt-3'; bodyE.append(errorDiv); const buttonsE = buttons(this, handleSubmit); buttonsE.classList.add('modal-footer'); bodyE.parentElement?.append(buttonsE); } // Setup callback this.doSetup(div); return div; } private getColor(type: NotificationType) { return 'bg-' + NotificationType[type].toLocaleLowerCase(); } private createMessage(_props: NotificationRenderProps, className?: string) { // Destruct const { type, content, title, inputProps = {} } = this; const { closable = true } = inputProps; // Div const div = document.createElement('div'); div.className = 'toast'; if ( type === NotificationType.Danger || type === NotificationType.Error || type === NotificationType.Warning ) { div.role = 'alert'; div.ariaLive = 'assertive'; } else { div.role = 'status'; div.ariaLive = 'polite'; } div.innerHTML = `
${ closable ? `` : '' }
`; const titleE = div.querySelector('.toast-title'); this.createContent(titleE, title); const bodyE = div.querySelector('.toast-body'); this.createContent(bodyE, content); // Setup callback this.doSetup(div); return div; } render( props: NotificationRenderProps, className?: string | undefined ): HTMLDivElement | undefined { const type = this.type; if (type === NotificationType.Loading) { return this.createLoading(props, className); } else if (type === NotificationType.Confirm) { return this.createConfirm(props, className); } else if (type === NotificationType.Prompt) { return this.createPrompt(props, className); } else if (type === NotificationType.Error || this.modal) { return this.createConfirm(props, className); } else { return this.createMessage(props, className); } } }