import { HostListener, Directive, ElementRef, Input, OnInit, AfterViewChecked, inject, } from "@angular/core"; /** * The sam-sticky directive is made to help nav bar stick on the page */ export interface OffsetParentLike { offsetTop: number; offsetParent: Element | null; } @Directive({ selector: "[sam-sticky]", standalone: false, }) export class SamStickyComponent implements OnInit, AfterViewChecked { private el = inject(ElementRef); // Research sticky polyfill // http://html5please.com/#sticky /** * Sets the minimum pixel width for sticky to trigger on. */ @Input() limit: number = 0; /** * Sets the container target class */ @Input() container: string; // Make a nav bar sticky when the diff between nav bar and its container is // larger than diffLimit private diffLimit: number = 100; // Set the nav bar top position when fixed private stickyTop: number = 10; // Get the initial width of the element private elemWidth: number; @HostListener("window:resize", ["$event"]) resize(event?: Event) { void event; // Set element to initial styles // to help finding the initial element width this.el.nativeElement.style.position = "static"; this.el.nativeElement.style.width = "auto"; this.elemWidth = this.el.nativeElement.offsetWidth; this.makeSticky(); } @HostListener("window:scroll", ["$event"]) scroll(event?: Event) { void event; this.makeSticky(); } ngOnInit() { if (this.el.nativeElement.offsetWidth !== this.elemWidth) { this.elemWidth = this.el.nativeElement.offsetWidth; } } ngAfterViewChecked() { if (this.el.nativeElement.offsetWidth !== this.elemWidth) { this.elemWidth = this.el.nativeElement.offsetWidth; } } makeSticky() { if (window.innerWidth <= this.limit) { this.setPosition("static"); this.el.nativeElement.style.width = "auto"; this.el.nativeElement.style.top = "auto"; } else { this.adjustStickyPos(); } } setPosition(position: string) { this.el.nativeElement.style.position = position; } getDocHeight() { const D = document; return Math.max( D.body.scrollHeight, D.documentElement.scrollHeight, D.body.offsetHeight, D.documentElement.offsetHeight, D.body.clientHeight, D.documentElement.clientHeight ); } getScrollTop() { const doc = document.documentElement; const top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); return top; } /** * Get the distance from the element to the top of the document */ getElemDistanceToTop(elem: OffsetParentLike): number { let distance = 0; let el: OffsetParentLike = elem; if (el.offsetParent) { do { distance += el.offsetTop; el = el.offsetParent as unknown as OffsetParentLike; } while (el); } return distance; } isTallestAmongSiblings(): boolean { let highest = true; const parentContainer = document.getElementsByClassName( this.container ) as HTMLCollectionOf; if (!parentContainer[0]) { return highest; } const directChild = this.findDirectChild(); if (!directChild) { return highest; } const height = directChild.offsetHeight; for (let i = 0; i < parentContainer[0].children.length; i++) { if ( directChild !== parentContainer[0].children[i] && (parentContainer[0].children[i] as HTMLElement).offsetHeight > height ) { highest = false; } } return highest; } findDirectChild(): HTMLElement | undefined { const parentContainer = document.getElementsByClassName( this.container ) as HTMLCollectionOf; let directChild: HTMLElement | undefined; let curNode: HTMLElement = this.el.nativeElement; if (curNode.parentNode) { do { if (curNode.parentNode === parentContainer[0]) { directChild = curNode; break; } curNode = curNode.parentNode as HTMLElement; } while (curNode); } return directChild; } adjustStickyPos() { if (this.isTallestAmongSiblings()) { this.setPosition("static"); return; } const defaultTopMargin = 20; const defaultOffset = 50; const parentContainer = document.getElementsByClassName( this.container ) as HTMLCollectionOf; const documentHeight = this.getDocHeight(); const scrollPosition = this.getScrollTop() + window.innerHeight; const parentContainerLimit = parentContainer[0].offsetHeight + parentContainer[0].offsetTop; const restOfDocumentHeight = documentHeight - parentContainerLimit; const stickyElementLimit = this.el.nativeElement.offsetTop + this.el.nativeElement.offsetHeight; const stickyElementTopMargin = defaultTopMargin + this.el.nativeElement.offsetTop * -1; const stopLimit = documentHeight - restOfDocumentHeight + (window.innerHeight - stickyElementLimit); const parentContainerToTop = this.getElemDistanceToTop(parentContainer[0]); // Start to make it sticky when: // 1.the element is scrolled to top // 2.the container's height is larger than the element height plus diffLimit if ( this.getScrollTop() !== 0 && this.getScrollTop() + this.stickyTop > parentContainerToTop + this.el.nativeElement.offsetTop && this.el.nativeElement.offsetHeight + this.diffLimit < parentContainer[0].offsetHeight ) { this.setPosition("fixed"); // Make the elem stick on top until the space is not enough to show the // elem if ( this.el.nativeElement.offsetHeight + this.getScrollTop() < parentContainerToTop + parentContainer[0].offsetHeight - defaultOffset ) { this.el.nativeElement.style.top = this.stickyTop + "px"; } else { const topPosition = (stickyElementTopMargin + (scrollPosition - stopLimit)) * -1; this.el.nativeElement.style.top = topPosition + "px"; } this.el.nativeElement.style.width = this.elemWidth + "px"; } else { this.setPosition("static"); } } }