@libs-ui/components-scroll-measure-items-direction-vertical 0.2.356-42 → 0.2.357-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.
package/README.md
CHANGED
|
@@ -1,3 +1,419 @@
|
|
|
1
|
-
# scroll-measure-items-direction-vertical
|
|
1
|
+
# @libs-ui/components-scroll-measure-items-direction-vertical
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Directive Angular thực hiện virtual scrolling theo chiều dọc với kích thước item động (dynamic height).
|
|
4
|
+
|
|
5
|
+
## Giới thiệu
|
|
6
|
+
|
|
7
|
+
`LibsUiScrollMeasureItemDirectionVerticalDirective` là một Angular directive cho phép ảo hóa danh sách lớn theo chiều dọc (vertical virtual scrolling). Directive đo chiều cao từng item bất đồng bộ thông qua hàm callback do consumer cung cấp, từ đó chỉ render các item thực sự nằm trong viewport, giúp hiệu năng không bị ảnh hưởng dù list có hàng nghìn phần tử. Khác với các thư viện cố định item height, directive này hỗ trợ các item có chiều cao khác nhau (heterogeneous heights).
|
|
8
|
+
|
|
9
|
+
## Tính năng
|
|
10
|
+
|
|
11
|
+
- ✅ Virtual scrolling chiều dọc — chỉ render item trong viewport
|
|
12
|
+
- ✅ Hỗ trợ item có chiều cao động (dynamic height) qua hàm đo bất đồng bộ
|
|
13
|
+
- ✅ Tìm kiếm binary search O(log N) cho mọi sự kiện scroll
|
|
14
|
+
- ✅ Buffer item (3 items) phía dưới viewport để tránh trắng màn khi cuộn nhanh
|
|
15
|
+
- ✅ API điều khiển: `scrollInto`, `scrollToPosition`, `scrollToIndex`, `reCalculatorViewPort`
|
|
16
|
+
- ✅ ResizeObserver — tự tính lại viewport khi container thay đổi kích thước
|
|
17
|
+
- ✅ Cleanup tự động qua `DestroyRef` khi directive bị destroy
|
|
18
|
+
- ✅ Standalone directive, không cần NgModule
|
|
19
|
+
|
|
20
|
+
## Khi nào sử dụng
|
|
21
|
+
|
|
22
|
+
- Danh sách có số lượng item lớn (> 200 phần tử) cần scroll theo chiều dọc
|
|
23
|
+
- Các item trong list có chiều cao khác nhau (chat messages, feed, timeline)
|
|
24
|
+
- Cần điều khiển scroll bằng code: scroll đến item cụ thể, scroll đến vị trí, scroll theo index
|
|
25
|
+
- Bảng dữ liệu, danh sách sản phẩm, log viewer, chat history
|
|
26
|
+
|
|
27
|
+
## Cài đặt
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install @libs-ui/components-scroll-measure-items-direction-vertical
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Import
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { LibsUiScrollMeasureItemDirectionVerticalDirective } from '@libs-ui/components-scroll-measure-items-direction-vertical';
|
|
37
|
+
import { IScrollMeasureItemDirectionVerticalFunctionsControl } from '@libs-ui/components-scroll-measure-items-direction-vertical';
|
|
38
|
+
import { IScrollMeasureStoreItemVerticalConvert } from '@libs-ui/components-scroll-measure-items-direction-vertical';
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Ví dụ sử dụng
|
|
42
|
+
|
|
43
|
+
### Ví dụ 1 — Danh sách cơ bản với chiều cao cố định
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// component.ts
|
|
47
|
+
import { Component, signal } from '@angular/core';
|
|
48
|
+
import {
|
|
49
|
+
LibsUiScrollMeasureItemDirectionVerticalDirective,
|
|
50
|
+
IScrollMeasureItemDirectionVerticalFunctionsControl,
|
|
51
|
+
} from '@libs-ui/components-scroll-measure-items-direction-vertical';
|
|
52
|
+
|
|
53
|
+
interface T_product {
|
|
54
|
+
id: number;
|
|
55
|
+
name: string;
|
|
56
|
+
price: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Component({
|
|
60
|
+
selector: 'app-product-list',
|
|
61
|
+
standalone: true,
|
|
62
|
+
imports: [LibsUiScrollMeasureItemDirectionVerticalDirective],
|
|
63
|
+
templateUrl: './product-list.component.html',
|
|
64
|
+
})
|
|
65
|
+
export class ProductListComponent {
|
|
66
|
+
protected items = signal<T_product[]>(
|
|
67
|
+
Array.from({ length: 500 }, (_, i) => ({
|
|
68
|
+
id: i + 1,
|
|
69
|
+
name: `Sản phẩm ${i + 1}`,
|
|
70
|
+
price: (i + 1) * 10000,
|
|
71
|
+
}))
|
|
72
|
+
);
|
|
73
|
+
protected viewPortItems = signal<T_product[]>([]);
|
|
74
|
+
private scrollControl: IScrollMeasureItemDirectionVerticalFunctionsControl | null = null;
|
|
75
|
+
|
|
76
|
+
// Hàm đo chiều cao — phải cộng cả margin/padding nếu có
|
|
77
|
+
protected measureHeight = async (item: T_product): Promise<number> => {
|
|
78
|
+
return 64; // item cao cố định 64px
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
protected handlerViewPortItems(items: T_product[]): void {
|
|
82
|
+
this.viewPortItems.set(items);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
protected handlerFunctionControl(control: IScrollMeasureItemDirectionVerticalFunctionsControl): void {
|
|
86
|
+
this.scrollControl = control;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
protected handlerScrollToTop(): void {
|
|
90
|
+
this.scrollControl?.scrollToIndex(0);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```html
|
|
96
|
+
<!-- product-list.component.html -->
|
|
97
|
+
<div
|
|
98
|
+
#scrollContainer
|
|
99
|
+
class="w-full h-[500px] overflow-y-auto border border-gray-200 rounded relative"
|
|
100
|
+
>
|
|
101
|
+
<div
|
|
102
|
+
class="block w-full"
|
|
103
|
+
LibsUiScrollMeasureItemDirectionVerticalDirective
|
|
104
|
+
[elementScroll]="scrollContainer"
|
|
105
|
+
[items]="items()"
|
|
106
|
+
[functionGetHeightItem]="measureHeight"
|
|
107
|
+
(outViewPortItem)="handlerViewPortItems($event)"
|
|
108
|
+
(outFunctionControl)="handlerFunctionControl($event)"
|
|
109
|
+
>
|
|
110
|
+
@for (item of viewPortItems(); track item.id) {
|
|
111
|
+
<div class="flex items-center justify-between px-4 py-3 border-b border-gray-100 h-[64px]">
|
|
112
|
+
<span class="font-medium">{{ item.name }}</span>
|
|
113
|
+
<span class="text-blue-600">{{ item.price | number }} đ</span>
|
|
114
|
+
</div>
|
|
115
|
+
}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<button
|
|
120
|
+
(click)="handlerScrollToTop()"
|
|
121
|
+
class="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
|
|
122
|
+
>
|
|
123
|
+
Lên đầu
|
|
124
|
+
</button>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### Ví dụ 2 — Danh sách với chiều cao động (dynamic height)
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// chat-list.component.ts
|
|
133
|
+
import { Component, signal } from '@angular/core';
|
|
134
|
+
import {
|
|
135
|
+
LibsUiScrollMeasureItemDirectionVerticalDirective,
|
|
136
|
+
IScrollMeasureItemDirectionVerticalFunctionsControl,
|
|
137
|
+
} from '@libs-ui/components-scroll-measure-items-direction-vertical';
|
|
138
|
+
|
|
139
|
+
interface T_chatMessage {
|
|
140
|
+
id: number;
|
|
141
|
+
text: string;
|
|
142
|
+
sender: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@Component({
|
|
146
|
+
selector: 'app-chat-list',
|
|
147
|
+
standalone: true,
|
|
148
|
+
imports: [LibsUiScrollMeasureItemDirectionVerticalDirective],
|
|
149
|
+
templateUrl: './chat-list.component.html',
|
|
150
|
+
})
|
|
151
|
+
export class ChatListComponent {
|
|
152
|
+
protected messages = signal<T_chatMessage[]>([
|
|
153
|
+
{ id: 1, sender: 'Alice', text: 'Xin chào!' },
|
|
154
|
+
{ id: 2, sender: 'Bob', text: 'Chào bạn! Hôm nay bạn có khỏe không? Mình vừa xem xong bộ phim rất hay, bạn có muốn nghe kể không?' },
|
|
155
|
+
{ id: 3, sender: 'Alice', text: 'Mình khỏe, cảm ơn bạn!' },
|
|
156
|
+
// ... nhiều messages hơn
|
|
157
|
+
]);
|
|
158
|
+
protected viewPortMessages = signal<T_chatMessage[]>([]);
|
|
159
|
+
private scrollControl: IScrollMeasureItemDirectionVerticalFunctionsControl | null = null;
|
|
160
|
+
|
|
161
|
+
// Chiều cao động: tính theo độ dài text (48px base + 20px/100 ký tự)
|
|
162
|
+
// BẮT BUỘC cộng margin: my-[4px] = 8px (4px top + 4px bottom)
|
|
163
|
+
protected measureMessageHeight = async (msg: T_chatMessage): Promise<number> => {
|
|
164
|
+
const lineCount = Math.ceil(msg.text.length / 40);
|
|
165
|
+
return 48 + lineCount * 20 + 8;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
protected handlerViewPortMessages(items: T_chatMessage[]): void {
|
|
169
|
+
this.viewPortMessages.set(items);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
protected handlerFunctionControl(control: IScrollMeasureItemDirectionVerticalFunctionsControl): void {
|
|
173
|
+
this.scrollControl = control;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Scroll đến tin nhắn mới nhất
|
|
177
|
+
protected handlerScrollToLatest(): void {
|
|
178
|
+
const lastIndex = this.messages().length - 1;
|
|
179
|
+
this.scrollControl?.scrollToIndex(lastIndex);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Scroll đến tin nhắn cụ thể theo object reference
|
|
183
|
+
protected handlerScrollToMessage(msg: T_chatMessage): void {
|
|
184
|
+
this.scrollControl?.scrollInto(msg);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```html
|
|
190
|
+
<!-- chat-list.component.html -->
|
|
191
|
+
<div class="flex flex-col h-[600px]">
|
|
192
|
+
<div
|
|
193
|
+
#chatContainer
|
|
194
|
+
class="flex-1 overflow-y-auto relative"
|
|
195
|
+
>
|
|
196
|
+
<div
|
|
197
|
+
class="block w-full px-4"
|
|
198
|
+
LibsUiScrollMeasureItemDirectionVerticalDirective
|
|
199
|
+
[elementScroll]="chatContainer"
|
|
200
|
+
[items]="messages()"
|
|
201
|
+
[functionGetHeightItem]="measureMessageHeight"
|
|
202
|
+
(outViewPortItem)="handlerViewPortMessages($event)"
|
|
203
|
+
(outFunctionControl)="handlerFunctionControl($event)"
|
|
204
|
+
>
|
|
205
|
+
@for (msg of viewPortMessages(); track msg.id) {
|
|
206
|
+
<div class="my-[4px] p-3 rounded-lg bg-gray-50 border border-gray-100">
|
|
207
|
+
<span class="text-xs font-semibold text-blue-600">{{ msg.sender }}</span>
|
|
208
|
+
<p class="text-sm text-gray-800 mt-1">{{ msg.text }}</p>
|
|
209
|
+
</div>
|
|
210
|
+
}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<button
|
|
215
|
+
(click)="handlerScrollToLatest()"
|
|
216
|
+
class="mx-4 mb-4 py-2 bg-blue-500 text-white rounded text-sm"
|
|
217
|
+
>
|
|
218
|
+
Tin nhắn mới nhất
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
### Ví dụ 3 — Sử dụng `reCalculatorViewPort` khi item thay đổi chiều cao
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// expandable-list.component.ts
|
|
229
|
+
import { Component, signal } from '@angular/core';
|
|
230
|
+
import {
|
|
231
|
+
LibsUiScrollMeasureItemDirectionVerticalDirective,
|
|
232
|
+
IScrollMeasureItemDirectionVerticalFunctionsControl,
|
|
233
|
+
} from '@libs-ui/components-scroll-measure-items-direction-vertical';
|
|
234
|
+
|
|
235
|
+
interface T_expandableItem {
|
|
236
|
+
id: number;
|
|
237
|
+
title: string;
|
|
238
|
+
detail: string;
|
|
239
|
+
expanded: boolean;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@Component({
|
|
243
|
+
selector: 'app-expandable-list',
|
|
244
|
+
standalone: true,
|
|
245
|
+
imports: [LibsUiScrollMeasureItemDirectionVerticalDirective],
|
|
246
|
+
templateUrl: './expandable-list.component.html',
|
|
247
|
+
})
|
|
248
|
+
export class ExpandableListComponent {
|
|
249
|
+
protected items = signal<T_expandableItem[]>(
|
|
250
|
+
Array.from({ length: 200 }, (_, i) => ({
|
|
251
|
+
id: i + 1,
|
|
252
|
+
title: `Mục ${i + 1}`,
|
|
253
|
+
detail: `Chi tiết cho mục ${i + 1}. Thông tin này xuất hiện khi expand.`,
|
|
254
|
+
expanded: false,
|
|
255
|
+
}))
|
|
256
|
+
);
|
|
257
|
+
protected viewPortItems = signal<T_expandableItem[]>([]);
|
|
258
|
+
private scrollControl: IScrollMeasureItemDirectionVerticalFunctionsControl | null = null;
|
|
259
|
+
|
|
260
|
+
protected measureHeight = async (item: T_expandableItem): Promise<number> => {
|
|
261
|
+
// 48px thu gọn, 96px khi mở rộng, cộng 4px margin
|
|
262
|
+
return item.expanded ? 96 + 4 : 48 + 4;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
protected handlerViewPortItems(items: T_expandableItem[]): void {
|
|
266
|
+
this.viewPortItems.set(items);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
protected handlerFunctionControl(control: IScrollMeasureItemDirectionVerticalFunctionsControl): void {
|
|
270
|
+
this.scrollControl = control;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Sau khi toggle, gọi reCalculatorViewPort để tính lại heights
|
|
274
|
+
protected handlerToggleItem(item: T_expandableItem): void {
|
|
275
|
+
item.expanded = !item.expanded;
|
|
276
|
+
this.items.set([...this.items()]);
|
|
277
|
+
// Tính lại viewport sau khi chiều cao thay đổi
|
|
278
|
+
this.scrollControl?.reCalculatorViewPort();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
```html
|
|
284
|
+
<!-- expandable-list.component.html -->
|
|
285
|
+
<div
|
|
286
|
+
#listContainer
|
|
287
|
+
class="w-full h-[400px] overflow-y-auto border border-gray-200 rounded relative"
|
|
288
|
+
>
|
|
289
|
+
<div
|
|
290
|
+
class="block w-full"
|
|
291
|
+
LibsUiScrollMeasureItemDirectionVerticalDirective
|
|
292
|
+
[elementScroll]="listContainer"
|
|
293
|
+
[items]="items()"
|
|
294
|
+
[functionGetHeightItem]="measureHeight"
|
|
295
|
+
(outViewPortItem)="handlerViewPortItems($event)"
|
|
296
|
+
(outFunctionControl)="handlerFunctionControl($event)"
|
|
297
|
+
>
|
|
298
|
+
@for (item of viewPortItems(); track item.id) {
|
|
299
|
+
<div
|
|
300
|
+
class="my-[2px] px-4 border border-gray-100 rounded cursor-pointer"
|
|
301
|
+
[class.h-[48px]]="!item.expanded"
|
|
302
|
+
[class.h-[96px]]="item.expanded"
|
|
303
|
+
(click)="handlerToggleItem(item)"
|
|
304
|
+
>
|
|
305
|
+
<div class="flex items-center h-[48px]">
|
|
306
|
+
<span class="font-medium">{{ item.title }}</span>
|
|
307
|
+
<span class="ml-auto text-gray-400 text-xs">{{ item.expanded ? '▲' : '▼' }}</span>
|
|
308
|
+
</div>
|
|
309
|
+
@if (item.expanded) {
|
|
310
|
+
<p class="text-sm text-gray-600 pb-2">{{ item.detail }}</p>
|
|
311
|
+
}
|
|
312
|
+
</div>
|
|
313
|
+
}
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## @Input()
|
|
319
|
+
|
|
320
|
+
| Input | Type | Default | Mô tả | Ví dụ |
|
|
321
|
+
|---|---|---|---|---|
|
|
322
|
+
| `elementScroll` | `HTMLElement` | **required** | Element chứa thanh cuộn dọc (`overflow-y: auto/scroll`). Thường là outer container được gắn template reference `#scrollContainer`. | `[elementScroll]="scrollContainer"` |
|
|
323
|
+
| `items` | `Array<any>` | **required** | Toàn bộ danh sách dữ liệu gốc. Directive đọc signal này để tính toán viewport và tái tính lại khi array thay đổi. | `[items]="items()"` |
|
|
324
|
+
| `functionGetHeightItem` | `(item: any) => Promise<number>` | **required** | Hàm bất đồng bộ nhận vào một item và trả về chiều cao (px) của item đó, bao gồm cả margin trên/dưới. Được gọi song song cho toàn bộ mảng để tối ưu tốc độ. | `[functionGetHeightItem]="measureHeight"` |
|
|
325
|
+
|
|
326
|
+
## @Output()
|
|
327
|
+
|
|
328
|
+
| Output | Type | Mô tả | Handler TS | Binding HTML |
|
|
329
|
+
|---|---|---|---|---|
|
|
330
|
+
| `(outViewPortItem)` | `Array<any>` | Emit danh sách items cần render trong viewport hiện tại (bao gồm buffer 3 items phía dưới). Component con phải lưu vào signal riêng và dùng trong `@for`. | `handlerViewPortItems(items: any[]): void { this.viewPortItems.set(items); }` | `(outViewPortItem)="handlerViewPortItems($event)"` |
|
|
331
|
+
| `(outFunctionControl)` | `IScrollMeasureItemDirectionVerticalFunctionsControl` | Emit object chứa đầy đủ API điều khiển scroll. Emit một lần sau `ngAfterViewInit`. | `handlerFunctionControl(ctrl: IScrollMeasureItemDirectionVerticalFunctionsControl): void { this.scrollControl = ctrl; }` | `(outFunctionControl)="handlerFunctionControl($event)"` |
|
|
332
|
+
| `(outDivVirtual)` | `HTMLDivElement` | Emit element ảo (absolute, visibility hidden) được inject vào scroll container để giữ tổng chiều cao scroll đúng. Ít khi cần dùng trực tiếp. | `handlerDivVirtual(el: HTMLDivElement): void { this.virtualEl = el; }` | `(outDivVirtual)="handlerDivVirtual($event)"` |
|
|
333
|
+
| `(outPaddingTop)` | `number` | Emit giá trị `padding-top` (px) đang được áp dụng lên inner container để bù khoảng trống các item bị ẩn phía trên viewport. | `handlerPaddingTop(value: number): void { this.currentPaddingTop = value; }` | `(outPaddingTop)="handlerPaddingTop($event)"` |
|
|
334
|
+
|
|
335
|
+
## FunctionsControl API
|
|
336
|
+
|
|
337
|
+
Nhận đối tượng `IScrollMeasureItemDirectionVerticalFunctionsControl` qua output `(outFunctionControl)` để điều khiển scroll bằng code:
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
import { IScrollMeasureItemDirectionVerticalFunctionsControl } from '@libs-ui/components-scroll-measure-items-direction-vertical';
|
|
341
|
+
|
|
342
|
+
private scrollControl: IScrollMeasureItemDirectionVerticalFunctionsControl | null = null;
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
| Method | Signature | Mô tả |
|
|
346
|
+
|---|---|---|
|
|
347
|
+
| `scrollInto` | `(item: any) => Promise<void>` | Scroll container đến vị trí của item theo object reference. Item phải tồn tại trong `items()`. |
|
|
348
|
+
| `scrollToPosition` | `(position: number) => Promise<void>` | Scroll container đến vị trí `scrollTop` tuyệt đối (px) chỉ định. |
|
|
349
|
+
| `scrollToIndex` | `(index: number) => Promise<void>` | Scroll container đến item tại index chỉ định trong mảng `items()`. |
|
|
350
|
+
| `reCalculatorViewPort` | `() => Promise<void>` | Tính lại toàn bộ chiều cao và viewport. Gọi sau khi chiều cao item thay đổi (expand/collapse, nội dung thay đổi). |
|
|
351
|
+
| `getViewPortItems` | `() => Array<any>` | Trả về mảng items hiện đang trong viewport (đồng bộ). |
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
// Cách sử dụng FunctionsControl
|
|
355
|
+
protected handlerScrollToTop(): void {
|
|
356
|
+
this.scrollControl?.scrollToIndex(0);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
protected handlerScrollToItem(item: T_product): void {
|
|
360
|
+
this.scrollControl?.scrollInto(item);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
protected handlerScrollToMiddle(): void {
|
|
364
|
+
const middleIndex = Math.floor(this.items().length / 2);
|
|
365
|
+
this.scrollControl?.scrollToIndex(middleIndex);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
protected handlerRefreshHeights(): void {
|
|
369
|
+
this.scrollControl?.reCalculatorViewPort();
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Types & Interfaces
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
import {
|
|
377
|
+
IScrollMeasureItemDirectionVerticalFunctionsControl,
|
|
378
|
+
IScrollMeasureStoreItemVerticalConvert,
|
|
379
|
+
} from '@libs-ui/components-scroll-measure-items-direction-vertical';
|
|
380
|
+
|
|
381
|
+
// API điều khiển scroll — nhận qua (outFunctionControl)
|
|
382
|
+
interface IScrollMeasureItemDirectionVerticalFunctionsControl {
|
|
383
|
+
scrollInto: (item: any) => Promise<void>;
|
|
384
|
+
scrollToPosition: (position: number) => Promise<void>;
|
|
385
|
+
scrollToIndex: (index: number) => Promise<void>;
|
|
386
|
+
reCalculatorViewPort: () => Promise<void>;
|
|
387
|
+
getViewPortItems: () => Array<any>;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Cấu trúc nội bộ của item đã được đo — dùng khi cần truy cập tọa độ
|
|
391
|
+
interface IScrollMeasureStoreItemVerticalConvert {
|
|
392
|
+
ref: any; // Tham chiếu item gốc từ mảng items
|
|
393
|
+
itemHeight: number; // Chiều cao đã đo (px)
|
|
394
|
+
start: number; // Tọa độ Y bắt đầu của item (px từ đỉnh container)
|
|
395
|
+
end: number; // Tọa độ Y kết thúc của item (px từ đỉnh container)
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## Lưu ý quan trọng
|
|
400
|
+
|
|
401
|
+
⚠️ **elementScroll là outer container**: Truyền `[elementScroll]` là element có `overflow-y: auto/scroll`, không phải element được gắn directive. Directive gắn vào inner container (div wrapper bên trong), còn `elementScroll` là outer div chứa thanh cuộn.
|
|
402
|
+
|
|
403
|
+
⚠️ **Inner container phải là `block`**: Element được gắn directive (inner container) PHẢI có class `block` hoặc `display: block`. Directive tự thêm `position: relative` và điều chỉnh `padding-top` lên element này, nên layout `flex` hay `grid` trực tiếp trên element đó sẽ gây sai vị trí.
|
|
404
|
+
|
|
405
|
+
⚠️ **Chiều cao trong `functionGetHeightItem` phải bao gồm margin**: Nếu item có `my-[4px]` (4px top + 4px bottom = 8px), cộng 8px vào giá trị trả về. Ví dụ: item cao 60px + margin 8px → trả về `68`.
|
|
406
|
+
|
|
407
|
+
⚠️ **Gọi `reCalculatorViewPort` sau khi item thay đổi chiều cao**: Khi expand/collapse item hoặc nội dung thay đổi chiều cao, phải gọi `scrollControl.reCalculatorViewPort()` để directive tính lại. Chỉ thay đổi `items()` signal không đủ vì directive chỉ đo lại heights khi array reference thay đổi.
|
|
408
|
+
|
|
409
|
+
⚠️ **Không dùng `track $index` trong `@for`**: Theo coding convention dự án, bắt buộc dùng `track item.id` hoặc `track item` (track object reference). Dùng `track $index` gây bug re-render lộn xộn với virtual scroll.
|
|
410
|
+
|
|
411
|
+
⚠️ **`viewPortItems` signal phải được khai báo riêng**: Directive không quản lý state của consumer. Consumer phải tự khai báo `protected viewPortItems = signal<T[]>([])` và cập nhật trong handler `(outViewPortItem)`.
|
|
412
|
+
|
|
413
|
+
## Demo
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
npx nx serve core-ui
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Truy cập: http://localhost:4500/scroll-measure-items/direction-vertical
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"libs-ui-components-scroll-measure-items-direction-vertical.mjs","sources":["../../../../../../libs-ui/components/scroll-measure-items/direction-vertical/src/direction-vertical.directive.ts","../../../../../../libs-ui/components/scroll-measure-items/direction-vertical/src/interfaces/direction-vertical.interface.ts","../../../../../../libs-ui/components/scroll-measure-items/direction-vertical/src/libs-ui-components-scroll-measure-items-direction-vertical.ts"],"sourcesContent":["// CONVENTION-EXCEPT: any — Generic directive works with unknown item types from consumer\n/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { AfterViewInit, DestroyRef, Directive, ElementRef, effect, inject, input, output, signal, untracked } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { fromEvent } from 'rxjs';\nimport { IScrollMeasureItemDirectionVerticalFunctionsControl, IScrollMeasureStoreItemVerticalConvert } from './interfaces/direction-vertical.interface';\n\n@Directive({\n // eslint-disable-next-line @angular-eslint/directive-selector\n selector: '[LibsUiScrollMeasureItemDirectionVerticalDirective]',\n standalone: true,\n})\nexport class LibsUiScrollMeasureItemDirectionVerticalDirective implements AfterViewInit {\n // #region PROPERTY\n /** Số item buffer render thêm sau viewport để tránh trắng vùng khi scroll. */\n private readonly bufferAhead = 3;\n\n private readonly viewPortItems = signal<Array<any>>([]);\n private readonly start = signal<number>(0);\n private readonly end = signal<number>(0);\n private readonly storeItems = signal<Array<IScrollMeasureStoreItemVerticalConvert>>([]);\n private readonly divVirtual = document.createElement('div');\n private resizeObserver?: ResizeObserver;\n /** Tham chiếu setTimeout của ngAfterViewInit để clear khi destroy — chặn ResizeObserver mồ côi. */\n private timerAfterViewInit?: number;\n /** Token nhận dạng lần effect chạy hiện tại. Async đo height xong sẽ bỏ kết quả nếu token đổi. */\n private executionToken = 0;\n /** Style + DOM hook của divVirtual chỉ cần khởi tạo một lần cho cả vòng đời directive. */\n private divVirtualInitialized = false;\n\n // #region INPUT\n readonly elementScroll = input.required<HTMLElement>();\n readonly functionGetHeightItem = input.required<(item: any) => Promise<number>>();\n readonly items = input.required<Array<any>>();\n\n // #region OUTPUT\n readonly outViewPortItem = output<Array<any>>();\n readonly outFunctionControl = output<IScrollMeasureItemDirectionVerticalFunctionsControl>();\n readonly outDivVirtual = output<HTMLDivElement>();\n readonly outPaddingTop = output<number>();\n\n // #region INJECT\n private readonly elementRef = inject(ElementRef);\n private readonly destroyRef = inject(DestroyRef);\n\n constructor() {\n effect(() => {\n const items = this.items();\n const myToken = ++this.executionToken;\n untracked(async () => {\n const scrollEl = this.ScrollContainer;\n const functionGetHeightItem = this.functionGetHeightItem();\n if (!scrollEl || !functionGetHeightItem) {\n return;\n }\n\n // Đo height tất cả item song song — rút ngắn thời gian cho list lớn.\n const heights = await Promise.all(items.map((it) => functionGetHeightItem(it)));\n // Bị huỷ vì có lượt items() mới chạy sau — bỏ kết quả cũ tránh đè state.\n if (myToken !== this.executionToken) {\n return;\n }\n\n this.start.set(0);\n const scrollTop = scrollEl.scrollTop;\n const containerHeight = scrollEl.getBoundingClientRect().height;\n let height = 0;\n let endLocal = 0;\n const newItems: Array<IScrollMeasureStoreItemVerticalConvert> = [];\n\n for (let i = 0; i < items.length; i++) {\n const itemHeight = heights[i];\n const newItem: IScrollMeasureStoreItemVerticalConvert = { ref: items[i], itemHeight, start: height, end: 0 };\n height += itemHeight;\n newItem.end = height;\n if (newItem.end <= containerHeight + scrollTop) {\n endLocal = i;\n }\n newItems.push(newItem);\n }\n // Set end một lần thay vì ghi mỗi vòng lặp — tránh notify consumer N lần.\n this.end.set(endLocal);\n this.storeItems.set(newItems);\n\n // Khởi tạo style + DOM hook của divVirtual đúng một lần.\n if (!this.divVirtualInitialized) {\n this.divVirtual.style.position = 'absolute';\n this.divVirtual.style.top = '0px';\n this.divVirtual.style.left = '0px';\n this.divVirtual.style.width = '1px';\n this.divVirtual.style.visibility = 'hidden'; // Đảm bảo an toàn không hiển thị\n this.elementRef.nativeElement.classList.add('relative');\n this.elementRef.nativeElement.classList.remove('h-full');\n this.elementRef.nativeElement.style.paddingTop = '0px';\n scrollEl.append(this.divVirtual);\n this.divVirtualInitialized = true;\n this.outDivVirtual.emit(this.divVirtual);\n }\n // Height tổng thể đổi theo dataset → cần update mỗi lượt.\n this.divVirtual.style.height = `${height}px`;\n\n // containerHeight lúc đầu có thể = 0 (DOM chưa layout), khiến end giữ\n // nguyên 0 và chỉ render bufferAhead item đầu. Re-đo bằng handlerScroll sau khi vòng\n // lặp await xong (layout thường đã hoàn tất) để end ra đúng giá trị.\n this.handlerScroll();\n });\n });\n\n this.destroyRef.onDestroy(() => {\n clearTimeout(this.timerAfterViewInit);\n this.divVirtual.remove();\n this.resizeObserver?.disconnect();\n });\n }\n\n ngAfterViewInit() {\n this.timerAfterViewInit = setTimeout(() => {\n fromEvent<Event>(this.ScrollContainer, 'scroll')\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(() => this.handlerScroll());\n this.outFunctionControl.emit(this.FunctionsControl);\n\n // Re-tính viewport sau khi container có kích thước thật.\n // Lần đầu render, getBoundingClientRect().height có thể = 0 nên end giữ giá trị 0,\n // chỉ hiển thị bufferAhead item đầu. ResizeObserver fire ngay khi observe và mỗi lần\n // container đổi size sẽ kéo end về đúng giá trị.\n this.resizeObserver = new ResizeObserver(() => this.handlerScroll());\n this.resizeObserver.observe(this.ScrollContainer);\n });\n }\n\n // #region PUBLIC API\n public get FunctionsControl(): IScrollMeasureItemDirectionVerticalFunctionsControl {\n return {\n scrollInto: this.scrollInto.bind(this),\n scrollToPosition: this.scrollToPosition.bind(this),\n scrollToIndex: this.scrollToIndex.bind(this),\n reCalculatorViewPort: this.reCalculatorViewPort.bind(this),\n getViewPortItems: () => this.viewPortItems(),\n };\n }\n\n // #region PRIVATE METHODS\n private get ScrollContainer(): HTMLElement {\n return this.elementScroll() || this.elementRef.nativeElement;\n }\n\n private findStartIndex(store: Array<IScrollMeasureStoreItemVerticalConvert>, scrollTop: number): number {\n let low = 0;\n let high = store.length - 1;\n let ans = store.length - 1; // Default to last item if scrolled past bottom (rubber band effect)\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n if (store[mid].end >= scrollTop) {\n ans = mid;\n high = mid - 1; // Try to find earlier\n } else {\n low = mid + 1;\n }\n }\n return ans;\n }\n\n private findEndIndex(store: Array<IScrollMeasureStoreItemVerticalConvert>, scrollBottom: number): number {\n let low = 0;\n let high = store.length - 1;\n let ans = 0; // Default to first item if rubber-banded above the top\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n if (store[mid].start <= scrollBottom) {\n ans = mid;\n low = mid + 1; // Try to find later\n } else {\n high = mid - 1;\n }\n }\n return ans;\n }\n\n private handlerScroll() {\n const store = this.storeItems();\n if (!store?.length) {\n return;\n }\n const scrollEl = this.ScrollContainer;\n const scrollTop = scrollEl.scrollTop;\n const containerHeight = scrollEl.getBoundingClientRect().height;\n\n // Utilize Binary Search for optimal O(log N) discovery\n const start = this.findStartIndex(store, scrollTop);\n const end = this.findEndIndex(store, scrollTop + containerHeight);\n\n if (end !== this.end()) {\n this.end.set(end);\n }\n\n if (start !== this.start()) {\n this.start.set(start);\n // Binary search guarantees `start` là index hợp lệ trong store.\n const paddingTop = store[start].start;\n this.elementRef.nativeElement.style.paddingTop = `${paddingTop}px`;\n this.outPaddingTop.emit(paddingTop);\n }\n this.viewPortItems.set(store.slice(this.start(), this.end() + this.bufferAhead).map((item) => item.ref));\n this.outViewPortItem.emit(this.viewPortItems());\n }\n\n private async scrollInto(itemScroll: any) {\n const itemFound = this.storeItems().find((item) => item.ref === itemScroll);\n if (!itemFound) {\n return;\n }\n this.ScrollContainer.scrollTop = itemFound.start;\n }\n\n private async scrollToPosition(position: number) {\n this.ScrollContainer.scrollTop = position;\n }\n\n private async scrollToIndex(index: number) {\n const itemOfIndex = this.storeItems()[index];\n if (!itemOfIndex) {\n return;\n }\n this.ScrollContainer.scrollTop = itemOfIndex.start;\n }\n\n private async reCalculatorViewPort() {\n const items = this.storeItems();\n const fn = this.functionGetHeightItem();\n // Đo song song để rút ngắn thời gian thay vì await tuần tự.\n const heights = await Promise.all(items.map((it) => fn(it.ref)));\n\n let height = 0;\n for (let i = 0; i < items.length; i++) {\n const item = items[i];\n const itemHeight = heights[i];\n item.itemHeight = itemHeight;\n item.start = height;\n height += itemHeight;\n item.end = height;\n }\n\n this.storeItems.set([...items]);\n this.divVirtual.style.height = `${height}px`;\n this.handlerScroll();\n }\n}\n","// CONVENTION-EXCEPT: any — Generic data structures from consumers\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nexport interface IScrollMeasureStoreItemVerticalConvert {\n ref: any;\n itemHeight: number;\n start: number; // Tọa độ Y (px) bắt đầu của item\n end: number; // Tọa độ Y (px) kết thúc của item\n}\n\nexport interface IScrollMeasureItemDirectionVerticalFunctionsControl {\n scrollInto: (item: any) => Promise<void>;\n scrollToPosition: (position: number) => Promise<void>;\n scrollToIndex: (index: number) => Promise<void>;\n reCalculatorViewPort: () => Promise<void>;\n getViewPortItems: () => Array<any>;\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":"libs-ui-components-scroll-measure-items-direction-vertical.mjs","sources":["../../../../../../libs-ui/components/scroll-measure-items/direction-vertical/src/direction-vertical.directive.ts","../../../../../../libs-ui/components/scroll-measure-items/direction-vertical/src/interfaces/direction-vertical.interface.ts","../../../../../../libs-ui/components/scroll-measure-items/direction-vertical/src/libs-ui-components-scroll-measure-items-direction-vertical.ts"],"sourcesContent":["// CONVENTION-EXCEPT: any — Generic directive works with unknown item types from consumer\n/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { AfterViewInit, DestroyRef, Directive, ElementRef, effect, inject, input, output, signal, untracked } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { fromEvent } from 'rxjs';\nimport { IScrollMeasureItemDirectionVerticalFunctionsControl, IScrollMeasureStoreItemVerticalConvert } from './interfaces/direction-vertical.interface';\n\n@Directive({\n // eslint-disable-next-line @angular-eslint/directive-selector\n selector: '[LibsUiScrollMeasureItemDirectionVerticalDirective]',\n standalone: true,\n})\nexport class LibsUiScrollMeasureItemDirectionVerticalDirective implements AfterViewInit {\n // #region PROPERTY\n /** Số item buffer render thêm sau viewport để tránh trắng vùng khi scroll. */\n private readonly bufferAhead = 3;\n\n private readonly viewPortItems = signal<Array<any>>([]);\n private readonly start = signal<number>(0);\n private readonly end = signal<number>(0);\n private readonly storeItems = signal<Array<IScrollMeasureStoreItemVerticalConvert>>([]);\n private readonly divVirtual = document.createElement('div');\n private resizeObserver?: ResizeObserver;\n /** Tham chiếu setTimeout của ngAfterViewInit để clear khi destroy — chặn ResizeObserver mồ côi. */\n private timerAfterViewInit?: number;\n /** Token nhận dạng lần effect chạy hiện tại. Async đo height xong sẽ bỏ kết quả nếu token đổi. */\n private executionToken = 0;\n /** Style + DOM hook của divVirtual chỉ cần khởi tạo một lần cho cả vòng đời directive. */\n private divVirtualInitialized = false;\n\n // #region INPUT\n readonly elementScroll = input.required<HTMLElement>();\n readonly functionGetHeightItem = input.required<(item: any) => Promise<number>>();\n readonly items = input.required<Array<any>>();\n\n // #region OUTPUT\n readonly outViewPortItem = output<Array<any>>();\n readonly outFunctionControl = output<IScrollMeasureItemDirectionVerticalFunctionsControl>();\n readonly outDivVirtual = output<HTMLDivElement>();\n readonly outPaddingTop = output<number>();\n\n // #region INJECT\n private readonly elementRef = inject(ElementRef);\n private readonly destroyRef = inject(DestroyRef);\n\n constructor() {\n effect(() => {\n const items = this.items();\n const myToken = ++this.executionToken;\n untracked(async () => {\n const scrollEl = this.ScrollContainer;\n const functionGetHeightItem = this.functionGetHeightItem();\n if (!scrollEl || !functionGetHeightItem) {\n return;\n }\n\n // Đo height tất cả item song song — rút ngắn thời gian cho list lớn.\n const heights = await Promise.all(items.map((it) => functionGetHeightItem(it)));\n // Bị huỷ vì có lượt items() mới chạy sau — bỏ kết quả cũ tránh đè state.\n if (myToken !== this.executionToken) {\n return;\n }\n\n this.start.set(0);\n const scrollTop = scrollEl.scrollTop;\n const containerHeight = scrollEl.getBoundingClientRect().height;\n let height = 0;\n let endLocal = 0;\n const newItems: Array<IScrollMeasureStoreItemVerticalConvert> = [];\n\n for (let i = 0; i < items.length; i++) {\n const itemHeight = heights[i];\n const newItem: IScrollMeasureStoreItemVerticalConvert = { ref: items[i], itemHeight, start: height, end: 0 };\n height += itemHeight;\n newItem.end = height;\n if (newItem.end <= containerHeight + scrollTop) {\n endLocal = i;\n }\n newItems.push(newItem);\n }\n // Set end một lần thay vì ghi mỗi vòng lặp — tránh notify consumer N lần.\n this.end.set(endLocal);\n this.storeItems.set(newItems);\n\n // Khởi tạo style + DOM hook của divVirtual đúng một lần.\n if (!this.divVirtualInitialized) {\n this.divVirtual.style.position = 'absolute';\n this.divVirtual.style.top = '0px';\n this.divVirtual.style.left = '0px';\n this.divVirtual.style.width = '1px';\n this.divVirtual.style.visibility = 'hidden'; // Đảm bảo an toàn không hiển thị\n this.elementRef.nativeElement.classList.add('relative');\n this.elementRef.nativeElement.classList.remove('h-full');\n this.elementRef.nativeElement.style.paddingTop = '0px';\n scrollEl.append(this.divVirtual);\n this.divVirtualInitialized = true;\n this.outDivVirtual.emit(this.divVirtual);\n }\n // Height tổng thể đổi theo dataset → cần update mỗi lượt.\n this.divVirtual.style.height = `${height}px`;\n\n // containerHeight lúc đầu có thể = 0 (DOM chưa layout), khiến end giữ\n // nguyên 0 và chỉ render bufferAhead item đầu. Re-đo bằng handlerScroll sau khi vòng\n // lặp await xong (layout thường đã hoàn tất) để end ra đúng giá trị.\n this.handlerScroll();\n });\n });\n\n this.destroyRef.onDestroy(() => {\n clearTimeout(this.timerAfterViewInit);\n this.divVirtual.remove();\n this.resizeObserver?.disconnect();\n });\n }\n\n ngAfterViewInit() {\n this.timerAfterViewInit = setTimeout(() => {\n fromEvent<Event>(this.ScrollContainer, 'scroll')\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(() => this.handlerScroll());\n this.outFunctionControl.emit(this.FunctionsControl);\n\n // Re-tính viewport sau khi container có kích thước thật.\n // Lần đầu render, getBoundingClientRect().height có thể = 0 nên end giữ giá trị 0,\n // chỉ hiển thị bufferAhead item đầu. ResizeObserver fire ngay khi observe và mỗi lần\n // container đổi size sẽ kéo end về đúng giá trị.\n this.resizeObserver = new ResizeObserver(() => this.handlerScroll());\n this.resizeObserver.observe(this.ScrollContainer);\n });\n }\n\n // #region PUBLIC API\n public get FunctionsControl(): IScrollMeasureItemDirectionVerticalFunctionsControl {\n return {\n scrollInto: this.scrollInto.bind(this),\n scrollToPosition: this.scrollToPosition.bind(this),\n scrollToIndex: this.scrollToIndex.bind(this),\n reCalculatorViewPort: this.reCalculatorViewPort.bind(this),\n getViewPortItems: () => this.viewPortItems(),\n };\n }\n\n // #region PRIVATE METHODS\n private get ScrollContainer(): HTMLElement {\n return this.elementScroll() || this.elementRef.nativeElement;\n }\n\n private findStartIndex(store: Array<IScrollMeasureStoreItemVerticalConvert>, scrollTop: number): number {\n let low = 0;\n let high = store.length - 1;\n let ans = store.length - 1; // Default to last item if scrolled past bottom (rubber band effect)\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n if (store[mid].end >= scrollTop) {\n ans = mid;\n high = mid - 1; // Try to find earlier\n } else {\n low = mid + 1;\n }\n }\n return ans;\n }\n\n private findEndIndex(store: Array<IScrollMeasureStoreItemVerticalConvert>, scrollBottom: number): number {\n let low = 0;\n let high = store.length - 1;\n let ans = 0; // Default to first item if rubber-banded above the top\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n if (store[mid].start <= scrollBottom) {\n ans = mid;\n low = mid + 1; // Try to find later\n } else {\n high = mid - 1;\n }\n }\n return ans;\n }\n\n private handlerScroll() {\n const store = this.storeItems();\n if (!store?.length) {\n return;\n }\n const scrollEl = this.ScrollContainer;\n const scrollTop = scrollEl.scrollTop;\n const containerHeight = scrollEl.getBoundingClientRect().height;\n\n // Utilize Binary Search for optimal O(log N) discovery\n const start = this.findStartIndex(store, scrollTop);\n const end = this.findEndIndex(store, scrollTop + containerHeight);\n\n if (end !== this.end()) {\n this.end.set(end);\n }\n\n if (start !== this.start()) {\n this.start.set(start);\n // Binary search guarantees `start` là index hợp lệ trong store.\n const paddingTop = store[start].start;\n this.elementRef.nativeElement.style.paddingTop = `${paddingTop}px`;\n this.outPaddingTop.emit(paddingTop);\n }\n this.viewPortItems.set(store.slice(this.start(), this.end() + this.bufferAhead).map((item) => item.ref));\n this.outViewPortItem.emit(this.viewPortItems());\n }\n\n private async scrollInto(itemScroll: any) {\n const itemFound = this.storeItems().find((item) => item.ref === itemScroll);\n if (!itemFound) {\n return;\n }\n this.ScrollContainer.scrollTop = itemFound.start;\n }\n\n private async scrollToPosition(position: number) {\n this.ScrollContainer.scrollTop = position;\n }\n\n private async scrollToIndex(index: number) {\n const itemOfIndex = this.storeItems()[index];\n if (!itemOfIndex) {\n return;\n }\n this.ScrollContainer.scrollTop = itemOfIndex.start;\n }\n\n private async reCalculatorViewPort() {\n const items = this.storeItems();\n const fn = this.functionGetHeightItem();\n // Đo song song để rút ngắn thời gian thay vì await tuần tự.\n const heights = await Promise.all(items.map((it) => fn(it.ref)));\n\n let height = 0;\n for (let i = 0; i < items.length; i++) {\n const item = items[i];\n const itemHeight = heights[i];\n item.itemHeight = itemHeight;\n item.start = height;\n height += itemHeight;\n item.end = height;\n }\n\n this.storeItems.set([...items]);\n this.divVirtual.style.height = `${height}px`;\n this.handlerScroll();\n }\n}\n","// CONVENTION-EXCEPT: any — Generic data structures from consumers\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nexport interface IScrollMeasureStoreItemVerticalConvert {\n ref: any;\n itemHeight: number;\n start: number; // Tọa độ Y (px) bắt đầu của item\n end: number; // Tọa độ Y (px) kết thúc của item\n}\n\nexport interface IScrollMeasureItemDirectionVerticalFunctionsControl {\n scrollInto: (item: any) => Promise<void>;\n scrollToPosition: (position: number) => Promise<void>;\n scrollToIndex: (index: number) => Promise<void>;\n reCalculatorViewPort: () => Promise<void>;\n getViewPortItems: () => Array<any>;\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|