import { DOCUMENT, NgComponentOutlet, isPlatformBrowser } from '@angular/common'; import { ChangeDetectionStrategy, Component, Injector, PLATFORM_ID, computed, effect, inject, viewChild, } from '@angular/core'; import { MODAL_DATA, ModalRef, ModalService } from '../../../core/services/modal.service'; import { ModalShellComponent } from '../modal-shell/modal-shell.component'; @Component({ selector: 'app-modal-host', imports: [NgComponentOutlet, ModalShellComponent], templateUrl: './modal-host.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ModalHostComponent { private readonly modalService = inject(ModalService); private readonly injector = inject(Injector); private readonly document = inject(DOCUMENT); private readonly platformId = inject(PLATFORM_ID); private readonly modalShell = viewChild(ModalShellComponent); private readonly isBrowser = isPlatformBrowser(this.platformId); readonly activeModal = this.modalService.activeModal; protected readonly contentInjector = computed(() => { const modal = this.activeModal(); if (!modal) { return undefined; } return Injector.create({ providers: [ { provide: ModalRef, useValue: modal.ref }, { provide: MODAL_DATA, useValue: modal.config.data ?? null }, ], parent: this.injector, }); }); constructor() { effect((onCleanup) => { const modal = this.activeModal(); if (!this.isBrowser || !modal) { return; } const body = this.document.body; const previousOverflow = body.style.overflow; const previousPaddingRight = body.style.paddingRight; const previousPosition = body.style.position; const previousTop = body.style.top; const previousLeft = body.style.left; const previousWidth = body.style.width; const view = this.document.defaultView; const scrollX = view?.scrollX ?? 0; const scrollY = view?.scrollY ?? 0; const scrollbarWidth = view ? Math.max(0, view.innerWidth - this.document.documentElement.clientWidth) : 0; const activeElement = this.document.activeElement; if (activeElement instanceof HTMLElement && activeElement !== body) { activeElement.blur(); } if (scrollbarWidth > 0 && view) { const currentPaddingRight = Number.parseFloat(view.getComputedStyle(body).paddingRight) || 0; body.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`; } body.style.overflow = 'hidden'; const isIos = view ? /iPad|iPhone|iPod/.test(view.navigator.userAgent) || (view.navigator.platform === 'MacIntel' && view.navigator.maxTouchPoints > 1) : false; if (isIos) { body.style.position = 'fixed'; body.style.top = `${-scrollY}px`; body.style.left = `${-scrollX}px`; body.style.width = '100%'; } let viewportFrame: number | undefined; const viewportSyncTimers: number[] = []; const syncVisualViewport = () => { if (!view) { return; } if (viewportFrame !== undefined) { view.cancelAnimationFrame(viewportFrame); } viewportFrame = view.requestAnimationFrame(() => { viewportFrame = undefined; this.modalShell()?.setVisualViewport(view.visualViewport, view.scrollX, view.scrollY); }); }; view?.visualViewport?.addEventListener('resize', syncVisualViewport); view?.visualViewport?.addEventListener('scroll', syncVisualViewport); view?.addEventListener('orientationchange', syncVisualViewport); syncVisualViewport(); if (view) { viewportSyncTimers.push( view.setTimeout(syncVisualViewport, 100), view.setTimeout(syncVisualViewport, 300), ); } const onKeyDown = (event: KeyboardEvent) => { if (event.key !== 'Escape' || !this.activeModal()?.config.closeOnEscape) { return; } event.preventDefault(); this.activeModal()?.ref.dismiss('escape'); }; this.document.addEventListener('keydown', onKeyDown); queueMicrotask(() => this.modalShell()?.focusInitialElement()); onCleanup(() => { this.document.removeEventListener('keydown', onKeyDown); view?.visualViewport?.removeEventListener('resize', syncVisualViewport); view?.visualViewport?.removeEventListener('scroll', syncVisualViewport); view?.removeEventListener('orientationchange', syncVisualViewport); if (viewportFrame !== undefined) { view?.cancelAnimationFrame(viewportFrame); } viewportSyncTimers.forEach((timer) => view?.clearTimeout(timer)); body.style.overflow = previousOverflow; body.style.paddingRight = previousPaddingRight; body.style.position = previousPosition; body.style.top = previousTop; body.style.left = previousLeft; body.style.width = previousWidth; if (isIos) { view?.scrollTo(scrollX, scrollY); } }); }); } protected onBackdropClick(): void { const modal = this.activeModal(); if (modal?.config.closeOnBackdrop) { modal.ref.dismiss('backdrop'); } } protected onCloseRequested(): void { this.activeModal()?.ref.dismiss('programmatic'); } }