@fy-/fws-vue 2.3.11 → 2.3.12
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/DefaultBreadcrumb.vue +31 -11
- package/components/ui/DefaultConfirm.vue +6 -6
- package/components/ui/DefaultDateSelection.vue +7 -1
- package/components/ui/DefaultDropdown.vue +15 -23
- package/components/ui/DefaultDropdownLink.vue +19 -11
- package/components/ui/DefaultGallery.vue +130 -46
- package/components/ui/DefaultInput.vue +15 -9
- package/components/ui/DefaultLoader.vue +16 -7
- package/components/ui/DefaultModal.vue +46 -44
- package/components/ui/DefaultNotif.vue +77 -76
- package/components/ui/DefaultPaging.vue +88 -57
- package/components/ui/DefaultSidebar.vue +37 -9
- package/components/ui/DefaultTagInput.vue +86 -45
- package/composables/seo.ts +57 -92
- package/composables/templating.ts +75 -45
- package/composables/translations.ts +18 -2
- package/package.json +1 -1
- package/stores/user.ts +83 -59
|
@@ -4,6 +4,7 @@ import { getURL, stringHash } from '@fy-/fws-js'
|
|
|
4
4
|
import { ChevronRightIcon, HomeIcon } from '@heroicons/vue/24/solid'
|
|
5
5
|
import { defineBreadcrumb } from '@unhead/schema-org'
|
|
6
6
|
import { useSchemaOrg } from '@unhead/schema-org/vue'
|
|
7
|
+
import { computed } from 'vue'
|
|
7
8
|
|
|
8
9
|
const props = withDefaults(
|
|
9
10
|
defineProps<{
|
|
@@ -16,27 +17,46 @@ const props = withDefaults(
|
|
|
16
17
|
},
|
|
17
18
|
)
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
// Memoize URL computation
|
|
21
|
+
const baseUrl = computed(() => {
|
|
22
|
+
const url = getURL()
|
|
23
|
+
return {
|
|
24
|
+
host: url.Host,
|
|
25
|
+
scheme: url.Scheme,
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Memoize breadcrumb schema format to avoid recalculation
|
|
30
|
+
const breadcrumbsSchemaFormat = computed(() => props.nav.map((item, index) => {
|
|
31
|
+
if (!item.to) {
|
|
32
|
+
return {
|
|
33
|
+
'position': index + 1,
|
|
34
|
+
'name': item.name,
|
|
35
|
+
'@type': 'ListItem',
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const fullUrl = `${baseUrl.value.host}${item.to}`.replace(/\/\//g, '/')
|
|
23
40
|
return {
|
|
24
41
|
'position': index + 1,
|
|
25
42
|
'name': item.name,
|
|
26
|
-
'item':
|
|
27
|
-
? `${getURL().Scheme}://${fullUrl}`
|
|
28
|
-
: undefined,
|
|
43
|
+
'item': `${baseUrl.value.scheme}://${fullUrl}`,
|
|
29
44
|
'@type': 'ListItem',
|
|
30
45
|
}
|
|
31
|
-
})
|
|
32
|
-
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
// Cache breadcrumb ID to avoid string operations on every render
|
|
49
|
+
const breadcrumbId = computed(() => {
|
|
50
|
+
if (!props.nav.length) return ''
|
|
33
51
|
const chain = props.nav.map(item => item.name).join(' > ')
|
|
34
52
|
return stringHash(chain)
|
|
35
|
-
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Only run schema.org setup if we have breadcrumbs
|
|
36
56
|
if (props.nav && props.nav.length) {
|
|
37
57
|
useSchemaOrg([
|
|
38
58
|
defineBreadcrumb({
|
|
39
|
-
'@id': `#${
|
|
59
|
+
'@id': `#${breadcrumbId.value}`,
|
|
40
60
|
'itemListElement': breadcrumbsSchemaFormat,
|
|
41
61
|
}),
|
|
42
62
|
])
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { useDebounceFn } from '@vueuse/core'
|
|
2
3
|
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
|
3
4
|
import { useEventBus } from '../../composables/event-bus'
|
|
4
5
|
import DefaultModal from './DefaultModal.vue'
|
|
@@ -17,12 +18,12 @@ interface ConfirmModalData {
|
|
|
17
18
|
onConfirm: Function
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
const _onConfirm = useDebounceFn(async () => {
|
|
21
22
|
if (onConfirm.value) {
|
|
22
23
|
await onConfirm.value()
|
|
23
24
|
}
|
|
24
25
|
resetConfirm()
|
|
25
|
-
}
|
|
26
|
+
}, 300)
|
|
26
27
|
|
|
27
28
|
function resetConfirm() {
|
|
28
29
|
title.value = null
|
|
@@ -43,9 +44,8 @@ function showConfirm(data: ConfirmModalData) {
|
|
|
43
44
|
// Emit event first to ensure it's registered before opening the modal
|
|
44
45
|
eventBus.emit('confirmModal', true)
|
|
45
46
|
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
setTimeout(() => {
|
|
47
|
+
// Use requestAnimationFrame instead of setTimeout for better performance
|
|
48
|
+
requestAnimationFrame(() => {
|
|
49
49
|
isOpen.value = true
|
|
50
50
|
eventBus.emit('confirmModal', true)
|
|
51
51
|
|
|
@@ -57,7 +57,7 @@ function showConfirm(data: ConfirmModalData) {
|
|
|
57
57
|
catch {
|
|
58
58
|
}
|
|
59
59
|
})
|
|
60
|
-
}
|
|
60
|
+
})
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
onMounted(() => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
+
import { useDebounceFn } from '@vueuse/core'
|
|
2
3
|
import { computed } from 'vue'
|
|
3
4
|
import { DefaultInput } from '../..'
|
|
4
5
|
|
|
@@ -22,10 +23,15 @@ const props = withDefaults(
|
|
|
22
23
|
|
|
23
24
|
const emit = defineEmits(['update:modelValue'])
|
|
24
25
|
|
|
26
|
+
// Use debounced emitter to reduce update frequency
|
|
27
|
+
const emitUpdate = useDebounceFn((value: DateInterval) => {
|
|
28
|
+
emit('update:modelValue', value)
|
|
29
|
+
}, 150)
|
|
30
|
+
|
|
25
31
|
const model = computed({
|
|
26
32
|
get: () => props.modelValue,
|
|
27
33
|
set: (items) => {
|
|
28
|
-
|
|
34
|
+
emitUpdate(items)
|
|
29
35
|
},
|
|
30
36
|
})
|
|
31
37
|
</script>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { onClickOutside, useEventListener } from '@vueuse/core'
|
|
3
|
+
import { onMounted, shallowRef } from 'vue'
|
|
3
4
|
import ScaleTransition from './transitions/ScaleTransition.vue'
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
@@ -16,31 +17,22 @@ const props = defineProps<{
|
|
|
16
17
|
closeDropdown: () => void
|
|
17
18
|
}>()
|
|
18
19
|
|
|
19
|
-
const dropdownRef =
|
|
20
|
-
|
|
21
|
-
// Custom implementation of click-outside functionality
|
|
22
|
-
function handleClickOutsideElement(event: MouseEvent) {
|
|
23
|
-
if (props.preventClickOutside) return
|
|
24
|
-
|
|
25
|
-
if (props.show && dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
|
|
26
|
-
props.handleClickOutside()
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function handleCloseOnEscape(event: KeyboardEvent) {
|
|
31
|
-
if (['Escape', 'Esc'].includes(event.key)) {
|
|
32
|
-
props.closeDropdown()
|
|
33
|
-
}
|
|
34
|
-
}
|
|
20
|
+
const dropdownRef = shallowRef<HTMLElement | null>(null)
|
|
35
21
|
|
|
22
|
+
// Use VueUse's onClickOutside for more efficient click handling
|
|
36
23
|
onMounted(() => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
24
|
+
onClickOutside(dropdownRef, (_event) => {
|
|
25
|
+
if (!props.preventClickOutside && props.show) {
|
|
26
|
+
props.handleClickOutside()
|
|
27
|
+
}
|
|
28
|
+
})
|
|
40
29
|
|
|
41
|
-
|
|
42
|
-
document
|
|
43
|
-
|
|
30
|
+
// Use VueUse's useEventListener for cleaner event management
|
|
31
|
+
useEventListener(document, 'keydown', (event: KeyboardEvent) => {
|
|
32
|
+
if (['Escape', 'Esc'].includes(event.key) && props.show) {
|
|
33
|
+
props.closeDropdown()
|
|
34
|
+
}
|
|
35
|
+
})
|
|
44
36
|
})
|
|
45
37
|
</script>
|
|
46
38
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { useDebounceFn } from '@vueuse/core'
|
|
2
3
|
import { computed } from 'vue'
|
|
3
4
|
|
|
4
5
|
const props = defineProps<{
|
|
@@ -7,21 +8,28 @@ const props = defineProps<{
|
|
|
7
8
|
color?: string
|
|
8
9
|
}>()
|
|
9
10
|
|
|
11
|
+
// Fixed class strings to avoid string recreation on each render
|
|
10
12
|
const baseClasses = `w-full px-4 py-3 flex items-center border-b opacity-60
|
|
11
|
-
dark:opacity-70 outline-none text-sm
|
|
13
|
+
dark:opacity-70 outline-none text-sm border-fv-neutral-200 dark:border-fv-neutral-600
|
|
12
14
|
transition-all duration-200`
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
else {
|
|
19
|
-
return `text-black dark:text-white active:bg-fv-neutral-100 dark:hover:bg-fv-neutral-600
|
|
20
|
-
dark:focus:bg-fv-neutral-600 hover:bg-fv-neutral-50`
|
|
21
|
-
}
|
|
22
|
-
})
|
|
16
|
+
// Cache color classes to avoid recomputation
|
|
17
|
+
const dangerClasses = 'text-red-500 dark:hover:text-red-50 hover:bg-red-50 active:bg-red-100 dark:hover:bg-red-900'
|
|
18
|
+
const defaultClasses = `text-black dark:text-white active:bg-fv-neutral-100 dark:hover:bg-fv-neutral-600
|
|
19
|
+
dark:focus:bg-fv-neutral-600 hover:bg-fv-neutral-50`
|
|
23
20
|
|
|
21
|
+
// Memoize color classes
|
|
22
|
+
const colorClasses = computed(() =>
|
|
23
|
+
props.color === 'danger' ? dangerClasses : defaultClasses,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
// Memoize final classes to avoid string concatenation on each render
|
|
24
27
|
const classes = computed(() => `${baseClasses} ${colorClasses.value}`)
|
|
28
|
+
|
|
29
|
+
// Debounce click handler to prevent rapid clicks
|
|
30
|
+
const debouncedClick = props.handleClick
|
|
31
|
+
? useDebounceFn(props.handleClick, 150)
|
|
32
|
+
: undefined
|
|
25
33
|
</script>
|
|
26
34
|
|
|
27
35
|
<template>
|
|
@@ -30,7 +38,7 @@ const classes = computed(() => `${baseClasses} ${colorClasses.value}`)
|
|
|
30
38
|
:class="classes"
|
|
31
39
|
role="menuitem"
|
|
32
40
|
type="button"
|
|
33
|
-
@click.prevent="props.handleClick"
|
|
41
|
+
@click.prevent="debouncedClick || props.handleClick"
|
|
34
42
|
>
|
|
35
43
|
<slot />
|
|
36
44
|
</button>
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
InformationCircleIcon,
|
|
10
10
|
XMarkIcon,
|
|
11
11
|
} from '@heroicons/vue/24/solid'
|
|
12
|
-
import {
|
|
12
|
+
import { useDebounceFn, useEventListener, useFullscreen, useResizeObserver } from '@vueuse/core'
|
|
13
|
+
import { computed, h, nextTick, onMounted, onUnmounted, reactive, ref, shallowRef, watch } from 'vue'
|
|
13
14
|
import { useEventBus } from '../../composables/event-bus'
|
|
14
15
|
import DefaultPaging from './DefaultPaging.vue'
|
|
15
16
|
|
|
@@ -21,6 +22,21 @@ const isFullscreen = ref<boolean>(false)
|
|
|
21
22
|
const infoPanel = ref<boolean>(true) // Show info panel by default
|
|
22
23
|
const touchStartTime = ref<number>(0)
|
|
23
24
|
const infoHeight = ref<number>(0)
|
|
25
|
+
const galleryContainerRef = shallowRef<HTMLElement | null>(null)
|
|
26
|
+
|
|
27
|
+
// Use VueUse's useFullscreen for better fullscreen handling
|
|
28
|
+
const { isFullscreen: isElementFullscreen, enter: enterFullscreen, exit: exitFullscreen } = useFullscreen(galleryContainerRef)
|
|
29
|
+
|
|
30
|
+
// Track when fullscreen changes externally (like Escape key)
|
|
31
|
+
watch(isElementFullscreen, (newValue) => {
|
|
32
|
+
isFullscreen.value = newValue
|
|
33
|
+
if (newValue) {
|
|
34
|
+
// Force update of image size when entering fullscreen
|
|
35
|
+
nextTick(() => {
|
|
36
|
+
updateImageSizes()
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
})
|
|
24
40
|
|
|
25
41
|
const props = withDefaults(
|
|
26
42
|
defineProps<{
|
|
@@ -72,14 +88,34 @@ const modelValue = computed({
|
|
|
72
88
|
const direction = ref<'next' | 'prev'>('next')
|
|
73
89
|
|
|
74
90
|
let controlsTimeout: number | null = null
|
|
91
|
+
let fullscreenResizeTimeout: number | null = null
|
|
92
|
+
|
|
93
|
+
// Used to maintain consistent image sizes
|
|
94
|
+
function updateImageSizes() {
|
|
95
|
+
nextTick(() => {
|
|
96
|
+
const imageContainers = document.querySelectorAll('.image-container img, .image-container .video-component') as NodeListOf<HTMLElement>
|
|
97
|
+
if (imageContainers && imageContainers.length > 0) {
|
|
98
|
+
imageContainers.forEach((img) => {
|
|
99
|
+
// Force a reflow to ensure correct sizing
|
|
100
|
+
if (img.style.maxHeight) {
|
|
101
|
+
const currentMaxHeight = img.style.maxHeight
|
|
102
|
+
img.style.maxHeight = ''
|
|
103
|
+
// Force browser to recalculate styles
|
|
104
|
+
void img.offsetHeight
|
|
105
|
+
img.style.maxHeight = currentMaxHeight
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
|
75
111
|
|
|
76
112
|
function setModal(value: boolean) {
|
|
77
113
|
if (value === true) {
|
|
78
114
|
if (props.onOpen) props.onOpen()
|
|
79
115
|
document.body.style.overflow = 'hidden' // Prevent scrolling when gallery is open
|
|
80
116
|
if (!import.meta.env.SSR) {
|
|
81
|
-
document
|
|
82
|
-
document
|
|
117
|
+
useEventListener(document, 'keydown', handleKeyboardInput)
|
|
118
|
+
useEventListener(document, 'keyup', handleKeyboardRelease)
|
|
83
119
|
}
|
|
84
120
|
// Auto-hide controls after 3 seconds on mobile
|
|
85
121
|
if (window.innerWidth < 1024) {
|
|
@@ -91,27 +127,23 @@ function setModal(value: boolean) {
|
|
|
91
127
|
else {
|
|
92
128
|
if (props.onClose) props.onClose()
|
|
93
129
|
document.body.style.overflow = '' // Restore scrolling
|
|
94
|
-
if
|
|
95
|
-
|
|
96
|
-
|
|
130
|
+
// Exit fullscreen if active
|
|
131
|
+
if (isFullscreen.value) {
|
|
132
|
+
exitFullscreen()
|
|
133
|
+
isFullscreen.value = false
|
|
97
134
|
}
|
|
98
135
|
// Clear timeout if modal is closed
|
|
99
136
|
if (controlsTimeout) {
|
|
100
137
|
clearTimeout(controlsTimeout)
|
|
101
138
|
controlsTimeout = null
|
|
102
139
|
}
|
|
103
|
-
// Exit fullscreen if active
|
|
104
|
-
if (isFullscreen.value && document.exitFullscreen) {
|
|
105
|
-
document.exitFullscreen().catch(() => {})
|
|
106
|
-
isFullscreen.value = false
|
|
107
|
-
}
|
|
108
140
|
}
|
|
109
141
|
isGalleryOpen.value = value
|
|
110
142
|
showControls.value = true
|
|
111
143
|
// Don't reset info panel state when opening/closing
|
|
112
144
|
}
|
|
113
145
|
|
|
114
|
-
|
|
146
|
+
const openGalleryImage = useDebounceFn((index: number | undefined) => {
|
|
115
147
|
if (index === undefined) {
|
|
116
148
|
modelValue.value = 0
|
|
117
149
|
}
|
|
@@ -119,7 +151,7 @@ function openGalleryImage(index: number | undefined) {
|
|
|
119
151
|
modelValue.value = Number.parseInt(index.toString())
|
|
120
152
|
}
|
|
121
153
|
setModal(true)
|
|
122
|
-
}
|
|
154
|
+
}, 50) // Debounce to prevent accidental double-opens
|
|
123
155
|
|
|
124
156
|
function goNextImage() {
|
|
125
157
|
direction.value = 'next'
|
|
@@ -200,24 +232,31 @@ function toggleInfoPanel() {
|
|
|
200
232
|
|
|
201
233
|
function toggleFullscreen() {
|
|
202
234
|
if (!isFullscreen.value) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
235
|
+
if (galleryContainerRef.value) {
|
|
236
|
+
enterFullscreen()
|
|
237
|
+
.then(() => {
|
|
238
|
+
isFullscreen.value = true
|
|
239
|
+
// Give browser time to adjust fullscreen before updating sizing
|
|
240
|
+
if (fullscreenResizeTimeout) clearTimeout(fullscreenResizeTimeout)
|
|
241
|
+
fullscreenResizeTimeout = window.setTimeout(() => {
|
|
242
|
+
updateImageSizes()
|
|
243
|
+
}, 50)
|
|
244
|
+
})
|
|
245
|
+
.catch(() => {})
|
|
208
246
|
}
|
|
209
247
|
}
|
|
210
248
|
else {
|
|
211
|
-
|
|
212
|
-
|
|
249
|
+
exitFullscreen()
|
|
250
|
+
.then(() => {
|
|
213
251
|
isFullscreen.value = false
|
|
214
|
-
})
|
|
215
|
-
|
|
252
|
+
})
|
|
253
|
+
.catch(() => {})
|
|
216
254
|
}
|
|
217
255
|
resetControlsTimer()
|
|
218
256
|
}
|
|
219
257
|
|
|
220
|
-
|
|
258
|
+
// Touch handling with debounce to prevent multiple rapid changes
|
|
259
|
+
const touchStart = useDebounceFn((event: TouchEvent) => {
|
|
221
260
|
const touch = event.touches[0]
|
|
222
261
|
const targetElement = touch.target as HTMLElement
|
|
223
262
|
|
|
@@ -231,9 +270,9 @@ function touchStart(event: TouchEvent) {
|
|
|
231
270
|
|
|
232
271
|
start.x = touch.screenX
|
|
233
272
|
start.y = touch.screenY
|
|
234
|
-
}
|
|
273
|
+
}, 50)
|
|
235
274
|
|
|
236
|
-
|
|
275
|
+
const touchEnd = useDebounceFn((event: TouchEvent) => {
|
|
237
276
|
const touch = event.changedTouches[0]
|
|
238
277
|
const targetElement = touch.target as HTMLElement
|
|
239
278
|
const touchDuration = Date.now() - touchStartTime.value
|
|
@@ -265,7 +304,7 @@ function touchEnd(event: TouchEvent) {
|
|
|
265
304
|
goPrevImage()
|
|
266
305
|
}
|
|
267
306
|
}
|
|
268
|
-
}
|
|
307
|
+
}, 50)
|
|
269
308
|
|
|
270
309
|
function getBorderColor(i: any) {
|
|
271
310
|
if (props.borderColor !== undefined) {
|
|
@@ -316,18 +355,23 @@ function closeGallery() {
|
|
|
316
355
|
setModal(false)
|
|
317
356
|
}
|
|
318
357
|
|
|
319
|
-
// Click outside gallery content to close
|
|
320
|
-
|
|
358
|
+
// Click outside gallery content to close - with debounce to prevent accidental closes
|
|
359
|
+
const handleBackdropClick = useDebounceFn((event: MouseEvent) => {
|
|
321
360
|
if (event.target === event.currentTarget) {
|
|
322
361
|
setModal(false)
|
|
323
362
|
}
|
|
324
|
-
}
|
|
363
|
+
}, 200)
|
|
325
364
|
|
|
326
|
-
|
|
327
|
-
|
|
365
|
+
// Watch for both image changes and fullscreen mode changes
|
|
366
|
+
watch([currentImage, isFullscreen], () => {
|
|
367
|
+
// Update the info height when image changes or fullscreen state changes
|
|
328
368
|
if (infoPanel.value) {
|
|
329
369
|
nextTick(() => {
|
|
330
370
|
updateInfoHeight()
|
|
371
|
+
// Fix image sizing issue when navigating in fullscreen
|
|
372
|
+
if (isFullscreen.value) {
|
|
373
|
+
updateImageSizes()
|
|
374
|
+
}
|
|
331
375
|
})
|
|
332
376
|
}
|
|
333
377
|
})
|
|
@@ -349,37 +393,59 @@ onMounted(() => {
|
|
|
349
393
|
eventBus.on(`${props.id}Gallery`, openGalleryImage)
|
|
350
394
|
eventBus.on(`${props.id}GalleryClose`, closeGallery)
|
|
351
395
|
|
|
396
|
+
// Store reference to the gallery container
|
|
397
|
+
galleryContainerRef.value = document.querySelector('.gallery-container')
|
|
398
|
+
|
|
352
399
|
// Initialize info height once mounted (only if info panel is shown)
|
|
353
400
|
if (infoPanel.value) {
|
|
354
401
|
updateInfoHeight()
|
|
355
402
|
}
|
|
356
403
|
|
|
357
|
-
//
|
|
358
|
-
const resizeObserver = new ResizeObserver(() => {
|
|
359
|
-
if (infoPanel.value) {
|
|
360
|
-
updateInfoHeight()
|
|
361
|
-
}
|
|
362
|
-
})
|
|
363
|
-
|
|
404
|
+
// Use vueUse's useResizeObserver instead of native ResizeObserver
|
|
364
405
|
const infoElement = document.querySelector('.info-panel-slot')
|
|
365
406
|
if (infoElement) {
|
|
366
|
-
|
|
407
|
+
useResizeObserver(infoElement as HTMLElement, () => {
|
|
408
|
+
if (infoPanel.value) {
|
|
409
|
+
updateInfoHeight()
|
|
410
|
+
}
|
|
411
|
+
})
|
|
367
412
|
}
|
|
413
|
+
|
|
414
|
+
// Listen for fullscreen changes to update image sizes
|
|
415
|
+
useEventListener(document, 'fullscreenchange', () => {
|
|
416
|
+
if (document.fullscreenElement) {
|
|
417
|
+
// This handles the case of using F11 or browser fullscreen controls
|
|
418
|
+
isFullscreen.value = true
|
|
419
|
+
updateImageSizes()
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
isFullscreen.value = false
|
|
423
|
+
}
|
|
424
|
+
})
|
|
368
425
|
})
|
|
369
426
|
|
|
370
427
|
onUnmounted(() => {
|
|
371
428
|
eventBus.off(`${props.id}Gallery`, openGalleryImage)
|
|
372
429
|
eventBus.off(`${props.id}GalleryImage`, openGalleryImage)
|
|
373
430
|
eventBus.off(`${props.id}GalleryClose`, closeGallery)
|
|
431
|
+
|
|
374
432
|
if (!import.meta.env.SSR) {
|
|
375
|
-
document.removeEventListener('keydown', handleKeyboardInput)
|
|
376
|
-
document.removeEventListener('keyup', handleKeyboardRelease)
|
|
377
433
|
document.body.style.overflow = '' // Ensure body scrolling is restored
|
|
378
434
|
}
|
|
435
|
+
|
|
379
436
|
// Clear any remaining timeouts
|
|
380
437
|
if (controlsTimeout) {
|
|
381
438
|
clearTimeout(controlsTimeout)
|
|
382
439
|
}
|
|
440
|
+
|
|
441
|
+
if (fullscreenResizeTimeout) {
|
|
442
|
+
clearTimeout(fullscreenResizeTimeout)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Ensure we exit fullscreen mode on unmount
|
|
446
|
+
if (isFullscreen.value) {
|
|
447
|
+
exitFullscreen().catch(() => {})
|
|
448
|
+
}
|
|
383
449
|
})
|
|
384
450
|
</script>
|
|
385
451
|
|
|
@@ -451,7 +517,7 @@ onUnmounted(() => {
|
|
|
451
517
|
class="flex-1 w-full max-w-full flex flex-col items-center justify-center absolute inset-0 z-[2]"
|
|
452
518
|
>
|
|
453
519
|
<div
|
|
454
|
-
class="flex-1 w-full max-w-full flex items-center justify-center"
|
|
520
|
+
class="flex-1 w-full max-w-full flex items-center justify-center image-container"
|
|
455
521
|
>
|
|
456
522
|
<template
|
|
457
523
|
v-if="videoComponent && isVideo(images[modelValue])"
|
|
@@ -460,9 +526,15 @@ onUnmounted(() => {
|
|
|
460
526
|
<component
|
|
461
527
|
:is="videoComponent"
|
|
462
528
|
:src="isVideo(images[modelValue])"
|
|
463
|
-
class="shadow max-w-full h-auto object-contain"
|
|
529
|
+
class="shadow max-w-full h-auto object-contain video-component"
|
|
464
530
|
:style="{
|
|
465
|
-
maxHeight:
|
|
531
|
+
maxHeight: isFullscreen
|
|
532
|
+
? infoPanel
|
|
533
|
+
? 'calc(90vh - var(--info-height, 0px) - 4rem)'
|
|
534
|
+
: 'calc(90vh - 4rem)'
|
|
535
|
+
: infoPanel
|
|
536
|
+
? 'calc(80vh - var(--info-height, 0px) - 4rem)'
|
|
537
|
+
: 'calc(80vh - 4rem)',
|
|
466
538
|
}"
|
|
467
539
|
/>
|
|
468
540
|
</ClientOnly>
|
|
@@ -472,7 +544,13 @@ onUnmounted(() => {
|
|
|
472
544
|
v-if="modelValueSrc && imageComponent === 'img'"
|
|
473
545
|
class="shadow max-w-full h-auto object-contain"
|
|
474
546
|
:style="{
|
|
475
|
-
maxHeight:
|
|
547
|
+
maxHeight: isFullscreen
|
|
548
|
+
? infoPanel
|
|
549
|
+
? 'calc(90vh - var(--info-height, 0px) - 4rem)'
|
|
550
|
+
: 'calc(90vh - 4rem)'
|
|
551
|
+
: infoPanel
|
|
552
|
+
? 'calc(80vh - var(--info-height, 0px) - 4rem)'
|
|
553
|
+
: 'calc(80vh - 4rem)',
|
|
476
554
|
}"
|
|
477
555
|
:src="modelValueSrc"
|
|
478
556
|
:alt="`Gallery image ${modelValue + 1}`"
|
|
@@ -485,7 +563,13 @@ onUnmounted(() => {
|
|
|
485
563
|
:alt="modelValueSrc.alt"
|
|
486
564
|
class="shadow max-w-full h-auto object-contain"
|
|
487
565
|
:style="{
|
|
488
|
-
maxHeight:
|
|
566
|
+
maxHeight: isFullscreen
|
|
567
|
+
? infoPanel
|
|
568
|
+
? 'calc(90vh - var(--info-height, 0px) - 4rem)'
|
|
569
|
+
: 'calc(90vh - 4rem)'
|
|
570
|
+
: infoPanel
|
|
571
|
+
? 'calc(80vh - var(--info-height, 0px) - 4rem)'
|
|
572
|
+
: 'calc(80vh - 4rem)',
|
|
489
573
|
}"
|
|
490
574
|
/>
|
|
491
575
|
</template>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { ErrorObject } from '@vuelidate/core'
|
|
3
|
-
import {
|
|
3
|
+
import { useDebounceFn } from '@vueuse/core'
|
|
4
|
+
import { computed, shallowRef, toRef } from 'vue'
|
|
4
5
|
import { useTranslation } from '../../composables/translations'
|
|
5
6
|
import DefaultTagInput from './DefaultTagInput.vue'
|
|
6
7
|
|
|
@@ -53,11 +54,13 @@ const props = withDefaults(
|
|
|
53
54
|
)
|
|
54
55
|
|
|
55
56
|
const translate = useTranslation()
|
|
56
|
-
|
|
57
|
+
// Use shallowRef for DOM elements to reduce reactivity overhead
|
|
58
|
+
const inputRef = shallowRef<HTMLInputElement>()
|
|
57
59
|
|
|
58
60
|
const errorProps = toRef(props, 'error')
|
|
59
61
|
const errorVuelidateProps = toRef(props, 'errorVuelidate')
|
|
60
62
|
|
|
63
|
+
// Memoized error calculation
|
|
61
64
|
const checkErrors = computed(() => {
|
|
62
65
|
if (errorProps.value) return errorProps.value
|
|
63
66
|
if (errorVuelidateProps.value && errorVuelidateProps.value.length > 0) {
|
|
@@ -84,20 +87,23 @@ const emit = defineEmits([
|
|
|
84
87
|
'blur',
|
|
85
88
|
])
|
|
86
89
|
|
|
87
|
-
|
|
90
|
+
// Debounced event handlers to reduce CPU usage from rapid events
|
|
91
|
+
const handleFocus = useDebounceFn(() => {
|
|
88
92
|
emit('focus', props.id)
|
|
89
|
-
}
|
|
90
|
-
|
|
93
|
+
}, 50)
|
|
94
|
+
|
|
95
|
+
const handleBlur = useDebounceFn(() => {
|
|
91
96
|
emit('blur', props.id)
|
|
92
|
-
}
|
|
97
|
+
}, 50)
|
|
93
98
|
|
|
94
|
-
// Copy input value to clipboard
|
|
95
|
-
|
|
99
|
+
// Copy input value to clipboard with debounce
|
|
100
|
+
const copyToClipboard = useDebounceFn(() => {
|
|
96
101
|
if (props.modelValue) {
|
|
97
102
|
navigator.clipboard.writeText(props.modelValue.toString())
|
|
98
103
|
}
|
|
99
|
-
}
|
|
104
|
+
}, 200)
|
|
100
105
|
|
|
106
|
+
// Optimized computed properties with explicit types
|
|
101
107
|
const model = computed<modelValueType>({
|
|
102
108
|
get: () => props.modelValue,
|
|
103
109
|
set: (value) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { useDebounceFn } from '@vueuse/core'
|
|
3
|
+
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
3
4
|
import { useEventBus } from '../../composables/event-bus'
|
|
4
5
|
|
|
5
6
|
const props = withDefaults(
|
|
@@ -13,18 +14,26 @@ const props = withDefaults(
|
|
|
13
14
|
id: '',
|
|
14
15
|
},
|
|
15
16
|
)
|
|
17
|
+
|
|
16
18
|
const eventBus = useEventBus()
|
|
17
19
|
const loading = ref<boolean>(false)
|
|
18
|
-
|
|
20
|
+
|
|
21
|
+
// Compute event name once to avoid string concatenation on each event
|
|
22
|
+
const eventName = computed(() => props.id ? `${props.id}-loading` : 'loading')
|
|
23
|
+
|
|
24
|
+
// Debounce the loading state change to prevent rapid toggles
|
|
25
|
+
const setLoading = useDebounceFn((value: boolean) => {
|
|
19
26
|
loading.value = value
|
|
20
|
-
}
|
|
27
|
+
}, 50)
|
|
28
|
+
|
|
29
|
+
// Setup event listeners with computed event name
|
|
21
30
|
onMounted(() => {
|
|
22
|
-
|
|
23
|
-
else eventBus.on('loading', setLoading)
|
|
31
|
+
eventBus.on(eventName.value, setLoading)
|
|
24
32
|
})
|
|
33
|
+
|
|
34
|
+
// Proper cleanup of event listeners
|
|
25
35
|
onUnmounted(() => {
|
|
26
|
-
|
|
27
|
-
else eventBus.off('loading', setLoading)
|
|
36
|
+
eventBus.off(eventName.value, setLoading)
|
|
28
37
|
})
|
|
29
38
|
</script>
|
|
30
39
|
|