vue-virtual-flow 0.0.4 → 0.0.5

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.vi.md CHANGED
@@ -22,6 +22,7 @@ vẫn phản hồi nhanh mà component không áp đặt giao diện của từn
22
22
  - [ChatVirtualScroll](#chatvirtualscroll)
23
23
  - [ShortMediaFeed](#shortmediafeed)
24
24
  - [VirtualCarousel](#virtualcarousel)
25
+ - [SkeletonLoader](#skeletonloader)
25
26
  - [Cách dùng chung](#cách-dùng-chung)
26
27
  - [Chiều cao viewport](#chiều-cao-viewport)
27
28
  - [Key ổn định cho item](#key-ổn-định-cho-item)
@@ -44,10 +45,19 @@ Import stylesheet của thư viện một lần tại entry point của ứng d
44
45
  import 'vue-virtual-flow/style.css'
45
46
  ```
46
47
 
47
- Package yêu cầu Vue `^3.4.0` và Node.js `>=18`.
48
+ | Dependency | Yêu cầu |
49
+ | --- | --- |
50
+ | Vue | `^3.4.0` |
51
+ | Node.js | `>=18` |
48
52
 
49
53
  ## Bắt đầu nhanh
50
54
 
55
+ | Cách đăng ký | Cách dùng |
56
+ | --- | --- |
57
+ | Import cục bộ | `import { VirtualList, SkeletonLoader } from 'vue-virtual-flow'` |
58
+ | Plugin global | `createApp(App).use(VueVirtualScroll)` |
59
+ | Style dùng chung | `import 'vue-virtual-flow/style.css'` |
60
+
51
61
  Import component cục bộ:
52
62
 
53
63
  ```vue
@@ -85,7 +95,7 @@ createApp(App).use(VueVirtualScroll).mount('#app')
85
95
 
86
96
  Plugin đăng ký `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`,
87
97
  `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll`, `ChatVirtualScroll`,
88
- `ShortMediaFeed` và `VirtualCarousel`.
98
+ `ShortMediaFeed`, `VirtualCarousel` và `SkeletonLoader`.
89
99
 
90
100
  ## Chọn component
91
101
 
@@ -99,9 +109,23 @@ Plugin đăng ký `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`,
99
109
  | `ChatVirtualScroll` | Tin nhắn mới được thêm ở cuối và lịch sử cũ được thêm ở đầu. | Container của component | Động, tự đo |
100
110
  | `ShortMediaFeed` | Mỗi lần cần snap một item đầy viewport. | Container của component | Một viewport cho mỗi item |
101
111
  | `VirtualCarousel` | Cần hiển thị và snap nhiều slide theo chiều ngang. | Container của component | Tính từ chiều rộng container |
112
+ | `SkeletonLoader` | Cần hiển thị khối giữ chỗ khi đang tải nội dung. | Không có | Tùy chỉnh chiều rộng và chiều cao |
102
113
 
103
114
  ## Hướng dẫn từng component
104
115
 
116
+ Event được lắng nghe bằng cú pháp template của Vue; các method được gọi qua
117
+ ref của component. Các bảng bên dưới dùng tên ref là `list`, `chat`, `feed`
118
+ hoặc `carousel`.
119
+
120
+ | Thao tác | Cách dùng |
121
+ | --- | --- |
122
+ | Khai báo ref có type | `const list = ref<VirtualScrollExpose>()` |
123
+ | Gắn ref vào component | `<VirtualScroll ref="list" ... />` |
124
+ | Gọi sau khi mount trong script | `list.value?.scrollToIndex(10)` |
125
+ | Gọi từ event trong template | `@click="list?.scrollToIndex(10)"` |
126
+ | Chọn cách cuộn | Truyền `{ behavior: 'smooth' }` cho `scrollTo` / `scrollToIndex`, hoặc `'smooth'` cho `scrollToTop`, `scrollToBottom`, `next`, `previous`. |
127
+ | Căn vị trí item trong danh sách | `scrollToIndex(index, { align: 'start' })` / `'center'` / `'end'` |
128
+
105
129
  ### VirtualList và DynamicVirtualScroll
106
130
 
107
131
  `VirtualList` là alias của `DynamicVirtualScroll`. Hai tên này render cùng một
@@ -199,19 +223,19 @@ và thao tác nhảy tới item chưa được đo chính xác hơn.
199
223
 
200
224
  #### Events
201
225
 
202
- | Event | Payload | Thời điểm emit |
203
- | --- | --- | --- |
204
- | `scroll` | `{ scrollTop, startIndex, endIndex }` | Khi container cuộn. `endIndex` là mốc không bao gồm item tại index đó. |
205
- | `load-more` | không có | Gần cuối danh sách khi `hasMore` là true và `loading` là false. |
206
- | `refresh` | không có | Khi kéo qua ngưỡng refresh rồi thả tay. |
226
+ | Event | Payload | Thời điểm emit | Cách dùng |
227
+ | --- | --- | --- | --- |
228
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | Khi container cuộn. `endIndex` là mốc không bao gồm item tại index đó. | `@scroll="onScroll"` |
229
+ | `load-more` | không có | Gần cuối danh sách khi `hasMore` là true và `loading` là false. | `@load-more="loadMore"` |
230
+ | `refresh` | không có | Khi kéo qua ngưỡng refresh rồi thả tay. | `@refresh="refreshFirstPage"` |
207
231
 
208
232
  #### API qua ref
209
233
 
210
- | Method | Mô tả |
211
- | --- | --- |
212
- | `scrollTo(position, options?)` | Cuộn tới offset pixel bên trong danh sách. |
213
- | `scrollToIndex(index, { align?, behavior? })` | Cuộn tới index bắt đầu từ 0. `align` nhận `start`, `center` hoặc `end`. |
214
- | `scrollToTop(behavior?)` | Cuộn về đầu danh sách. |
234
+ | Method / trạng thái | Mô tả | Ví dụ trong template |
235
+ | --- | --- | --- |
236
+ | `scrollTo(position, options?)` | Cuộn tới offset pixel bên trong danh sách. | `list?.scrollTo(200, { behavior: 'smooth' })` |
237
+ | `scrollToIndex(index, options?)` | Cuộn tới index bắt đầu từ 0. `align` nhận `start`, `center` hoặc `end`. | `list?.scrollToIndex(10, { behavior: 'smooth' })` |
238
+ | `scrollToTop(behavior?)` | Cuộn về đầu danh sách. | `list?.scrollToTop()` |
215
239
 
216
240
  Smooth scroll ở khoảng cách xa tự chuyển thành nhảy tức thời vì các hàng chưa
217
241
  đo có thể làm thay đổi offset đích trong lúc cuộn.
@@ -285,13 +309,30 @@ thay đổi kích thước, hãy dùng `VirtualList`.
285
309
  | `refreshing` | `boolean` | `false` | Giữ indicator mở trong lúc refresh bất đồng bộ. |
286
310
  | `pullRefreshThreshold` | `number` | `64` | Khoảng kéo cần thiết để emit `refresh`, tính bằng pixel. |
287
311
 
288
- #### Slots, events và API qua ref
312
+ #### Slots
289
313
 
290
- - Slots: `default({ item, index })`, `empty`, `loading` và
291
- `refresh({ pullDistance, progress, refreshing })`.
292
- - Events: `scroll({ scrollTop, startIndex, endIndex })`, `load-more` và
293
- `refresh`.
294
- - Methods: `scrollTo`, `scrollToIndex` và `scrollToTop`.
314
+ | Slot | Scope | Mô tả |
315
+ | --- | --- | --- |
316
+ | `default` | `{ item, index }` | Render từng hàng đang được mount. |
317
+ | `empty` | không có | Render khi `items` rỗng và `loading` là false. |
318
+ | `loading` | không có | Hàng loading; mặc định là “Loading more…”. |
319
+ | `refresh` | `{ pullDistance, progress, refreshing }` | Indicator kéo để làm mới. `progress` được giới hạn từ `0` tới `1`. |
320
+
321
+ #### Events
322
+
323
+ | Event | Payload | Thời điểm emit | Cách dùng |
324
+ | --- | --- | --- | --- |
325
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | Khi container cuộn. `endIndex` là mốc không bao gồm item tại index đó. | `@scroll="onScroll"` |
326
+ | `load-more` | không có | Gần cuối danh sách khi `hasMore` là true và `loading` là false. | `@load-more="loadMore"` |
327
+ | `refresh` | không có | Khi kéo qua ngưỡng refresh rồi thả tay. | `@refresh="refreshFirstPage"` |
328
+
329
+ #### API qua ref
330
+
331
+ | Method / trạng thái | Mô tả | Ví dụ trong template |
332
+ | --- | --- | --- |
333
+ | `scrollTo(position, options?)` | Cuộn tới offset pixel bên trong danh sách. | `list?.scrollTo(200, { behavior: 'smooth' })` |
334
+ | `scrollToIndex(index, options?)` | Cuộn tới item có index bắt đầu từ 0. `align` nhận `start`, `center` hoặc `end`. | `list?.scrollToIndex(10, { behavior: 'smooth' })` |
335
+ | `scrollToTop(behavior?)` | Cuộn về đầu danh sách. | `list?.scrollToTop()` |
295
336
 
296
337
  ### WindowDynamicVirtualScroll
297
338
 
@@ -355,14 +396,30 @@ const list = ref<VirtualScrollExpose>()
355
396
  | `refreshing` | `boolean` | `false` | Giữ indicator mở trong lúc refresh bất đồng bộ. |
356
397
  | `pullRefreshThreshold` | `number` | `64` | Khoảng kéo cần thiết để emit `refresh`, tính bằng pixel. |
357
398
 
358
- #### Slots, events và API qua ref
399
+ #### Slots
359
400
 
360
- - Slots: `default({ item, index })`, `empty`, `loading` và
361
- `refresh({ pullDistance, progress, refreshing })`.
362
- - Events: `scroll({ scrollTop, startIndex, endIndex })`, `load-more` và
363
- `refresh`. `scrollTop` được tính tương đối từ đầu component.
364
- - Methods: `scrollTo`, `scrollToIndex` và `scrollToTop`. Các method này cuộn
365
- cửa sổ trình duyệt; `scrollTo` nhận offset tương đối so với đầu danh sách.
401
+ | Slot | Scope | Mô tả |
402
+ | --- | --- | --- |
403
+ | `default` | `{ item, index }` | Render từng hàng đang được mount. |
404
+ | `empty` | không có | Render khi `items` rỗng và `loading` là false. |
405
+ | `loading` | không có | Hàng loading; mặc định là “Loading more…”. |
406
+ | `refresh` | `{ pullDistance, progress, refreshing }` | Indicator kéo để làm mới. `progress` được giới hạn từ `0` tới `1`. |
407
+
408
+ #### Events
409
+
410
+ | Event | Payload | Thời điểm emit | Cách dùng |
411
+ | --- | --- | --- | --- |
412
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | Khi cửa sổ cuộn. `scrollTop` tính tương đối từ đầu component. `endIndex` là mốc không bao gồm item tại index đó. | `@scroll="onScroll"` |
413
+ | `load-more` | không có | Gần cuối danh sách khi `hasMore` là true và `loading` là false. | `@load-more="loadMore"` |
414
+ | `refresh` | không có | Khi kéo qua ngưỡng refresh rồi thả tay. | `@refresh="refreshFirstPage"` |
415
+
416
+ #### API qua ref
417
+
418
+ | Method / trạng thái | Mô tả | Ví dụ trong template |
419
+ | --- | --- | --- |
420
+ | `scrollTo(position, options?)` | Cuộn cửa sổ tới offset pixel tương đối so với đầu danh sách. | `list?.scrollTo(200, { behavior: 'smooth' })` |
421
+ | `scrollToIndex(index, options?)` | Cuộn cửa sổ tới item có index bắt đầu từ 0. `align` nhận `start`, `center` hoặc `end`. | `list?.scrollToIndex(10, { behavior: 'smooth' })` |
422
+ | `scrollToTop(behavior?)` | Cuộn cửa sổ về đầu component. | `list?.scrollToTop()` |
366
423
 
367
424
  ### WindowGirdVirtualScroll
368
425
 
@@ -370,6 +427,8 @@ Dùng `WindowGirdVirtualScroll` cho grid nhiều cột có card với chiều ca
370
427
  đã biết và dùng cửa sổ trình duyệt làm viewport. Component virtualize theo cả
371
428
  hàng để các cột đang hiển thị luôn được mount cùng nhau.
372
429
 
430
+ #### Ví dụ
431
+
373
432
  ```vue
374
433
  <script setup lang="ts">
375
434
  import { WindowGirdVirtualScroll } from 'vue-virtual-flow'
@@ -399,11 +458,49 @@ const products = Array.from({ length: 10_000 }, (_, id) => ({
399
458
  </template>
400
459
  ```
401
460
 
402
- `itemSize` là chiều cao của một hàng grid; nội dung vượt quá kích thước này sẽ
403
- bị ẩn. `columns` mặc định là `2`, `gap` mặc định là `0`, còn `overscan` mặc
404
- định render thêm một hàng trước và sau vùng nhìn thấy. Các prop list còn lại,
405
- event, named slot và method qua ref giống `WindowDynamicVirtualScroll`. Default
406
- slot nhận thêm `rowIndex` và `columnIndex`.
461
+ #### Props
462
+
463
+ | Prop | Kiểu | Mặc định | Mô tả |
464
+ | --- | --- | --- | --- |
465
+ | `items` | `readonly T[]` | bắt buộc | Dữ liệu truyền vào default slot. |
466
+ | `itemSize` | `number` | bắt buộc | Chiều cao cố định của mỗi hàng grid, tính bằng pixel. Nội dung vượt quá bị ẩn. |
467
+ | `columns` | `number` | `2` | Số cột trên mỗi hàng. |
468
+ | `gap` | `number` | `0` | Khoảng cách giữa các hàng và cột, tính bằng pixel. |
469
+ | `overscan` | `number` | `1` | Số hàng mount thêm trước và sau vùng đang hiển thị. |
470
+ | `itemKey` | `keyof T \| (item, index) => PropertyKey` | `id`, `key` hoặc index | Định danh ổn định cho mỗi item. |
471
+ | `ariaLabel` | `string` | `"Window virtual grid"` | Nhãn hỗ trợ khả năng tiếp cận cho grid. |
472
+ | `hasMore` | `boolean` | `false` | Cho biết vẫn còn trang dữ liệu tiếp theo. |
473
+ | `loading` | `boolean` | `false` | Ngăn request trùng và hiển thị hàng loading. |
474
+ | `loadingItemSize` | `number` | `itemSize` | Chiều cao dành cho hàng loading, tính bằng pixel. |
475
+ | `loadMoreThreshold` | `number` | `200` | Khoảng cách từ đáy cửa sổ tới cuối grid để emit `load-more`, tính bằng pixel. |
476
+ | `pullToRefresh` | `boolean` | `false` | Bật refresh khi trang đang ở `window.scrollY = 0`. |
477
+ | `refreshing` | `boolean` | `false` | Giữ indicator mở trong lúc refresh bất đồng bộ. |
478
+ | `pullRefreshThreshold` | `number` | `64` | Khoảng kéo cần thiết để emit `refresh`, tính bằng pixel. |
479
+
480
+ #### Slots
481
+
482
+ | Slot | Scope | Mô tả |
483
+ | --- | --- | --- |
484
+ | `default` | `{ item, index, rowIndex, columnIndex }` | Render từng ô đang được mount. |
485
+ | `empty` | không có | Render khi `items` rỗng và `loading` là false. |
486
+ | `loading` | không có | Hàng loading; mặc định là “Loading more…”. |
487
+ | `refresh` | `{ pullDistance, progress, refreshing }` | Indicator kéo để làm mới. `progress` được giới hạn từ `0` tới `1`. |
488
+
489
+ #### Events
490
+
491
+ | Event | Payload | Thời điểm emit | Cách dùng |
492
+ | --- | --- | --- | --- |
493
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | Khi cửa sổ cuộn. `scrollTop` tính tương đối từ đầu component. `endIndex` là mốc không bao gồm item tại index đó. | `@scroll="onScroll"` |
494
+ | `load-more` | không có | Gần cuối danh sách khi `hasMore` là true và `loading` là false. | `@load-more="loadMore"` |
495
+ | `refresh` | không có | Khi kéo qua ngưỡng refresh rồi thả tay. | `@refresh="refreshFirstPage"` |
496
+
497
+ #### API qua ref
498
+
499
+ | Method / trạng thái | Mô tả | Ví dụ trong template |
500
+ | --- | --- | --- |
501
+ | `scrollTo(position, options?)` | Cuộn cửa sổ tới offset pixel tương đối so với đầu danh sách. | `list?.scrollTo(200, { behavior: 'smooth' })` |
502
+ | `scrollToIndex(index, options?)` | Cuộn cửa sổ tới hàng chứa item có index bắt đầu từ 0. `align` nhận `start`, `center` hoặc `end`. | `list?.scrollToIndex(10, { behavior: 'smooth' })` |
503
+ | `scrollToTop(behavior?)` | Cuộn cửa sổ về đầu component. | `list?.scrollToTop()` |
407
504
 
408
505
  ### ChatVirtualScroll
409
506
 
@@ -493,21 +590,21 @@ chiều cao được thêm phía trên viewport và khôi phục vị trí của
493
590
 
494
591
  #### Events
495
592
 
496
- | Event | Payload | Thời điểm emit |
497
- | --- | --- | --- |
498
- | `scroll` | `{ scrollTop, startIndex, endIndex }` | Khi container cuộn. |
499
- | `load-older` | không có | Gần đầu khi `hasOlder` là true và `loadingOlder` là false. |
500
- | `bottom-change` | `boolean` | Khi viewport đi vào hoặc rời ngưỡng ở cuối. |
593
+ | Event | Payload | Thời điểm emit | Cách dùng |
594
+ | --- | --- | --- | --- |
595
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | Khi container cuộn. | `@scroll="onScroll"` |
596
+ | `load-older` | không có | Gần đầu khi `hasOlder` là true và `loadingOlder` là false. | `@load-older="loadOlder"` |
597
+ | `bottom-change` | `boolean` | Khi viewport đi vào hoặc rời ngưỡng ở cuối. | `@bottom-change="onBottomChange"` |
501
598
 
502
599
  #### API qua ref
503
600
 
504
- | Member | Mô tả |
505
- | --- | --- |
506
- | `isAtBottom` | Boolean readonly cho biết người dùng có đang gần cuối hay không. |
507
- | `scrollTo(position, options?)` | Cuộn tới offset pixel. |
508
- | `scrollToIndex(index, { align?, behavior? })` | Cuộn tới index tin nhắn bắt đầu từ 0. |
509
- | `scrollToTop(behavior?)` | Cuộn tới tin nhắn cũ nhất đã tải. |
510
- | `scrollToBottom(behavior?)` | Cuộn tới tin nhắn mới nhất đã tải. |
601
+ | Method / trạng thái | Mô tả | Ví dụ trong template |
602
+ | --- | --- | --- |
603
+ | `isAtBottom` | Boolean readonly cho biết người dùng có đang gần cuối hay không. | `chat?.isAtBottom` |
604
+ | `scrollTo(position, options?)` | Cuộn tới offset pixel. | `chat?.scrollTo(200, { behavior: 'smooth' })` |
605
+ | `scrollToIndex(index, options?)` | Cuộn tới index tin nhắn bắt đầu từ 0. | `chat?.scrollToIndex(10, { behavior: 'smooth' })` |
606
+ | `scrollToTop(behavior?)` | Cuộn tới tin nhắn cũ nhất đã tải. | `chat?.scrollToTop()` |
607
+ | `scrollToBottom(behavior?)` | Cuộn tới tin nhắn mới nhất đã tải. | `chat?.scrollToBottom('smooth')` |
511
608
 
512
609
  ### ShortMediaFeed
513
610
 
@@ -575,22 +672,39 @@ vậy hãy lưu playback state cần duy trì ở component cha.
575
672
  | `loading` | `boolean` | `false` | Ngăn request `load-more` bị lặp. |
576
673
  | `loadMoreThreshold` | `number` | `2` | Số item còn lại để kích hoạt `load-more`. |
577
674
 
578
- #### Slots và events
675
+ #### Slots
676
+
677
+ | Slot | Scope | Mô tả |
678
+ | --- | --- | --- |
679
+ | `default` | `{ item, index, active }` | Render item đang mount; `active` đánh dấu item đang snap. |
680
+ | `empty` | không có | Render khi `items` rỗng. |
681
+
682
+ #### Events
579
683
 
580
- | Loại | Tên | Payload/scope | Mô tả |
684
+ | Event | Payload | Thời điểm emit | Cách dùng |
581
685
  | --- | --- | --- | --- |
582
- | Slot | `default` | `{ item, index, active }` | Render item đang mount; `active` đánh dấu item đang snap. |
583
- | Slot | `empty` | không có | Render khi `items` rỗng. |
584
- | Event | `update:activeIndex` | `number` | Hỗ trợ `v-model:active-index`. |
585
- | Event | `change` | `{ index, item }` | Khi item active thay đổi. |
586
- | Event | `reach-start` | không có | Khi điều hướng chuyển tới item đầu. |
587
- | Event | `reach-end` | không có | Khi điều hướng chuyển tới item cuối. |
588
- | Event | `load-more` | không có | Khi số item còn lại đạt `loadMoreThreshold`. |
686
+ | `update:activeIndex` | `number` | Hỗ trợ `v-model:active-index`. | `v-model:active-index="activeIndex"` |
687
+ | `change` | `{ index, item }` | Khi item active thay đổi. | `@change="onChange"` |
688
+ | `reach-start` | không có | Khi điều hướng chuyển tới item đầu. | `@reach-start="onReachStart"` |
689
+ | `reach-end` | không có | Khi điều hướng chuyển tới item cuối. | `@reach-end="onReachEnd"` |
690
+ | `load-more` | không có | Khi số item còn lại đạt `loadMoreThreshold`. | `@load-more="loadMore"` |
589
691
 
590
- #### API qua ref và bàn phím
692
+ #### API qua ref
693
+
694
+ | Method / trạng thái | Mô tả | Ví dụ trong template |
695
+ | --- | --- | --- |
696
+ | `scrollToIndex(index, options?)` | Cuộn tới item có index bắt đầu từ 0, nhận `ScrollToOptions` chuẩn. | `feed?.scrollToIndex(10, { behavior: 'smooth' })` |
591
697
 
592
- `scrollToIndex(index, options?)` cuộn tới item bằng `ScrollToOptions` tiêu
593
- chuẩn. Viewport hỗ trợ Arrow Up/Down, Page Up/Down, Home và End.
698
+ #### Điều khiển bàn phím
699
+
700
+ Các phím sau hoạt động khi viewport có focus:
701
+
702
+ | Phím | Thao tác |
703
+ | --- | --- |
704
+ | `ArrowDown`, `PageDown` | Chuyển tới item tiếp theo. |
705
+ | `ArrowUp`, `PageUp` | Chuyển tới item trước. |
706
+ | `Home` | Chuyển tới vị trí đầu. |
707
+ | `End` | Chuyển tới item cuối. |
594
708
 
595
709
  Với feed không giới hạn, hãy tải theo trang bằng `load-more` và chỉ giữ một cửa
596
710
  sổ dữ liệu hợp lý ở component cha. Virtualization giới hạn DOM và media element
@@ -673,31 +787,74 @@ thường tối đa năm slide được mount ở giữa collection.
673
787
  | `autoplayDelay` | `number` | `3000` | Thời gian chờ giữa các lần tự động chuyển, tính bằng mili giây; giá trị dưới `1` được giới hạn và giá trị không hữu hạn dùng mặc định. |
674
788
  | `autoplayLoop` | `boolean` | `true` | Quay lại slide đầu tiên khi đến cuối. |
675
789
  | `pauseOnHover` | `boolean` | `true` | Tạm dừng autoplay khi con trỏ nằm trên carousel. |
790
+ | `responsive` | `ResponsiveBreakpoint[]` | `undefined` | Ghi đè số slide hiển thị, bước cuộn và khoảng cách theo breakpoint chiều rộng container. |
676
791
 
677
- #### Slots và events
792
+ #### Cấu hình responsive
678
793
 
679
- | Loại | Tên | Payload/scope | Mô tả |
794
+ Component chọn `breakpoint` lớn nhất không vượt quá chiều rộng container của
795
+ carousel. Nếu không có breakpoint phù hợp, component dùng các prop gốc.
796
+ Trường bị bỏ qua dùng prop gốc, không kế thừa từ breakpoint khác.
797
+
798
+ | Trường | Kiểu | Mặc định | Mô tả |
680
799
  | --- | --- | --- | --- |
681
- | Slot | `default` | `{ item, index, active, visible }` | Render từng slide đang mount. |
682
- | Slot | `empty` | không có | Render khi `items` rỗng. |
683
- | Event | `update:activeIndex` | `number` | Hỗ trợ `v-model:active-index`. |
684
- | Event | `change` | `{ index, item }` | Khi slide đầu tiên trong vùng nhìn thay đổi. |
685
- | Event | `reach-start` | không có | Khi điều hướng chuyển tới vị trí đầu. |
686
- | Event | `reach-end` | không có | Khi điều hướng chuyển tới vị trí đầu-slide hợp lệ cuối cùng. |
687
- | Event | `load-more` | không có | Khi số slide còn lại đạt `loadMoreThreshold`. |
800
+ | `breakpoint` | `number` | bắt buộc | Chiều rộng tối thiểu của container, tính bằng pixel. |
801
+ | `slidesPerView` | `number` | `slidesPerView` | Số slide hiển thị tại breakpoint này. |
802
+ | `slidesToScroll` | `number` | `slidesToScroll` | Số slide di chuyển bởi `next()` và `previous()`. |
803
+ | `gap` | `number` | `gap` | Khoảng cách giữa các slide, tính bằng pixel. |
804
+
805
+ ```vue
806
+ <VirtualCarousel
807
+ :items="products"
808
+ :slides-per-view="1"
809
+ :responsive="[
810
+ { breakpoint: 640, slidesPerView: 2, gap: 12 },
811
+ { breakpoint: 1024, slidesPerView: 3, slidesToScroll: 2, gap: 16 },
812
+ ]"
813
+ >
814
+ <template #default="{ item }"><ProductCard :product="item" /></template>
815
+ </VirtualCarousel>
816
+ ```
817
+
818
+ #### Slots
819
+
820
+ | Slot | Scope | Mô tả |
821
+ | --- | --- | --- |
822
+ | `default` | `{ item, index, active, visible }` | Render từng slide đang mount. |
823
+ | `empty` | không có | Render khi `items` rỗng. |
824
+
825
+ #### Events
826
+
827
+ | Event | Payload | Thời điểm emit | Cách dùng |
828
+ | --- | --- | --- | --- |
829
+ | `update:activeIndex` | `number` | Hỗ trợ `v-model:active-index`. | `v-model:active-index="activeIndex"` |
830
+ | `change` | `{ index, item }` | Khi slide đầu tiên trong vùng nhìn thay đổi. | `@change="onChange"` |
831
+ | `reach-start` | không có | Khi điều hướng chuyển tới vị trí đầu. | `@reach-start="onReachStart"` |
832
+ | `reach-end` | không có | Khi điều hướng chuyển tới vị trí đầu-slide hợp lệ cuối cùng. | `@reach-end="onReachEnd"` |
833
+ | `load-more` | không có | Khi số slide còn lại đạt `loadMoreThreshold`. | `@load-more="loadMore"` |
834
+
835
+ #### API qua ref
836
+
837
+ | Method / trạng thái | Mô tả | Ví dụ trong template |
838
+ | --- | --- | --- |
839
+ | `next(behavior?)` | Tiến thêm `slidesToScroll` slide; mặc định cuộn mượt. | `carousel?.next()` |
840
+ | `previous(behavior?)` | Lùi lại `slidesToScroll` slide. | `carousel?.previous()` |
841
+ | `scrollToIndex(index, options?)` | Đưa index thành slide đầu tiên trong vùng nhìn. | `carousel?.scrollToIndex(10, { behavior: 'smooth' })` |
842
+ | `startAutoplay()` | Bắt đầu hoặc khởi động lại timer khi `autoplay` là true và có item. | `carousel?.startAutoplay()` |
843
+ | `stopAutoplay()` | Xóa timer hiện tại; đặt `autoplay=false` nếu muốn giữ chế độ tự động ở trạng thái tắt. | `carousel?.stopAutoplay()` |
688
844
 
689
- #### API qua ref và bàn phím
845
+ #### Điều khiển bàn phím
690
846
 
691
- | Method | Mô tả |
847
+ Các phím sau hoạt động khi viewport có focus:
848
+
849
+ | Phím | Thao tác |
692
850
  | --- | --- |
693
- | `next(behavior?)` | Tiến thêm `slidesToScroll` slide; mặc định cuộn mượt. |
694
- | `previous(behavior?)` | Lùi lại `slidesToScroll` slide. |
695
- | `scrollToIndex(index, options?)` | Đưa index thành slide đầu tiên trong vùng nhìn. |
696
- | `startAutoplay()` | Bắt đầu hoặc khởi động lại quá trình tự động chuyển slide. |
697
- | `stopAutoplay()` | Dừng quá trình tự động chuyển slide. |
851
+ | `ArrowRight`, `PageDown` | Tiến thêm `slidesToScroll` slide, tương đương `next()`. |
852
+ | `ArrowLeft`, `PageUp` | Lùi lại `slidesToScroll` slide, tương đương `previous()`. |
853
+ | `Home` | Chuyển tới vị trí đầu. |
854
+ | `End` | Chuyển tới vị trí đầu-slide hợp lệ cuối cùng. |
698
855
 
699
- Viewport hỗ trợ Arrow Left/Right, Page Up/Down, Home và End. Smooth scroll ở
700
- khoảng cách xa tự chuyển thành tức thời để không lộ khoảng trống virtualized.
856
+ Smooth scroll ở khoảng cách xa tự chuyển thành tức thời để không lộ khoảng
857
+ trống virtualized.
701
858
 
702
859
  #### Ví dụ autoplay
703
860
 
@@ -730,18 +887,134 @@ const products = ref(loadProducts())
730
887
  </template>
731
888
  ```
732
889
 
733
- Autoplay cũng tự bắt đầu khi collection `items` ban đầu rỗng nhận được dữ liệu.
734
- `stopAutoplay()` tạm dừng timer và `startAutoplay()` tiếp tục chạy khi prop
735
- `autoplay` vẫn được bật. Các thay đổi của `autoplayDelay`, `autoplayLoop` và
736
- `pauseOnHover` có hiệu lực ngay trong lúc component đang chạy.
890
+ | Thao tác / điều kiện | Kết quả |
891
+ | --- | --- |
892
+ | `autoplay=true` | Tự chạy, kể cả khi collection `items` ban đầu rỗng nhận được dữ liệu. |
893
+ | `stopAutoplay()` | Xóa timer hiện tại. Điều hướng hoặc sự kiện con trỏ sau đó có thể khởi động lại nếu `autoplay` vẫn là true. |
894
+ | `startAutoplay()` | Bắt đầu hoặc khởi động lại timer khi `autoplay` là true và có item. |
895
+ | `autoplay=false` | Tắt tự động chuyển slide cho tới khi prop được bật lại. |
896
+ | `autoplayLoop` | Khi tới cuối, true quay về đầu; false dừng timer. |
897
+ | `pauseOnHover` | Khi là true, rê chuột vào sẽ tạm dừng; rời chuột sẽ chạy tiếp nếu autoplay đang bật. |
898
+ | Đổi cấu hình autoplay | `autoplayDelay`, `autoplayLoop` và `pauseOnHover` có hiệu lực ngay khi component đang chạy. |
899
+
900
+ ### SkeletonLoader
901
+
902
+ `SkeletonLoader` hiển thị khối giữ chỗ với hiệu ứng shimmer. Có thể dùng độc lập
903
+ hoặc trong slot `loading` của danh sách. Component luôn hiển thị khi được mount;
904
+ dùng `v-if` để điều khiển khi dùng độc lập, hoặc để prop `loading` của danh sách
905
+ điều khiển slot.
906
+
907
+ #### Ví dụ
908
+
909
+ Import stylesheet của thư viện một lần như bên dưới hoặc tại entry point của
910
+ ứng dụng. Đặt `loading` thành false khi request dữ liệu hoàn tất.
911
+
912
+ ```vue
913
+ <script setup lang="ts">
914
+ import { ref } from 'vue'
915
+ import { SkeletonLoader } from 'vue-virtual-flow'
916
+ import 'vue-virtual-flow/style.css'
917
+
918
+ const loading = ref(true)
919
+ </script>
920
+
921
+ <template>
922
+ <SkeletonLoader
923
+ v-if="loading"
924
+ width="100%"
925
+ :height="120"
926
+ aria-label="Đang tải nội dung…"
927
+ />
928
+ <p v-else>Nội dung đã sẵn sàng.</p>
929
+ </template>
930
+ ```
931
+
932
+ #### Props
933
+
934
+ | Prop | Kiểu | Mặc định | Mô tả |
935
+ | --- | --- | --- | --- |
936
+ | `width` | `number \| string` | `'100%'` | Chiều rộng: số tính bằng pixel, chuỗi nhận độ dài CSS như `'60%'` hoặc `'12rem'`. |
937
+ | `height` | `number \| string` | `16` | Chiều cao: số tính bằng pixel, chuỗi nhận độ dài CSS. |
938
+ | `variant` | `'rectangle' \| 'rounded' \| 'circle'` | `'rounded'` | Hình dạng khối giữ chỗ. Đặt chiều rộng bằng chiều cao để tạo hình tròn. |
939
+ | `animated` | `boolean` | `true` | Bật hiệu ứng shimmer. |
940
+ | `ariaLabel` | `string` | `'Loading…'` | Nhãn cho trình đọc màn hình của khối có `role="status"`. |
941
+
942
+ ```vue
943
+ <SkeletonLoader variant="circle" :width="48" :height="48" />
944
+ <SkeletonLoader variant="rectangle" :height="180" />
945
+ <SkeletonLoader width="60%" :height="20" :animated="false" />
946
+ ```
947
+
948
+ Hiệu ứng tự dừng khi người dùng bật tùy chọn giảm chuyển động. Khi ghép nhiều
949
+ skeleton trang trí cùng một thông báo loading chung, truyền `aria-hidden="true"`
950
+ cho từng skeleton để tránh lặp nhãn với trình đọc màn hình.
951
+
952
+ #### Slots, events và API qua ref
953
+
954
+ | API | Hỗ trợ |
955
+ | --- | --- |
956
+ | Slots | Không có. Component render một khối giữ chỗ. |
957
+ | Events | Không có event tùy chỉnh. |
958
+ | Method qua ref | Không có. Điều khiển component bằng props và `v-if`. |
959
+
960
+ #### Thao tác
961
+
962
+ | Thao tác | Cách dùng |
963
+ | --- | --- |
964
+ | Hiện hoặc ẩn | `<SkeletonLoader v-if="loading" />` |
965
+ | Dùng hình tròn | `<SkeletonLoader variant="circle" :width="48" :height="48" />` |
966
+ | Tắt shimmer | `<SkeletonLoader :animated="false" />` |
967
+ | Đặt nhãn cho trình đọc màn hình | `aria-label="Đang tải…"` |
968
+ | Ẩn skeleton trang trí khỏi trình đọc màn hình | `aria-hidden="true"` |
969
+
970
+ #### Dùng trong slot loading
971
+
972
+ Import `SkeletonLoader` cùng với `VirtualList`. Với state `items` và `loading`
973
+ hiện có, thay thông báo loading bằng skeleton:
974
+
975
+ ```vue
976
+ <VirtualList :items="items" :loading="loading" :loading-item-size="64">
977
+ <template #default="{ item }"><FeedCard :item="item" /></template>
978
+ <template #loading>
979
+ <SkeletonLoader :height="40" aria-label="Đang tải trang tiếp theo…" />
980
+ </template>
981
+ </VirtualList>
982
+ ```
983
+
984
+ | Cấu hình | Cách dùng |
985
+ | --- | --- |
986
+ | Component hỗ trợ slot `loading` | `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`, `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll`. |
987
+ | `loading` trên danh sách | Đặt thành true để hiển thị slot; false để ẩn. |
988
+ | `height` trên skeleton | Đặt chiều cao khối giữ chỗ, ví dụ `40` pixel. |
989
+ | `loadingItemSize` trên danh sách | Dành đủ chỗ cho skeleton và padding dọc của hàng. Ví dụ: `40px + 1.5rem = 64px` với cỡ chữ gốc 16px. |
990
+
991
+ #### Tùy chỉnh giao diện
992
+
993
+ Đặt các biến CSS sau trên skeleton hoặc phần tử cha:
994
+
995
+ | Biến CSS | Mặc định | Mô tả |
996
+ | --- | --- | --- |
997
+ | `--skeleton-background` | `#e2e8f0` | Màu nền. |
998
+ | `--skeleton-highlight` | `rgb(255 255 255 / 60%)` | Màu vệt sáng của hiệu ứng shimmer. |
999
+ | `--skeleton-radius` | `8px` | Bán kính bo góc cho variant `rounded`. |
1000
+
1001
+ ```vue
1002
+ <SkeletonLoader
1003
+ :height="80"
1004
+ style="--skeleton-background: #334155; --skeleton-highlight: #475569; --skeleton-radius: 12px"
1005
+ />
1006
+ ```
737
1007
 
738
1008
  ## Cách dùng chung
739
1009
 
740
1010
  ### Chiều cao viewport
741
1011
 
742
- Các component dùng container nhận một số hoặc chuỗi chiều cao CSS. Giá trị số
743
- được đổi thành pixel. `height="fill"` là cách viết ngắn của
744
- `height="100%"`:
1012
+ | Giá trị / component | Cách hoạt động |
1013
+ | --- | --- |
1014
+ | `:height="400"` | Giá trị số được đổi thành pixel. |
1015
+ | `height="60vh"` | Chuỗi chiều cao CSS được giữ nguyên. |
1016
+ | `height="fill"` / `height="100%"` | Dùng chiều cao phần tử cha. Thẻ cha cần có chiều cao xác định; bản development cảnh báo nếu viewport tính thành `0px`. |
1017
+ | `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll` | Dùng cửa sổ trình duyệt và không nhận prop `height`. |
745
1018
 
746
1019
  ```vue
747
1020
  <div class="list-container">
@@ -758,15 +1031,14 @@ Các component dùng container nhận một số hoặc chuỗi chiều cao CSS.
758
1031
  }
759
1032
  ```
760
1033
 
761
- Chiều cao phần trăm chỉ hoạt động khi thẻ cha có chiều cao xác định. Bản
762
- development cảnh báo nếu viewport được tính thành `0px`.
763
- `WindowDynamicVirtualScroll` và `WindowGirdVirtualScroll` dùng cửa sổ trình
764
- duyệt nên không có prop `height`.
765
-
766
1034
  ### Key ổn định cho item
767
1035
 
768
- Với object, component tự thử `item.id` rồi tới `item.key`. Item nguyên thủy dùng
769
- index làm fallback. Hãy truyền `itemKey` nếu định danh có tên khác:
1036
+ | Cấu hình / dữ liệu | Cách chọn key |
1037
+ | --- | --- |
1038
+ | `:item-key="(item, index) => …"` | Dùng giá trị hàm trả về. |
1039
+ | `item-key="uuid"` | Dùng `item.uuid` hợp lệ; nếu không có, thử `id`, `key`, rồi index. |
1040
+ | Object không truyền `itemKey` | Thử `item.id`, sau đó `item.key`, rồi index. |
1041
+ | Giá trị nguyên thủy không có hàm key | Dùng index làm fallback. |
770
1042
 
771
1043
  ```vue
772
1044
  <VirtualList :items="users" item-key="uuid" />
@@ -782,8 +1054,11 @@ lại dữ liệu. Key cũng gắn kết quả đo chiều cao với đúng item
782
1054
 
783
1055
  ### Tải thêm dữ liệu
784
1056
 
785
- Các component dạng list dùng ngưỡng khoảng cách pixel. `ShortMediaFeed` và
786
- `VirtualCarousel` dùng ngưỡng số item còn lại.
1057
+ | Component | `loadMoreThreshold` | Event |
1058
+ | --- | --- | --- |
1059
+ | `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`, `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll` | Khoảng cách tới cuối danh sách, tính bằng pixel. | `load-more` |
1060
+ | `ShortMediaFeed` | Số item phía sau item active. | `load-more` |
1061
+ | `VirtualCarousel` | Số slide phía sau nhóm đang hiển thị. | `load-more` |
787
1062
 
788
1063
  ```vue
789
1064
  <script setup lang="ts">
@@ -815,14 +1090,20 @@ async function loadMore() {
815
1090
  </template>
816
1091
  ```
817
1092
 
818
- Event chỉ emit tối đa một lần với số lượng item hiện tại. Nối thêm item sẽ cho
819
- phép request tiếp theo. Giữ `loading` là true trong toàn bộ request và đặt
820
- `hasMore` thành false sau trang cuối.
1093
+ | Bước / điều kiện | Thao tác / kết quả |
1094
+ | --- | --- |
1095
+ | Nhận `load-more` | Chỉ bắt đầu request tiếp theo khi `loading` là false và `hasMore` là true. |
1096
+ | Request đang chạy | Giữ `loading=true` trong toàn bộ request. |
1097
+ | Nhận trang tiếp theo | Nối thêm item. Event thông thường chỉ emit một lần với số lượng item hiện tại; tăng số lượng cho phép request tiếp theo. |
1098
+ | Request hoàn tất | Đặt `loading=false`; dùng `finally` khi request có thể thất bại. |
1099
+ | Trang cuối | Đặt `hasMore=false` để ngừng request tiếp theo. |
821
1100
 
822
1101
  ### Kéo để làm mới
823
1102
 
824
- Pull-to-refresh có trên `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`,
825
- `WindowDynamicVirtualScroll` và `WindowGirdVirtualScroll`:
1103
+ | Component | Vị trí bắt đầu thao tác |
1104
+ | --- | --- |
1105
+ | `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll` | Đầu container của component. |
1106
+ | `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll` | `window.scrollY = 0` |
826
1107
 
827
1108
  ```vue
828
1109
  <VirtualList
@@ -839,21 +1120,51 @@ Pull-to-refresh có trên `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`
839
1120
  </VirtualList>
840
1121
  ```
841
1122
 
842
- Thao tác một ngón chỉ bắt đầu khi đang ở đầu danh sách. Đặt `refreshing` thành
843
- true trong lúc chạy refresh bất đồng bộ, sau đó trả về false để đóng indicator.
1123
+ | Bước / trạng thái | Thao tác / kết quả |
1124
+ | --- | --- |
1125
+ | Bật thao tác | `pull-to-refresh` |
1126
+ | Kéo xuống bằng một ngón khi đang ở đầu | Cập nhật scope của slot `refresh`: `pullDistance`, `progress` và `refreshing`. |
1127
+ | Thả sau khi đạt `pullRefreshThreshold` | Emit `refresh` không có payload. |
1128
+ | Xử lý `refresh` | Đặt `refreshing=true` trong lúc tải lại dữ liệu để giữ indicator mở. |
1129
+ | Hoàn tất làm mới | Đặt `refreshing=false` để đóng indicator. |
844
1130
 
845
1131
  ## Các type được export
846
1132
 
1133
+ | Type | Mục đích |
1134
+ | --- | --- |
1135
+ | `ChatVirtualScrollExpose` | API ref và trạng thái `isAtBottom` của chat. |
1136
+ | `ChatVirtualScrollProps<T>` | Props của component chat. |
1137
+ | `DynamicVirtualScrollProps<T>` | Props của danh sách chiều cao động. |
1138
+ | `ItemKey<T>` | Tên thuộc tính hoặc hàm xác định key của item. |
1139
+ | `ResponsiveBreakpoint` | Breakpoint container và cấu hình ghi đè của carousel. |
1140
+ | `ScrollAlignment` | `'start'`, `'center'` hoặc `'end'` khi cuộn tới index trong danh sách. |
1141
+ | `ShortMediaFeedChangeEvent<T>` | Payload event `change` của feed: `{ index, item }`. |
1142
+ | `ShortMediaFeedExpose` | API ref của feed. |
1143
+ | `ShortMediaFeedProps<T>` | Props của feed. |
1144
+ | `SkeletonLoaderProps` | Kích thước, hình dạng, hiệu ứng và nhãn hỗ trợ tiếp cận của skeleton. |
1145
+ | `VirtualCarouselChangeEvent<T>` | Payload event `change` của carousel: `{ index, item }`. |
1146
+ | `VirtualCarouselExpose` | API ref điều hướng và autoplay của carousel. |
1147
+ | `VirtualCarouselProps<T>` | Props của carousel, gồm cấu hình responsive. |
1148
+ | `VirtualListExpose` | Alias của `VirtualScrollExpose`. |
1149
+ | `VirtualListProps<T>` | Alias của `DynamicVirtualScrollProps<T>`. |
1150
+ | `VirtualScrollEvent` | Payload scroll: `{ scrollTop, startIndex, endIndex }`; `endIndex` không bao gồm item tại index đó. |
1151
+ | `VirtualScrollExpose` | API ref chung cho danh sách cố định, động và danh sách/grid cuộn theo cửa sổ. |
1152
+ | `VirtualScrollProps<T>` | Props của danh sách chiều cao cố định. |
1153
+ | `WindowDynamicVirtualScrollProps<T>` | Props của danh sách chiều cao động cuộn theo cửa sổ. |
1154
+ | `WindowGirdVirtualScrollProps<T>` | Props của grid hàng cố định cuộn theo cửa sổ. |
1155
+
847
1156
  ```ts
848
1157
  import type {
849
1158
  ChatVirtualScrollExpose,
850
1159
  ChatVirtualScrollProps,
851
1160
  DynamicVirtualScrollProps,
852
1161
  ItemKey,
1162
+ ResponsiveBreakpoint,
853
1163
  ScrollAlignment,
854
1164
  ShortMediaFeedChangeEvent,
855
1165
  ShortMediaFeedExpose,
856
1166
  ShortMediaFeedProps,
1167
+ SkeletonLoaderProps,
857
1168
  VirtualCarouselChangeEvent,
858
1169
  VirtualCarouselExpose,
859
1170
  VirtualCarouselProps,
@@ -869,6 +1180,22 @@ import type {
869
1180
 
870
1181
  ## Phát triển và phát hành
871
1182
 
1183
+ | Lệnh / lifecycle | Thao tác |
1184
+ | --- | --- |
1185
+ | `bun install` | Cài đặt dependency. |
1186
+ | `bunx playwright install chromium firefox webkit` | Cài trình duyệt cho smoke test. |
1187
+ | `bun run dev` | Chạy playground. |
1188
+ | `bun run typecheck` | Kiểm tra type TypeScript và Vue. |
1189
+ | `bun run test` | Chạy unit test. |
1190
+ | `bun run test:watch` | Chạy unit test ở chế độ theo dõi thay đổi. |
1191
+ | `bun run build` | Kiểm tra type và build package kèm khai báo type. |
1192
+ | `bun run typecheck:package` | Kiểm tra type phía dùng package ESM và CommonJS sau khi build. |
1193
+ | `bun run lint:package` | Kiểm tra exports và cách đóng gói type sau khi build. |
1194
+ | `bun run test:browser` | Chạy test trình duyệt bằng Playwright. |
1195
+ | `bun run check` | Chạy toàn bộ: unit test, build, type phía dùng package, lint package và test trình duyệt. |
1196
+ | `npm pack --dry-run` | Kiểm tra các file sẽ được publish. |
1197
+ | `prepublishOnly` | Tự chạy `bun run check` trước khi publish. |
1198
+
872
1199
  ```bash
873
1200
  bun install
874
1201
  bunx playwright install chromium firefox webkit