@imaginario27/air-ui-ds 1.16.0 → 1.17.0
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.
|
@@ -1,119 +1,457 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
:
|
|
34
|
-
:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="['flex flex-col', 'w-full', 'gap-2']">
|
|
3
|
+
<!-- Label -->
|
|
4
|
+
<label
|
|
5
|
+
v-if="label"
|
|
6
|
+
:for="id"
|
|
7
|
+
:class="[
|
|
8
|
+
'text-sm',
|
|
9
|
+
'font-semibold',
|
|
10
|
+
'text-left',
|
|
11
|
+
hasError && 'text-text-error',
|
|
12
|
+
]"
|
|
13
|
+
>
|
|
14
|
+
{{ label }}
|
|
15
|
+
</label>
|
|
16
|
+
|
|
17
|
+
<!-- Help Text (top) -->
|
|
18
|
+
<HelpText
|
|
19
|
+
v-if="helpTextPosition === Position.TOP"
|
|
20
|
+
:text="helpText"
|
|
21
|
+
:error="error"
|
|
22
|
+
/>
|
|
23
|
+
|
|
24
|
+
<div class="w-full flex flex-col gap-4">
|
|
25
|
+
<template
|
|
26
|
+
v-for="(item, index) in items"
|
|
27
|
+
:key="index"
|
|
28
|
+
>
|
|
29
|
+
<!-- Drop placeholder -->
|
|
30
|
+
<DragPlaceholder
|
|
31
|
+
v-if="sortingType === RepeatingFieldSortingType.DRAG && draggedIndex !== null && dragOverIndex === index && draggedIndex !== index"
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
:text="dragPlaceholderText"
|
|
34
|
+
:showText="showDragPlaceholderText"
|
|
35
|
+
:textClass="dragPlaceholderTextClass"
|
|
36
|
+
:class="['h-10', dragPlaceholderClass]"
|
|
37
|
+
@dragover.prevent="handleDragOver(index, $event)"
|
|
38
|
+
@drop.prevent="handleDrop(index, $event)"
|
|
39
|
+
/>
|
|
40
|
+
|
|
41
|
+
<div class="flex flex-col gap-2">
|
|
42
|
+
<div
|
|
43
|
+
class="flex items-start gap-4"
|
|
44
|
+
@dragover.prevent="handleDragOver(index, $event)"
|
|
45
|
+
@drop.prevent="handleDrop(index, $event)"
|
|
46
|
+
>
|
|
47
|
+
<!-- Drag handle -->
|
|
48
|
+
<button
|
|
49
|
+
v-if="sortingType === RepeatingFieldSortingType.DRAG"
|
|
50
|
+
type="button"
|
|
51
|
+
:draggable="!disabled"
|
|
52
|
+
:aria-label="dragHandleAriaLabel"
|
|
53
|
+
:disabled="disabled"
|
|
54
|
+
:class="[
|
|
55
|
+
'flex items-center justify-center',
|
|
56
|
+
'bg-transparent border-0 p-0 mt-2',
|
|
57
|
+
'cursor-move! active:cursor-grabbing!',
|
|
58
|
+
'text-icon-neutral-default',
|
|
59
|
+
'disabled:cursor-not-allowed',
|
|
60
|
+
]"
|
|
61
|
+
@dragstart="handleDragStart(index, $event)"
|
|
62
|
+
@dragend="handleDragEnd"
|
|
63
|
+
@keydown="handleDragHandleKeydown(index, $event)"
|
|
64
|
+
>
|
|
65
|
+
<Icon :name="dragHandleIcon" iconClass="w-[16px] h-[16px]" />
|
|
66
|
+
</button>
|
|
67
|
+
|
|
68
|
+
<!-- Content slot -->
|
|
69
|
+
<div class="grow">
|
|
70
|
+
<slot
|
|
71
|
+
:id="id"
|
|
72
|
+
:item="item"
|
|
73
|
+
:index="index"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- Vertical action buttons (desktop only; actionsOrientation is vertical) -->
|
|
78
|
+
<div
|
|
79
|
+
v-if="actionsOrientation === Orientation.VERTICAL"
|
|
80
|
+
class="hidden md:flex flex-col gap-2"
|
|
81
|
+
>
|
|
82
|
+
<ActionIconButton
|
|
83
|
+
v-if="sortingType === RepeatingFieldSortingType.BUTTONS"
|
|
84
|
+
:styleType="ButtonStyleType.NEUTRAL_TRANSPARENT"
|
|
85
|
+
:icon="moveUpIcon"
|
|
86
|
+
:size="ButtonSize.SM"
|
|
87
|
+
:ariaLabel="moveUpAriaLabel"
|
|
88
|
+
:disabled="isMoveUpDisabled(index)"
|
|
89
|
+
@click="moveItemUp(index)"
|
|
90
|
+
/>
|
|
91
|
+
|
|
92
|
+
<ActionIconButton
|
|
93
|
+
v-if="sortingType === RepeatingFieldSortingType.BUTTONS"
|
|
94
|
+
:styleType="ButtonStyleType.NEUTRAL_TRANSPARENT"
|
|
95
|
+
:icon="moveDownIcon"
|
|
96
|
+
:size="ButtonSize.SM"
|
|
97
|
+
:ariaLabel="moveDownAriaLabel"
|
|
98
|
+
:disabled="isMoveDownDisabled(index)"
|
|
99
|
+
@click="moveItemDown(index)"
|
|
100
|
+
/>
|
|
101
|
+
|
|
102
|
+
<!-- Add button (show only for last item) -->
|
|
103
|
+
<ActionIconButton
|
|
104
|
+
v-if="index === items.length - 1"
|
|
105
|
+
:styleType="ButtonStyleType.NEUTRAL_OUTLINED"
|
|
106
|
+
icon="mdi:plus-circle-outline"
|
|
107
|
+
:size="ButtonSize.SM"
|
|
108
|
+
:ariaLabel="addItemAriaLabel"
|
|
109
|
+
:disabled="disabled"
|
|
110
|
+
@click="addItem"
|
|
111
|
+
/>
|
|
112
|
+
|
|
113
|
+
<!-- Delete button (show for all except when there's only one item) -->
|
|
114
|
+
<ActionIconButton
|
|
115
|
+
v-if="items.length > 1"
|
|
116
|
+
:styleType="ButtonStyleType.DELETE_SOFT"
|
|
117
|
+
icon="mdi:minus-circle-outline"
|
|
118
|
+
:size="ButtonSize.SM"
|
|
119
|
+
:ariaLabel="removeItemAriaLabel"
|
|
120
|
+
:disabled="disabled"
|
|
121
|
+
@click="removeItem(index)"
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<!-- Horizontal action buttons: always shown when actionsOrientation is horizontal,
|
|
127
|
+
and as the mobile fallback for the vertical action column above. Buttons stack
|
|
128
|
+
in a column on mobile and use isMobileFullWidth to span the full row width. -->
|
|
129
|
+
<div
|
|
130
|
+
:class="[
|
|
131
|
+
actionsOrientation === Orientation.HORIZONTAL ? 'flex' : 'flex md:hidden',
|
|
132
|
+
'flex-col md:flex-row flex-wrap items-stretch md:items-center gap-2',
|
|
133
|
+
]"
|
|
134
|
+
>
|
|
135
|
+
<ActionButton
|
|
136
|
+
v-if="sortingType === RepeatingFieldSortingType.BUTTONS"
|
|
137
|
+
:styleType="ButtonStyleType.NEUTRAL_TRANSPARENT"
|
|
138
|
+
:text="moveUpButtonText"
|
|
139
|
+
:icon="moveUpIcon"
|
|
140
|
+
:iconPosition="IconPosition.LEFT"
|
|
141
|
+
:size="ButtonSize.MD"
|
|
142
|
+
:isMobileFullWidth="true"
|
|
143
|
+
:disabled="isMoveUpDisabled(index)"
|
|
144
|
+
@click="moveItemUp(index)"
|
|
145
|
+
/>
|
|
146
|
+
|
|
147
|
+
<ActionButton
|
|
148
|
+
v-if="sortingType === RepeatingFieldSortingType.BUTTONS"
|
|
149
|
+
:styleType="ButtonStyleType.NEUTRAL_TRANSPARENT"
|
|
150
|
+
:text="moveDownButtonText"
|
|
151
|
+
:icon="moveDownIcon"
|
|
152
|
+
:iconPosition="IconPosition.LEFT"
|
|
153
|
+
:size="ButtonSize.MD"
|
|
154
|
+
:isMobileFullWidth="true"
|
|
155
|
+
:disabled="isMoveDownDisabled(index)"
|
|
156
|
+
@click="moveItemDown(index)"
|
|
157
|
+
/>
|
|
158
|
+
|
|
159
|
+
<!-- Add button (show only for last item) -->
|
|
160
|
+
<ActionButton
|
|
161
|
+
v-if="index === items.length - 1"
|
|
162
|
+
:styleType="ButtonStyleType.NEUTRAL_OUTLINED"
|
|
163
|
+
:text="addButtonText"
|
|
164
|
+
icon="mdi:plus-circle-outline"
|
|
165
|
+
:iconPosition="IconPosition.LEFT"
|
|
166
|
+
:size="ButtonSize.MD"
|
|
167
|
+
:isMobileFullWidth="true"
|
|
168
|
+
:disabled="disabled"
|
|
169
|
+
@click="addItem"
|
|
170
|
+
/>
|
|
171
|
+
|
|
172
|
+
<!-- Delete button (show for all except when there's only one item) -->
|
|
173
|
+
<ActionButton
|
|
174
|
+
v-if="items.length > 1"
|
|
175
|
+
:styleType="ButtonStyleType.DELETE_SOFT"
|
|
176
|
+
:text="removeButtonText"
|
|
177
|
+
icon="mdi:minus-circle-outline"
|
|
178
|
+
:iconPosition="IconPosition.LEFT"
|
|
179
|
+
:size="ButtonSize.MD"
|
|
180
|
+
:isMobileFullWidth="true"
|
|
181
|
+
:disabled="disabled"
|
|
182
|
+
@click="removeItem(index)"
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</template>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<!-- Help Text (bottom) -->
|
|
190
|
+
<HelpText
|
|
191
|
+
v-if="helpTextPosition === Position.BOTTOM"
|
|
192
|
+
:text="helpText"
|
|
193
|
+
:error="error"
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
</template>
|
|
197
|
+
|
|
198
|
+
<script setup lang="ts">
|
|
199
|
+
const props = defineProps({
|
|
200
|
+
id: {
|
|
201
|
+
type: String as PropType<string>,
|
|
202
|
+
required: true,
|
|
203
|
+
},
|
|
204
|
+
label: String as PropType<string>,
|
|
205
|
+
modelValue: {
|
|
206
|
+
type: Array as PropType<unknown[]>,
|
|
207
|
+
default: () => [],
|
|
208
|
+
},
|
|
209
|
+
defaultValue: {
|
|
210
|
+
type: [Object, Array, String, Number, Boolean] as PropType<unknown>,
|
|
211
|
+
default: () => ({}),
|
|
212
|
+
},
|
|
213
|
+
addItemAriaLabel: {
|
|
214
|
+
type: String as PropType<string>,
|
|
215
|
+
default: 'Add item',
|
|
216
|
+
},
|
|
217
|
+
removeItemAriaLabel: {
|
|
218
|
+
type: String as PropType<string>,
|
|
219
|
+
default: 'Remove item',
|
|
220
|
+
},
|
|
221
|
+
addButtonText: {
|
|
222
|
+
type: String as PropType<string>,
|
|
223
|
+
default: 'Add item',
|
|
224
|
+
},
|
|
225
|
+
removeButtonText: {
|
|
226
|
+
type: String as PropType<string>,
|
|
227
|
+
default: 'Remove item',
|
|
228
|
+
},
|
|
229
|
+
actionsOrientation: {
|
|
230
|
+
type: String as PropType<Orientation>,
|
|
231
|
+
default: Orientation.VERTICAL,
|
|
232
|
+
validator: (value: Orientation) => Object.values(Orientation).includes(value),
|
|
233
|
+
},
|
|
234
|
+
sortingType: {
|
|
235
|
+
type: String as PropType<RepeatingFieldSortingType>,
|
|
236
|
+
default: RepeatingFieldSortingType.NONE,
|
|
237
|
+
validator: (value: RepeatingFieldSortingType) => Object.values(RepeatingFieldSortingType).includes(value),
|
|
238
|
+
},
|
|
239
|
+
moveUpAriaLabel: {
|
|
240
|
+
type: String as PropType<string>,
|
|
241
|
+
default: 'Move item up',
|
|
242
|
+
},
|
|
243
|
+
moveDownAriaLabel: {
|
|
244
|
+
type: String as PropType<string>,
|
|
245
|
+
default: 'Move item down',
|
|
246
|
+
},
|
|
247
|
+
moveUpButtonText: {
|
|
248
|
+
type: String as PropType<string>,
|
|
249
|
+
default: 'Move up',
|
|
250
|
+
},
|
|
251
|
+
moveDownButtonText: {
|
|
252
|
+
type: String as PropType<string>,
|
|
253
|
+
default: 'Move down',
|
|
254
|
+
},
|
|
255
|
+
dragHandleAriaLabel: {
|
|
256
|
+
type: String as PropType<string>,
|
|
257
|
+
default: 'Drag to reorder item',
|
|
258
|
+
},
|
|
259
|
+
moveUpIcon: {
|
|
260
|
+
type: String as PropType<string>,
|
|
261
|
+
default: 'mdi:arrow-up',
|
|
262
|
+
},
|
|
263
|
+
moveDownIcon: {
|
|
264
|
+
type: String as PropType<string>,
|
|
265
|
+
default: 'mdi:arrow-down',
|
|
266
|
+
},
|
|
267
|
+
dragHandleIcon: {
|
|
268
|
+
type: String as PropType<string>,
|
|
269
|
+
default: 'mdi:drag-vertical',
|
|
270
|
+
},
|
|
271
|
+
dragPlaceholderText: {
|
|
272
|
+
type: String as PropType<string>,
|
|
273
|
+
default: 'Drop here',
|
|
274
|
+
},
|
|
275
|
+
showDragPlaceholderText: {
|
|
276
|
+
type: Boolean as PropType<boolean>,
|
|
277
|
+
default: true,
|
|
278
|
+
},
|
|
279
|
+
dragPlaceholderClass: String as PropType<string>,
|
|
280
|
+
dragPlaceholderTextClass: String as PropType<string>,
|
|
281
|
+
helpText: String as PropType<string>,
|
|
282
|
+
helpTextPosition: {
|
|
283
|
+
type: String as PropType<Position>,
|
|
284
|
+
default: Position.BOTTOM,
|
|
285
|
+
validator: (value: Position) => Object.values(Position).includes(value),
|
|
286
|
+
},
|
|
287
|
+
error: {
|
|
288
|
+
type: String as PropType<string>,
|
|
289
|
+
default: '',
|
|
290
|
+
},
|
|
291
|
+
required: {
|
|
292
|
+
type: Boolean as PropType<boolean>,
|
|
293
|
+
default: false,
|
|
294
|
+
},
|
|
295
|
+
validator: {
|
|
296
|
+
type: Function as PropType<(value: unknown[]) => string | null>,
|
|
297
|
+
default: () => null,
|
|
298
|
+
},
|
|
299
|
+
disabled: {
|
|
300
|
+
type: Boolean as PropType<boolean>,
|
|
301
|
+
default: false,
|
|
302
|
+
},
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
const emit = defineEmits(['update:modelValue', 'update:error'])
|
|
306
|
+
|
|
307
|
+
// Composables
|
|
308
|
+
const validationMode = useInjectedValidationMode()
|
|
309
|
+
|
|
310
|
+
// Computed
|
|
311
|
+
const hasError = computed(() => props.error !== '')
|
|
312
|
+
|
|
313
|
+
const cloneValue = <T,>(value: T): T => structuredClone(toRaw(value))
|
|
314
|
+
|
|
315
|
+
const createDefaultItem = () => cloneValue(props.defaultValue)
|
|
316
|
+
|
|
317
|
+
const normalizeItems = (list: unknown[]): unknown[] => (list.length ? [...list] : [createDefaultItem()])
|
|
318
|
+
|
|
319
|
+
// Local draft state: structural edits (add, remove, move) sync to the
|
|
320
|
+
// parent via emit; item content is owned by whatever the slot renders.
|
|
321
|
+
const localItems = ref<unknown[]>(normalizeItems(props.modelValue))
|
|
322
|
+
|
|
323
|
+
watch(
|
|
324
|
+
() => props.modelValue,
|
|
325
|
+
value => {
|
|
326
|
+
localItems.value = normalizeItems(value)
|
|
327
|
+
},
|
|
328
|
+
{ deep: true }
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
const items = computed(() => localItems.value)
|
|
332
|
+
|
|
333
|
+
// Drag state
|
|
334
|
+
const draggedIndex = ref<number | null>(null)
|
|
335
|
+
const dragOverIndex = ref<number | null>(null)
|
|
336
|
+
|
|
337
|
+
const addItem = () => {
|
|
338
|
+
const nextItems = [...localItems.value, createDefaultItem()]
|
|
339
|
+
localItems.value = nextItems
|
|
340
|
+
emit('update:modelValue', nextItems)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const removeItem = (index: number) => {
|
|
344
|
+
if (localItems.value.length <= 1) {
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const nextItems = localItems.value.filter((_, currentIndex) => currentIndex !== index)
|
|
349
|
+
localItems.value = nextItems
|
|
350
|
+
emit('update:modelValue', nextItems)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const isMoveUpDisabled = (index: number): boolean => props.disabled || index <= 0
|
|
354
|
+
|
|
355
|
+
const isMoveDownDisabled = (index: number): boolean => props.disabled || index >= items.value.length - 1
|
|
356
|
+
|
|
357
|
+
const moveItem = (fromIndex: number, toIndex: number) => {
|
|
358
|
+
if (props.disabled) {
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (toIndex < 0 || toIndex > items.value.length - 1 || toIndex === fromIndex) {
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const nextItems = [...localItems.value]
|
|
367
|
+
const [movedItem] = nextItems.splice(fromIndex, 1)
|
|
368
|
+
nextItems.splice(toIndex, 0, movedItem)
|
|
369
|
+
|
|
370
|
+
localItems.value = nextItems
|
|
371
|
+
emit('update:modelValue', nextItems)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const moveItemUp = (index: number) => moveItem(index, index - 1)
|
|
375
|
+
const moveItemDown = (index: number) => moveItem(index, index + 1)
|
|
376
|
+
|
|
377
|
+
const handleDragStart = (index: number, event: DragEvent) => {
|
|
378
|
+
if (props.disabled) {
|
|
379
|
+
event.preventDefault()
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
draggedIndex.value = index
|
|
384
|
+
dragOverIndex.value = index
|
|
385
|
+
event.dataTransfer?.setData('text/plain', String(index))
|
|
386
|
+
if (event.dataTransfer) {
|
|
387
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const handleDragOver = (index: number, event: DragEvent) => {
|
|
392
|
+
if (props.sortingType !== RepeatingFieldSortingType.DRAG || draggedIndex.value === null) {
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
event.preventDefault()
|
|
397
|
+
dragOverIndex.value = index
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const handleDrop = (index: number, event: DragEvent) => {
|
|
401
|
+
if (props.sortingType !== RepeatingFieldSortingType.DRAG || draggedIndex.value === null) {
|
|
402
|
+
return
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
event.preventDefault()
|
|
406
|
+
|
|
407
|
+
const fromIndex = draggedIndex.value
|
|
408
|
+
// The placeholder renders directly above the hovered row, so the dragged
|
|
409
|
+
// row must land immediately before it. Removing fromIndex first shifts
|
|
410
|
+
// every index after it back by one, so a downward move needs -1 to still
|
|
411
|
+
// land before the hovered row instead of after it.
|
|
412
|
+
const toIndex = index > fromIndex ? index - 1 : index
|
|
413
|
+
moveItem(fromIndex, toIndex)
|
|
414
|
+
|
|
415
|
+
draggedIndex.value = null
|
|
416
|
+
dragOverIndex.value = null
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const handleDragEnd = () => {
|
|
420
|
+
draggedIndex.value = null
|
|
421
|
+
dragOverIndex.value = null
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const handleDragHandleKeydown = (index: number, event: KeyboardEvent) => {
|
|
425
|
+
if (props.disabled) {
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (event.key === 'ArrowUp') {
|
|
430
|
+
event.preventDefault()
|
|
431
|
+
moveItemUp(index)
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (event.key === 'ArrowDown') {
|
|
436
|
+
event.preventDefault()
|
|
437
|
+
moveItemDown(index)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const runValidation = (value: unknown[]) => {
|
|
442
|
+
if (!props.required || !props.validator) return
|
|
443
|
+
|
|
444
|
+
const result = props.validator(value)
|
|
445
|
+
emit('update:error', result ?? '')
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
watch(
|
|
449
|
+
() => props.modelValue,
|
|
450
|
+
value => {
|
|
451
|
+
if (validationMode.value === FormValidationMode.BLUR) {
|
|
452
|
+
runValidation(value)
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
{ deep: true }
|
|
456
|
+
)
|
|
457
|
+
</script>
|