@kanso-protocol/virtual-list 0.1.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.
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, TemplateRef, Directive, EventEmitter, ChangeDetectorRef, NgZone, ContentChild, ViewChild, Output, Input, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Marker directive — pairs the row template with `<kp-virtual-list>`.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* <kp-virtual-list ...>
|
|
10
|
+
* <ng-template kpVirtualRow let-item let-i="index">
|
|
11
|
+
* <div>row {{ i }}: {{ item.name }}</div>
|
|
12
|
+
* </ng-template>
|
|
13
|
+
* </kp-virtual-list>
|
|
14
|
+
*/
|
|
15
|
+
class KpVirtualRowDirective {
|
|
16
|
+
template = inject((TemplateRef));
|
|
17
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: KpVirtualRowDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
18
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.7", type: KpVirtualRowDirective, isStandalone: true, selector: "[kpVirtualRow]", ngImport: i0 });
|
|
19
|
+
}
|
|
20
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: KpVirtualRowDirective, decorators: [{
|
|
21
|
+
type: Directive,
|
|
22
|
+
args: [{
|
|
23
|
+
selector: '[kpVirtualRow]',
|
|
24
|
+
standalone: true,
|
|
25
|
+
}]
|
|
26
|
+
}] });
|
|
27
|
+
/**
|
|
28
|
+
* Kanso Protocol — VirtualList
|
|
29
|
+
*
|
|
30
|
+
* Window-mode virtual scroller for **fixed-height rows**. Renders only the
|
|
31
|
+
* rows currently visible in the viewport (plus a configurable `[overscan]`
|
|
32
|
+
* buffer). Lets the consumer hand any item shape via a projected
|
|
33
|
+
* `<ng-template kpVirtualRow let-item let-i="index">`.
|
|
34
|
+
*
|
|
35
|
+
* Why fixed-height: keeps the math O(1) per scroll event (`scrollTop /
|
|
36
|
+
* itemHeight`), no measurement pass, no layout thrash. Variable-height
|
|
37
|
+
* support is on the roadmap (`KpVariableVirtualListComponent`); for now,
|
|
38
|
+
* size each row to a uniform height.
|
|
39
|
+
*
|
|
40
|
+
* Use this for tables / message lists / log views with thousands of rows.
|
|
41
|
+
* Below ~100 rows just render them — virtualization adds complexity for
|
|
42
|
+
* negligible gain.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* <kp-virtual-list
|
|
46
|
+
* [items]="rows"
|
|
47
|
+
* [itemHeight]="40"
|
|
48
|
+
* [viewportHeight]="480"
|
|
49
|
+
* [overscan]="6"
|
|
50
|
+
* (rangeChange)="onRange($event)"
|
|
51
|
+
* >
|
|
52
|
+
* <ng-template kpVirtualRow let-row let-i="index">
|
|
53
|
+
* <div class="row" [class.row--alt]="i % 2">{{ row.name }}</div>
|
|
54
|
+
* </ng-template>
|
|
55
|
+
* </kp-virtual-list>
|
|
56
|
+
*/
|
|
57
|
+
class KpVirtualListComponent {
|
|
58
|
+
/** The full list. The component never iterates the whole thing — only the visible window. */
|
|
59
|
+
items = [];
|
|
60
|
+
/** Pixel height of one row. Required; must be uniform across rows. */
|
|
61
|
+
itemHeight = 40;
|
|
62
|
+
/** Pixel height of the scroll viewport. */
|
|
63
|
+
viewportHeight = 400;
|
|
64
|
+
/** Extra rows rendered above + below the visible window to soften scroll-flicker. */
|
|
65
|
+
overscan = 4;
|
|
66
|
+
/** Optional trackBy for the projected rows. Defaults to index — fine for stable lists. */
|
|
67
|
+
trackBy = null;
|
|
68
|
+
rangeChange = new EventEmitter();
|
|
69
|
+
viewportRef;
|
|
70
|
+
rowTemplate;
|
|
71
|
+
visibleStart = 0;
|
|
72
|
+
visibleEnd = 0;
|
|
73
|
+
cdr = inject(ChangeDetectorRef);
|
|
74
|
+
zone = inject(NgZone);
|
|
75
|
+
ngOnChanges(changes) {
|
|
76
|
+
if ('items' in changes || 'itemHeight' in changes || 'viewportHeight' in changes || 'overscan' in changes) {
|
|
77
|
+
// Synchronous recompute: ngOnChanges fires BEFORE template eval in the
|
|
78
|
+
// same CD pass, so visibleStart/End read by the template are already
|
|
79
|
+
// up-to-date — no ExpressionChangedAfterItHasBeenChecked.
|
|
80
|
+
this.recompute(/* fromInputChange */ true);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
ngOnDestroy() {
|
|
84
|
+
/* HostListener auto-unbinds */
|
|
85
|
+
}
|
|
86
|
+
get totalHeight() {
|
|
87
|
+
return this.items.length * this.itemHeight;
|
|
88
|
+
}
|
|
89
|
+
get visibleItems() {
|
|
90
|
+
return this.items.slice(this.visibleStart, this.visibleEnd);
|
|
91
|
+
}
|
|
92
|
+
get windowTransform() {
|
|
93
|
+
return `translate3d(0, ${this.visibleStart * this.itemHeight}px, 0)`;
|
|
94
|
+
}
|
|
95
|
+
trackByOrIndex(index, item) {
|
|
96
|
+
if (this.trackBy)
|
|
97
|
+
return this.trackBy(this.visibleStart + index, item);
|
|
98
|
+
return this.visibleStart + index;
|
|
99
|
+
}
|
|
100
|
+
/** Imperatively scroll to a specific row index. */
|
|
101
|
+
scrollToIndex(index, position = 'start') {
|
|
102
|
+
const el = this.viewportRef?.nativeElement;
|
|
103
|
+
if (!el)
|
|
104
|
+
return;
|
|
105
|
+
const top = position === 'start'
|
|
106
|
+
? index * this.itemHeight
|
|
107
|
+
: position === 'end'
|
|
108
|
+
? index * this.itemHeight - this.viewportHeight + this.itemHeight
|
|
109
|
+
: index * this.itemHeight - this.viewportHeight / 2 + this.itemHeight / 2;
|
|
110
|
+
el.scrollTop = Math.max(0, Math.min(top, this.totalHeight - this.viewportHeight));
|
|
111
|
+
}
|
|
112
|
+
onScroll() {
|
|
113
|
+
this.recompute(/* fromInputChange */ false);
|
|
114
|
+
}
|
|
115
|
+
recompute(fromInputChange) {
|
|
116
|
+
const el = this.viewportRef?.nativeElement;
|
|
117
|
+
const scrollTop = el?.scrollTop ?? 0;
|
|
118
|
+
const total = this.items.length;
|
|
119
|
+
let start = 0;
|
|
120
|
+
let end = 0;
|
|
121
|
+
if (total > 0 && this.itemHeight > 0) {
|
|
122
|
+
const visibleCount = Math.ceil(this.viewportHeight / this.itemHeight);
|
|
123
|
+
const firstVisible = Math.floor(scrollTop / this.itemHeight);
|
|
124
|
+
start = Math.max(0, firstVisible - this.overscan);
|
|
125
|
+
end = Math.min(total, firstVisible + visibleCount + this.overscan);
|
|
126
|
+
}
|
|
127
|
+
if (this.visibleStart === start && this.visibleEnd === end)
|
|
128
|
+
return;
|
|
129
|
+
this.visibleStart = start;
|
|
130
|
+
this.visibleEnd = end;
|
|
131
|
+
if (!fromInputChange) {
|
|
132
|
+
// Scroll-driven recompute happens outside CD; mark so the visible
|
|
133
|
+
// window re-renders.
|
|
134
|
+
this.cdr.markForCheck();
|
|
135
|
+
}
|
|
136
|
+
// Always defer the emit. From input changes: avoids reentrancy during CD.
|
|
137
|
+
// From scroll: lets consumers mutate state without re-entering the
|
|
138
|
+
// current event loop synchronously.
|
|
139
|
+
queueMicrotask(() => this.rangeChange.emit({ start, end }));
|
|
140
|
+
}
|
|
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" }, host: { attributes: { "role": "list" }, properties: { "attr.aria-rowcount": "items.length" } }, 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
|
+
<div
|
|
144
|
+
#viewport
|
|
145
|
+
class="kp-virtual-list__viewport"
|
|
146
|
+
[style.height.px]="viewportHeight"
|
|
147
|
+
(scroll)="onScroll()"
|
|
148
|
+
>
|
|
149
|
+
<div class="kp-virtual-list__spacer" [style.height.px]="totalHeight">
|
|
150
|
+
<div
|
|
151
|
+
class="kp-virtual-list__window"
|
|
152
|
+
[style.transform]="windowTransform"
|
|
153
|
+
>
|
|
154
|
+
@for (item of visibleItems; track trackByOrIndex($index, item); let i = $index) {
|
|
155
|
+
<div
|
|
156
|
+
class="kp-virtual-list__row"
|
|
157
|
+
role="listitem"
|
|
158
|
+
[attr.aria-rowindex]="visibleStart + i + 1"
|
|
159
|
+
[style.height.px]="itemHeight"
|
|
160
|
+
>
|
|
161
|
+
@if (rowTemplate) {
|
|
162
|
+
<ng-container
|
|
163
|
+
*ngTemplateOutlet="rowTemplate.template; context: { $implicit: item, index: visibleStart + i }"
|
|
164
|
+
/>
|
|
165
|
+
}
|
|
166
|
+
</div>
|
|
167
|
+
}
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</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 });
|
|
172
|
+
}
|
|
173
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: KpVirtualListComponent, decorators: [{
|
|
174
|
+
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: `
|
|
179
|
+
<div
|
|
180
|
+
#viewport
|
|
181
|
+
class="kp-virtual-list__viewport"
|
|
182
|
+
[style.height.px]="viewportHeight"
|
|
183
|
+
(scroll)="onScroll()"
|
|
184
|
+
>
|
|
185
|
+
<div class="kp-virtual-list__spacer" [style.height.px]="totalHeight">
|
|
186
|
+
<div
|
|
187
|
+
class="kp-virtual-list__window"
|
|
188
|
+
[style.transform]="windowTransform"
|
|
189
|
+
>
|
|
190
|
+
@for (item of visibleItems; track trackByOrIndex($index, item); let i = $index) {
|
|
191
|
+
<div
|
|
192
|
+
class="kp-virtual-list__row"
|
|
193
|
+
role="listitem"
|
|
194
|
+
[attr.aria-rowindex]="visibleStart + i + 1"
|
|
195
|
+
[style.height.px]="itemHeight"
|
|
196
|
+
>
|
|
197
|
+
@if (rowTemplate) {
|
|
198
|
+
<ng-container
|
|
199
|
+
*ngTemplateOutlet="rowTemplate.template; context: { $implicit: item, index: visibleStart + i }"
|
|
200
|
+
/>
|
|
201
|
+
}
|
|
202
|
+
</div>
|
|
203
|
+
}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</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"] }]
|
|
208
|
+
}], propDecorators: { items: [{
|
|
209
|
+
type: Input
|
|
210
|
+
}], itemHeight: [{
|
|
211
|
+
type: Input
|
|
212
|
+
}], viewportHeight: [{
|
|
213
|
+
type: Input
|
|
214
|
+
}], overscan: [{
|
|
215
|
+
type: Input
|
|
216
|
+
}], trackBy: [{
|
|
217
|
+
type: Input
|
|
218
|
+
}], rangeChange: [{
|
|
219
|
+
type: Output
|
|
220
|
+
}], viewportRef: [{
|
|
221
|
+
type: ViewChild,
|
|
222
|
+
args: ['viewport', { static: true }]
|
|
223
|
+
}], rowTemplate: [{
|
|
224
|
+
type: ContentChild,
|
|
225
|
+
args: [KpVirtualRowDirective]
|
|
226
|
+
}] } });
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Generated bundle index. Do not edit.
|
|
230
|
+
*/
|
|
231
|
+
|
|
232
|
+
export { KpVirtualListComponent, KpVirtualRowDirective };
|
|
233
|
+
//# sourceMappingURL=kanso-protocol-virtual-list.mjs.map
|
|
@@ -0,0 +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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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kanso-protocol/virtual-list",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"peerDependencies": {
|
|
6
|
+
"@angular/core": "^18.0.0",
|
|
7
|
+
"@angular/common": "^18.0.0",
|
|
8
|
+
"@kanso-protocol/core": "^0.0.1"
|
|
9
|
+
},
|
|
10
|
+
"description": "Kanso Protocol — virtual-list (component).",
|
|
11
|
+
"author": "GregNBlack",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/GregNBlack/kanso-protocol.git",
|
|
15
|
+
"directory": "packages/components/virtual-list"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://gregnblack.github.io/kanso-protocol/?path=/docs/components-virtual-list--docs",
|
|
18
|
+
"bugs": "https://github.com/GregNBlack/kanso-protocol/issues",
|
|
19
|
+
"keywords": [
|
|
20
|
+
"design-system",
|
|
21
|
+
"angular",
|
|
22
|
+
"kanso",
|
|
23
|
+
"virtual-list",
|
|
24
|
+
"virtualization",
|
|
25
|
+
"windowing"
|
|
26
|
+
],
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"module": "fesm2022/kanso-protocol-virtual-list.mjs",
|
|
29
|
+
"typings": "types/kanso-protocol-virtual-list.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
"./package.json": {
|
|
32
|
+
"default": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./types/kanso-protocol-virtual-list.d.ts",
|
|
36
|
+
"default": "./fesm2022/kanso-protocol-virtual-list.mjs"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"type": "module",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"tslib": "^2.3.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { OnChanges, OnDestroy, EventEmitter, TemplateRef, SimpleChanges } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
interface KpVirtualRange {
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Marker directive — pairs the row template with `<kp-virtual-list>`.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <kp-virtual-list ...>
|
|
13
|
+
* <ng-template kpVirtualRow let-item let-i="index">
|
|
14
|
+
* <div>row {{ i }}: {{ item.name }}</div>
|
|
15
|
+
* </ng-template>
|
|
16
|
+
* </kp-virtual-list>
|
|
17
|
+
*/
|
|
18
|
+
declare class KpVirtualRowDirective<T = unknown> {
|
|
19
|
+
readonly template: TemplateRef<any>;
|
|
20
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<KpVirtualRowDirective<any>, never>;
|
|
21
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<KpVirtualRowDirective<any>, "[kpVirtualRow]", never, {}, {}, never, never, true, never>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Kanso Protocol — VirtualList
|
|
25
|
+
*
|
|
26
|
+
* Window-mode virtual scroller for **fixed-height rows**. Renders only the
|
|
27
|
+
* rows currently visible in the viewport (plus a configurable `[overscan]`
|
|
28
|
+
* buffer). Lets the consumer hand any item shape via a projected
|
|
29
|
+
* `<ng-template kpVirtualRow let-item let-i="index">`.
|
|
30
|
+
*
|
|
31
|
+
* Why fixed-height: keeps the math O(1) per scroll event (`scrollTop /
|
|
32
|
+
* itemHeight`), no measurement pass, no layout thrash. Variable-height
|
|
33
|
+
* support is on the roadmap (`KpVariableVirtualListComponent`); for now,
|
|
34
|
+
* size each row to a uniform height.
|
|
35
|
+
*
|
|
36
|
+
* Use this for tables / message lists / log views with thousands of rows.
|
|
37
|
+
* Below ~100 rows just render them — virtualization adds complexity for
|
|
38
|
+
* negligible gain.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* <kp-virtual-list
|
|
42
|
+
* [items]="rows"
|
|
43
|
+
* [itemHeight]="40"
|
|
44
|
+
* [viewportHeight]="480"
|
|
45
|
+
* [overscan]="6"
|
|
46
|
+
* (rangeChange)="onRange($event)"
|
|
47
|
+
* >
|
|
48
|
+
* <ng-template kpVirtualRow let-row let-i="index">
|
|
49
|
+
* <div class="row" [class.row--alt]="i % 2">{{ row.name }}</div>
|
|
50
|
+
* </ng-template>
|
|
51
|
+
* </kp-virtual-list>
|
|
52
|
+
*/
|
|
53
|
+
declare class KpVirtualListComponent<T = unknown> implements OnChanges, OnDestroy {
|
|
54
|
+
/** The full list. The component never iterates the whole thing — only the visible window. */
|
|
55
|
+
items: readonly T[];
|
|
56
|
+
/** Pixel height of one row. Required; must be uniform across rows. */
|
|
57
|
+
itemHeight: number;
|
|
58
|
+
/** Pixel height of the scroll viewport. */
|
|
59
|
+
viewportHeight: number;
|
|
60
|
+
/** Extra rows rendered above + below the visible window to soften scroll-flicker. */
|
|
61
|
+
overscan: number;
|
|
62
|
+
/** Optional trackBy for the projected rows. Defaults to index — fine for stable lists. */
|
|
63
|
+
trackBy: ((index: number, item: T) => unknown) | null;
|
|
64
|
+
readonly rangeChange: EventEmitter<KpVirtualRange>;
|
|
65
|
+
private viewportRef;
|
|
66
|
+
rowTemplate?: KpVirtualRowDirective<T>;
|
|
67
|
+
visibleStart: number;
|
|
68
|
+
visibleEnd: number;
|
|
69
|
+
private readonly cdr;
|
|
70
|
+
private readonly zone;
|
|
71
|
+
ngOnChanges(changes: SimpleChanges): void;
|
|
72
|
+
ngOnDestroy(): void;
|
|
73
|
+
get totalHeight(): number;
|
|
74
|
+
get visibleItems(): readonly T[];
|
|
75
|
+
get windowTransform(): string;
|
|
76
|
+
trackByOrIndex(index: number, item: T): unknown;
|
|
77
|
+
/** Imperatively scroll to a specific row index. */
|
|
78
|
+
scrollToIndex(index: number, position?: 'start' | 'center' | 'end'): void;
|
|
79
|
+
onScroll(): void;
|
|
80
|
+
private recompute;
|
|
81
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<KpVirtualListComponent<any>, never>;
|
|
82
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<KpVirtualListComponent<any>, "kp-virtual-list", never, { "items": { "alias": "items"; "required": false; }; "itemHeight": { "alias": "itemHeight"; "required": false; }; "viewportHeight": { "alias": "viewportHeight"; "required": false; }; "overscan": { "alias": "overscan"; "required": false; }; "trackBy": { "alias": "trackBy"; "required": false; }; }, { "rangeChange": "rangeChange"; }, ["rowTemplate"], never, true, never>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { KpVirtualListComponent, KpVirtualRowDirective };
|
|
86
|
+
export type { KpVirtualRange };
|