UNPKG

ngx-category-scroll

Version:

An Angular component for smooth scrollable category navigation with section sync.

187 lines (182 loc) 11.5 kB
import * as i1 from '@angular/common'; import { CommonModule } from '@angular/common'; import * as i0 from '@angular/core'; import { EventEmitter, Output, Input, ViewChild, ViewChildren, Component } from '@angular/core'; class NgxCategoryScroll { ngOnChanges(changes) { this.addObserver(); setTimeout(() => { this.sectionRefs?.forEach((ref) => { this.observer.observe(ref.nativeElement); this.visibilityMap.set(ref.nativeElement, 0); }); }, 500); } template; categories; selectedIndex = 0; isCategoryClick = false; bottomListRef; sectionRefs; topItemRefs; container; listContainerCss = ''; categoryContainerCss = ''; selectedCategory = new EventEmitter(); selectedItem = new EventEmitter(); visibilityMap = new Map(); observer; ngAfterViewInit() { this.addObserver(); this.sectionRefs?.forEach((ref) => { this.observer.observe(ref.nativeElement); this.visibilityMap.set(ref.nativeElement, 0); }); } addObserver() { // const sectionElements = document.querySelectorAll('.category-section'); // const observer = new IntersectionObserver((entries: any) => { // // console.log('entries:...', entries) // // let greter: any = entries[0]; // // entries.forEach((entry: any) => { // // if (entry.isIntersecting) { // // console.log(entry) // // } // // if (entry.isIntersecting > (greter?.isIntersecting ?? 0)) { // // greter = entry; // // } // // }); // // if (greter) { // // const index = greter.target.getAttribute('data-index'); // // this.inView(null, Number(index)) // // } // let maxRatio = 0; // let mostVisibleElement: any = null; // entries.forEach((entry: any) => { // if (entry.isIntersecting && entry.intersectionRatio > maxRatio) { // maxRatio = entry.intersectionRatio; // mostVisibleElement = entry.target; // } // }); // if (mostVisibleElement) { // console.log('Most visible element:', mostVisibleElement); // console.log('Visibility ratio:', maxRatio); // // Do something like add a highlight class // // document.querySelectorAll('.category-section').forEach(el => { // // el.classList.remove('highlight'); // // }); // // mostVisibleElement?.classList.add('highlight'); // const index = mostVisibleElement.getAttribute('data-index'); // this.inView(null, Number(index)) // } // }, { // threshold: buildThresholdList() // }); // sectionElements.forEach(section => observer.observe(section)); // function buildThresholdList() { // const thresholds = []; // for (let i = 0; i <= 1.0; i += 0.005) { // thresholds.push(i); // } // return thresholds; // } this.visibilityMap = new Map(); this.observer = new IntersectionObserver((entries) => { // Update visibility status entries.forEach(entry => { if (entry.isIntersecting) { this.visibilityMap.set(entry.target, entry.intersectionRatio); } else { this.visibilityMap.set(entry.target, 0); } }); // Find the element with max visibility let mostVisible = null; let maxRatio = 0; this.visibilityMap.forEach((ratio, el) => { if (ratio > maxRatio) { maxRatio = ratio; mostVisible = el; } }); if (mostVisible) { // Optional: highlight it // document.querySelectorAll('.category-section').forEach(el => // el.classList.remove('highlight') // ); // mostVisible.classList.add('highlight'); const index = mostVisible.getAttribute('data-index'); this.inView(null, Number(index)); } }, { threshold: buildThresholdList() }); function buildThresholdList() { const steps = 10; return Array.from({ length: steps + 1 }, (_, i) => i / steps); } // Observe all your elements // document.querySelectorAll('.category-section').forEach((el: any) => { // observer.observe(el); // visibilityMap.set(el, 0); // initialize // }); } scrollToCategory(category, index) { this.isCategoryClick = true; this.selectedIndex = index; const section = this.sectionRefs.toArray()[index]; section.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); const el = this.topItemRefs.toArray()[index]; el?.nativeElement.scrollIntoView({ behavior: 'smooth', inline: 'center' }); setTimeout(() => { this.isCategoryClick = false; }, 1000); } inView(cat, index) { this.selectedIndex = index; if (!this.isCategoryClick) { const el = this.topItemRefs.toArray()[index]; el?.nativeElement.scrollIntoView({ behavior: 'smooth', inline: 'center' }); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgxCategoryScroll, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: NgxCategoryScroll, isStandalone: true, selector: "ngx-category-scroll", inputs: { template: "template", categories: ["data", "categories"], listContainerCss: "listContainerCss", categoryContainerCss: "categoryContainerCss" }, outputs: { selectedCategory: "selectedCategory", selectedItem: "selectedItem" }, viewQueries: [{ propertyName: "bottomListRef", first: true, predicate: ["bottomScroll"], descendants: true }, { propertyName: "container", first: true, predicate: ["container"], descendants: true }, { propertyName: "sectionRefs", predicate: ["sectionRef"], descendants: true }, { propertyName: "topItemRefs", predicate: ["topItemRef"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- Top horizontal category list -->\r\n<div #container>\r\n <div class=\"top-category-container {{categoryContainerCss}}\" id=\"top-category-scroll\">\r\n <div class=\"category-item\" *ngFor=\"let cat of categories; let i = index\" [class.active]=\"selectedIndex === i\"\r\n (click)=\"scrollToCategory(cat, i)\" [attr.data-index]=\"i\" [attr.data-id]=\"cat.id\" [attr.data-name]=\"cat.name\"\r\n #topItemRef>\r\n {{ cat.name }}\r\n </div>\r\n </div>\r\n\r\n <!-- Bottom vertical list -->\r\n <div class=\"bottom-list {{listContainerCss}}\">\r\n <div *ngFor=\"let cat of categories; let i = index\" class=\"category-section\" [attr.data-index]=\"i\"\r\n [attr.data-id]=\"cat.id\" [attr.data-name]=\"cat.name\" #sectionRef>\r\n <h2 class=\"category-title\">{{ cat.name }}</h2>\r\n <div *ngFor=\"let item of cat.items\" class=\"item\">\r\n @if(template) {\r\n <ng-container *ngTemplateOutlet=\"template; context: { $implicit: item }\"></ng-container>\r\n }\r\n @else {\r\n {{item.name}}\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n</div>", styles: [".top-category-container{display:flex;overflow-x:auto;white-space:nowrap;padding:10px;background:#fff;position:sticky;top:0;z-index:100;border-bottom:1px solid #ccc}.category-item{padding:8px 16px;margin-right:10px;border-radius:20px;background:#eee;cursor:pointer;flex-shrink:0}.category-item.active{background-color:#007bff;color:#fff;font-weight:700}.bottom-list{height:calc(100vh - 60px);overflow-y:auto;padding:16px 16px 200px}.category-section{margin-bottom:40px}.item{padding:20px 0;background:#fff;margin:5px 0;border-radius:5px;color:#000}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgxCategoryScroll, decorators: [{ type: Component, args: [{ selector: 'ngx-category-scroll', imports: [CommonModule], template: "<!-- Top horizontal category list -->\r\n<div #container>\r\n <div class=\"top-category-container {{categoryContainerCss}}\" id=\"top-category-scroll\">\r\n <div class=\"category-item\" *ngFor=\"let cat of categories; let i = index\" [class.active]=\"selectedIndex === i\"\r\n (click)=\"scrollToCategory(cat, i)\" [attr.data-index]=\"i\" [attr.data-id]=\"cat.id\" [attr.data-name]=\"cat.name\"\r\n #topItemRef>\r\n {{ cat.name }}\r\n </div>\r\n </div>\r\n\r\n <!-- Bottom vertical list -->\r\n <div class=\"bottom-list {{listContainerCss}}\">\r\n <div *ngFor=\"let cat of categories; let i = index\" class=\"category-section\" [attr.data-index]=\"i\"\r\n [attr.data-id]=\"cat.id\" [attr.data-name]=\"cat.name\" #sectionRef>\r\n <h2 class=\"category-title\">{{ cat.name }}</h2>\r\n <div *ngFor=\"let item of cat.items\" class=\"item\">\r\n @if(template) {\r\n <ng-container *ngTemplateOutlet=\"template; context: { $implicit: item }\"></ng-container>\r\n }\r\n @else {\r\n {{item.name}}\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n</div>", styles: [".top-category-container{display:flex;overflow-x:auto;white-space:nowrap;padding:10px;background:#fff;position:sticky;top:0;z-index:100;border-bottom:1px solid #ccc}.category-item{padding:8px 16px;margin-right:10px;border-radius:20px;background:#eee;cursor:pointer;flex-shrink:0}.category-item.active{background-color:#007bff;color:#fff;font-weight:700}.bottom-list{height:calc(100vh - 60px);overflow-y:auto;padding:16px 16px 200px}.category-section{margin-bottom:40px}.item{padding:20px 0;background:#fff;margin:5px 0;border-radius:5px;color:#000}\n"] }] }], propDecorators: { template: [{ type: Input }], categories: [{ type: Input, args: ["data"] }], bottomListRef: [{ type: ViewChild, args: ['bottomScroll'] }], sectionRefs: [{ type: ViewChildren, args: ['sectionRef'] }], topItemRefs: [{ type: ViewChildren, args: ['topItemRef'] }], container: [{ type: ViewChild, args: ['container'] }], listContainerCss: [{ type: Input }], categoryContainerCss: [{ type: Input }], selectedCategory: [{ type: Output }], selectedItem: [{ type: Output }] } }); /* * Public API Surface of ngx-category-scroll */ /** * Generated bundle index. Do not edit. */ export { NgxCategoryScroll }; //# sourceMappingURL=ngx-category-scroll.mjs.map