import { ElementRef, Injectable, inject } from '@angular/core'; import { ConnectedPosition, Overlay, OverlayRef } from '@angular/cdk/overlay'; import { ComponentPortal } from '@angular/cdk/portal'; import { Subscription, filter, fromEvent } from 'rxjs'; import { PluginActionMenuComponent } from './plugin-action-menu.component'; import { ViewportService } from '../../../../core/platform'; const GAP = 10; const VIEWPORT_MARGIN = 8; const POSITIONS: ConnectedPosition[] = [ { originX: 'end', originY: 'top', overlayX: 'start', overlayY: 'top', offsetX: GAP }, { originX: 'end', originY: 'bottom', overlayX: 'start', overlayY: 'bottom', offsetX: GAP }, { originX: 'start', originY: 'top', overlayX: 'end', overlayY: 'top', offsetX: -GAP }, { originX: 'start', originY: 'bottom', overlayX: 'end', overlayY: 'bottom', offsetX: -GAP } ]; @Injectable({ providedIn: 'root' }) export class PluginActionMenuService { private readonly overlay = inject(Overlay); private readonly viewport = inject(ViewportService); private currentOrigin: HTMLElement | null = null; private overlayRef: OverlayRef | null = null; private overlaySubscriptions: Subscription | null = null; private scrollBlocker: (() => void) | null = null; open(origin: ElementRef | HTMLElement): void { const rawEl = origin instanceof ElementRef ? origin.nativeElement : origin; if (this.overlayRef) { const sameOrigin = rawEl === this.currentOrigin; this.close(); if (sameOrigin) { return; } } const elementRef = origin instanceof ElementRef ? origin : new ElementRef(origin); const isMobile = this.viewport.isMobile(); this.currentOrigin = rawEl; if (isMobile) { const positionStrategy = this.overlay .position() .global() .left('0') .right('0') .bottom('0'); this.overlayRef = this.overlay.create({ positionStrategy, scrollStrategy: this.overlay.scrollStrategies.block(), hasBackdrop: true, backdropClass: 'cdk-overlay-dark-backdrop', panelClass: 'metoyou-bottom-sheet-panel' }); } else { const positionStrategy = this.overlay .position() .flexibleConnectedTo(elementRef) .withPositions(POSITIONS) .withViewportMargin(VIEWPORT_MARGIN) .withPush(true); this.overlayRef = this.overlay.create({ positionStrategy, scrollStrategy: this.overlay.scrollStrategies.noop() }); } this.syncThemeVars(); const componentRef = this.overlayRef.attach(new ComponentPortal(PluginActionMenuComponent)); const subscriptions = new Subscription(); subscriptions.add(componentRef.instance.closed.subscribe(() => this.close())); if (isMobile) { subscriptions.add(this.overlayRef.backdropClick().subscribe(() => this.close())); this.overlaySubscriptions = subscriptions; return; } subscriptions.add(fromEvent(document, 'pointerdown') .pipe( filter((event) => { const target = event.target as Node; if (this.overlayRef?.overlayElement.contains(target)) { return false; } if (this.currentOrigin?.contains(target)) { return false; } return true; }) ) .subscribe(() => this.close())); this.overlaySubscriptions = subscriptions; this.blockScroll(); } close(): void { this.scrollBlocker?.(); this.scrollBlocker = null; this.overlaySubscriptions?.unsubscribe(); this.overlaySubscriptions = null; if (this.overlayRef) { this.overlayRef.dispose(); this.overlayRef = null; this.currentOrigin = null; } } private blockScroll(): void { const handler = (event: Event): void => { if (this.overlayRef?.overlayElement.contains(event.target as Node)) { return; } event.preventDefault(); }; const opts: AddEventListenerOptions = { passive: false, capture: true }; document.addEventListener('wheel', handler, opts); document.addEventListener('touchmove', handler, opts); this.scrollBlocker = () => { document.removeEventListener('wheel', handler, opts); document.removeEventListener('touchmove', handler, opts); }; } private syncThemeVars(): void { const appRoot = document.querySelector('[data-theme-key="appRoot"]'); const container = document.querySelector('.cdk-overlay-container'); if (!appRoot || !container) { return; } for (const prop of Array.from(appRoot.style)) { if (prop.startsWith('--')) { container.style.setProperty(prop, appRoot.style.getPropertyValue(prop)); } } } }