0
# Utilities and Advanced
1
2
Components for utility functions, time management, virtual scrolling, pagination, and other advanced features.
3
4
## Capabilities
5
6
### UseTimestamp Component
7
8
Provides reactive current timestamp with customizable update intervals.
9
10
```typescript { .api }
11
/**
12
* Component that provides reactive timestamp
13
* @example
14
* <UseTimestamp v-slot="{ timestamp }">
15
* <div>Current time: {{ new Date(timestamp).toLocaleString() }}</div>
16
* </UseTimestamp>
17
*/
18
interface UseTimestampProps {
19
/** Offset to add to timestamp (ms) @default 0 */
20
offset?: number;
21
/** Update interval in milliseconds @default 1000 */
22
interval?: number | 'requestAnimationFrame';
23
/** Start immediately @default true */
24
immediate?: boolean;
25
/** Update on focus changes @default true */
26
controls?: boolean;
27
}
28
29
/** Slot data exposed by UseTimestamp component */
30
interface UseTimestampReturn {
31
/** Current timestamp in milliseconds */
32
timestamp: Ref<number>;
33
/** Pause timestamp updates */
34
pause: () => void;
35
/** Resume timestamp updates */
36
resume: () => void;
37
}
38
```
39
40
**Usage Examples:**
41
42
```vue
43
<template>
44
<!-- Basic timestamp -->
45
<UseTimestamp v-slot="{ timestamp }">
46
<div class="timestamp-display">
47
<h3>Live Clock</h3>
48
<div class="clock">
49
<div class="time">{{ formatTime(timestamp) }}</div>
50
<div class="date">{{ formatDate(timestamp) }}</div>
51
</div>
52
</div>
53
</UseTimestamp>
54
55
<!-- High frequency timestamp -->
56
<UseTimestamp :interval="16" v-slot="{ timestamp }">
57
<div class="high-freq-demo">
58
<h3>High Frequency Timer (60 FPS)</h3>
59
<div class="milliseconds">{{ timestamp % 1000 }}ms</div>
60
<div class="animation-box" :style="{ transform: getAnimationTransform(timestamp) }">
61
🚀
62
</div>
63
</div>
64
</UseTimestamp>
65
66
<!-- Controlled timestamp -->
67
<UseTimestamp
68
:immediate="false"
69
:interval="100"
70
v-slot="{ timestamp, pause, resume }"
71
>
72
<div class="controlled-timestamp">
73
<h3>Controlled Timer</h3>
74
<div class="timer-display">{{ Math.floor((timestamp % 60000) / 1000) }}s</div>
75
<div class="timer-controls">
76
<button @click="resume">Start</button>
77
<button @click="pause">Stop</button>
78
</div>
79
</div>
80
</UseTimestamp>
81
82
<!-- Timezone display -->
83
<UseTimestamp v-slot="{ timestamp }">
84
<div class="timezone-demo">
85
<h3>World Clock</h3>
86
<div class="timezone-grid">
87
<div v-for="tz in timezones" :key="tz.zone" class="timezone-item">
88
<div class="tz-name">{{ tz.name }}</div>
89
<div class="tz-time">{{ formatTimeInTimezone(timestamp, tz.zone) }}</div>
90
</div>
91
</div>
92
</div>
93
</UseTimestamp>
94
</template>
95
96
<script setup>
97
import { ref } from 'vue';
98
import { UseTimestamp } from '@vueuse/components';
99
100
const timezones = [
101
{ name: 'New York', zone: 'America/New_York' },
102
{ name: 'London', zone: 'Europe/London' },
103
{ name: 'Tokyo', zone: 'Asia/Tokyo' },
104
{ name: 'Sydney', zone: 'Australia/Sydney' }
105
];
106
107
function formatTime(timestamp) {
108
return new Date(timestamp).toLocaleTimeString();
109
}
110
111
function formatDate(timestamp) {
112
return new Date(timestamp).toLocaleDateString();
113
}
114
115
function formatTimeInTimezone(timestamp, timezone) {
116
return new Date(timestamp).toLocaleTimeString('en-US', { timeZone: timezone });
117
}
118
119
function getAnimationTransform(timestamp) {
120
const angle = (timestamp / 10) % 360;
121
return `rotate(${angle}deg) translateX(50px)`;
122
}
123
</script>
124
125
<style>
126
.timestamp-display, .high-freq-demo, .controlled-timestamp, .timezone-demo {
127
border: 1px solid #ddd;
128
border-radius: 8px;
129
padding: 20px;
130
margin: 15px 0;
131
text-align: center;
132
}
133
134
.clock {
135
font-family: 'Courier New', monospace;
136
background: #1a1a1a;
137
color: #00ff00;
138
padding: 20px;
139
border-radius: 8px;
140
margin: 15px 0;
141
}
142
143
.time {
144
font-size: 2em;
145
font-weight: bold;
146
}
147
148
.date {
149
font-size: 1.2em;
150
margin-top: 5px;
151
opacity: 0.8;
152
}
153
154
.milliseconds {
155
font-family: 'Courier New', monospace;
156
font-size: 1.5em;
157
font-weight: bold;
158
color: #2196f3;
159
margin: 15px 0;
160
}
161
162
.animation-box {
163
display: inline-block;
164
font-size: 2em;
165
margin: 20px;
166
}
167
168
.timer-display {
169
font-size: 3em;
170
font-weight: bold;
171
color: #4caf50;
172
font-family: 'Courier New', monospace;
173
margin: 15px 0;
174
}
175
176
.timer-controls {
177
display: flex;
178
justify-content: center;
179
gap: 15px;
180
}
181
182
.timer-controls button {
183
padding: 10px 20px;
184
font-size: 16px;
185
border: none;
186
border-radius: 6px;
187
cursor: pointer;
188
background: #2196f3;
189
color: white;
190
}
191
192
.timezone-grid {
193
display: grid;
194
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
195
gap: 15px;
196
margin-top: 20px;
197
}
198
199
.timezone-item {
200
padding: 15px;
201
border: 1px solid #ddd;
202
border-radius: 6px;
203
background: #f9f9f9;
204
}
205
206
.tz-name {
207
font-weight: bold;
208
margin-bottom: 8px;
209
color: #666;
210
}
211
212
.tz-time {
213
font-family: 'Courier New', monospace;
214
font-size: 1.1em;
215
color: #333;
216
}
217
</style>
218
```
219
220
### UseTimeAgo Component
221
222
Formats timestamps as relative time strings with automatic updates.
223
224
```typescript { .api }
225
/**
226
* Component that formats dates as relative time
227
* @example
228
* <UseTimeAgo :time="date" v-slot="{ timeAgo }">
229
* <div>{{ timeAgo }}</div>
230
* </UseTimeAgo>
231
*/
232
interface UseTimeAgoProps {
233
/** The date/time to format */
234
time: MaybeRefOrGetter<Date | number | string>;
235
/** Update interval in milliseconds @default 30000 */
236
updateInterval?: number;
237
/** Maximum unit to display @default 'month' */
238
max?: TimeAgoUnit;
239
/** Full date format when max exceeded @default 'YYYY-MM-DD' */
240
fullDateFormatter?: (date: Date) => string;
241
/** Custom messages for different time units */
242
messages?: TimeAgoMessages;
243
/** Show just now instead of 0 seconds @default false */
244
showSecond?: boolean;
245
/** Rounding method @default 'round' */
246
rounding?: 'floor' | 'ceil' | 'round';
247
}
248
249
/** Slot data exposed by UseTimeAgo component */
250
interface UseTimeAgoReturn {
251
/** Formatted relative time string */
252
timeAgo: Ref<string>;
253
}
254
255
type TimeAgoUnit = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year';
256
257
interface TimeAgoMessages {
258
justNow?: string;
259
past?: string | ((n: string) => string);
260
future?: string | ((n: string) => string);
261
month?: string | string[];
262
year?: string | string[];
263
day?: string | string[];
264
week?: string | string[];
265
hour?: string | string[];
266
minute?: string | string[];
267
second?: string | string[];
268
}
269
```
270
271
**Usage Examples:**
272
273
```vue
274
<template>
275
<!-- Basic time ago -->
276
<div class="time-ago-demo">
277
<h3>Time Ago Examples</h3>
278
<div class="time-examples">
279
<div v-for="example in timeExamples" :key="example.label" class="time-example">
280
<div class="example-label">{{ example.label }}:</div>
281
<UseTimeAgo :time="example.time" v-slot="{ timeAgo }">
282
<div class="example-time">{{ timeAgo }}</div>
283
</UseTimeAgo>
284
</div>
285
</div>
286
</div>
287
288
<!-- Custom messages -->
289
<UseTimeAgo
290
:time="customTime"
291
:messages="customMessages"
292
v-slot="{ timeAgo }"
293
>
294
<div class="custom-time-ago">
295
<h3>Custom Messages</h3>
296
<p>{{ timeAgo }}</p>
297
</div>
298
</UseTimeAgo>
299
300
<!-- Activity feed -->
301
<div class="activity-feed">
302
<h3>Activity Feed</h3>
303
<div class="activities">
304
<div v-for="activity in activities" :key="activity.id" class="activity-item">
305
<div class="activity-icon">{{ activity.icon }}</div>
306
<div class="activity-content">
307
<div class="activity-text">{{ activity.text }}</div>
308
<UseTimeAgo :time="activity.timestamp" v-slot="{ timeAgo }">
309
<div class="activity-time">{{ timeAgo }}</div>
310
</UseTimeAgo>
311
</div>
312
</div>
313
</div>
314
</div>
315
316
<!-- Blog posts with different formats -->
317
<div class="blog-demo">
318
<h3>Blog Posts</h3>
319
<div class="posts">
320
<div v-for="post in blogPosts" :key="post.id" class="post-item">
321
<h4>{{ post.title }}</h4>
322
<p>{{ post.excerpt }}</p>
323
<div class="post-meta">
324
<span>By {{ post.author }}</span>
325
<UseTimeAgo
326
:time="post.publishedAt"
327
:max="'week'"
328
:full-date-formatter="formatFullDate"
329
v-slot="{ timeAgo }"
330
>
331
<span class="post-time">{{ timeAgo }}</span>
332
</UseTimeAgo>
333
</div>
334
</div>
335
</div>
336
</div>
337
</template>
338
339
<script setup>
340
import { ref, computed } from 'vue';
341
import { UseTimeAgo } from '@vueuse/components';
342
343
const now = Date.now();
344
345
const timeExamples = [
346
{ label: 'Just now', time: now - 5000 },
347
{ label: '2 minutes ago', time: now - 2 * 60 * 1000 },
348
{ label: '1 hour ago', time: now - 60 * 60 * 1000 },
349
{ label: 'Yesterday', time: now - 25 * 60 * 60 * 1000 },
350
{ label: 'Last week', time: now - 8 * 24 * 60 * 60 * 1000 },
351
{ label: 'Last month', time: now - 35 * 24 * 60 * 60 * 1000 }
352
];
353
354
const customTime = ref(now - 30 * 60 * 1000);
355
356
const customMessages = {
357
justNow: 'right now',
358
past: n => `${n} ago`,
359
future: n => `in ${n}`,
360
second: ['second', 'seconds'],
361
minute: ['minute', 'minutes'],
362
hour: ['hour', 'hours'],
363
day: ['day', 'days'],
364
week: ['week', 'weeks'],
365
month: ['month', 'months'],
366
year: ['year', 'years']
367
};
368
369
const activities = ref([
370
{
371
id: 1,
372
icon: '📝',
373
text: 'John created a new document',
374
timestamp: now - 5 * 60 * 1000
375
},
376
{
377
id: 2,
378
icon: '💬',
379
text: 'Sarah commented on your post',
380
timestamp: now - 15 * 60 * 1000
381
},
382
{
383
id: 3,
384
icon: '❤️',
385
text: 'Mike liked your photo',
386
timestamp: now - 2 * 60 * 60 * 1000
387
},
388
{
389
id: 4,
390
icon: '🔗',
391
text: 'Anna shared your article',
392
timestamp: now - 6 * 60 * 60 * 1000
393
},
394
{
395
id: 5,
396
icon: '🎉',
397
text: 'You reached 100 followers!',
398
timestamp: now - 2 * 24 * 60 * 60 * 1000
399
}
400
]);
401
402
const blogPosts = ref([
403
{
404
id: 1,
405
title: 'Getting Started with Vue 3',
406
excerpt: 'Learn the basics of Vue 3 composition API...',
407
author: 'Alex Chen',
408
publishedAt: now - 3 * 24 * 60 * 60 * 1000
409
},
410
{
411
id: 2,
412
title: 'Advanced TypeScript Tips',
413
excerpt: 'Discover advanced TypeScript patterns...',
414
author: 'Maria Garcia',
415
publishedAt: now - 10 * 24 * 60 * 60 * 1000
416
},
417
{
418
id: 3,
419
title: 'Building Scalable Applications',
420
excerpt: 'Best practices for large-scale development...',
421
author: 'David Kim',
422
publishedAt: now - 45 * 24 * 60 * 60 * 1000
423
}
424
]);
425
426
function formatFullDate(date) {
427
return date.toLocaleDateString('en-US', {
428
year: 'numeric',
429
month: 'long',
430
day: 'numeric'
431
});
432
}
433
</script>
434
435
<style>
436
.time-ago-demo, .custom-time-ago, .activity-feed, .blog-demo {
437
border: 1px solid #ddd;
438
border-radius: 8px;
439
padding: 20px;
440
margin: 15px 0;
441
}
442
443
.time-examples {
444
display: grid;
445
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
446
gap: 15px;
447
margin-top: 15px;
448
}
449
450
.time-example {
451
padding: 15px;
452
border: 1px solid #eee;
453
border-radius: 6px;
454
background: #f9f9f9;
455
}
456
457
.example-label {
458
font-weight: bold;
459
color: #666;
460
margin-bottom: 8px;
461
}
462
463
.example-time {
464
color: #2196f3;
465
font-size: 1.1em;
466
}
467
468
.custom-time-ago {
469
text-align: center;
470
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
471
color: white;
472
}
473
474
.activities {
475
display: flex;
476
flex-direction: column;
477
gap: 12px;
478
margin-top: 15px;
479
}
480
481
.activity-item {
482
display: flex;
483
align-items: flex-start;
484
gap: 12px;
485
padding: 15px;
486
border: 1px solid #eee;
487
border-radius: 8px;
488
background: #fafafa;
489
}
490
491
.activity-icon {
492
font-size: 1.5em;
493
flex-shrink: 0;
494
}
495
496
.activity-content {
497
flex: 1;
498
}
499
500
.activity-text {
501
font-weight: 500;
502
margin-bottom: 4px;
503
}
504
505
.activity-time {
506
font-size: 0.9em;
507
color: #666;
508
}
509
510
.posts {
511
display: flex;
512
flex-direction: column;
513
gap: 20px;
514
margin-top: 15px;
515
}
516
517
.post-item {
518
padding: 20px;
519
border: 1px solid #ddd;
520
border-radius: 8px;
521
background: white;
522
}
523
524
.post-item h4 {
525
margin: 0 0 10px 0;
526
color: #333;
527
}
528
529
.post-item p {
530
margin: 0 0 15px 0;
531
color: #666;
532
line-height: 1.5;
533
}
534
535
.post-meta {
536
display: flex;
537
justify-content: space-between;
538
align-items: center;
539
font-size: 0.9em;
540
color: #888;
541
border-top: 1px solid #eee;
542
padding-top: 15px;
543
}
544
545
.post-time {
546
font-weight: 500;
547
}
548
</style>
549
```
550
551
### UseVirtualList Component
552
553
Implements virtual scrolling for large datasets with optimal performance.
554
555
```typescript { .api }
556
/**
557
* Component that implements virtual scrolling for large lists
558
* @example
559
* <UseVirtualList :list="items" :options="{ itemHeight: 50 }" v-slot="{ list, containerProps, wrapperProps }">
560
* <div v-bind="containerProps">
561
* <div v-bind="wrapperProps">
562
* <div v-for="{ data, index } in list" :key="index">{{ data.name }}</div>
563
* </div>
564
* </div>
565
* </UseVirtualList>
566
*/
567
interface UseVirtualListProps<T = any> {
568
/** Array of items to virtualize */
569
list: T[];
570
/** Virtual list options */
571
options: UseVirtualListOptions;
572
/** Container height @default 300 */
573
height?: number;
574
}
575
576
/** Slot data exposed by UseVirtualList component */
577
interface UseVirtualListReturn<T = any> {
578
/** Visible list items with their data and indices */
579
list: ComputedRef<UseVirtualListItem<T>[]>;
580
/** Props to bind to the scrollable container */
581
containerProps: ComputedRef<UseVirtualListContainerProps>;
582
/** Props to bind to the wrapper element */
583
wrapperProps: ComputedRef<UseVirtualListWrapperProps>;
584
/** Scroll to specific index */
585
scrollTo: (index: number) => void;
586
}
587
588
interface UseVirtualListOptions {
589
/** Height of each item in pixels @default 50 */
590
itemHeight: number | ((index: number) => number);
591
/** Number of extra items to render outside viewport @default 5 */
592
overscan?: number;
593
}
594
595
interface UseVirtualListItem<T = any> {
596
data: T;
597
index: number;
598
}
599
600
interface UseVirtualListContainerProps {
601
ref: Ref<HTMLElement | undefined>;
602
onScroll: (e: Event) => void;
603
style: CSSProperties;
604
}
605
606
interface UseVirtualListWrapperProps {
607
style: ComputedRef<CSSProperties>;
608
}
609
```
610
611
**Usage Examples:**
612
613
```vue
614
<template>
615
<!-- Basic virtual list -->
616
<div class="virtual-list-demo">
617
<h3>Virtual List - {{ largeList.length.toLocaleString() }} Items</h3>
618
<UseVirtualList
619
:list="largeList"
620
:options="{ itemHeight: 50, overscan: 10 }"
621
:height="400"
622
v-slot="{ list, containerProps, wrapperProps, scrollTo }"
623
>
624
<div class="virtual-controls">
625
<button @click="scrollTo(0)">Go to Top</button>
626
<button @click="scrollTo(Math.floor(largeList.length / 2))">Go to Middle</button>
627
<button @click="scrollTo(largeList.length - 1)">Go to Bottom</button>
628
<input
629
type="number"
630
:max="largeList.length - 1"
631
placeholder="Jump to index"
632
@keyup.enter="scrollTo(parseInt($event.target.value))"
633
/>
634
</div>
635
<div v-bind="containerProps" class="virtual-container">
636
<div v-bind="wrapperProps" class="virtual-wrapper">
637
<div
638
v-for="{ data, index } in list"
639
:key="index"
640
class="virtual-item"
641
>
642
<div class="item-index">#{{ index }}</div>
643
<div class="item-name">{{ data.name }}</div>
644
<div class="item-value">{{ data.value }}</div>
645
</div>
646
</div>
647
</div>
648
</UseVirtualList>
649
</div>
650
651
<!-- Variable height virtual list -->
652
<div class="variable-height-demo">
653
<h3>Variable Height Virtual List</h3>
654
<UseVirtualList
655
:list="variableHeightList"
656
:options="{ itemHeight: getItemHeight, overscan: 3 }"
657
:height="350"
658
v-slot="{ list, containerProps, wrapperProps }"
659
>
660
<div v-bind="containerProps" class="variable-container">
661
<div v-bind="wrapperProps" class="variable-wrapper">
662
<div
663
v-for="{ data, index } in list"
664
:key="index"
665
class="variable-item"
666
:class="data.type"
667
>
668
<div class="item-header">
669
<span class="item-type">{{ data.type }}</span>
670
<span class="item-id">#{{ index }}</span>
671
</div>
672
<div class="item-content">{{ data.content }}</div>
673
<div v-if="data.details" class="item-details">
674
{{ data.details }}
675
</div>
676
</div>
677
</div>
678
</div>
679
</UseVirtualList>
680
</div>
681
682
<!-- Chat message virtual list -->
683
<div class="chat-demo">
684
<h3>Virtual Chat Messages</h3>
685
<UseVirtualList
686
:list="chatMessages"
687
:options="{ itemHeight: getChatMessageHeight, overscan: 5 }"
688
:height="300"
689
v-slot="{ list, containerProps, wrapperProps, scrollTo }"
690
>
691
<div class="chat-controls">
692
<button @click="addMessage">Add Message</button>
693
<button @click="scrollTo(chatMessages.length - 1)">Scroll to Latest</button>
694
</div>
695
<div v-bind="containerProps" class="chat-container">
696
<div v-bind="wrapperProps" class="chat-wrapper">
697
<div
698
v-for="{ data, index } in list"
699
:key="index"
700
class="chat-message"
701
:class="{ own: data.isOwn }"
702
>
703
<div class="message-info">
704
<span class="message-author">{{ data.author }}</span>
705
<span class="message-time">{{ formatChatTime(data.timestamp) }}</span>
706
</div>
707
<div class="message-text">{{ data.text }}</div>
708
</div>
709
</div>
710
</div>
711
</UseVirtualList>
712
</div>
713
714
<!-- Performance comparison -->
715
<div class="performance-demo">
716
<h3>Performance Comparison</h3>
717
<div class="performance-controls">
718
<button @click="showVirtual = !showVirtual">
719
{{ showVirtual ? 'Show Regular List' : 'Show Virtual List' }}
720
</button>
721
<span class="performance-info">
722
Rendering {{ showVirtual ? 'virtualized' : 'all' }} items
723
</span>
724
</div>
725
726
<div v-if="!showVirtual" class="regular-list">
727
<div
728
v-for="(item, index) in performanceList.slice(0, 1000)"
729
:key="index"
730
class="performance-item"
731
>
732
{{ item.name }} - {{ item.description }}
733
</div>
734
</div>
735
736
<UseVirtualList
737
v-else
738
:list="performanceList"
739
:options="{ itemHeight: 60 }"
740
:height="400"
741
v-slot="{ list, containerProps, wrapperProps }"
742
>
743
<div v-bind="containerProps" class="performance-container">
744
<div v-bind="wrapperProps" class="performance-wrapper">
745
<div
746
v-for="{ data, index } in list"
747
:key="index"
748
class="performance-item"
749
>
750
{{ data.name }} - {{ data.description }}
751
</div>
752
</div>
753
</div>
754
</UseVirtualList>
755
</div>
756
</template>
757
758
<script setup>
759
import { ref, computed } from 'vue';
760
import { UseVirtualList } from '@vueuse/components';
761
762
// Generate large list
763
const largeList = ref([]);
764
for (let i = 0; i < 100000; i++) {
765
largeList.value.push({
766
name: `Item ${i + 1}`,
767
value: Math.floor(Math.random() * 1000)
768
});
769
}
770
771
// Variable height list
772
const variableHeightList = ref([]);
773
const itemTypes = ['header', 'content', 'footer'];
774
for (let i = 0; i < 1000; i++) {
775
const type = itemTypes[Math.floor(Math.random() * itemTypes.length)];
776
variableHeightList.value.push({
777
type,
778
content: `This is ${type} item ${i + 1}`,
779
details: type === 'content' ? `Additional details for item ${i + 1}` : null
780
});
781
}
782
783
// Chat messages
784
const chatMessages = ref([]);
785
const authors = ['Alice', 'Bob', 'Charlie', 'Diana'];
786
for (let i = 0; i < 500; i++) {
787
const author = authors[Math.floor(Math.random() * authors.length)];
788
chatMessages.value.push({
789
author,
790
text: `Message ${i + 1}: ${generateRandomMessage()}`,
791
timestamp: Date.now() - (500 - i) * 60000,
792
isOwn: author === 'Alice'
793
});
794
}
795
796
// Performance list
797
const performanceList = ref([]);
798
for (let i = 0; i < 50000; i++) {
799
performanceList.value.push({
800
name: `Performance Item ${i + 1}`,
801
description: `This is a description for performance testing item number ${i + 1}`
802
});
803
}
804
805
const showVirtual = ref(true);
806
807
function getItemHeight(index) {
808
const item = variableHeightList.value[index];
809
if (!item) return 50;
810
811
switch (item.type) {
812
case 'header': return 80;
813
case 'content': return item.details ? 120 : 80;
814
case 'footer': return 60;
815
default: return 50;
816
}
817
}
818
819
function getChatMessageHeight(index) {
820
const message = chatMessages.value[index];
821
if (!message) return 60;
822
823
// Estimate height based on message length
824
const baseHeight = 60;
825
const extraHeight = Math.ceil(message.text.length / 50) * 20;
826
return Math.min(baseHeight + extraHeight, 150);
827
}
828
829
function addMessage() {
830
const newMessage = {
831
author: 'Alice',
832
text: `New message: ${generateRandomMessage()}`,
833
timestamp: Date.now(),
834
isOwn: true
835
};
836
chatMessages.value.push(newMessage);
837
}
838
839
function generateRandomMessage() {
840
const messages = [
841
"Hello everyone!",
842
"How are you doing today?",
843
"This is a test message.",
844
"Virtual scrolling is amazing!",
845
"Performance is great with large lists.",
846
"Check out this new feature.",
847
"What do you think about this implementation?",
848
"Looking forward to your feedback!"
849
];
850
return messages[Math.floor(Math.random() * messages.length)];
851
}
852
853
function formatChatTime(timestamp) {
854
return new Date(timestamp).toLocaleTimeString();
855
}
856
</script>
857
858
<style>
859
.virtual-list-demo, .variable-height-demo, .chat-demo, .performance-demo {
860
border: 1px solid #ddd;
861
border-radius: 8px;
862
padding: 20px;
863
margin: 15px 0;
864
}
865
866
.virtual-controls, .chat-controls, .performance-controls {
867
display: flex;
868
gap: 10px;
869
align-items: center;
870
margin-bottom: 15px;
871
flex-wrap: wrap;
872
}
873
874
.virtual-controls button, .chat-controls button, .performance-controls button {
875
padding: 6px 12px;
876
border: 1px solid #ddd;
877
border-radius: 4px;
878
cursor: pointer;
879
background: #f5f5f5;
880
}
881
882
.virtual-controls input {
883
padding: 6px 8px;
884
border: 1px solid #ddd;
885
border-radius: 4px;
886
width: 120px;
887
}
888
889
.virtual-container, .variable-container, .chat-container, .performance-container {
890
border: 2px solid #eee;
891
border-radius: 6px;
892
background: #fafafa;
893
}
894
895
.virtual-item, .variable-item, .performance-item {
896
display: flex;
897
align-items: center;
898
gap: 15px;
899
padding: 15px;
900
border-bottom: 1px solid #eee;
901
background: white;
902
}
903
904
.virtual-item:hover, .variable-item:hover, .performance-item:hover {
905
background: #f0f8ff;
906
}
907
908
.item-index {
909
font-weight: bold;
910
color: #666;
911
min-width: 60px;
912
}
913
914
.item-name {
915
font-weight: bold;
916
flex: 1;
917
}
918
919
.item-value {
920
color: #2196f3;
921
font-family: monospace;
922
}
923
924
.variable-item.header {
925
background: #e3f2fd;
926
font-weight: bold;
927
}
928
929
.variable-item.footer {
930
background: #f3e5f5;
931
font-style: italic;
932
}
933
934
.item-header {
935
display: flex;
936
justify-content: space-between;
937
width: 100%;
938
align-items: center;
939
}
940
941
.item-type {
942
text-transform: uppercase;
943
font-size: 0.8em;
944
font-weight: bold;
945
padding: 2px 8px;
946
border-radius: 4px;
947
background: #ddd;
948
}
949
950
.item-content {
951
margin-top: 8px;
952
}
953
954
.item-details {
955
margin-top: 5px;
956
font-size: 0.9em;
957
color: #666;
958
}
959
960
.chat-message {
961
padding: 12px 15px;
962
border-bottom: 1px solid #eee;
963
background: white;
964
}
965
966
.chat-message.own {
967
background: #e8f5e8;
968
}
969
970
.message-info {
971
display: flex;
972
justify-content: space-between;
973
margin-bottom: 5px;
974
font-size: 0.9em;
975
}
976
977
.message-author {
978
font-weight: bold;
979
color: #333;
980
}
981
982
.message-time {
983
color: #666;
984
}
985
986
.message-text {
987
line-height: 1.4;
988
}
989
990
.regular-list {
991
height: 400px;
992
border: 2px solid #eee;
993
border-radius: 6px;
994
overflow-y: auto;
995
background: #fafafa;
996
}
997
998
.performance-info {
999
font-size: 0.9em;
1000
color: #666;
1001
font-style: italic;
1002
}
1003
</style>
1004
```
1005
1006
### UseOffsetPagination Component
1007
1008
Manages offset-based pagination with reactive state and event emissions.
1009
1010
```typescript { .api }
1011
/**
1012
* Component that manages offset-based pagination
1013
* @example
1014
* <UseOffsetPagination
1015
* v-slot="{ currentPage, pageSize, pageCount, isFirstPage, isLastPage, prev, next }"
1016
* :total="1000"
1017
* :page-size="20"
1018
* >
1019
* <div>Page {{ currentPage }} of {{ pageCount }}</div>
1020
* </UseOffsetPagination>
1021
*/
1022
interface UseOffsetPaginationProps {
1023
/** Total number of items */
1024
total: MaybeRefOrGetter<number>;
1025
/** Number of items per page @default 10 */
1026
pageSize?: MaybeRefOrGetter<number>;
1027
/** Current page number @default 1 */
1028
page?: MaybeRefOrGetter<number>;
1029
/** Callback when page changes */
1030
onPageChange?: (returnValue: UseOffsetPaginationReturn) => unknown;
1031
/** Callback when page size changes */
1032
onPageSizeChange?: (returnValue: UseOffsetPaginationReturn) => unknown;
1033
/** Callback when page count changes */
1034
onPageCountChange?: (returnValue: UseOffsetPaginationReturn) => unknown;
1035
}
1036
1037
/** Slot data exposed by UseOffsetPagination component */
1038
interface UseOffsetPaginationReturn {
1039
/** Current page number */
1040
currentPage: Ref<number>;
1041
/** Current page size */
1042
pageSize: Ref<number>;
1043
/** Total number of pages */
1044
pageCount: ComputedRef<number>;
1045
/** Whether on first page */
1046
isFirstPage: ComputedRef<boolean>;
1047
/** Whether on last page */
1048
isLastPage: ComputedRef<boolean>;
1049
/** Go to previous page */
1050
prev: () => void;
1051
/** Go to next page */
1052
next: () => void;
1053
/** Go to specific page */
1054
goToPage: (page: number) => void;
1055
}
1056
1057
/** Component events */
1058
interface UseOffsetPaginationEmits {
1059
/** Emitted when page changes */
1060
'page-change': (returnValue: UseOffsetPaginationReturn) => void;
1061
/** Emitted when page size changes */
1062
'page-size-change': (returnValue: UseOffsetPaginationReturn) => void;
1063
/** Emitted when page count changes */
1064
'page-count-change': (returnValue: UseOffsetPaginationReturn) => void;
1065
}
1066
```
1067
1068
**Usage Examples:**
1069
1070
```vue
1071
<template>
1072
<!-- Basic pagination -->
1073
<div class="pagination-demo">
1074
<h3>Basic Pagination</h3>
1075
<UseOffsetPagination
1076
:total="500"
1077
:page-size="20"
1078
v-slot="{ currentPage, pageSize, pageCount, isFirstPage, isLastPage, prev, next, goToPage }"
1079
@page-change="handlePageChange"
1080
>
1081
<div class="data-section">
1082
<div class="data-info">
1083
Showing {{ (currentPage - 1) * pageSize + 1 }}-{{ Math.min(currentPage * pageSize, 500) }} of 500 items
1084
</div>
1085
<div class="data-grid">
1086
<div v-for="item in getCurrentPageData(currentPage, pageSize)" :key="item.id" class="data-item">
1087
{{ item.name }} - {{ item.description }}
1088
</div>
1089
</div>
1090
</div>
1091
1092
<div class="pagination-controls">
1093
<button @click="prev" :disabled="isFirstPage" class="nav-btn">
1094
← Previous
1095
</button>
1096
1097
<div class="page-numbers">
1098
<button
1099
v-for="page in getVisiblePages(currentPage, pageCount)"
1100
:key="page"
1101
@click="goToPage(page)"
1102
:class="{ active: page === currentPage, ellipsis: page === '...' }"
1103
:disabled="page === '...'"
1104
class="page-btn"
1105
>
1106
{{ page }}
1107
</button>
1108
</div>
1109
1110
<button @click="next" :disabled="isLastPage" class="nav-btn">
1111
Next →
1112
</button>
1113
</div>
1114
1115
<div class="pagination-info">
1116
Page {{ currentPage }} of {{ pageCount }}
1117
</div>
1118
</UseOffsetPagination>
1119
</div>
1120
1121
<!-- Advanced pagination with size selector -->
1122
<div class="advanced-pagination">
1123
<h3>Advanced Pagination</h3>
1124
<UseOffsetPagination
1125
:total="advancedTotal"
1126
:page-size="advancedPageSize"
1127
:page="advancedPage"
1128
v-slot="pagination"
1129
@page-change="handleAdvancedPageChange"
1130
@page-size-change="handlePageSizeChange"
1131
>
1132
<div class="advanced-controls">
1133
<label class="page-size-selector">
1134
Items per page:
1135
<select v-model="advancedPageSize">
1136
<option :value="10">10</option>
1137
<option :value="25">25</option>
1138
<option :value="50">50</option>
1139
<option :value="100">100</option>
1140
</select>
1141
</label>
1142
1143
<div class="jump-to-page">
1144
<label>
1145
Jump to page:
1146
<input
1147
type="number"
1148
:min="1"
1149
:max="pagination.pageCount"
1150
@keyup.enter="pagination.goToPage(parseInt($event.target.value))"
1151
class="page-input"
1152
/>
1153
</label>
1154
</div>
1155
</div>
1156
1157
<div class="advanced-data">
1158
<div class="results-summary">
1159
Results {{ (pagination.currentPage - 1) * pagination.pageSize + 1 }}-{{
1160
Math.min(pagination.currentPage * pagination.pageSize, advancedTotal)
1161
}} of {{ advancedTotal.toLocaleString() }}
1162
</div>
1163
1164
<table class="data-table">
1165
<thead>
1166
<tr>
1167
<th>ID</th>
1168
<th>Name</th>
1169
<th>Category</th>
1170
<th>Status</th>
1171
<th>Created</th>
1172
</tr>
1173
</thead>
1174
<tbody>
1175
<tr v-for="item in getAdvancedPageData(pagination.currentPage, pagination.pageSize)" :key="item.id">
1176
<td>{{ item.id }}</td>
1177
<td>{{ item.name }}</td>
1178
<td>{{ item.category }}</td>
1179
<td>
1180
<span :class="`status-${item.status}`" class="status-badge">
1181
{{ item.status }}
1182
</span>
1183
</td>
1184
<td>{{ formatDate(item.created) }}</td>
1185
</tr>
1186
</tbody>
1187
</table>
1188
1189
<div class="table-pagination">
1190
<button @click="pagination.prev" :disabled="pagination.isFirstPage">
1191
← Prev
1192
</button>
1193
<span class="page-info">
1194
{{ pagination.currentPage }} / {{ pagination.pageCount }}
1195
</span>
1196
<button @click="pagination.next" :disabled="pagination.isLastPage">
1197
Next →
1198
</button>
1199
</div>
1200
</div>
1201
</UseOffsetPagination>
1202
</div>
1203
1204
<!-- Search with pagination -->
1205
<div class="search-pagination">
1206
<h3>Search with Pagination</h3>
1207
<div class="search-controls">
1208
<input
1209
v-model="searchQuery"
1210
placeholder="Search items..."
1211
class="search-input"
1212
@input="handleSearch"
1213
/>
1214
<button @click="clearSearch" class="clear-btn">Clear</button>
1215
</div>
1216
1217
<UseOffsetPagination
1218
:total="filteredTotal"
1219
:page-size="10"
1220
:page="searchPage"
1221
v-slot="pagination"
1222
@page-change="handleSearchPageChange"
1223
>
1224
<div v-if="searchQuery" class="search-results-info">
1225
Found {{ filteredTotal }} results for "{{ searchQuery }}"
1226
</div>
1227
1228
<div class="search-results">
1229
<div v-if="filteredTotal === 0" class="no-results">
1230
No results found for "{{ searchQuery }}"
1231
</div>
1232
<div v-else class="results-list">
1233
<div
1234
v-for="item in getFilteredPageData(pagination.currentPage, pagination.pageSize)"
1235
:key="item.id"
1236
class="result-item"
1237
>
1238
<div class="result-title">{{ highlightSearchTerm(item.name, searchQuery) }}</div>
1239
<div class="result-description">{{ highlightSearchTerm(item.description, searchQuery) }}</div>
1240
</div>
1241
</div>
1242
</div>
1243
1244
<div v-if="filteredTotal > 0" class="search-pagination-controls">
1245
<button @click="pagination.prev" :disabled="pagination.isFirstPage">
1246
← Previous
1247
</button>
1248
<span>{{ pagination.currentPage }} of {{ pagination.pageCount }}</span>
1249
<button @click="pagination.next" :disabled="pagination.isLastPage">
1250
Next →
1251
</button>
1252
</div>
1253
</UseOffsetPagination>
1254
</div>
1255
</template>
1256
1257
<script setup>
1258
import { ref, computed } from 'vue';
1259
import { UseOffsetPagination } from '@vueuse/components';
1260
1261
// Basic pagination data
1262
const basicData = ref([]);
1263
for (let i = 1; i <= 500; i++) {
1264
basicData.value.push({
1265
id: i,
1266
name: `Item ${i}`,
1267
description: `Description for item ${i}`
1268
});
1269
}
1270
1271
// Advanced pagination data
1272
const advancedPageSize = ref(25);
1273
const advancedPage = ref(1);
1274
const advancedTotal = 2547;
1275
1276
const advancedData = ref([]);
1277
const categories = ['Electronics', 'Clothing', 'Books', 'Home & Garden', 'Sports'];
1278
const statuses = ['active', 'inactive', 'pending'];
1279
1280
for (let i = 1; i <= advancedTotal; i++) {
1281
advancedData.value.push({
1282
id: i,
1283
name: `Product ${i}`,
1284
category: categories[Math.floor(Math.random() * categories.length)],
1285
status: statuses[Math.floor(Math.random() * statuses.length)],
1286
created: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000)
1287
});
1288
}
1289
1290
// Search pagination
1291
const searchQuery = ref('');
1292
const searchPage = ref(1);
1293
const searchData = ref([]);
1294
1295
// Initialize search data
1296
for (let i = 1; i <= 1000; i++) {
1297
searchData.value.push({
1298
id: i,
1299
name: `Item ${i} - ${generateRandomWords()}`,
1300
description: `This is a detailed description for item ${i}. ${generateRandomDescription()}`
1301
});
1302
}
1303
1304
const filteredData = computed(() => {
1305
if (!searchQuery.value) return searchData.value;
1306
1307
const query = searchQuery.value.toLowerCase();
1308
return searchData.value.filter(item =>
1309
item.name.toLowerCase().includes(query) ||
1310
item.description.toLowerCase().includes(query)
1311
);
1312
});
1313
1314
const filteredTotal = computed(() => filteredData.value.length);
1315
1316
function getCurrentPageData(page, pageSize) {
1317
const start = (page - 1) * pageSize;
1318
const end = start + pageSize;
1319
return basicData.value.slice(start, end);
1320
}
1321
1322
function getAdvancedPageData(page, pageSize) {
1323
const start = (page - 1) * pageSize;
1324
const end = start + pageSize;
1325
return advancedData.value.slice(start, end);
1326
}
1327
1328
function getFilteredPageData(page, pageSize) {
1329
const start = (page - 1) * pageSize;
1330
const end = start + pageSize;
1331
return filteredData.value.slice(start, end);
1332
}
1333
1334
function getVisiblePages(currentPage, totalPages) {
1335
const pages = [];
1336
const maxVisible = 7;
1337
1338
if (totalPages <= maxVisible) {
1339
for (let i = 1; i <= totalPages; i++) {
1340
pages.push(i);
1341
}
1342
} else {
1343
pages.push(1);
1344
1345
if (currentPage > 4) {
1346
pages.push('...');
1347
}
1348
1349
const start = Math.max(2, currentPage - 1);
1350
const end = Math.min(totalPages - 1, currentPage + 1);
1351
1352
for (let i = start; i <= end; i++) {
1353
pages.push(i);
1354
}
1355
1356
if (currentPage < totalPages - 3) {
1357
pages.push('...');
1358
}
1359
1360
if (totalPages > 1) {
1361
pages.push(totalPages);
1362
}
1363
}
1364
1365
return pages;
1366
}
1367
1368
function handlePageChange(pagination) {
1369
console.log('Page changed:', pagination.currentPage);
1370
}
1371
1372
function handleAdvancedPageChange(pagination) {
1373
advancedPage.value = pagination.currentPage;
1374
}
1375
1376
function handlePageSizeChange(pagination) {
1377
advancedPageSize.value = pagination.pageSize;
1378
advancedPage.value = 1; // Reset to first page
1379
}
1380
1381
function handleSearch() {
1382
searchPage.value = 1; // Reset to first page on search
1383
}
1384
1385
function handleSearchPageChange(pagination) {
1386
searchPage.value = pagination.currentPage;
1387
}
1388
1389
function clearSearch() {
1390
searchQuery.value = '';
1391
searchPage.value = 1;
1392
}
1393
1394
function highlightSearchTerm(text, query) {
1395
if (!query) return text;
1396
1397
const regex = new RegExp(`(${query})`, 'gi');
1398
return text.replace(regex, '<mark>$1</mark>');
1399
}
1400
1401
function formatDate(date) {
1402
return date.toLocaleDateString();
1403
}
1404
1405
function generateRandomWords() {
1406
const words = ['Amazing', 'Fantastic', 'Great', 'Awesome', 'Cool', 'Nice', 'Super', 'Mega'];
1407
return words[Math.floor(Math.random() * words.length)];
1408
}
1409
1410
function generateRandomDescription() {
1411
const descriptions = [
1412
'High quality product with excellent features.',
1413
'Perfect for everyday use and special occasions.',
1414
'Durable construction with modern design.',
1415
'Affordable pricing with premium quality.',
1416
'Innovative technology meets practical design.'
1417
];
1418
return descriptions[Math.floor(Math.random() * descriptions.length)];
1419
}
1420
</script>
1421
1422
<style>
1423
.pagination-demo, .advanced-pagination, .search-pagination {
1424
border: 1px solid #ddd;
1425
border-radius: 8px;
1426
padding: 20px;
1427
margin: 15px 0;
1428
}
1429
1430
.data-section {
1431
margin-bottom: 20px;
1432
}
1433
1434
.data-info {
1435
margin-bottom: 15px;
1436
font-weight: 500;
1437
color: #666;
1438
}
1439
1440
.data-grid {
1441
display: grid;
1442
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
1443
gap: 10px;
1444
margin-bottom: 20px;
1445
}
1446
1447
.data-item, .result-item {
1448
padding: 10px;
1449
border: 1px solid #ddd;
1450
border-radius: 4px;
1451
background: #f9f9f9;
1452
}
1453
1454
.pagination-controls, .table-pagination, .search-pagination-controls {
1455
display: flex;
1456
align-items: center;
1457
justify-content: center;
1458
gap: 10px;
1459
margin: 20px 0;
1460
}
1461
1462
.nav-btn, .page-btn {
1463
padding: 8px 12px;
1464
border: 1px solid #ddd;
1465
border-radius: 4px;
1466
cursor: pointer;
1467
background: white;
1468
}
1469
1470
.nav-btn:disabled {
1471
opacity: 0.5;
1472
cursor: not-allowed;
1473
}
1474
1475
.page-numbers {
1476
display: flex;
1477
gap: 2px;
1478
}
1479
1480
.page-btn.active {
1481
background: #2196f3;
1482
color: white;
1483
border-color: #2196f3;
1484
}
1485
1486
.page-btn.ellipsis {
1487
cursor: default;
1488
background: transparent;
1489
border: none;
1490
}
1491
1492
.pagination-info {
1493
text-align: center;
1494
color: #666;
1495
font-size: 0.9em;
1496
}
1497
1498
.advanced-controls {
1499
display: flex;
1500
gap: 20px;
1501
align-items: center;
1502
margin-bottom: 20px;
1503
flex-wrap: wrap;
1504
}
1505
1506
.page-size-selector select, .page-input {
1507
margin-left: 8px;
1508
padding: 4px 8px;
1509
border: 1px solid #ddd;
1510
border-radius: 4px;
1511
}
1512
1513
.page-input {
1514
width: 80px;
1515
}
1516
1517
.data-table {
1518
width: 100%;
1519
border-collapse: collapse;
1520
margin: 15px 0;
1521
}
1522
1523
.data-table th, .data-table td {
1524
padding: 12px;
1525
text-align: left;
1526
border-bottom: 1px solid #ddd;
1527
}
1528
1529
.data-table th {
1530
background: #f5f5f5;
1531
font-weight: 600;
1532
}
1533
1534
.status-badge {
1535
padding: 4px 8px;
1536
border-radius: 4px;
1537
font-size: 0.8em;
1538
font-weight: 500;
1539
text-transform: uppercase;
1540
}
1541
1542
.status-active {
1543
background: #e8f5e8;
1544
color: #2e7d32;
1545
}
1546
1547
.status-inactive {
1548
background: #ffebee;
1549
color: #c62828;
1550
}
1551
1552
.status-pending {
1553
background: #fff3e0;
1554
color: #f57c00;
1555
}
1556
1557
.search-controls {
1558
display: flex;
1559
gap: 10px;
1560
margin-bottom: 20px;
1561
}
1562
1563
.search-input {
1564
flex: 1;
1565
padding: 10px;
1566
border: 1px solid #ddd;
1567
border-radius: 4px;
1568
font-size: 16px;
1569
}
1570
1571
.clear-btn {
1572
padding: 10px 15px;
1573
border: 1px solid #ddd;
1574
border-radius: 4px;
1575
cursor: pointer;
1576
background: #f5f5f5;
1577
}
1578
1579
.search-results-info {
1580
margin-bottom: 15px;
1581
padding: 10px;
1582
background: #e3f2fd;
1583
border-radius: 4px;
1584
color: #1976d2;
1585
}
1586
1587
.no-results {
1588
text-align: center;
1589
padding: 40px;
1590
color: #666;
1591
font-style: italic;
1592
}
1593
1594
.results-list {
1595
display: flex;
1596
flex-direction: column;
1597
gap: 10px;
1598
}
1599
1600
.result-title {
1601
font-weight: bold;
1602
margin-bottom: 5px;
1603
}
1604
1605
.result-description {
1606
color: #666;
1607
font-size: 0.9em;
1608
}
1609
1610
:global(mark) {
1611
background: #ffeb3b;
1612
padding: 0 2px;
1613
border-radius: 2px;
1614
}
1615
</style>
1616
```
1617
1618
## Additional Utility Components
1619
1620
### UseNetwork Component
1621
1622
```typescript { .api }
1623
/**
1624
* Component that provides network connectivity information
1625
*/
1626
interface UseNetworkReturn {
1627
isOnline: Ref<boolean>;
1628
downlink: Ref<number | undefined>;
1629
downlinkMax: Ref<number | undefined>;
1630
effectiveType: Ref<string | undefined>;
1631
saveData: Ref<boolean | undefined>;
1632
type: Ref<string | undefined>;
1633
}
1634
```
1635
1636
### UseOnline Component
1637
1638
```typescript { .api }
1639
/**
1640
* Component that tracks online/offline status
1641
*/
1642
interface UseOnlineReturn {
1643
isOnline: Ref<boolean>;
1644
}
1645
```
1646
1647
### UseIdle Component
1648
1649
```typescript { .api }
1650
/**
1651
* Component that tracks user idle state
1652
*/
1653
interface UseIdleReturn {
1654
idle: Ref<boolean>;
1655
lastActive: Ref<number>;
1656
reset: () => void;
1657
}
1658
```
1659
1660
### UseImage Component
1661
1662
```typescript { .api }
1663
/**
1664
* Component that manages image loading state
1665
*/
1666
interface UseImageReturn {
1667
isLoading: Ref<boolean>;
1668
error: Ref<Event | null>;
1669
isReady: Ref<boolean>;
1670
}
1671
```
1672
1673
### UseObjectUrl Component
1674
1675
```typescript { .api }
1676
/**
1677
* Component that creates and manages object URLs
1678
*/
1679
interface UseObjectUrlReturn {
1680
url: Ref<string | undefined>;
1681
release: () => void;
1682
}
1683
```
1684
1685
## Type Definitions
1686
1687
```typescript { .api }
1688
/** Common types used across utility and advanced components */
1689
type MaybeRefOrGetter<T> = T | Ref<T> | (() => T);
1690
1691
interface RenderableComponent {
1692
/** The element that the component should be rendered as @default 'div' */
1693
as?: object | string;
1694
}
1695
1696
/** Time and timestamp types */
1697
interface UseTimestampOptions {
1698
offset?: number;
1699
interval?: number | 'requestAnimationFrame';
1700
immediate?: boolean;
1701
controls?: boolean;
1702
}
1703
1704
/** Pagination types */
1705
interface UseOffsetPaginationOptions {
1706
total: MaybeRefOrGetter<number>;
1707
pageSize?: MaybeRefOrGetter<number>;
1708
page?: MaybeRefOrGetter<number>;
1709
}
1710
1711
/** Virtual list types */
1712
interface UseVirtualListItem<T = any> {
1713
data: T;
1714
index: number;
1715
}
1716
1717
/** Network information types */
1718
interface NetworkInformation extends EventTarget {
1719
readonly connection?: NetworkConnection;
1720
readonly onLine: boolean;
1721
}
1722
1723
interface NetworkConnection {
1724
readonly downlink: number;
1725
readonly downlinkMax: number;
1726
readonly effectiveType: '2g' | '3g' | '4g' | 'slow-2g';
1727
readonly rtt: number;
1728
readonly saveData: boolean;
1729
readonly type: ConnectionType;
1730
}
1731
1732
type ConnectionType = 'bluetooth' | 'cellular' | 'ethernet' | 'mixed' | 'none' | 'other' | 'unknown' | 'wifi' | 'wimax';
1733
```