Files
shopit-front/src/app/shared/components/modal-host/modal-host.component.ts

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');
}
}