@wyxos/vibe 1.6.23 → 1.6.25
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/lib/index.js +731 -705
- package/lib/vibe.css +1 -1
- package/package.json +1 -1
- package/src/Masonry.vue +1037 -973
- package/src/useMasonryScroll.ts +3 -2
package/src/Masonry.vue
CHANGED
|
@@ -1,973 +1,1037 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
|
3
|
-
import calculateLayout from "./calculateLayout";
|
|
4
|
-
import { debounce } from 'lodash-es'
|
|
5
|
-
import {
|
|
6
|
-
getColumnCount,
|
|
7
|
-
getBreakpointName,
|
|
8
|
-
calculateContainerHeight,
|
|
9
|
-
getItemAttributes,
|
|
10
|
-
calculateColumnHeights
|
|
11
|
-
} from './masonryUtils'
|
|
12
|
-
import { useMasonryTransitions } from './useMasonryTransitions'
|
|
13
|
-
import { useMasonryScroll } from './useMasonryScroll'
|
|
14
|
-
import { useSwipeMode as useSwipeModeComposable } from './useSwipeMode'
|
|
15
|
-
import { useMasonryPagination } from './useMasonryPagination'
|
|
16
|
-
import { useMasonryItems } from './useMasonryItems'
|
|
17
|
-
import { useMasonryLayout } from './useMasonryLayout'
|
|
18
|
-
import { useMasonryVirtualization } from './useMasonryVirtualization'
|
|
19
|
-
import { useMasonryDimensions } from './useMasonryDimensions'
|
|
20
|
-
import MasonryItem from './components/MasonryItem.vue'
|
|
21
|
-
import { normalizeError } from './utils/errorHandler'
|
|
22
|
-
|
|
23
|
-
const props = defineProps({
|
|
24
|
-
getNextPage: {
|
|
25
|
-
type: Function,
|
|
26
|
-
default: () => { }
|
|
27
|
-
},
|
|
28
|
-
loadAtPage: {
|
|
29
|
-
type: [Number, String],
|
|
30
|
-
default: null
|
|
31
|
-
},
|
|
32
|
-
items: {
|
|
33
|
-
type: Array,
|
|
34
|
-
default: () => []
|
|
35
|
-
},
|
|
36
|
-
layout: {
|
|
37
|
-
type: Object
|
|
38
|
-
},
|
|
39
|
-
paginationType: {
|
|
40
|
-
type: String,
|
|
41
|
-
default: 'page', // or 'cursor'
|
|
42
|
-
validator: (v: string) => ['page', 'cursor'].includes(v)
|
|
43
|
-
},
|
|
44
|
-
skipInitialLoad: {
|
|
45
|
-
type: Boolean,
|
|
46
|
-
default: false
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
type:
|
|
55
|
-
default:
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
type: Number,
|
|
59
|
-
default:
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
type: Number,
|
|
68
|
-
default:
|
|
69
|
-
},
|
|
70
|
-
|
|
71
|
-
type: Number,
|
|
72
|
-
default:
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
type: Number,
|
|
85
|
-
default:
|
|
86
|
-
},
|
|
87
|
-
|
|
88
|
-
type:
|
|
89
|
-
default:
|
|
90
|
-
},
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
type:
|
|
94
|
-
default:
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
type:
|
|
98
|
-
default:
|
|
99
|
-
},
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
type:
|
|
111
|
-
default:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
'
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
'
|
|
184
|
-
'
|
|
185
|
-
|
|
186
|
-
'
|
|
187
|
-
'
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
{
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
//
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
//
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
const
|
|
390
|
-
const
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
//
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
//
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
//
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
//
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
)
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
if
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
//
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
//
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
const
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
})
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
|
3
|
+
import calculateLayout from "./calculateLayout";
|
|
4
|
+
import { debounce } from 'lodash-es'
|
|
5
|
+
import {
|
|
6
|
+
getColumnCount,
|
|
7
|
+
getBreakpointName,
|
|
8
|
+
calculateContainerHeight,
|
|
9
|
+
getItemAttributes,
|
|
10
|
+
calculateColumnHeights
|
|
11
|
+
} from './masonryUtils'
|
|
12
|
+
import { useMasonryTransitions } from './useMasonryTransitions'
|
|
13
|
+
import { useMasonryScroll } from './useMasonryScroll'
|
|
14
|
+
import { useSwipeMode as useSwipeModeComposable } from './useSwipeMode'
|
|
15
|
+
import { useMasonryPagination } from './useMasonryPagination'
|
|
16
|
+
import { useMasonryItems } from './useMasonryItems'
|
|
17
|
+
import { useMasonryLayout } from './useMasonryLayout'
|
|
18
|
+
import { useMasonryVirtualization } from './useMasonryVirtualization'
|
|
19
|
+
import { useMasonryDimensions } from './useMasonryDimensions'
|
|
20
|
+
import MasonryItem from './components/MasonryItem.vue'
|
|
21
|
+
import { normalizeError } from './utils/errorHandler'
|
|
22
|
+
|
|
23
|
+
const props = defineProps({
|
|
24
|
+
getNextPage: {
|
|
25
|
+
type: Function,
|
|
26
|
+
default: () => { }
|
|
27
|
+
},
|
|
28
|
+
loadAtPage: {
|
|
29
|
+
type: [Number, String],
|
|
30
|
+
default: null
|
|
31
|
+
},
|
|
32
|
+
items: {
|
|
33
|
+
type: Array,
|
|
34
|
+
default: () => []
|
|
35
|
+
},
|
|
36
|
+
layout: {
|
|
37
|
+
type: Object
|
|
38
|
+
},
|
|
39
|
+
paginationType: {
|
|
40
|
+
type: String,
|
|
41
|
+
default: 'page', // or 'cursor'
|
|
42
|
+
validator: (v: string) => ['page', 'cursor'].includes(v)
|
|
43
|
+
},
|
|
44
|
+
skipInitialLoad: {
|
|
45
|
+
type: Boolean,
|
|
46
|
+
default: false
|
|
47
|
+
},
|
|
48
|
+
// Initial pagination state when skipInitialLoad is true and items are provided
|
|
49
|
+
initialPage: {
|
|
50
|
+
type: [Number, String],
|
|
51
|
+
default: null
|
|
52
|
+
},
|
|
53
|
+
initialNextPage: {
|
|
54
|
+
type: [Number, String],
|
|
55
|
+
default: null
|
|
56
|
+
},
|
|
57
|
+
pageSize: {
|
|
58
|
+
type: Number,
|
|
59
|
+
default: 40
|
|
60
|
+
},
|
|
61
|
+
// Backfill configuration
|
|
62
|
+
backfillEnabled: {
|
|
63
|
+
type: Boolean,
|
|
64
|
+
default: true
|
|
65
|
+
},
|
|
66
|
+
backfillDelayMs: {
|
|
67
|
+
type: Number,
|
|
68
|
+
default: 2000
|
|
69
|
+
},
|
|
70
|
+
backfillMaxCalls: {
|
|
71
|
+
type: Number,
|
|
72
|
+
default: 10
|
|
73
|
+
},
|
|
74
|
+
// Retry configuration
|
|
75
|
+
retryMaxAttempts: {
|
|
76
|
+
type: Number,
|
|
77
|
+
default: 3
|
|
78
|
+
},
|
|
79
|
+
retryInitialDelayMs: {
|
|
80
|
+
type: Number,
|
|
81
|
+
default: 2000
|
|
82
|
+
},
|
|
83
|
+
retryBackoffStepMs: {
|
|
84
|
+
type: Number,
|
|
85
|
+
default: 2000
|
|
86
|
+
},
|
|
87
|
+
transitionDurationMs: {
|
|
88
|
+
type: Number,
|
|
89
|
+
default: 450
|
|
90
|
+
},
|
|
91
|
+
// Shorter, snappier duration specifically for item removal (leave)
|
|
92
|
+
leaveDurationMs: {
|
|
93
|
+
type: Number,
|
|
94
|
+
default: 160
|
|
95
|
+
},
|
|
96
|
+
transitionEasing: {
|
|
97
|
+
type: String,
|
|
98
|
+
default: 'cubic-bezier(.22,.61,.36,1)'
|
|
99
|
+
},
|
|
100
|
+
// Force motion even when user has reduced-motion enabled
|
|
101
|
+
forceMotion: {
|
|
102
|
+
type: Boolean,
|
|
103
|
+
default: false
|
|
104
|
+
},
|
|
105
|
+
virtualBufferPx: {
|
|
106
|
+
type: Number,
|
|
107
|
+
default: 600
|
|
108
|
+
},
|
|
109
|
+
loadThresholdPx: {
|
|
110
|
+
type: Number,
|
|
111
|
+
default: 200
|
|
112
|
+
},
|
|
113
|
+
autoRefreshOnEmpty: {
|
|
114
|
+
type: Boolean,
|
|
115
|
+
default: false
|
|
116
|
+
},
|
|
117
|
+
// Layout mode: 'auto' (detect from screen size), 'masonry', or 'swipe'
|
|
118
|
+
layoutMode: {
|
|
119
|
+
type: String,
|
|
120
|
+
default: 'auto',
|
|
121
|
+
validator: (v: string) => ['auto', 'masonry', 'swipe'].includes(v)
|
|
122
|
+
},
|
|
123
|
+
// Breakpoint for switching to swipe mode (in pixels or Tailwind breakpoint name)
|
|
124
|
+
mobileBreakpoint: {
|
|
125
|
+
type: [Number, String],
|
|
126
|
+
default: 768 // 'md' breakpoint
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const defaultLayout = {
|
|
131
|
+
sizes: { base: 1, sm: 2, md: 3, lg: 4, xl: 5, '2xl': 6 },
|
|
132
|
+
gutterX: 10,
|
|
133
|
+
gutterY: 10,
|
|
134
|
+
header: 0,
|
|
135
|
+
footer: 0,
|
|
136
|
+
paddingLeft: 0,
|
|
137
|
+
paddingRight: 0,
|
|
138
|
+
placement: 'masonry'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const layout = computed(() => ({
|
|
142
|
+
...defaultLayout,
|
|
143
|
+
...props.layout,
|
|
144
|
+
sizes: {
|
|
145
|
+
...defaultLayout.sizes,
|
|
146
|
+
...(props.layout?.sizes || {})
|
|
147
|
+
}
|
|
148
|
+
}))
|
|
149
|
+
|
|
150
|
+
const wrapper = ref<HTMLElement | null>(null)
|
|
151
|
+
const containerWidth = ref<number>(typeof window !== 'undefined' ? window.innerWidth : 1024)
|
|
152
|
+
const containerHeight = ref<number>(typeof window !== 'undefined' ? window.innerHeight : 768)
|
|
153
|
+
const fixedDimensions = ref<{ width?: number; height?: number } | null>(null)
|
|
154
|
+
let resizeObserver: ResizeObserver | null = null
|
|
155
|
+
|
|
156
|
+
// Get breakpoint value from Tailwind breakpoint name
|
|
157
|
+
function getBreakpointValue(breakpoint: string): number {
|
|
158
|
+
const breakpoints: Record<string, number> = {
|
|
159
|
+
'sm': 640,
|
|
160
|
+
'md': 768,
|
|
161
|
+
'lg': 1024,
|
|
162
|
+
'xl': 1280,
|
|
163
|
+
'2xl': 1536
|
|
164
|
+
}
|
|
165
|
+
return breakpoints[breakpoint] || 768
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Determine if we should use swipe mode
|
|
169
|
+
const useSwipeMode = computed(() => {
|
|
170
|
+
if (props.layoutMode === 'masonry') return false
|
|
171
|
+
if (props.layoutMode === 'swipe') return true
|
|
172
|
+
|
|
173
|
+
// Auto mode: check container width
|
|
174
|
+
const breakpoint = typeof props.mobileBreakpoint === 'string'
|
|
175
|
+
? getBreakpointValue(props.mobileBreakpoint)
|
|
176
|
+
: props.mobileBreakpoint
|
|
177
|
+
|
|
178
|
+
return containerWidth.value < breakpoint
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
const emits = defineEmits([
|
|
183
|
+
'update:items',
|
|
184
|
+
'backfill:start',
|
|
185
|
+
'backfill:tick',
|
|
186
|
+
'backfill:stop',
|
|
187
|
+
'retry:start',
|
|
188
|
+
'retry:tick',
|
|
189
|
+
'retry:stop',
|
|
190
|
+
'remove-all:complete',
|
|
191
|
+
// Re-emit item-level preload events from the default MasonryItem
|
|
192
|
+
'item:preload:success',
|
|
193
|
+
'item:preload:error',
|
|
194
|
+
// Mouse events from MasonryItem content
|
|
195
|
+
'item:mouse-enter',
|
|
196
|
+
'item:mouse-leave'
|
|
197
|
+
])
|
|
198
|
+
|
|
199
|
+
const masonry = computed<any>({
|
|
200
|
+
get: () => props.items,
|
|
201
|
+
set: (val) => emits('update:items', val)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const columns = ref<number>(7)
|
|
205
|
+
const container = ref<HTMLElement | null>(null)
|
|
206
|
+
const paginationHistory = ref<any[]>([])
|
|
207
|
+
const currentPage = ref<any>(null) // Track the actual current page being displayed
|
|
208
|
+
const isLoading = ref<boolean>(false)
|
|
209
|
+
const masonryContentHeight = ref<number>(0)
|
|
210
|
+
const hasReachedEnd = ref<boolean>(false) // Track when we've reached the last page
|
|
211
|
+
const loadError = ref<Error | null>(null) // Track load errors
|
|
212
|
+
|
|
213
|
+
// Current breakpoint
|
|
214
|
+
const currentBreakpoint = computed(() => getBreakpointName(containerWidth.value))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
// Initialize dimensions composable first (needed by layout composable)
|
|
218
|
+
const dimensions = useMasonryDimensions({
|
|
219
|
+
masonry: masonry as any
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// Extract dimension checking function
|
|
223
|
+
const { checkItemDimensions, invalidDimensionIds, reset: resetDimensions } = dimensions
|
|
224
|
+
|
|
225
|
+
// Initialize layout composable (needs checkItemDimensions from dimensions composable)
|
|
226
|
+
const layoutComposable = useMasonryLayout({
|
|
227
|
+
masonry: masonry as any,
|
|
228
|
+
useSwipeMode,
|
|
229
|
+
container,
|
|
230
|
+
columns,
|
|
231
|
+
containerWidth,
|
|
232
|
+
masonryContentHeight,
|
|
233
|
+
layout,
|
|
234
|
+
fixedDimensions,
|
|
235
|
+
checkItemDimensions
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
// Extract layout functions
|
|
239
|
+
const { refreshLayout, setFixedDimensions: setFixedDimensionsLayout, onResize: onResizeLayout } = layoutComposable
|
|
240
|
+
|
|
241
|
+
// Initialize virtualization composable
|
|
242
|
+
const virtualization = useMasonryVirtualization({
|
|
243
|
+
masonry: masonry as any,
|
|
244
|
+
container,
|
|
245
|
+
columns,
|
|
246
|
+
virtualBufferPx: props.virtualBufferPx,
|
|
247
|
+
loadThresholdPx: props.loadThresholdPx,
|
|
248
|
+
handleScroll: () => { } // Will be set after pagination is initialized
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
// Extract virtualization state and functions
|
|
252
|
+
const { viewportTop, viewportHeight, virtualizing, scrollProgress, visibleMasonry, updateScrollProgress, updateViewport: updateViewportVirtualization, reset: resetVirtualization } = virtualization
|
|
253
|
+
|
|
254
|
+
// Initialize transitions composable with virtualization support
|
|
255
|
+
const { onEnter, onBeforeEnter, onBeforeLeave, onLeave } = useMasonryTransitions(
|
|
256
|
+
{ container, masonry: masonry as any },
|
|
257
|
+
{ leaveDurationMs: props.leaveDurationMs, virtualizing }
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
// Transition functions for template (wrapped to match expected signature)
|
|
261
|
+
const enter = onEnter
|
|
262
|
+
const beforeEnter = onBeforeEnter
|
|
263
|
+
const beforeLeave = onBeforeLeave
|
|
264
|
+
const leave = onLeave
|
|
265
|
+
|
|
266
|
+
// Initialize pagination composable
|
|
267
|
+
const pagination = useMasonryPagination({
|
|
268
|
+
getNextPage: props.getNextPage as (page: any) => Promise<{ items: any[]; nextPage: any }>,
|
|
269
|
+
masonry: masonry as any,
|
|
270
|
+
isLoading,
|
|
271
|
+
hasReachedEnd,
|
|
272
|
+
loadError,
|
|
273
|
+
currentPage,
|
|
274
|
+
paginationHistory,
|
|
275
|
+
refreshLayout,
|
|
276
|
+
retryMaxAttempts: props.retryMaxAttempts,
|
|
277
|
+
retryInitialDelayMs: props.retryInitialDelayMs,
|
|
278
|
+
retryBackoffStepMs: props.retryBackoffStepMs,
|
|
279
|
+
backfillEnabled: props.backfillEnabled,
|
|
280
|
+
backfillDelayMs: props.backfillDelayMs,
|
|
281
|
+
backfillMaxCalls: props.backfillMaxCalls,
|
|
282
|
+
pageSize: props.pageSize,
|
|
283
|
+
autoRefreshOnEmpty: props.autoRefreshOnEmpty,
|
|
284
|
+
emits
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
// Extract pagination functions
|
|
288
|
+
const { loadPage, loadNext, refreshCurrentPage, cancelLoad, maybeBackfillToTarget } = pagination
|
|
289
|
+
|
|
290
|
+
// Initialize swipe mode composable (needs loadNext and loadPage from pagination)
|
|
291
|
+
const swipeMode = useSwipeModeComposable({
|
|
292
|
+
useSwipeMode,
|
|
293
|
+
masonry: masonry as any,
|
|
294
|
+
isLoading,
|
|
295
|
+
loadNext,
|
|
296
|
+
loadPage,
|
|
297
|
+
paginationHistory
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
// Initialize scroll handler (needs loadNext from pagination)
|
|
301
|
+
const { handleScroll } = useMasonryScroll({
|
|
302
|
+
container,
|
|
303
|
+
masonry: masonry as any,
|
|
304
|
+
columns,
|
|
305
|
+
containerHeight: masonryContentHeight,
|
|
306
|
+
isLoading,
|
|
307
|
+
pageSize: props.pageSize,
|
|
308
|
+
refreshLayout,
|
|
309
|
+
setItemsRaw: (items: any[]) => {
|
|
310
|
+
masonry.value = items
|
|
311
|
+
},
|
|
312
|
+
loadNext,
|
|
313
|
+
loadThresholdPx: props.loadThresholdPx
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
// Update virtualization handleScroll to use the scroll handler
|
|
317
|
+
virtualization.handleScroll.value = handleScroll
|
|
318
|
+
|
|
319
|
+
// Initialize items composable
|
|
320
|
+
const items = useMasonryItems({
|
|
321
|
+
masonry: masonry as any,
|
|
322
|
+
useSwipeMode,
|
|
323
|
+
refreshLayout,
|
|
324
|
+
refreshCurrentPage,
|
|
325
|
+
loadNext,
|
|
326
|
+
maybeBackfillToTarget,
|
|
327
|
+
autoRefreshOnEmpty: props.autoRefreshOnEmpty,
|
|
328
|
+
paginationHistory
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
// Extract item management functions
|
|
332
|
+
const { remove, removeMany, restore, restoreMany, removeAll: removeAllItems } = items
|
|
333
|
+
|
|
334
|
+
// setFixedDimensions is now in useMasonryLayout composable
|
|
335
|
+
// Wrapper function to maintain API compatibility and handle wrapper restoration
|
|
336
|
+
function setFixedDimensions(dimensions: { width?: number; height?: number } | null) {
|
|
337
|
+
setFixedDimensionsLayout(dimensions, updateScrollProgress)
|
|
338
|
+
if (!dimensions && wrapper.value) {
|
|
339
|
+
// When clearing fixed dimensions, restore from wrapper
|
|
340
|
+
containerWidth.value = wrapper.value.clientWidth
|
|
341
|
+
containerHeight.value = wrapper.value.clientHeight
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
defineExpose({
|
|
346
|
+
isLoading,
|
|
347
|
+
refreshLayout,
|
|
348
|
+
// Container dimensions (wrapper element)
|
|
349
|
+
containerWidth,
|
|
350
|
+
containerHeight,
|
|
351
|
+
// Masonry content height (for backward compatibility, old containerHeight)
|
|
352
|
+
contentHeight: masonryContentHeight,
|
|
353
|
+
// Current page
|
|
354
|
+
currentPage,
|
|
355
|
+
// End of list tracking
|
|
356
|
+
hasReachedEnd,
|
|
357
|
+
// Load error tracking
|
|
358
|
+
loadError,
|
|
359
|
+
// Set fixed dimensions (overrides ResizeObserver)
|
|
360
|
+
setFixedDimensions,
|
|
361
|
+
remove,
|
|
362
|
+
removeMany,
|
|
363
|
+
removeAll: removeAllItems,
|
|
364
|
+
restore,
|
|
365
|
+
restoreMany,
|
|
366
|
+
loadNext,
|
|
367
|
+
loadPage,
|
|
368
|
+
refreshCurrentPage,
|
|
369
|
+
reset,
|
|
370
|
+
destroy,
|
|
371
|
+
init,
|
|
372
|
+
restoreItems,
|
|
373
|
+
paginationHistory,
|
|
374
|
+
cancelLoad,
|
|
375
|
+
scrollToTop,
|
|
376
|
+
scrollTo,
|
|
377
|
+
totalItems: computed(() => (masonry.value as any[]).length),
|
|
378
|
+
currentBreakpoint
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
// Layout functions are now in useMasonryLayout composable
|
|
382
|
+
// Removed: calculateHeight, refreshLayout - now from layoutComposable
|
|
383
|
+
|
|
384
|
+
// Expose swipe mode computed values and state for template
|
|
385
|
+
const currentItem = swipeMode.currentItem
|
|
386
|
+
const nextItem = swipeMode.nextItem
|
|
387
|
+
const previousItem = swipeMode.previousItem
|
|
388
|
+
const currentSwipeIndex = swipeMode.currentSwipeIndex
|
|
389
|
+
const swipeOffset = swipeMode.swipeOffset
|
|
390
|
+
const isDragging = swipeMode.isDragging
|
|
391
|
+
const swipeContainer = swipeMode.swipeContainer
|
|
392
|
+
|
|
393
|
+
// Swipe gesture handlers (delegated to composable)
|
|
394
|
+
const handleTouchStart = swipeMode.handleTouchStart
|
|
395
|
+
const handleTouchMove = swipeMode.handleTouchMove
|
|
396
|
+
const handleTouchEnd = swipeMode.handleTouchEnd
|
|
397
|
+
const handleMouseDown = swipeMode.handleMouseDown
|
|
398
|
+
const handleMouseMove = swipeMode.handleMouseMove
|
|
399
|
+
const handleMouseUp = swipeMode.handleMouseUp
|
|
400
|
+
const goToNextItem = swipeMode.goToNextItem
|
|
401
|
+
const goToPreviousItem = swipeMode.goToPreviousItem
|
|
402
|
+
const snapToCurrentItem = swipeMode.snapToCurrentItem
|
|
403
|
+
|
|
404
|
+
// refreshCurrentPage is now in useMasonryPagination composable
|
|
405
|
+
|
|
406
|
+
// Item management functions (remove, removeMany, restore, restoreMany, removeAll) are now in useMasonryItems composable
|
|
407
|
+
|
|
408
|
+
function scrollToTop(options?: ScrollToOptions) {
|
|
409
|
+
if (container.value) {
|
|
410
|
+
container.value.scrollTo({
|
|
411
|
+
top: 0,
|
|
412
|
+
behavior: options?.behavior ?? 'smooth',
|
|
413
|
+
...options
|
|
414
|
+
})
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function scrollTo(options: { top?: number; left?: number; behavior?: ScrollBehavior }) {
|
|
419
|
+
if (container.value) {
|
|
420
|
+
container.value.scrollTo({
|
|
421
|
+
top: options.top ?? container.value.scrollTop,
|
|
422
|
+
left: options.left ?? container.value.scrollLeft,
|
|
423
|
+
behavior: options.behavior ?? 'auto',
|
|
424
|
+
})
|
|
425
|
+
// Update viewport state immediately after scrolling
|
|
426
|
+
if (container.value) {
|
|
427
|
+
viewportTop.value = container.value.scrollTop
|
|
428
|
+
viewportHeight.value = container.value.clientHeight || window.innerHeight
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// removeAll is now in useMasonryItems composable (removeAllItems)
|
|
434
|
+
|
|
435
|
+
// onResize is now in useMasonryLayout composable (onResizeLayout)
|
|
436
|
+
function onResize() {
|
|
437
|
+
onResizeLayout()
|
|
438
|
+
if (container.value) {
|
|
439
|
+
viewportTop.value = container.value.scrollTop
|
|
440
|
+
viewportHeight.value = container.value.clientHeight
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// maybeBackfillToTarget, cancelLoad are now in useMasonryPagination composable
|
|
445
|
+
// Removed: backfillActive, cancelRequested - now internal to pagination composable
|
|
446
|
+
|
|
447
|
+
function reset() {
|
|
448
|
+
// Cancel ongoing work
|
|
449
|
+
cancelLoad()
|
|
450
|
+
|
|
451
|
+
if (container.value) {
|
|
452
|
+
container.value.scrollTo({
|
|
453
|
+
top: 0,
|
|
454
|
+
behavior: 'smooth'
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
masonry.value = []
|
|
459
|
+
containerHeight.value = 0
|
|
460
|
+
currentPage.value = props.loadAtPage // Reset current page tracking
|
|
461
|
+
paginationHistory.value = [props.loadAtPage]
|
|
462
|
+
hasReachedEnd.value = false // Reset end flag
|
|
463
|
+
loadError.value = null // Reset error flag
|
|
464
|
+
|
|
465
|
+
// Reset virtualization state
|
|
466
|
+
resetVirtualization()
|
|
467
|
+
|
|
468
|
+
// Reset auto-initialization flag so watcher can work again if needed
|
|
469
|
+
hasInitializedWithItems = false
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function destroy() {
|
|
473
|
+
// Cancel any ongoing loads
|
|
474
|
+
cancelLoad()
|
|
475
|
+
|
|
476
|
+
// Reset all state
|
|
477
|
+
masonry.value = []
|
|
478
|
+
masonryContentHeight.value = 0
|
|
479
|
+
currentPage.value = null
|
|
480
|
+
paginationHistory.value = []
|
|
481
|
+
hasReachedEnd.value = false
|
|
482
|
+
loadError.value = null
|
|
483
|
+
isLoading.value = false
|
|
484
|
+
|
|
485
|
+
// Reset swipe mode state
|
|
486
|
+
currentSwipeIndex.value = 0
|
|
487
|
+
swipeOffset.value = 0
|
|
488
|
+
isDragging.value = false
|
|
489
|
+
|
|
490
|
+
// Reset virtualization state
|
|
491
|
+
resetVirtualization()
|
|
492
|
+
|
|
493
|
+
// Reset invalid dimension tracking
|
|
494
|
+
resetDimensions()
|
|
495
|
+
|
|
496
|
+
// Scroll to top if container exists
|
|
497
|
+
if (container.value) {
|
|
498
|
+
container.value.scrollTo({
|
|
499
|
+
top: 0,
|
|
500
|
+
behavior: 'auto' // Instant scroll for destroy
|
|
501
|
+
})
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Scroll handler is now handled by virtualization composable's updateViewport
|
|
506
|
+
const debouncedScrollHandler = debounce(async () => {
|
|
507
|
+
if (useSwipeMode.value) return // Skip scroll handling in swipe mode
|
|
508
|
+
await updateViewportVirtualization()
|
|
509
|
+
}, 200)
|
|
510
|
+
|
|
511
|
+
const debouncedResizeHandler = debounce(onResize, 200)
|
|
512
|
+
|
|
513
|
+
// Window resize handler (combines swipe and general resize logic)
|
|
514
|
+
function handleWindowResize() {
|
|
515
|
+
// Delegate swipe-specific resize handling
|
|
516
|
+
swipeMode.handleWindowResize()
|
|
517
|
+
|
|
518
|
+
// General resize handling (if needed)
|
|
519
|
+
// Note: containerWidth is updated by ResizeObserver
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function init(items: any[], page: any, next: any) {
|
|
523
|
+
currentPage.value = page // Track the initial current page
|
|
524
|
+
paginationHistory.value = [page]
|
|
525
|
+
paginationHistory.value.push(next)
|
|
526
|
+
// Update hasReachedEnd if next is null
|
|
527
|
+
hasReachedEnd.value = next == null
|
|
528
|
+
// Diagnostics: check incoming initial items
|
|
529
|
+
checkItemDimensions(items as any[], 'init')
|
|
530
|
+
|
|
531
|
+
if (useSwipeMode.value) {
|
|
532
|
+
// In swipe mode, just add items without layout calculation
|
|
533
|
+
masonry.value = [...(masonry.value as any[]), ...items]
|
|
534
|
+
// Reset swipe index if we're at the start
|
|
535
|
+
if (currentSwipeIndex.value === 0 && masonry.value.length > 0) {
|
|
536
|
+
swipeOffset.value = 0
|
|
537
|
+
}
|
|
538
|
+
} else {
|
|
539
|
+
refreshLayout([...(masonry.value as any[]), ...items])
|
|
540
|
+
|
|
541
|
+
// Update viewport state from container's scroll position
|
|
542
|
+
// Critical after refresh when browser may restore scroll position
|
|
543
|
+
if (container.value) {
|
|
544
|
+
viewportTop.value = container.value.scrollTop
|
|
545
|
+
viewportHeight.value = container.value.clientHeight || window.innerHeight
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Update again after DOM updates to catch browser scroll restoration
|
|
549
|
+
// The debounced scroll handler will also catch any scroll changes
|
|
550
|
+
nextTick(() => {
|
|
551
|
+
if (container.value) {
|
|
552
|
+
viewportTop.value = container.value.scrollTop
|
|
553
|
+
viewportHeight.value = container.value.clientHeight || window.innerHeight
|
|
554
|
+
updateScrollProgress()
|
|
555
|
+
}
|
|
556
|
+
})
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Restore items when skipInitialLoad is true.
|
|
562
|
+
* This method should be called instead of directly assigning to v-model:items
|
|
563
|
+
* when restoring items from saved state.
|
|
564
|
+
* @param items - Items to restore
|
|
565
|
+
* @param page - Current page number/cursor
|
|
566
|
+
* @param next - Next page cursor (or null if at end)
|
|
567
|
+
*/
|
|
568
|
+
async function restoreItems(items: any[], page: any, next: any) {
|
|
569
|
+
// If skipInitialLoad is false, fall back to init behavior
|
|
570
|
+
if (!props.skipInitialLoad) {
|
|
571
|
+
init(items, page, next)
|
|
572
|
+
return
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// When skipInitialLoad is true, we need to restore items without triggering initial load
|
|
576
|
+
currentPage.value = page
|
|
577
|
+
paginationHistory.value = [page]
|
|
578
|
+
if (next !== null && next !== undefined) {
|
|
579
|
+
paginationHistory.value.push(next)
|
|
580
|
+
}
|
|
581
|
+
// Only set hasReachedEnd to true if next is explicitly null (end of list)
|
|
582
|
+
// undefined means "unknown" - don't assume end of list
|
|
583
|
+
hasReachedEnd.value = next === null
|
|
584
|
+
loadError.value = null
|
|
585
|
+
|
|
586
|
+
// Diagnostics: check incoming items
|
|
587
|
+
checkItemDimensions(items as any[], 'restoreItems')
|
|
588
|
+
|
|
589
|
+
// Set items directly (v-model will sync) and refresh layout
|
|
590
|
+
// Follow the same pattern as init() and getContent()
|
|
591
|
+
if (useSwipeMode.value) {
|
|
592
|
+
// In swipe mode, just set items without layout calculation
|
|
593
|
+
masonry.value = items
|
|
594
|
+
// Reset swipe index if we're at the start
|
|
595
|
+
if (currentSwipeIndex.value === 0 && masonry.value.length > 0) {
|
|
596
|
+
swipeOffset.value = 0
|
|
597
|
+
}
|
|
598
|
+
} else {
|
|
599
|
+
// In masonry mode, refresh layout with the restored items
|
|
600
|
+
refreshLayout(items)
|
|
601
|
+
|
|
602
|
+
// Update viewport state from container's scroll position
|
|
603
|
+
if (container.value) {
|
|
604
|
+
viewportTop.value = container.value.scrollTop
|
|
605
|
+
viewportHeight.value = container.value.clientHeight || window.innerHeight
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Update again after DOM updates to catch browser scroll restoration
|
|
609
|
+
await nextTick()
|
|
610
|
+
if (container.value) {
|
|
611
|
+
viewportTop.value = container.value.scrollTop
|
|
612
|
+
viewportHeight.value = container.value.clientHeight || window.innerHeight
|
|
613
|
+
updateScrollProgress()
|
|
614
|
+
|
|
615
|
+
// Check if user is already at the bottom after restoration
|
|
616
|
+
// If so, trigger loading to restore scroll-to-bottom functionality
|
|
617
|
+
// Wait for layout to be fully calculated before checking
|
|
618
|
+
await nextTick()
|
|
619
|
+
const columnHeights = calculateColumnHeights(masonry.value as any, columns.value)
|
|
620
|
+
const tallest = columnHeights.length ? Math.max(...columnHeights) : 0
|
|
621
|
+
const scrollerBottom = container.value.scrollTop + container.value.clientHeight
|
|
622
|
+
const threshold = typeof props.loadThresholdPx === 'number' ? props.loadThresholdPx : 200
|
|
623
|
+
const triggerPoint = threshold >= 0
|
|
624
|
+
? Math.max(0, tallest - threshold)
|
|
625
|
+
: Math.max(0, tallest + threshold)
|
|
626
|
+
const nearBottom = scrollerBottom >= triggerPoint
|
|
627
|
+
|
|
628
|
+
// If user is at bottom and there's a next page, trigger loading
|
|
629
|
+
// This restores scroll-to-bottom functionality after tab restoration
|
|
630
|
+
if (nearBottom && !hasReachedEnd.value && !isLoading.value && paginationHistory.value.length > 0) {
|
|
631
|
+
const nextPage = paginationHistory.value[paginationHistory.value.length - 1]
|
|
632
|
+
if (nextPage != null) {
|
|
633
|
+
// Use handleScroll with forceCheck=true to bypass isScrollingDown check
|
|
634
|
+
await handleScroll(columnHeights, true)
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Watch for layout changes and update columns + refresh layout dynamically
|
|
642
|
+
watch(
|
|
643
|
+
layout,
|
|
644
|
+
() => {
|
|
645
|
+
if (useSwipeMode.value) {
|
|
646
|
+
// In swipe mode, no layout recalculation needed
|
|
647
|
+
return
|
|
648
|
+
}
|
|
649
|
+
if (container.value) {
|
|
650
|
+
columns.value = getColumnCount(layout.value as any, containerWidth.value)
|
|
651
|
+
refreshLayout(masonry.value as any)
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
{ deep: true }
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
// Watch for layout-mode prop changes to ensure proper mode switching
|
|
658
|
+
watch(() => props.layoutMode, () => {
|
|
659
|
+
// Force update containerWidth when layout-mode changes to ensure useSwipeMode computes correctly
|
|
660
|
+
if (fixedDimensions.value && fixedDimensions.value.width !== undefined) {
|
|
661
|
+
containerWidth.value = fixedDimensions.value.width
|
|
662
|
+
} else if (wrapper.value) {
|
|
663
|
+
containerWidth.value = wrapper.value.clientWidth
|
|
664
|
+
}
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
// Watch container element to attach scroll listener when available
|
|
668
|
+
watch(container, (el) => {
|
|
669
|
+
if (el && !useSwipeMode.value) {
|
|
670
|
+
// Attach scroll listener for masonry mode
|
|
671
|
+
el.removeEventListener('scroll', debouncedScrollHandler) // Just in case
|
|
672
|
+
el.addEventListener('scroll', debouncedScrollHandler, { passive: true })
|
|
673
|
+
} else if (el) {
|
|
674
|
+
// Remove scroll listener if switching to swipe mode
|
|
675
|
+
el.removeEventListener('scroll', debouncedScrollHandler)
|
|
676
|
+
}
|
|
677
|
+
}, { immediate: true })
|
|
678
|
+
|
|
679
|
+
// Watch for items when skipInitialLoad is true to auto-initialize pagination state
|
|
680
|
+
// This handles cases where items are provided after mount or updated externally
|
|
681
|
+
let hasInitializedWithItems = false
|
|
682
|
+
watch(
|
|
683
|
+
() => [props.items, props.skipInitialLoad, props.initialPage, props.initialNextPage] as const,
|
|
684
|
+
([items, skipInitialLoad, initialPage, initialNextPage]) => {
|
|
685
|
+
// Only auto-initialize if:
|
|
686
|
+
// 1. skipInitialLoad is true
|
|
687
|
+
// 2. Items exist
|
|
688
|
+
// 3. We haven't already initialized with items (to avoid re-initializing on every update)
|
|
689
|
+
if (
|
|
690
|
+
skipInitialLoad &&
|
|
691
|
+
items &&
|
|
692
|
+
items.length > 0 &&
|
|
693
|
+
!hasInitializedWithItems
|
|
694
|
+
) {
|
|
695
|
+
hasInitializedWithItems = true
|
|
696
|
+
const page = initialPage !== null && initialPage !== undefined
|
|
697
|
+
? initialPage
|
|
698
|
+
: (props.loadAtPage as any)
|
|
699
|
+
const next = initialNextPage !== undefined
|
|
700
|
+
? initialNextPage
|
|
701
|
+
: undefined // undefined means "unknown", null means "end of list"
|
|
702
|
+
|
|
703
|
+
restoreItems(items as any[], page, next)
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
{ immediate: false }
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
// Watch for swipe mode changes to refresh layout and setup/teardown handlers
|
|
710
|
+
watch(useSwipeMode, (newValue, oldValue) => {
|
|
711
|
+
// Skip if this is the initial watch call and values are the same
|
|
712
|
+
if (oldValue === undefined && newValue === false) return
|
|
713
|
+
|
|
714
|
+
nextTick(() => {
|
|
715
|
+
if (newValue) {
|
|
716
|
+
// Switching to Swipe Mode
|
|
717
|
+
document.addEventListener('mousemove', handleMouseMove)
|
|
718
|
+
document.addEventListener('mouseup', handleMouseUp)
|
|
719
|
+
|
|
720
|
+
// Remove scroll listener
|
|
721
|
+
if (container.value) {
|
|
722
|
+
container.value.removeEventListener('scroll', debouncedScrollHandler)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Reset index if needed
|
|
726
|
+
currentSwipeIndex.value = 0
|
|
727
|
+
swipeOffset.value = 0
|
|
728
|
+
if (masonry.value.length > 0) {
|
|
729
|
+
snapToCurrentItem()
|
|
730
|
+
}
|
|
731
|
+
} else {
|
|
732
|
+
// Switching to Masonry Mode
|
|
733
|
+
document.removeEventListener('mousemove', handleMouseMove)
|
|
734
|
+
document.removeEventListener('mouseup', handleMouseUp)
|
|
735
|
+
|
|
736
|
+
if (container.value && wrapper.value) {
|
|
737
|
+
// Ensure containerWidth is up to date - use fixed dimensions if set
|
|
738
|
+
if (fixedDimensions.value && fixedDimensions.value.width !== undefined) {
|
|
739
|
+
containerWidth.value = fixedDimensions.value.width
|
|
740
|
+
} else {
|
|
741
|
+
containerWidth.value = wrapper.value.clientWidth
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Attach scroll listener (container watcher will handle this, but ensure it's attached)
|
|
745
|
+
container.value.removeEventListener('scroll', debouncedScrollHandler) // Just in case
|
|
746
|
+
container.value.addEventListener('scroll', debouncedScrollHandler, { passive: true })
|
|
747
|
+
|
|
748
|
+
// Refresh layout with updated width
|
|
749
|
+
if (masonry.value.length > 0) {
|
|
750
|
+
columns.value = getColumnCount(layout.value as any, containerWidth.value)
|
|
751
|
+
refreshLayout(masonry.value as any)
|
|
752
|
+
|
|
753
|
+
// Update viewport state
|
|
754
|
+
viewportTop.value = container.value.scrollTop
|
|
755
|
+
viewportHeight.value = container.value.clientHeight
|
|
756
|
+
updateScrollProgress()
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
})
|
|
761
|
+
}, { immediate: true })
|
|
762
|
+
|
|
763
|
+
// Watch for swipe container element to attach touch listeners
|
|
764
|
+
watch(swipeContainer, (el) => {
|
|
765
|
+
if (el) {
|
|
766
|
+
el.addEventListener('touchstart', handleTouchStart, { passive: false })
|
|
767
|
+
el.addEventListener('touchmove', handleTouchMove, { passive: false })
|
|
768
|
+
el.addEventListener('touchend', handleTouchEnd)
|
|
769
|
+
el.addEventListener('mousedown', handleMouseDown)
|
|
770
|
+
}
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
// Watch for items changes in swipe mode to reset index if needed
|
|
774
|
+
watch(() => masonry.value.length, (newLength, oldLength) => {
|
|
775
|
+
if (useSwipeMode.value && newLength > 0 && oldLength === 0) {
|
|
776
|
+
// First items loaded, ensure we're at index 0
|
|
777
|
+
currentSwipeIndex.value = 0
|
|
778
|
+
nextTick(() => snapToCurrentItem())
|
|
779
|
+
}
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
// Watch wrapper element to setup ResizeObserver for container width
|
|
783
|
+
watch(wrapper, (el) => {
|
|
784
|
+
if (resizeObserver) {
|
|
785
|
+
resizeObserver.disconnect()
|
|
786
|
+
resizeObserver = null
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (el && typeof ResizeObserver !== 'undefined') {
|
|
790
|
+
resizeObserver = new ResizeObserver((entries) => {
|
|
791
|
+
// Skip updates if fixed dimensions are set
|
|
792
|
+
if (fixedDimensions.value) return
|
|
793
|
+
|
|
794
|
+
for (const entry of entries) {
|
|
795
|
+
const newWidth = entry.contentRect.width
|
|
796
|
+
const newHeight = entry.contentRect.height
|
|
797
|
+
if (containerWidth.value !== newWidth) {
|
|
798
|
+
containerWidth.value = newWidth
|
|
799
|
+
}
|
|
800
|
+
if (containerHeight.value !== newHeight) {
|
|
801
|
+
containerHeight.value = newHeight
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
})
|
|
805
|
+
resizeObserver.observe(el)
|
|
806
|
+
// Initial dimensions (only if not fixed)
|
|
807
|
+
if (!fixedDimensions.value) {
|
|
808
|
+
containerWidth.value = el.clientWidth
|
|
809
|
+
containerHeight.value = el.clientHeight
|
|
810
|
+
}
|
|
811
|
+
} else if (el) {
|
|
812
|
+
// Fallback if ResizeObserver not available
|
|
813
|
+
if (!fixedDimensions.value) {
|
|
814
|
+
containerWidth.value = el.clientWidth
|
|
815
|
+
containerHeight.value = el.clientHeight
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}, { immediate: true })
|
|
819
|
+
|
|
820
|
+
// Watch containerWidth changes to refresh layout in masonry mode
|
|
821
|
+
watch(containerWidth, (newWidth, oldWidth) => {
|
|
822
|
+
if (newWidth !== oldWidth && newWidth > 0 && !useSwipeMode.value && container.value && masonry.value.length > 0) {
|
|
823
|
+
// Use nextTick to ensure DOM has updated
|
|
824
|
+
nextTick(() => {
|
|
825
|
+
columns.value = getColumnCount(layout.value as any, newWidth)
|
|
826
|
+
refreshLayout(masonry.value as any)
|
|
827
|
+
updateScrollProgress()
|
|
828
|
+
})
|
|
829
|
+
}
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
onMounted(async () => {
|
|
833
|
+
try {
|
|
834
|
+
// Wait for next tick to ensure wrapper is mounted
|
|
835
|
+
await nextTick()
|
|
836
|
+
|
|
837
|
+
// Container dimensions are managed by ResizeObserver
|
|
838
|
+
// Only set initial values if ResizeObserver isn't available
|
|
839
|
+
if (wrapper.value && !resizeObserver) {
|
|
840
|
+
containerWidth.value = wrapper.value.clientWidth
|
|
841
|
+
containerHeight.value = wrapper.value.clientHeight
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (!useSwipeMode.value) {
|
|
845
|
+
columns.value = getColumnCount(layout.value as any, containerWidth.value)
|
|
846
|
+
if (container.value) {
|
|
847
|
+
viewportTop.value = container.value.scrollTop
|
|
848
|
+
viewportHeight.value = container.value.clientHeight
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const initialPage = props.loadAtPage as any
|
|
853
|
+
paginationHistory.value = [initialPage]
|
|
854
|
+
|
|
855
|
+
if (!props.skipInitialLoad) {
|
|
856
|
+
await loadPage(paginationHistory.value[0] as any)
|
|
857
|
+
} else if (props.items && props.items.length > 0) {
|
|
858
|
+
// When skipInitialLoad is true and items are provided, initialize pagination state
|
|
859
|
+
// Use initialPage/initialNextPage props if provided, otherwise use loadAtPage
|
|
860
|
+
// Only set next to null if initialNextPage is explicitly null (not undefined)
|
|
861
|
+
const page = props.initialPage !== null && props.initialPage !== undefined
|
|
862
|
+
? props.initialPage
|
|
863
|
+
: (props.loadAtPage as any)
|
|
864
|
+
const next = props.initialNextPage !== undefined
|
|
865
|
+
? props.initialNextPage
|
|
866
|
+
: undefined // undefined means "unknown", null means "end of list"
|
|
867
|
+
|
|
868
|
+
await restoreItems(props.items as any[], page, next)
|
|
869
|
+
// Mark as initialized to prevent watcher from running again
|
|
870
|
+
hasInitializedWithItems = true
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (!useSwipeMode.value) {
|
|
874
|
+
updateScrollProgress()
|
|
875
|
+
} else {
|
|
876
|
+
// In swipe mode, snap to first item
|
|
877
|
+
nextTick(() => snapToCurrentItem())
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
} catch (error) {
|
|
881
|
+
// If error is from loadPage, it's already handled via loadError
|
|
882
|
+
// Only log truly unexpected initialization errors
|
|
883
|
+
if (!loadError.value) {
|
|
884
|
+
console.error('Error during component initialization:', error)
|
|
885
|
+
// Set loadError for unexpected errors too
|
|
886
|
+
loadError.value = normalizeError(error)
|
|
887
|
+
}
|
|
888
|
+
isLoading.value = false
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Scroll listener is handled by watcher now for consistency
|
|
892
|
+
window.addEventListener('resize', debouncedResizeHandler)
|
|
893
|
+
window.addEventListener('resize', handleWindowResize)
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
onUnmounted(() => {
|
|
897
|
+
if (resizeObserver) {
|
|
898
|
+
resizeObserver.disconnect()
|
|
899
|
+
resizeObserver = null
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
container.value?.removeEventListener('scroll', debouncedScrollHandler)
|
|
903
|
+
window.removeEventListener('resize', debouncedResizeHandler)
|
|
904
|
+
window.removeEventListener('resize', handleWindowResize)
|
|
905
|
+
|
|
906
|
+
if (swipeContainer.value) {
|
|
907
|
+
swipeContainer.value.removeEventListener('touchstart', handleTouchStart)
|
|
908
|
+
swipeContainer.value.removeEventListener('touchmove', handleTouchMove)
|
|
909
|
+
swipeContainer.value.removeEventListener('touchend', handleTouchEnd)
|
|
910
|
+
swipeContainer.value.removeEventListener('mousedown', handleMouseDown)
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Clean up mouse handlers
|
|
914
|
+
document.removeEventListener('mousemove', handleMouseMove)
|
|
915
|
+
document.removeEventListener('mouseup', handleMouseUp)
|
|
916
|
+
})
|
|
917
|
+
</script>
|
|
918
|
+
|
|
919
|
+
<template>
|
|
920
|
+
<div ref="wrapper" class="w-full h-full flex flex-col relative">
|
|
921
|
+
<!-- Swipe Feed Mode (Mobile/Tablet) -->
|
|
922
|
+
<div v-if="useSwipeMode" class="overflow-hidden w-full flex-1 swipe-container touch-none select-none"
|
|
923
|
+
:class="{ 'force-motion': props.forceMotion, 'cursor-grab': !isDragging, 'cursor-grabbing': isDragging }"
|
|
924
|
+
ref="swipeContainer" style="height: 100%; max-height: 100%; position: relative;">
|
|
925
|
+
<div class="relative w-full" :style="{
|
|
926
|
+
transform: `translateY(${swipeOffset}px)`,
|
|
927
|
+
transition: isDragging ? 'none' : `transform ${transitionDurationMs}ms ${transitionEasing}`,
|
|
928
|
+
height: `${masonry.length * 100}%`
|
|
929
|
+
}">
|
|
930
|
+
<div v-for="(item, index) in masonry" :key="`${item.page}-${item.id}`" class="absolute top-0 left-0 w-full"
|
|
931
|
+
:style="{
|
|
932
|
+
top: `${index * (100 / masonry.length)}%`,
|
|
933
|
+
height: `${100 / masonry.length}%`
|
|
934
|
+
}">
|
|
935
|
+
<div class="w-full h-full flex items-center justify-center p-4">
|
|
936
|
+
<div class="w-full h-full max-w-full max-h-full relative">
|
|
937
|
+
<slot :item="item" :remove="remove" :index="item.originalIndex ?? masonry.indexOf(item)">
|
|
938
|
+
<MasonryItem :item="item" :remove="remove" :header-height="layout.header" :footer-height="layout.footer"
|
|
939
|
+
:in-swipe-mode="true" :is-active="index === currentSwipeIndex"
|
|
940
|
+
@preload:success="(p) => emits('item:preload:success', p)"
|
|
941
|
+
@preload:error="(p) => emits('item:preload:error', p)"
|
|
942
|
+
@mouse-enter="(p) => emits('item:mouse-enter', p)" @mouse-leave="(p) => emits('item:mouse-leave', p)">
|
|
943
|
+
<!-- Pass through header and footer slots to MasonryItem -->
|
|
944
|
+
<template #header="slotProps">
|
|
945
|
+
<slot name="item-header" v-bind="slotProps" />
|
|
946
|
+
</template>
|
|
947
|
+
<template #footer="slotProps">
|
|
948
|
+
<slot name="item-footer" v-bind="slotProps" />
|
|
949
|
+
</template>
|
|
950
|
+
</MasonryItem>
|
|
951
|
+
</slot>
|
|
952
|
+
</div>
|
|
953
|
+
</div>
|
|
954
|
+
</div>
|
|
955
|
+
</div>
|
|
956
|
+
<!-- End of list message for swipe mode -->
|
|
957
|
+
<div v-if="hasReachedEnd && masonry.length > 0" class="w-full py-8 text-center">
|
|
958
|
+
<slot name="end-message">
|
|
959
|
+
<p class="text-gray-500 dark:text-gray-400">You've reached the end</p>
|
|
960
|
+
</slot>
|
|
961
|
+
</div>
|
|
962
|
+
<!-- Error message for swipe mode -->
|
|
963
|
+
<div v-if="loadError && masonry.length > 0" class="w-full py-8 text-center">
|
|
964
|
+
<slot name="error-message" :error="loadError">
|
|
965
|
+
<p class="text-red-500 dark:text-red-400">Failed to load content: {{ loadError.message }}</p>
|
|
966
|
+
</slot>
|
|
967
|
+
</div>
|
|
968
|
+
</div>
|
|
969
|
+
|
|
970
|
+
<!-- Masonry Grid Mode (Desktop) -->
|
|
971
|
+
<div v-else class="overflow-auto w-full flex-1 masonry-container" :class="{ 'force-motion': props.forceMotion }"
|
|
972
|
+
ref="container">
|
|
973
|
+
<div class="relative"
|
|
974
|
+
:style="{ height: `${masonryContentHeight}px`, '--masonry-duration': `${transitionDurationMs}ms`, '--masonry-leave-duration': `${leaveDurationMs}ms`, '--masonry-ease': transitionEasing }">
|
|
975
|
+
<transition-group name="masonry" :css="false" @enter="enter" @before-enter="beforeEnter" @leave="leave"
|
|
976
|
+
@before-leave="beforeLeave">
|
|
977
|
+
<div v-for="(item, i) in visibleMasonry" :key="`${item.page}-${item.id}`" class="absolute masonry-item"
|
|
978
|
+
v-bind="getItemAttributes(item, i)">
|
|
979
|
+
<!-- Use default slot if provided, otherwise use MasonryItem -->
|
|
980
|
+
<slot :item="item" :remove="remove" :index="item.originalIndex ?? masonry.indexOf(item)">
|
|
981
|
+
<MasonryItem :item="item" :remove="remove" :header-height="layout.header" :footer-height="layout.footer"
|
|
982
|
+
:in-swipe-mode="false" :is-active="false" @preload:success="(p) => emits('item:preload:success', p)"
|
|
983
|
+
@preload:error="(p) => emits('item:preload:error', p)"
|
|
984
|
+
@mouse-enter="(p) => emits('item:mouse-enter', p)" @mouse-leave="(p) => emits('item:mouse-leave', p)">
|
|
985
|
+
<!-- Pass through header and footer slots to MasonryItem -->
|
|
986
|
+
<template #header="slotProps">
|
|
987
|
+
<slot name="item-header" v-bind="slotProps" />
|
|
988
|
+
</template>
|
|
989
|
+
<template #footer="slotProps">
|
|
990
|
+
<slot name="item-footer" v-bind="slotProps" />
|
|
991
|
+
</template>
|
|
992
|
+
</MasonryItem>
|
|
993
|
+
</slot>
|
|
994
|
+
</div>
|
|
995
|
+
</transition-group>
|
|
996
|
+
</div>
|
|
997
|
+
<!-- End of list message -->
|
|
998
|
+
<div v-if="hasReachedEnd && masonry.length > 0" class="w-full py-8 text-center">
|
|
999
|
+
<slot name="end-message">
|
|
1000
|
+
<p class="text-gray-500 dark:text-gray-400">You've reached the end</p>
|
|
1001
|
+
</slot>
|
|
1002
|
+
</div>
|
|
1003
|
+
<!-- Error message -->
|
|
1004
|
+
<div v-if="loadError && masonry.length > 0" class="w-full py-8 text-center">
|
|
1005
|
+
<slot name="error-message" :error="loadError">
|
|
1006
|
+
<p class="text-red-500 dark:text-red-400">Failed to load content: {{ loadError.message }}</p>
|
|
1007
|
+
</slot>
|
|
1008
|
+
</div>
|
|
1009
|
+
</div>
|
|
1010
|
+
</div>
|
|
1011
|
+
</template>
|
|
1012
|
+
|
|
1013
|
+
<style scoped>
|
|
1014
|
+
.masonry-container {
|
|
1015
|
+
overflow-anchor: none;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
.masonry-item {
|
|
1019
|
+
will-change: transform, opacity;
|
|
1020
|
+
contain: layout paint;
|
|
1021
|
+
transition: transform var(--masonry-duration, 450ms) var(--masonry-ease, cubic-bezier(.22, .61, .36, 1)),
|
|
1022
|
+
opacity var(--masonry-leave-duration, 160ms) ease-out var(--masonry-opacity-delay, 0ms);
|
|
1023
|
+
backface-visibility: hidden;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
.masonry-move {
|
|
1027
|
+
transition: transform var(--masonry-duration, 450ms) var(--masonry-ease, cubic-bezier(.22, .61, .36, 1));
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1031
|
+
|
|
1032
|
+
.masonry-container:not(.force-motion) .masonry-item,
|
|
1033
|
+
.masonry-container:not(.force-motion) .masonry-move {
|
|
1034
|
+
transition-duration: 1ms !important;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
</style>
|