import { Directive, ElementRef, Renderer2, OnDestroy, AfterViewInit, Input, } from '@angular/core'; @Directive({ selector: '[scrollShadowBorder]', }) export class ScrollShadowBorderDirective implements OnDestroy, AfterViewInit { @Input() hasNoCardVariation: boolean = false; // elements private scrollableElement!: HTMLElement; private stickyElement!: HTMLElement; private containerElement!: HTMLElement; private leftOverlayElement?: HTMLElement; // listeners private unregisterScroll?: () => void; // observers private resizeObserver!: ResizeObserver; constructor( private elementRef: ElementRef, // renderer private renderer: Renderer2 ) {} ngAfterViewInit(): void { this.setupBorderShadow(); } private setupBorderShadow(): void { setTimeout(() => { this.setupElements(); this.setupScrollListener(); this.setupResizeObserver(); this.updateShadowStates(); }, 100); } private setupElements(): void { this.containerElement = this.elementRef.nativeElement; // Find sticky section this.stickyElement = this.containerElement.querySelector( '.w-fit-content' ) as HTMLElement; // Find scrollable section this.scrollableElement = this.containerElement.querySelector( '.modal-items-scrollable-columns' ) as HTMLElement; // Create left overlay element once (positioned on container) if (!this.leftOverlayElement) { const overlay = this.renderer.createElement('div'); this.leftOverlayElement = overlay as HTMLElement; // Left overlay element styles - shadow border const overlayStyles: { [key: string]: string } = { position: 'absolute', top: '4px', left: '0', width: '8px', height: this.hasNoCardVariation ? '100%' : 'calc(100% - 16px)', background: 'linear-gradient(90deg, #dadada 0%, rgba(218, 218, 218, 0) 100%)', 'pointer-events': 'none', display: 'none', }; // Add styles to the left overlay element Object.entries(overlayStyles).forEach(([styleName, styleValue]) => { this.renderer.setStyle(overlay, styleName, styleValue); }); this.renderer.appendChild(this.containerElement, overlay); } } private setupScrollListener(): void { if (!this.scrollableElement) return; // Remove existing listener if any present if (this.unregisterScroll) this.unregisterScroll(); this.unregisterScroll = this.renderer.listen( this.scrollableElement, 'scroll', () => this.updateShadowStates() ); } private setupResizeObserver(): void { if (!this.stickyElement) return; // Disconnect existing observer if any if (this.resizeObserver) this.resizeObserver.disconnect(); this.resizeObserver = new ResizeObserver(() => { this.updateShadowStates(); }); this.resizeObserver.observe(this.stickyElement); } private updateShadowStates(): void { if (!this.scrollableElement) return; const { scrollLeft, scrollWidth, clientWidth } = this.scrollableElement; // Left shadow shows when scrolled right const showLeftShadow = scrollLeft > 0; // Right shadow shows when there's scrollable content AND not scrolled to the end const isScrolledToEnd = scrollLeft >= scrollWidth - clientWidth - 1; const showRightShadow = scrollWidth > clientWidth && !isScrolledToEnd; // Apply right shadow class (component parity) if (showRightShadow) { this.renderer.addClass(this.containerElement, 'show-right-shadow'); if (this.hasNoCardVariation) { this.renderer.addClass( this.containerElement, 'show-right-shadow-import-modal' ); } } else { this.renderer.removeClass( this.containerElement, 'show-right-shadow' ); if (this.hasNoCardVariation) { this.renderer.removeClass( this.containerElement, 'show-right-shadow-import-modal' ); } } // Position and toggle a left overlay absolutely at the end of sticky area if (this.leftOverlayElement) { let stickyX = 0; if (this.stickyElement) { const containerRect = this.containerElement.getBoundingClientRect(); const stickyRect = this.stickyElement.getBoundingClientRect(); stickyX = Math.max( 0, Math.round(stickyRect.right - containerRect.left) ); } this.renderer.setStyle( this.leftOverlayElement, 'transform', `translateX(${stickyX}px)` ); this.renderer.setStyle( this.leftOverlayElement, 'display', showLeftShadow ? 'block' : 'none' ); } } private removeScrollListener(): void { if (this.unregisterScroll) { this.unregisterScroll(); this.unregisterScroll = undefined; } } private removeResizeObserver(): void { if (this.resizeObserver) this.resizeObserver.disconnect(); } ngOnDestroy(): void { this.removeScrollListener(); this.removeResizeObserver(); } }