@fy-/fws-vue 2.4.2 → 2.4.4
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/components/ui/DefaultGallery.vue +73 -2
- package/index.ts +30 -3
- package/package.json +1 -1
|
@@ -18,13 +18,20 @@ import {
|
|
|
18
18
|
useWindowSize,
|
|
19
19
|
} from '@vueuse/core'
|
|
20
20
|
import { computed, h, nextTick, onMounted, onUnmounted, reactive, ref, shallowRef, watch } from 'vue'
|
|
21
|
+
import { useRoute } from 'vue-router'
|
|
21
22
|
import { useEventBus } from '../../composables/event-bus'
|
|
23
|
+
import { useServerRouter } from '../../stores/serverRouter'
|
|
22
24
|
import { ClientOnly } from '../ssr/ClientOnly'
|
|
23
25
|
import DefaultPaging from './DefaultPaging.vue'
|
|
24
26
|
|
|
25
27
|
// Core state
|
|
26
28
|
const isGalleryOpen = ref<boolean>(false)
|
|
27
29
|
const eventBus = useEventBus()
|
|
30
|
+
const route = useRoute()
|
|
31
|
+
const history = useServerRouter()
|
|
32
|
+
// Cross-page navigation: set while a prev/next page change is in flight
|
|
33
|
+
const pendingPageFlip = ref<'next' | 'prev' | null>(null)
|
|
34
|
+
let pendingPageFlipTimeout: number | null = null
|
|
28
35
|
const sidePanel = ref<boolean>(true)
|
|
29
36
|
const showControls = ref<boolean>(true)
|
|
30
37
|
const isFullscreen = ref<boolean>(false)
|
|
@@ -322,6 +329,13 @@ function setModal(value: boolean) {
|
|
|
322
329
|
clearTimeout(controlsTimeout)
|
|
323
330
|
controlsTimeout = null
|
|
324
331
|
}
|
|
332
|
+
|
|
333
|
+
// Abort any in-flight cross-page navigation
|
|
334
|
+
pendingPageFlip.value = null
|
|
335
|
+
if (pendingPageFlipTimeout) {
|
|
336
|
+
clearTimeout(pendingPageFlipTimeout)
|
|
337
|
+
pendingPageFlipTimeout = null
|
|
338
|
+
}
|
|
325
339
|
}
|
|
326
340
|
isGalleryOpen.value = value
|
|
327
341
|
showControls.value = true
|
|
@@ -343,12 +357,60 @@ const openGalleryImage = useDebounceFn((index: number | undefined) => {
|
|
|
343
357
|
})
|
|
344
358
|
}, 50)
|
|
345
359
|
|
|
360
|
+
// Cross-page navigation: push the new page into the route query, exactly like
|
|
361
|
+
// DefaultPaging does. The consumer's paging watchers react to the route change
|
|
362
|
+
// and refetch, replacing `images` — the watcher below then lands the index.
|
|
363
|
+
function goToGalleryPage(page: number) {
|
|
364
|
+
const newQuery = { ...route.query }
|
|
365
|
+
newQuery.page = page.toString()
|
|
366
|
+
history.push({
|
|
367
|
+
path: history.currentRoute.path,
|
|
368
|
+
query: newQuery,
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function startPageFlip(dir: 'next' | 'prev', page: number) {
|
|
373
|
+
pendingPageFlip.value = dir
|
|
374
|
+
// Safety: if the refetch never lands (network error), unlock navigation
|
|
375
|
+
if (pendingPageFlipTimeout) clearTimeout(pendingPageFlipTimeout)
|
|
376
|
+
pendingPageFlipTimeout = window.setTimeout(() => {
|
|
377
|
+
pendingPageFlip.value = null
|
|
378
|
+
}, 5000)
|
|
379
|
+
goToGalleryPage(page)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// When the images array is replaced while a page flip is pending, land on the
|
|
383
|
+
// first image (next) or last image (prev) of the new page. Default pre-flush
|
|
384
|
+
// watcher: runs before render, so the old index never flashes.
|
|
385
|
+
watch(
|
|
386
|
+
() => props.images,
|
|
387
|
+
(newImages) => {
|
|
388
|
+
if (!pendingPageFlip.value) return
|
|
389
|
+
if (newImages && newImages.length > 0) {
|
|
390
|
+
modelValue.value = pendingPageFlip.value === 'next' ? 0 : newImages.length - 1
|
|
391
|
+
}
|
|
392
|
+
pendingPageFlip.value = null
|
|
393
|
+
if (pendingPageFlipTimeout) {
|
|
394
|
+
clearTimeout(pendingPageFlipTimeout)
|
|
395
|
+
pendingPageFlipTimeout = null
|
|
396
|
+
}
|
|
397
|
+
nextTick(() => {
|
|
398
|
+
updateImageSizes()
|
|
399
|
+
})
|
|
400
|
+
},
|
|
401
|
+
)
|
|
402
|
+
|
|
346
403
|
// Navigation functions
|
|
347
404
|
function goNextImage() {
|
|
348
405
|
direction.value = 'next'
|
|
406
|
+
if (pendingPageFlip.value) return // page change in flight — ignore extra input
|
|
349
407
|
if (modelValue.value < props.images.length - 1) {
|
|
350
408
|
modelValue.value++
|
|
351
409
|
}
|
|
410
|
+
else if (props.paging && props.paging.page_no < props.paging.page_max) {
|
|
411
|
+
// Last image of the page → go to the first image of the next page
|
|
412
|
+
startPageFlip('next', props.paging.page_no + 1)
|
|
413
|
+
}
|
|
352
414
|
else {
|
|
353
415
|
modelValue.value = 0
|
|
354
416
|
}
|
|
@@ -361,9 +423,14 @@ function goNextImage() {
|
|
|
361
423
|
|
|
362
424
|
function goPrevImage() {
|
|
363
425
|
direction.value = 'prev'
|
|
426
|
+
if (pendingPageFlip.value) return // page change in flight — ignore extra input
|
|
364
427
|
if (modelValue.value > 0) {
|
|
365
428
|
modelValue.value--
|
|
366
429
|
}
|
|
430
|
+
else if (props.paging && props.paging.page_no > 1) {
|
|
431
|
+
// First image of the page → go to the last image of the previous page
|
|
432
|
+
startPageFlip('prev', props.paging.page_no - 1)
|
|
433
|
+
}
|
|
367
434
|
else {
|
|
368
435
|
modelValue.value = props.images.length - 1 > 0 ? props.images.length - 1 : 0
|
|
369
436
|
}
|
|
@@ -599,6 +666,10 @@ onUnmounted(() => {
|
|
|
599
666
|
clearTimeout(fullscreenResizeTimeout)
|
|
600
667
|
}
|
|
601
668
|
|
|
669
|
+
if (pendingPageFlipTimeout) {
|
|
670
|
+
clearTimeout(pendingPageFlipTimeout)
|
|
671
|
+
}
|
|
672
|
+
|
|
602
673
|
// Ensure we exit fullscreen mode on unmount
|
|
603
674
|
if (isFullscreen.value) {
|
|
604
675
|
exitFullscreen().catch(() => {})
|
|
@@ -698,7 +769,7 @@ onUnmounted(() => {
|
|
|
698
769
|
leave-to-class="opacity-0"
|
|
699
770
|
>
|
|
700
771
|
<div
|
|
701
|
-
v-if="showControls && images.length > 1"
|
|
772
|
+
v-if="showControls && (images.length > 1 || (paging && paging.page_max > 1))"
|
|
702
773
|
class="absolute left-0 z-40 h-full flex items-center px-2 md:px-4"
|
|
703
774
|
>
|
|
704
775
|
<button
|
|
@@ -772,7 +843,7 @@ onUnmounted(() => {
|
|
|
772
843
|
leave-to-class="opacity-0"
|
|
773
844
|
>
|
|
774
845
|
<div
|
|
775
|
-
v-if="showControls && images.length > 1"
|
|
846
|
+
v-if="showControls && (images.length > 1 || (paging && paging.page_max > 1))"
|
|
776
847
|
class="absolute right-0 z-40 h-full flex items-center px-2 md:px-4"
|
|
777
848
|
:class="{ 'lg:mr-64': sidePanel }"
|
|
778
849
|
>
|
package/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { TFunction } from 'i18next'
|
|
1
2
|
import type { Emitter } from 'mitt'
|
|
2
3
|
import type { App, Plugin } from 'vue'
|
|
3
4
|
import type { Events } from './composables/event-bus'
|
|
4
5
|
import i18next from 'i18next'
|
|
5
6
|
import mitt from 'mitt'
|
|
7
|
+
import { shallowRef } from 'vue'
|
|
6
8
|
import CmsArticleBoxed from './components/fws/CmsArticleBoxed.vue'
|
|
7
9
|
import CmsArticleSingle from './components/fws/CmsArticleSingle.vue'
|
|
8
10
|
import DataTable from './components/fws/DataTable.vue'
|
|
@@ -70,15 +72,40 @@ function createFWS(): Plugin {
|
|
|
70
72
|
// @ts-expect-error: Emitter<Events> is not assignable to Emitter<Events>
|
|
71
73
|
const eventBus: Emitter<Events> = mitt<Events>()
|
|
72
74
|
|
|
75
|
+
// i18next's `t` is NOT reactive. If a component renders a key during a window
|
|
76
|
+
// where the resource isn't loaded yet (cold deep-link, client-nav before a late
|
|
77
|
+
// locale fetch, hydration race) it paints the RAW token and Vue never re-renders
|
|
78
|
+
// — nothing reactive changed, so the miss sticks. Wrap `t` so it depends on a
|
|
79
|
+
// tick bumped by i18next's own events: any raw key then self-heals the instant
|
|
80
|
+
// resources land, and `changeLanguage` live-updates every string. Client-only:
|
|
81
|
+
// on the server strings are already resolved by the `await i18nextPromise` before
|
|
82
|
+
// render, and `install()` runs per-request there — adding listeners to the global
|
|
83
|
+
// i18next singleton each request would leak. So SSR keeps the plain `t`.
|
|
84
|
+
let translate: TFunction = i18next.t
|
|
85
|
+
if (typeof window !== 'undefined') {
|
|
86
|
+
const tick = shallowRef(0)
|
|
87
|
+
const bump = (): void => {
|
|
88
|
+
tick.value++
|
|
89
|
+
}
|
|
90
|
+
i18next.on('loaded', bump)
|
|
91
|
+
i18next.on('added', bump)
|
|
92
|
+
i18next.on('languageChanged', bump)
|
|
93
|
+
const rawT = i18next.t as unknown as (...a: unknown[]) => unknown
|
|
94
|
+
translate = ((...args: Parameters<typeof i18next.t>) => {
|
|
95
|
+
void tick.value
|
|
96
|
+
return rawT(...args)
|
|
97
|
+
}) as unknown as TFunction
|
|
98
|
+
}
|
|
99
|
+
|
|
73
100
|
return {
|
|
74
101
|
install(app: App) {
|
|
75
102
|
if (app.config.globalProperties) {
|
|
76
103
|
app.config.globalProperties.$eventBus = eventBus
|
|
77
104
|
app.provide('fwsVueEventBus', eventBus)
|
|
78
105
|
|
|
79
|
-
// i18next
|
|
80
|
-
app.config.globalProperties.$t =
|
|
81
|
-
app.provide('fwsVueTranslate',
|
|
106
|
+
// i18next (reactive wrapper — see note above)
|
|
107
|
+
app.config.globalProperties.$t = translate
|
|
108
|
+
app.provide('fwsVueTranslate', translate)
|
|
82
109
|
|
|
83
110
|
// Templating
|
|
84
111
|
app.config.globalProperties.$cropText = cropText
|