@fy-/fws-vue 2.3.10 → 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.
@@ -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
- const breadcrumbsSchemaFormat = props.nav.map((item, index) => {
20
- const fullUrl = `${
21
- getURL().Host
22
- }${item.to}`.replace(/\/\//g, '/')
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': item.to
27
- ? `${getURL().Scheme}://${fullUrl}`
28
- : undefined,
43
+ 'item': `${baseUrl.value.scheme}://${fullUrl}`,
29
44
  '@type': 'ListItem',
30
45
  }
31
- })
32
- function getBreadcrumbID() {
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': `#${getBreadcrumbID()}`,
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
- async function _onConfirm() {
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,19 +44,20 @@ 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
- // Force this to happen at the end of the event loop
47
- // to ensure it happens after any other modal operations
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
 
52
52
  nextTick(() => {
53
53
  previouslyFocusedElement = document.activeElement as HTMLElement
54
- if (modalRef.value) {
55
- modalRef.value.focus()
54
+ try {
55
+ modalRef.value?.focus()
56
+ }
57
+ catch {
56
58
  }
57
59
  })
58
- }, 0)
60
+ })
59
61
  }
60
62
 
61
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
- emit('update:modelValue', items)
34
+ emitUpdate(items)
29
35
  },
30
36
  })
31
37
  </script>
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
- import { onMounted, onUnmounted, ref } from 'vue'
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 = ref<HTMLElement | null>(null)
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
- document.addEventListener('keydown', handleCloseOnEscape)
38
- document.addEventListener('click', handleClickOutsideElement)
39
- })
24
+ onClickOutside(dropdownRef, (_event) => {
25
+ if (!props.preventClickOutside && props.show) {
26
+ props.handleClickOutside()
27
+ }
28
+ })
40
29
 
41
- onUnmounted(() => {
42
- document.removeEventListener('keydown', handleCloseOnEscape)
43
- document.removeEventListener('click', handleClickOutsideElement)
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 border-fv-neutral-200 dark:border-fv-neutral-600
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
- const colorClasses = computed(() => {
15
- if (props.color === 'danger') {
16
- return 'text-red-500 dark:hover:text-red-50 hover:bg-red-50 active:bg-red-100 dark:hover:bg-red-900'
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 { computed, h, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
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.addEventListener('keydown', handleKeyboardInput)
82
- document.addEventListener('keyup', handleKeyboardRelease)
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 (!import.meta.env.SSR) {
95
- document.removeEventListener('keydown', handleKeyboardInput)
96
- document.removeEventListener('keyup', handleKeyboardRelease)
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
- function openGalleryImage(index: number | undefined) {
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
- const element = document.querySelector('.gallery-container') as HTMLElement
204
- if (element && element.requestFullscreen) {
205
- element.requestFullscreen().then(() => {
206
- isFullscreen.value = true
207
- }).catch(() => {})
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
- if (document.exitFullscreen) {
212
- document.exitFullscreen().then(() => {
249
+ exitFullscreen()
250
+ .then(() => {
213
251
  isFullscreen.value = false
214
- }).catch(() => {})
215
- }
252
+ })
253
+ .catch(() => {})
216
254
  }
217
255
  resetControlsTimer()
218
256
  }
219
257
 
220
- function touchStart(event: TouchEvent) {
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
- function touchEnd(event: TouchEvent) {
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
- function handleBackdropClick(event: MouseEvent) {
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
- watch(currentImage, () => {
327
- // Only update the info height, don't change user's preference
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
- // Set up a resize observer to track info panel height changes
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
- resizeObserver.observe(infoElement)
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: infoPanel ? 'calc(80vh - var(--info-height, 0px) - 4rem)' : 'calc(80vh - 4rem)',
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: infoPanel ? 'calc(80vh - var(--info-height, 0px) - 4rem)' : 'calc(80vh - 4rem)',
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: infoPanel ? 'calc(80vh - var(--info-height, 0px) - 4rem)' : 'calc(80vh - 4rem)',
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 { computed, ref, toRef } from 'vue'
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
- const inputRef = ref<HTMLInputElement>()
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
- function handleFocus() {
90
+ // Debounced event handlers to reduce CPU usage from rapid events
91
+ const handleFocus = useDebounceFn(() => {
88
92
  emit('focus', props.id)
89
- }
90
- function handleBlur() {
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
- function copyToClipboard() {
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 { onMounted, onUnmounted, ref } from 'vue'
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
- function setLoading(value: boolean) {
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
- if (props.id) eventBus.on(`${props.id}-loading`, setLoading)
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
- if (props.id) eventBus.off(`${props.id}-loading`, setLoading)
27
- else eventBus.off('loading', setLoading)
36
+ eventBus.off(eventName.value, setLoading)
28
37
  })
29
38
  </script>
30
39