@fy-/fws-vue 2.3.79 → 2.3.81

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.
@@ -0,0 +1,174 @@
1
+ <script setup lang="ts">
2
+ import { useDebounceFn } from '@vueuse/core'
3
+ import { nextTick, onMounted, onUnmounted, ref } from 'vue'
4
+ import { useEventBus } from '../../composables/event-bus'
5
+ import DefaultInput from './DefaultInput.vue'
6
+ import DefaultModal from './DefaultModal.vue'
7
+
8
+ const eventBus = useEventBus()
9
+ const title = ref<string | null>(null)
10
+ const desc = ref<string | null>(null)
11
+ const inputLabel = ref<string | undefined>(undefined)
12
+ const inputPlaceholder = ref<string | undefined>(undefined)
13
+ const expectedValue = ref<string | null>(null)
14
+ const errorMessage = ref<string | null>(null)
15
+ const onConfirm = ref<Function | null>(null)
16
+ const isOpen = ref<boolean>(false)
17
+ const modalRef = ref<HTMLElement | null>(null)
18
+ const inputValue = ref<string>('')
19
+ const inputError = ref<string>('')
20
+ let previouslyFocusedElement: HTMLElement | null = null
21
+
22
+ interface ConfirmWithInputModalData {
23
+ title: string
24
+ desc: string
25
+ inputLabel?: string
26
+ inputPlaceholder?: string
27
+ expectedValue?: string
28
+ errorMessage?: string
29
+ onConfirm: (inputValue: string) => void | Promise<void>
30
+ }
31
+
32
+ const _onConfirm = useDebounceFn(async () => {
33
+ // If expectedValue is set, validate the input
34
+ if (expectedValue.value && inputValue.value !== expectedValue.value) {
35
+ inputError.value = errorMessage.value || 'Incorrect value entered'
36
+ return
37
+ }
38
+
39
+ if (onConfirm.value) {
40
+ await onConfirm.value(inputValue.value)
41
+ }
42
+ resetConfirm()
43
+ }, 300)
44
+
45
+ function resetConfirm() {
46
+ title.value = null
47
+ desc.value = null
48
+ inputLabel.value = undefined
49
+ inputPlaceholder.value = undefined
50
+ expectedValue.value = null
51
+ errorMessage.value = null
52
+ onConfirm.value = null
53
+ isOpen.value = false
54
+ inputValue.value = ''
55
+ inputError.value = ''
56
+ eventBus.emit('confirmWithInputModal', false)
57
+ if (previouslyFocusedElement) {
58
+ previouslyFocusedElement.focus()
59
+ }
60
+ }
61
+
62
+ function showConfirmWithInput(data: ConfirmWithInputModalData) {
63
+ title.value = data.title
64
+ desc.value = data.desc
65
+ inputLabel.value = data.inputLabel || undefined
66
+ inputPlaceholder.value = data.inputPlaceholder || undefined
67
+ expectedValue.value = data.expectedValue || null
68
+ errorMessage.value = data.errorMessage || null
69
+ onConfirm.value = data.onConfirm
70
+ inputValue.value = ''
71
+ inputError.value = ''
72
+
73
+ // Emit event first to ensure it's registered before opening the modal
74
+ eventBus.emit('confirmWithInputModal', true)
75
+
76
+ // Use requestAnimationFrame instead of setTimeout for better performance
77
+ requestAnimationFrame(() => {
78
+ isOpen.value = true
79
+ eventBus.emit('confirmWithInputModal', true)
80
+
81
+ nextTick(() => {
82
+ previouslyFocusedElement = document.activeElement as HTMLElement
83
+ try {
84
+ modalRef.value?.focus()
85
+ }
86
+ catch {
87
+ }
88
+ })
89
+ })
90
+ }
91
+
92
+ onMounted(() => {
93
+ eventBus.on('resetConfirmWithInput', resetConfirm)
94
+ eventBus.on('showConfirmWithInput', showConfirmWithInput)
95
+ })
96
+
97
+ onUnmounted(() => {
98
+ eventBus.off('resetConfirmWithInput', resetConfirm)
99
+ eventBus.off('showConfirmWithInput', showConfirmWithInput)
100
+ })
101
+ </script>
102
+
103
+ <template>
104
+ <DefaultModal
105
+ id="confirmWithInput"
106
+ ref="modalRef"
107
+ m-size="!max-w-3xl w-full"
108
+ >
109
+ <div
110
+ class="bg-gradient-to-br from-gray-900/70 to-gray-800/50 rounded-lg border border-gray-700/30 overflow-hidden"
111
+ :aria-labelledby="title ? 'confirm-input-modal-title' : undefined"
112
+ :aria-describedby="desc ? 'confirm-input-modal-desc' : undefined"
113
+ aria-modal="true"
114
+ role="dialog"
115
+ tabindex="-1"
116
+ >
117
+ <!-- Header with gradient background -->
118
+ <div
119
+ v-if="title"
120
+ class="bg-gradient-to-r from-indigo-900/30 to-indigo-800/20 p-4 border-b border-indigo-700/30"
121
+ >
122
+ <h3
123
+ id="confirm-input-modal-title"
124
+ class="text-xl font-semibold text-white"
125
+ >
126
+ {{ title }}
127
+ </h3>
128
+ </div>
129
+
130
+ <!-- Content area with styled box -->
131
+ <div class="p-5 text-fv-neutral-100">
132
+ <div v-if="desc" class="bg-gradient-to-r from-blue-950/50 to-indigo-950/50 p-4 rounded-lg border border-blue-700/30 mb-6 shadow-md">
133
+ <p
134
+ id="confirm-input-modal-desc"
135
+ class="text-sm sm:text-base text-gray-200 prose prose-invert prose-sm min-w-full"
136
+ v-html="desc"
137
+ />
138
+ </div>
139
+
140
+ <!-- Input field -->
141
+ <div class="mb-6">
142
+ <DefaultInput
143
+ id="confirmInput"
144
+ v-model="inputValue"
145
+ type="text"
146
+ :label="inputLabel"
147
+ :placeholder="inputPlaceholder"
148
+ :error="inputError"
149
+ autocomplete="off"
150
+ class="w-full"
151
+ @keyup.enter="_onConfirm()"
152
+ />
153
+ </div>
154
+
155
+ <!-- Action buttons with modern styling -->
156
+ <div class="flex justify-center gap-4 mt-6">
157
+ <button
158
+ class="btn accent large shadow-lg hover:shadow-fuchsia-500/30 transition-all duration-300 hover:scale-105 active:scale-95 px-8 rounded-lg"
159
+ :disabled="!inputValue.trim()"
160
+ @click="_onConfirm()"
161
+ >
162
+ {{ $t("confirm_modal_cta_confirm") }}
163
+ </button>
164
+ <button
165
+ class="btn neutral large shadow-md transform transition hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-gray-500 px-8 rounded-lg"
166
+ @click="resetConfirm()"
167
+ >
168
+ {{ $t("confirm_modal_cta_cancel") }}
169
+ </button>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </DefaultModal>
174
+ </template>
@@ -894,22 +894,19 @@ onUnmounted(() => {
894
894
 
895
895
  <!-- Edit Mode Controls (only if editEnabled is true) -->
896
896
  <div v-if="editEnabled && images.length > 0 && (mode === 'grid' || mode === 'mason' || mode === 'custom')">
897
- <!-- Edit Mode Toggle Button -->
898
- <div class="flex justify-end mb-3">
897
+ <!-- Edit Mode Toggle Button (only show when not in edit mode) -->
898
+ <div v-if="!localEditMode" class="flex justify-end mb-3">
899
899
  <button
900
- class="px-4 py-2 rounded-lg font-medium text-sm transition-colors"
901
- :class="localEditMode
902
- ? 'bg-red-600 hover:bg-red-700 text-white'
903
- : 'bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200'"
900
+ class="px-4 py-2 rounded-lg font-medium text-sm transition-colors bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200"
904
901
  @click="toggleEditMode"
905
902
  >
906
- {{ localEditMode ? cancelButtonText : editButtonText }}
903
+ {{ editButtonText }}
907
904
  </button>
908
905
  </div>
909
906
 
910
- <!-- Bulk Actions Bar (shown when items are selected) -->
907
+ <!-- Bulk Actions Bar TOP (shown when in edit mode) -->
911
908
  <div
912
- v-if="localEditMode && localSelectedItems.size > 0"
909
+ v-if="localEditMode"
913
910
  class="flex flex-wrap items-center justify-between gap-2 p-3 mb-3 bg-gray-100 dark:bg-gray-800 rounded-lg border border-gray-300 dark:border-gray-700"
914
911
  >
915
912
  <div class="flex items-center gap-3">
@@ -929,12 +926,22 @@ onUnmounted(() => {
929
926
  {{ clearSelectionText }}
930
927
  </button>
931
928
  </div>
932
- <button
933
- class="px-4 py-1.5 text-sm bg-red-600 hover:bg-red-700 text-white rounded-md transition-colors font-medium"
934
- @click="handleBulkAction"
935
- >
936
- {{ bulkActionText }}
937
- </button>
929
+ <div class="flex items-center gap-2">
930
+ <button
931
+ class="px-4 py-1.5 text-sm bg-gray-600 hover:bg-gray-700 text-white rounded-md transition-colors font-medium"
932
+ @click="toggleEditMode"
933
+ >
934
+ {{ cancelButtonText }}
935
+ </button>
936
+ <button
937
+ class="px-4 py-1.5 text-sm bg-red-600 hover:bg-red-700 text-white rounded-md transition-colors font-medium"
938
+ :disabled="localSelectedItems.size === 0"
939
+ :class="{ 'opacity-50 cursor-not-allowed': localSelectedItems.size === 0 }"
940
+ @click="handleBulkAction"
941
+ >
942
+ {{ bulkActionText }}
943
+ </button>
944
+ </div>
938
945
  </div>
939
946
  </div>
940
947
 
@@ -1082,6 +1089,47 @@ onUnmounted(() => {
1082
1089
  </div>
1083
1090
  </div>
1084
1091
 
1092
+ <!-- Bulk Actions Bar BOTTOM (duplicate bar below gallery when in edit mode) -->
1093
+ <div v-if="editEnabled && localEditMode && images.length > 0 && (mode === 'grid' || mode === 'mason' || mode === 'custom')" class="mt-3">
1094
+ <div
1095
+ class="flex flex-wrap items-center justify-between gap-2 p-3 bg-gray-100 dark:bg-gray-800 rounded-lg border border-gray-300 dark:border-gray-700"
1096
+ >
1097
+ <div class="flex items-center gap-3">
1098
+ <span class="text-sm font-medium">
1099
+ {{ localSelectedItems.size }} {{ selectedCountText }}
1100
+ </span>
1101
+ <button
1102
+ class="px-3 py-1 text-sm bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded-md transition-colors"
1103
+ @click="selectAll"
1104
+ >
1105
+ {{ selectAllText }}
1106
+ </button>
1107
+ <button
1108
+ class="px-3 py-1 text-sm bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded-md transition-colors"
1109
+ @click="clearSelection"
1110
+ >
1111
+ {{ clearSelectionText }}
1112
+ </button>
1113
+ </div>
1114
+ <div class="flex items-center gap-2">
1115
+ <button
1116
+ class="px-4 py-1.5 text-sm bg-gray-600 hover:bg-gray-700 text-white rounded-md transition-colors font-medium"
1117
+ @click="toggleEditMode"
1118
+ >
1119
+ {{ cancelButtonText }}
1120
+ </button>
1121
+ <button
1122
+ class="px-4 py-1.5 text-sm bg-red-600 hover:bg-red-700 text-white rounded-md transition-colors font-medium"
1123
+ :disabled="localSelectedItems.size === 0"
1124
+ :class="{ 'opacity-50 cursor-not-allowed': localSelectedItems.size === 0 }"
1125
+ @click="handleBulkAction"
1126
+ >
1127
+ {{ bulkActionText }}
1128
+ </button>
1129
+ </div>
1130
+ </div>
1131
+ </div>
1132
+
1085
1133
  <!-- Button Mode -->
1086
1134
  <button
1087
1135
  v-if="mode === 'button'"
package/index.ts CHANGED
@@ -17,6 +17,7 @@ import UserProfileStrict from './components/fws/UserProfileStrict.vue'
17
17
  import { ClientOnly } from './components/ssr/ClientOnly'
18
18
  import DefaultBreadcrumb from './components/ui/DefaultBreadcrumb.vue'
19
19
  import DefaultConfirm from './components/ui/DefaultConfirm.vue'
20
+ import DefaultConfirmWithInput from './components/ui/DefaultConfirmWithInput.vue'
20
21
  import DefaultDropdown from './components/ui/DefaultDropdown.vue'
21
22
 
22
23
  import DefaultDropdownLink from './components/ui/DefaultDropdownLink.vue'
@@ -103,6 +104,7 @@ export {
103
104
  DataTable,
104
105
  DefaultBreadcrumb,
105
106
  DefaultConfirm,
107
+ DefaultConfirmWithInput,
106
108
  DefaultDropdown,
107
109
  DefaultDropdownLink,
108
110
  DefaultGallery,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fy-/fws-vue",
3
- "version": "2.3.79",
3
+ "version": "2.3.81",
4
4
  "author": "Florian 'Fy' Gasquez <m@fy.to>",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/fy-to/FWJS#readme",