@kanso-protocol/virtual-list 2.0.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -139,23 +139,32 @@ class KpVirtualListComponent {
|
|
|
139
139
|
queueMicrotask(() => this.rangeChange.emit({ start, end }));
|
|
140
140
|
}
|
|
141
141
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: KpVirtualListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
142
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", type: KpVirtualListComponent, isStandalone: true, selector: "kp-virtual-list", inputs: { items: "items", itemHeight: "itemHeight", viewportHeight: "viewportHeight", overscan: "overscan", trackBy: "trackBy" }, outputs: { rangeChange: "rangeChange" },
|
|
142
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", type: KpVirtualListComponent, isStandalone: true, selector: "kp-virtual-list", inputs: { items: "items", itemHeight: "itemHeight", viewportHeight: "viewportHeight", overscan: "overscan", trackBy: "trackBy" }, outputs: { rangeChange: "rangeChange" }, queries: [{ propertyName: "rowTemplate", first: true, predicate: KpVirtualRowDirective, descendants: true }], viewQueries: [{ propertyName: "viewportRef", first: true, predicate: ["viewport"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `
|
|
143
143
|
<div
|
|
144
144
|
#viewport
|
|
145
145
|
class="kp-virtual-list__viewport"
|
|
146
146
|
[style.height.px]="viewportHeight"
|
|
147
|
+
tabindex="0"
|
|
147
148
|
(scroll)="onScroll()"
|
|
148
149
|
>
|
|
149
150
|
<div class="kp-virtual-list__spacer" [style.height.px]="totalHeight">
|
|
151
|
+
<!-- role="list" sits on the immediate parent of the listitems so
|
|
152
|
+
aria-required-children resolves against direct children (the
|
|
153
|
+
viewport/spacer wrappers between the host and the items
|
|
154
|
+
confused axe's parent-child walk). aria-setsize/posinset are
|
|
155
|
+
per-item, which is what the ARIA spec actually permits — they
|
|
156
|
+
aren't valid on role="list" itself. -->
|
|
150
157
|
<div
|
|
151
158
|
class="kp-virtual-list__window"
|
|
159
|
+
role="list"
|
|
152
160
|
[style.transform]="windowTransform"
|
|
153
161
|
>
|
|
154
162
|
@for (item of visibleItems; track trackByOrIndex($index, item); let i = $index) {
|
|
155
163
|
<div
|
|
156
164
|
class="kp-virtual-list__row"
|
|
157
165
|
role="listitem"
|
|
158
|
-
[attr.aria-
|
|
166
|
+
[attr.aria-setsize]="items.length"
|
|
167
|
+
[attr.aria-posinset]="visibleStart + i + 1"
|
|
159
168
|
[style.height.px]="itemHeight"
|
|
160
169
|
>
|
|
161
170
|
@if (rowTemplate) {
|
|
@@ -168,30 +177,36 @@ class KpVirtualListComponent {
|
|
|
168
177
|
</div>
|
|
169
178
|
</div>
|
|
170
179
|
</div>
|
|
171
|
-
`, isInline: true, styles: [":host{display:block;width:100%;font-family:var(--kp-font-family-sans, \"Onest\", system-ui, sans-serif)}.kp-virtual-list__viewport{width:100%;overflow-y:auto;overflow-x:hidden;position:relative;contain:strict}.kp-virtual-list__spacer{position:relative;width:100%}.kp-virtual-list__window{position:absolute;inset-block-start:0;inset-inline:0;will-change:transform}.kp-virtual-list__row{box-sizing:border-box;width:100%}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
180
|
+
`, isInline: true, styles: [":host{display:block;width:100%;font-family:var(--kp-font-family-sans, \"Onest\", system-ui, sans-serif)}.kp-virtual-list__viewport{width:100%;overflow-y:auto;overflow-x:hidden;position:relative;contain:strict;background:var(--kp-color-surface-base)}.kp-virtual-list__spacer{position:relative;width:100%}.kp-virtual-list__window{position:absolute;inset-block-start:0;inset-inline:0;will-change:transform}.kp-virtual-list__row{box-sizing:border-box;width:100%}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
172
181
|
}
|
|
173
182
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: KpVirtualListComponent, decorators: [{
|
|
174
183
|
type: Component,
|
|
175
|
-
args: [{ selector: 'kp-virtual-list', imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
176
|
-
role: 'list',
|
|
177
|
-
'[attr.aria-rowcount]': 'items.length',
|
|
178
|
-
}, template: `
|
|
184
|
+
args: [{ selector: 'kp-virtual-list', imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, host: {}, template: `
|
|
179
185
|
<div
|
|
180
186
|
#viewport
|
|
181
187
|
class="kp-virtual-list__viewport"
|
|
182
188
|
[style.height.px]="viewportHeight"
|
|
189
|
+
tabindex="0"
|
|
183
190
|
(scroll)="onScroll()"
|
|
184
191
|
>
|
|
185
192
|
<div class="kp-virtual-list__spacer" [style.height.px]="totalHeight">
|
|
193
|
+
<!-- role="list" sits on the immediate parent of the listitems so
|
|
194
|
+
aria-required-children resolves against direct children (the
|
|
195
|
+
viewport/spacer wrappers between the host and the items
|
|
196
|
+
confused axe's parent-child walk). aria-setsize/posinset are
|
|
197
|
+
per-item, which is what the ARIA spec actually permits — they
|
|
198
|
+
aren't valid on role="list" itself. -->
|
|
186
199
|
<div
|
|
187
200
|
class="kp-virtual-list__window"
|
|
201
|
+
role="list"
|
|
188
202
|
[style.transform]="windowTransform"
|
|
189
203
|
>
|
|
190
204
|
@for (item of visibleItems; track trackByOrIndex($index, item); let i = $index) {
|
|
191
205
|
<div
|
|
192
206
|
class="kp-virtual-list__row"
|
|
193
207
|
role="listitem"
|
|
194
|
-
[attr.aria-
|
|
208
|
+
[attr.aria-setsize]="items.length"
|
|
209
|
+
[attr.aria-posinset]="visibleStart + i + 1"
|
|
195
210
|
[style.height.px]="itemHeight"
|
|
196
211
|
>
|
|
197
212
|
@if (rowTemplate) {
|
|
@@ -204,7 +219,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImpor
|
|
|
204
219
|
</div>
|
|
205
220
|
</div>
|
|
206
221
|
</div>
|
|
207
|
-
`, styles: [":host{display:block;width:100%;font-family:var(--kp-font-family-sans, \"Onest\", system-ui, sans-serif)}.kp-virtual-list__viewport{width:100%;overflow-y:auto;overflow-x:hidden;position:relative;contain:strict}.kp-virtual-list__spacer{position:relative;width:100%}.kp-virtual-list__window{position:absolute;inset-block-start:0;inset-inline:0;will-change:transform}.kp-virtual-list__row{box-sizing:border-box;width:100%}\n"] }]
|
|
222
|
+
`, styles: [":host{display:block;width:100%;font-family:var(--kp-font-family-sans, \"Onest\", system-ui, sans-serif)}.kp-virtual-list__viewport{width:100%;overflow-y:auto;overflow-x:hidden;position:relative;contain:strict;background:var(--kp-color-surface-base)}.kp-virtual-list__spacer{position:relative;width:100%}.kp-virtual-list__window{position:absolute;inset-block-start:0;inset-inline:0;will-change:transform}.kp-virtual-list__row{box-sizing:border-box;width:100%}\n"] }]
|
|
208
223
|
}], propDecorators: { items: [{
|
|
209
224
|
type: Input
|
|
210
225
|
}], itemHeight: [{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kanso-protocol-virtual-list.mjs","sources":["../../../../../packages/components/virtual-list/src/virtual-list.component.ts","../../../../../packages/components/virtual-list/src/kanso-protocol-virtual-list.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ContentChild,\n Directive,\n ElementRef,\n EventEmitter,\n HostListener,\n Input,\n NgZone,\n OnChanges,\n OnDestroy,\n Output,\n SimpleChanges,\n TemplateRef,\n ViewChild,\n inject,\n} from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\n\nexport interface KpVirtualRange {\n start: number;\n end: number;\n}\n\n/**\n * Marker directive — pairs the row template with `<kp-virtual-list>`.\n *\n * @example\n * <kp-virtual-list ...>\n * <ng-template kpVirtualRow let-item let-i=\"index\">\n * <div>row {{ i }}: {{ item.name }}</div>\n * </ng-template>\n * </kp-virtual-list>\n */\n@Directive({\n selector: '[kpVirtualRow]',\n standalone: true,\n})\nexport class KpVirtualRowDirective<T = unknown> {\n readonly template = inject(TemplateRef<{ $implicit: T; index: number }>);\n}\n\n/**\n * Kanso Protocol — VirtualList\n *\n * Window-mode virtual scroller for **fixed-height rows**. Renders only the\n * rows currently visible in the viewport (plus a configurable `[overscan]`\n * buffer). Lets the consumer hand any item shape via a projected\n * `<ng-template kpVirtualRow let-item let-i=\"index\">`.\n *\n * Why fixed-height: keeps the math O(1) per scroll event (`scrollTop /\n * itemHeight`), no measurement pass, no layout thrash. Variable-height\n * support is on the roadmap (`KpVariableVirtualListComponent`); for now,\n * size each row to a uniform height.\n *\n * Use this for tables / message lists / log views with thousands of rows.\n * Below ~100 rows just render them — virtualization adds complexity for\n * negligible gain.\n *\n * @example\n * <kp-virtual-list\n * [items]=\"rows\"\n * [itemHeight]=\"40\"\n * [viewportHeight]=\"480\"\n * [overscan]=\"6\"\n * (rangeChange)=\"onRange($event)\"\n * >\n * <ng-template kpVirtualRow let-row let-i=\"index\">\n * <div class=\"row\" [class.row--alt]=\"i % 2\">{{ row.name }}</div>\n * </ng-template>\n * </kp-virtual-list>\n */\n@Component({\n selector: 'kp-virtual-list',\n imports: [NgTemplateOutlet],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n role: 'list',\n '[attr.aria-rowcount]': 'items.length',\n },\n template: `\n <div\n #viewport\n class=\"kp-virtual-list__viewport\"\n [style.height.px]=\"viewportHeight\"\n (scroll)=\"onScroll()\"\n >\n <div class=\"kp-virtual-list__spacer\" [style.height.px]=\"totalHeight\">\n <div\n class=\"kp-virtual-list__window\"\n [style.transform]=\"windowTransform\"\n >\n @for (item of visibleItems; track trackByOrIndex($index, item); let i = $index) {\n <div\n class=\"kp-virtual-list__row\"\n role=\"listitem\"\n [attr.aria-rowindex]=\"visibleStart + i + 1\"\n [style.height.px]=\"itemHeight\"\n >\n @if (rowTemplate) {\n <ng-container\n *ngTemplateOutlet=\"rowTemplate.template; context: { $implicit: item, index: visibleStart + i }\"\n />\n }\n </div>\n }\n </div>\n </div>\n </div>\n `,\n styles: [`\n :host {\n display: block;\n width: 100%;\n font-family: var(--kp-font-family-sans, 'Onest', system-ui, sans-serif);\n }\n .kp-virtual-list__viewport {\n width: 100%;\n overflow-y: auto;\n overflow-x: hidden;\n position: relative;\n contain: strict;\n }\n .kp-virtual-list__spacer {\n position: relative;\n width: 100%;\n }\n .kp-virtual-list__window {\n position: absolute;\n inset-block-start: 0;\n inset-inline: 0;\n will-change: transform;\n }\n .kp-virtual-list__row {\n box-sizing: border-box;\n width: 100%;\n }\n `],\n})\nexport class KpVirtualListComponent<T = unknown> implements OnChanges, OnDestroy {\n /** The full list. The component never iterates the whole thing — only the visible window. */\n @Input() items: readonly T[] = [];\n\n /** Pixel height of one row. Required; must be uniform across rows. */\n @Input() itemHeight = 40;\n\n /** Pixel height of the scroll viewport. */\n @Input() viewportHeight = 400;\n\n /** Extra rows rendered above + below the visible window to soften scroll-flicker. */\n @Input() overscan = 4;\n\n /** Optional trackBy for the projected rows. Defaults to index — fine for stable lists. */\n @Input() trackBy: ((index: number, item: T) => unknown) | null = null;\n\n @Output() readonly rangeChange = new EventEmitter<KpVirtualRange>();\n\n @ViewChild('viewport', { static: true }) private viewportRef!: ElementRef<HTMLDivElement>;\n @ContentChild(KpVirtualRowDirective) rowTemplate?: KpVirtualRowDirective<T>;\n\n visibleStart = 0;\n visibleEnd = 0;\n\n private readonly cdr = inject(ChangeDetectorRef);\n private readonly zone = inject(NgZone);\n\n ngOnChanges(changes: SimpleChanges): void {\n if ('items' in changes || 'itemHeight' in changes || 'viewportHeight' in changes || 'overscan' in changes) {\n // Synchronous recompute: ngOnChanges fires BEFORE template eval in the\n // same CD pass, so visibleStart/End read by the template are already\n // up-to-date — no ExpressionChangedAfterItHasBeenChecked.\n this.recompute(/* fromInputChange */ true);\n }\n }\n\n ngOnDestroy(): void {\n /* HostListener auto-unbinds */\n }\n\n get totalHeight(): number {\n return this.items.length * this.itemHeight;\n }\n\n get visibleItems(): readonly T[] {\n return this.items.slice(this.visibleStart, this.visibleEnd);\n }\n\n get windowTransform(): string {\n return `translate3d(0, ${this.visibleStart * this.itemHeight}px, 0)`;\n }\n\n trackByOrIndex(index: number, item: T): unknown {\n if (this.trackBy) return this.trackBy(this.visibleStart + index, item);\n return this.visibleStart + index;\n }\n\n /** Imperatively scroll to a specific row index. */\n scrollToIndex(index: number, position: 'start' | 'center' | 'end' = 'start'): void {\n const el = this.viewportRef?.nativeElement;\n if (!el) return;\n const top =\n position === 'start'\n ? index * this.itemHeight\n : position === 'end'\n ? index * this.itemHeight - this.viewportHeight + this.itemHeight\n : index * this.itemHeight - this.viewportHeight / 2 + this.itemHeight / 2;\n el.scrollTop = Math.max(0, Math.min(top, this.totalHeight - this.viewportHeight));\n }\n\n onScroll(): void {\n this.recompute(/* fromInputChange */ false);\n }\n\n private recompute(fromInputChange: boolean): void {\n const el = this.viewportRef?.nativeElement;\n const scrollTop = el?.scrollTop ?? 0;\n const total = this.items.length;\n\n let start = 0;\n let end = 0;\n if (total > 0 && this.itemHeight > 0) {\n const visibleCount = Math.ceil(this.viewportHeight / this.itemHeight);\n const firstVisible = Math.floor(scrollTop / this.itemHeight);\n start = Math.max(0, firstVisible - this.overscan);\n end = Math.min(total, firstVisible + visibleCount + this.overscan);\n }\n\n if (this.visibleStart === start && this.visibleEnd === end) return;\n\n this.visibleStart = start;\n this.visibleEnd = end;\n\n if (!fromInputChange) {\n // Scroll-driven recompute happens outside CD; mark so the visible\n // window re-renders.\n this.cdr.markForCheck();\n }\n // Always defer the emit. From input changes: avoids reentrancy during CD.\n // From scroll: lets consumers mutate state without re-entering the\n // current event loop synchronously.\n queueMicrotask(() => this.rangeChange.emit({ start, end }));\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"kanso-protocol-virtual-list.mjs","sources":["../../../../../packages/components/virtual-list/src/virtual-list.component.ts","../../../../../packages/components/virtual-list/src/kanso-protocol-virtual-list.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ContentChild,\n Directive,\n ElementRef,\n EventEmitter,\n HostListener,\n Input,\n NgZone,\n OnChanges,\n OnDestroy,\n Output,\n SimpleChanges,\n TemplateRef,\n ViewChild,\n inject,\n} from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\n\nexport interface KpVirtualRange {\n start: number;\n end: number;\n}\n\n/**\n * Marker directive — pairs the row template with `<kp-virtual-list>`.\n *\n * @example\n * <kp-virtual-list ...>\n * <ng-template kpVirtualRow let-item let-i=\"index\">\n * <div>row {{ i }}: {{ item.name }}</div>\n * </ng-template>\n * </kp-virtual-list>\n */\n@Directive({\n selector: '[kpVirtualRow]',\n standalone: true,\n})\nexport class KpVirtualRowDirective<T = unknown> {\n readonly template = inject(TemplateRef<{ $implicit: T; index: number }>);\n}\n\n/**\n * Kanso Protocol — VirtualList\n *\n * Window-mode virtual scroller for **fixed-height rows**. Renders only the\n * rows currently visible in the viewport (plus a configurable `[overscan]`\n * buffer). Lets the consumer hand any item shape via a projected\n * `<ng-template kpVirtualRow let-item let-i=\"index\">`.\n *\n * Why fixed-height: keeps the math O(1) per scroll event (`scrollTop /\n * itemHeight`), no measurement pass, no layout thrash. Variable-height\n * support is on the roadmap (`KpVariableVirtualListComponent`); for now,\n * size each row to a uniform height.\n *\n * Use this for tables / message lists / log views with thousands of rows.\n * Below ~100 rows just render them — virtualization adds complexity for\n * negligible gain.\n *\n * @example\n * <kp-virtual-list\n * [items]=\"rows\"\n * [itemHeight]=\"40\"\n * [viewportHeight]=\"480\"\n * [overscan]=\"6\"\n * (rangeChange)=\"onRange($event)\"\n * >\n * <ng-template kpVirtualRow let-row let-i=\"index\">\n * <div class=\"row\" [class.row--alt]=\"i % 2\">{{ row.name }}</div>\n * </ng-template>\n * </kp-virtual-list>\n */\n@Component({\n selector: 'kp-virtual-list',\n imports: [NgTemplateOutlet],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {},\n template: `\n <div\n #viewport\n class=\"kp-virtual-list__viewport\"\n [style.height.px]=\"viewportHeight\"\n tabindex=\"0\"\n (scroll)=\"onScroll()\"\n >\n <div class=\"kp-virtual-list__spacer\" [style.height.px]=\"totalHeight\">\n <!-- role=\"list\" sits on the immediate parent of the listitems so\n aria-required-children resolves against direct children (the\n viewport/spacer wrappers between the host and the items\n confused axe's parent-child walk). aria-setsize/posinset are\n per-item, which is what the ARIA spec actually permits — they\n aren't valid on role=\"list\" itself. -->\n <div\n class=\"kp-virtual-list__window\"\n role=\"list\"\n [style.transform]=\"windowTransform\"\n >\n @for (item of visibleItems; track trackByOrIndex($index, item); let i = $index) {\n <div\n class=\"kp-virtual-list__row\"\n role=\"listitem\"\n [attr.aria-setsize]=\"items.length\"\n [attr.aria-posinset]=\"visibleStart + i + 1\"\n [style.height.px]=\"itemHeight\"\n >\n @if (rowTemplate) {\n <ng-container\n *ngTemplateOutlet=\"rowTemplate.template; context: { $implicit: item, index: visibleStart + i }\"\n />\n }\n </div>\n }\n </div>\n </div>\n </div>\n `,\n styles: [`\n :host {\n display: block;\n width: 100%;\n font-family: var(--kp-font-family-sans, 'Onest', system-ui, sans-serif);\n }\n .kp-virtual-list__viewport {\n width: 100%;\n overflow-y: auto;\n overflow-x: hidden;\n position: relative;\n contain: strict;\n /* Explicit bg so axe-core's color-contrast walk-up resolves to a\n known surface for the row content. With contain:strict + an\n implicit transparent bg, the walk-up gets confused and reports\n the row text against an indeterminate parent bg. The bg here\n is invisible in practice because every row template paints its\n own surface on top. */\n background: var(--kp-color-surface-base);\n }\n .kp-virtual-list__spacer {\n position: relative;\n width: 100%;\n }\n .kp-virtual-list__window {\n position: absolute;\n inset-block-start: 0;\n inset-inline: 0;\n will-change: transform;\n }\n .kp-virtual-list__row {\n box-sizing: border-box;\n width: 100%;\n }\n `],\n})\nexport class KpVirtualListComponent<T = unknown> implements OnChanges, OnDestroy {\n /** The full list. The component never iterates the whole thing — only the visible window. */\n @Input() items: readonly T[] = [];\n\n /** Pixel height of one row. Required; must be uniform across rows. */\n @Input() itemHeight = 40;\n\n /** Pixel height of the scroll viewport. */\n @Input() viewportHeight = 400;\n\n /** Extra rows rendered above + below the visible window to soften scroll-flicker. */\n @Input() overscan = 4;\n\n /** Optional trackBy for the projected rows. Defaults to index — fine for stable lists. */\n @Input() trackBy: ((index: number, item: T) => unknown) | null = null;\n\n @Output() readonly rangeChange = new EventEmitter<KpVirtualRange>();\n\n @ViewChild('viewport', { static: true }) private viewportRef!: ElementRef<HTMLDivElement>;\n @ContentChild(KpVirtualRowDirective) rowTemplate?: KpVirtualRowDirective<T>;\n\n visibleStart = 0;\n visibleEnd = 0;\n\n private readonly cdr = inject(ChangeDetectorRef);\n private readonly zone = inject(NgZone);\n\n ngOnChanges(changes: SimpleChanges): void {\n if ('items' in changes || 'itemHeight' in changes || 'viewportHeight' in changes || 'overscan' in changes) {\n // Synchronous recompute: ngOnChanges fires BEFORE template eval in the\n // same CD pass, so visibleStart/End read by the template are already\n // up-to-date — no ExpressionChangedAfterItHasBeenChecked.\n this.recompute(/* fromInputChange */ true);\n }\n }\n\n ngOnDestroy(): void {\n /* HostListener auto-unbinds */\n }\n\n get totalHeight(): number {\n return this.items.length * this.itemHeight;\n }\n\n get visibleItems(): readonly T[] {\n return this.items.slice(this.visibleStart, this.visibleEnd);\n }\n\n get windowTransform(): string {\n return `translate3d(0, ${this.visibleStart * this.itemHeight}px, 0)`;\n }\n\n trackByOrIndex(index: number, item: T): unknown {\n if (this.trackBy) return this.trackBy(this.visibleStart + index, item);\n return this.visibleStart + index;\n }\n\n /** Imperatively scroll to a specific row index. */\n scrollToIndex(index: number, position: 'start' | 'center' | 'end' = 'start'): void {\n const el = this.viewportRef?.nativeElement;\n if (!el) return;\n const top =\n position === 'start'\n ? index * this.itemHeight\n : position === 'end'\n ? index * this.itemHeight - this.viewportHeight + this.itemHeight\n : index * this.itemHeight - this.viewportHeight / 2 + this.itemHeight / 2;\n el.scrollTop = Math.max(0, Math.min(top, this.totalHeight - this.viewportHeight));\n }\n\n onScroll(): void {\n this.recompute(/* fromInputChange */ false);\n }\n\n private recompute(fromInputChange: boolean): void {\n const el = this.viewportRef?.nativeElement;\n const scrollTop = el?.scrollTop ?? 0;\n const total = this.items.length;\n\n let start = 0;\n let end = 0;\n if (total > 0 && this.itemHeight > 0) {\n const visibleCount = Math.ceil(this.viewportHeight / this.itemHeight);\n const firstVisible = Math.floor(scrollTop / this.itemHeight);\n start = Math.max(0, firstVisible - this.overscan);\n end = Math.min(total, firstVisible + visibleCount + this.overscan);\n }\n\n if (this.visibleStart === start && this.visibleEnd === end) return;\n\n this.visibleStart = start;\n this.visibleEnd = end;\n\n if (!fromInputChange) {\n // Scroll-driven recompute happens outside CD; mark so the visible\n // window re-renders.\n this.cdr.markForCheck();\n }\n // Always defer the emit. From input changes: avoids reentrancy during CD.\n // From scroll: lets consumers mutate state without re-entering the\n // current event loop synchronously.\n queueMicrotask(() => this.rangeChange.emit({ start, end }));\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kanso-protocol/virtual-list",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"peerDependencies": {
|
|
6
6
|
"@angular/core": ">=21.0.0",
|
|
7
7
|
"@angular/common": ">=21.0.0",
|
|
8
|
-
"@kanso-protocol/core": ">=
|
|
8
|
+
"@kanso-protocol/core": ">=3.0.0"
|
|
9
9
|
},
|
|
10
10
|
"description": "Kanso Protocol — virtual-list (component).",
|
|
11
11
|
"author": "GregNBlack",
|