ngx-category-scroll
Version:
An Angular component for smooth scrollable category navigation with section sync.
187 lines (182 loc) • 11.5 kB
JavaScript
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