174 lines
5.3 KiB
TypeScript
174 lines
5.3 KiB
TypeScript
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<Injector | undefined>(() => {
|
|
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');
|
|
}
|
|
}
|