@fy-/fws-vue 2.2.57 → 2.2.58
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/DefaultInput.vue +205 -89
- package/components/ui/DefaultTagInput.vue +193 -60
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { ErrorObject } from '@vuelidate/core'
|
|
3
|
-
import { computed, ref, toRef } from 'vue'
|
|
3
|
+
import { computed, ref, toRef, watch } from 'vue'
|
|
4
4
|
import { useTranslation } from '../../composables/translations'
|
|
5
5
|
import DefaultTagInput from './DefaultTagInput.vue'
|
|
6
6
|
|
|
@@ -54,6 +54,7 @@ const props = withDefaults(
|
|
|
54
54
|
|
|
55
55
|
const translate = useTranslation()
|
|
56
56
|
const inputRef = ref<HTMLInputElement>()
|
|
57
|
+
const floatingActive = ref(false)
|
|
57
58
|
|
|
58
59
|
const errorProps = toRef(props, 'error')
|
|
59
60
|
const errorVuelidateProps = toRef(props, 'errorVuelidate')
|
|
@@ -67,6 +68,11 @@ const checkErrors = computed(() => {
|
|
|
67
68
|
return null
|
|
68
69
|
})
|
|
69
70
|
|
|
71
|
+
// Determine if floating label should be active (input has value or is focused)
|
|
72
|
+
watch(() => props.modelValue, (newVal) => {
|
|
73
|
+
if (newVal) floatingActive.value = true
|
|
74
|
+
}, { immediate: true })
|
|
75
|
+
|
|
70
76
|
function focus() {
|
|
71
77
|
if (inputRef.value) inputRef.value.focus()
|
|
72
78
|
}
|
|
@@ -85,12 +91,21 @@ const emit = defineEmits([
|
|
|
85
91
|
])
|
|
86
92
|
|
|
87
93
|
function handleFocus() {
|
|
94
|
+
floatingActive.value = true
|
|
88
95
|
emit('focus', props.id)
|
|
89
96
|
}
|
|
90
97
|
function handleBlur() {
|
|
98
|
+
if (!props.modelValue) floatingActive.value = false
|
|
91
99
|
emit('blur', props.id)
|
|
92
100
|
}
|
|
93
101
|
|
|
102
|
+
// Copy input value to clipboard
|
|
103
|
+
function copyToClipboard() {
|
|
104
|
+
if (props.modelValue) {
|
|
105
|
+
navigator.clipboard.writeText(props.modelValue.toString())
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
94
109
|
const model = computed<modelValueType>({
|
|
95
110
|
get: () => props.modelValue,
|
|
96
111
|
set: (value) => {
|
|
@@ -155,62 +170,93 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
155
170
|
"
|
|
156
171
|
class="relative"
|
|
157
172
|
>
|
|
173
|
+
<!-- Static label -->
|
|
158
174
|
<label
|
|
159
|
-
v-if="showLabel && label"
|
|
175
|
+
v-if="showLabel && label && type === 'range'"
|
|
160
176
|
:for="id"
|
|
161
177
|
class="block mb-2 text-sm font-medium text-fv-neutral-900 dark:text-white"
|
|
162
178
|
>
|
|
163
|
-
{{ label }}
|
|
164
|
-
<template v-if="type === 'range'">
|
|
165
|
-
({{ model }})
|
|
166
|
-
</template>
|
|
179
|
+
{{ label }} ({{ model }})
|
|
167
180
|
</label>
|
|
168
181
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
:name="id"
|
|
182
|
+
<!-- Floating label (for all except range) -->
|
|
183
|
+
<label
|
|
184
|
+
v-if="showLabel && label && type !== 'range'"
|
|
185
|
+
:for="id"
|
|
186
|
+
class="absolute text-sm duration-300 transform -translate-y-4 scale-75 top-2 z-10 origin-[0] bg-fv-neutral-50 dark:bg-fv-neutral-700 px-2 peer-focus:px-2 peer-placeholder-shown:scale-100 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-1/2 peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4 rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto start-1 pointer-events-none rounded"
|
|
175
187
|
:class="{
|
|
176
|
-
'
|
|
177
|
-
'
|
|
178
|
-
'
|
|
188
|
+
'text-fv-neutral-600 dark:text-fv-neutral-400': !floatingActive,
|
|
189
|
+
'text-fv-primary-600 dark:text-fv-primary-400': floatingActive,
|
|
190
|
+
'text-red-600 dark:text-red-400': checkErrors,
|
|
191
|
+
'-translate-y-4 scale-75 top-2 bg-fv-neutral-50 dark:bg-fv-neutral-700 px-2': floatingActive,
|
|
192
|
+
'-translate-y-1/2 scale-100 top-1/2 bg-transparent': !floatingActive,
|
|
179
193
|
}"
|
|
180
|
-
:autocomplete="autocomplete"
|
|
181
|
-
:min="type === 'range' ? minRange : undefined"
|
|
182
|
-
:max="type === 'range' ? maxRange : undefined"
|
|
183
|
-
:placeholder="placeholder"
|
|
184
|
-
:disabled="disabled"
|
|
185
|
-
:aria-describedby="help ? `${id}-help` : undefined"
|
|
186
|
-
:required="req"
|
|
187
|
-
:aria-invalid="checkErrors ? 'true' : 'false'"
|
|
188
|
-
@focus="handleFocus"
|
|
189
|
-
@blur="handleBlur"
|
|
190
194
|
>
|
|
195
|
+
{{ label }}
|
|
196
|
+
</label>
|
|
191
197
|
|
|
192
|
-
<!--
|
|
193
|
-
<
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
class="
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
198
|
+
<!-- Input element -->
|
|
199
|
+
<div class="relative">
|
|
200
|
+
<input
|
|
201
|
+
:id="id"
|
|
202
|
+
ref="inputRef"
|
|
203
|
+
v-model="model"
|
|
204
|
+
:type="type === 'datepicker' ? 'date' : type === 'phone' ? 'tel' : type"
|
|
205
|
+
:name="id"
|
|
206
|
+
:class="{
|
|
207
|
+
'error': checkErrors,
|
|
208
|
+
'input-with-floating-label': showLabel && label && type !== 'range',
|
|
209
|
+
'bg-fv-neutral-50 border border-fv-neutral-300 text-fv-neutral-900 text-sm rounded-lg block w-full dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-400 dark:text-white transition-all duration-200': type !== 'range',
|
|
210
|
+
'peer h-10 px-4 pt-5 pb-2': showLabel && label && type !== 'range',
|
|
211
|
+
'p-2.5': !(showLabel && label) || type === 'range',
|
|
212
|
+
'border-fv-primary-500 dark:border-fv-primary-500 focus:border-fv-primary-600 dark:focus:border-fv-primary-600': floatingActive && !checkErrors,
|
|
213
|
+
'focus:ring-2 focus:ring-fv-primary-300 dark:focus:ring-fv-primary-800 focus:ring-opacity-50': type !== 'range',
|
|
214
|
+
'w-full h-3 bg-fv-neutral-200 rounded-lg appearance-none cursor-pointer dark:bg-fv-neutral-700': type === 'range',
|
|
215
|
+
'pr-10': copyButton,
|
|
216
|
+
}"
|
|
217
|
+
:autocomplete="autocomplete"
|
|
218
|
+
:min="type === 'range' ? minRange : undefined"
|
|
219
|
+
:max="type === 'range' ? maxRange : undefined"
|
|
220
|
+
:placeholder="(!showLabel || !label) ? placeholder : ' '"
|
|
221
|
+
:disabled="disabled"
|
|
222
|
+
:aria-describedby="help ? `${id}-help` : undefined"
|
|
223
|
+
:required="req"
|
|
224
|
+
:aria-invalid="checkErrors ? 'true' : 'false'"
|
|
225
|
+
@focus="handleFocus"
|
|
226
|
+
@blur="handleBlur"
|
|
206
227
|
>
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
<
|
|
210
|
-
|
|
228
|
+
|
|
229
|
+
<!-- Copy button -->
|
|
230
|
+
<button
|
|
231
|
+
v-if="copyButton && model"
|
|
232
|
+
type="button"
|
|
233
|
+
aria-label="Copy to clipboard"
|
|
234
|
+
class="absolute inset-y-0 right-0 flex items-center pr-3 text-fv-neutral-500 hover:text-fv-primary-600 dark:text-fv-neutral-400 dark:hover:text-fv-primary-400"
|
|
235
|
+
@click="copyToClipboard"
|
|
211
236
|
>
|
|
212
|
-
|
|
213
|
-
|
|
237
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
238
|
+
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
|
|
239
|
+
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
|
|
240
|
+
</svg>
|
|
241
|
+
</button>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<!-- Range Input Extra Labels -->
|
|
245
|
+
<template v-if="type === 'range'">
|
|
246
|
+
<div class="relative pt-6 mt-2">
|
|
247
|
+
<div class="flex justify-between gap-1 text-xs text-fv-neutral-500 dark:text-fv-neutral-400 px-1">
|
|
248
|
+
<span>{{ minRange }}</span>
|
|
249
|
+
<span>{{ ((maxRange - minRange) / 3 + minRange).toFixed(0) }}</span>
|
|
250
|
+
<span>{{ (((maxRange - minRange) / 3) * 2 + minRange).toFixed(0) }}</span>
|
|
251
|
+
<span>{{ maxRange }}</span>
|
|
252
|
+
</div>
|
|
253
|
+
<div class="w-full h-1 bg-fv-neutral-200 dark:bg-fv-neutral-700 rounded-full mt-1">
|
|
254
|
+
<div
|
|
255
|
+
class="h-full bg-fv-primary-500 dark:bg-fv-primary-600 rounded-full"
|
|
256
|
+
:style="{ width: `${((model as number || minRange) - minRange) / (maxRange - minRange) * 100}%` }"
|
|
257
|
+
/>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
214
260
|
</template>
|
|
215
261
|
</div>
|
|
216
262
|
|
|
@@ -261,16 +307,17 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
261
307
|
:required="req"
|
|
262
308
|
:aria-invalid="checkErrors ? 'true' : 'false'"
|
|
263
309
|
class="block p-2.5 w-full text-sm text-fv-neutral-900 bg-fv-neutral-50 rounded-lg
|
|
264
|
-
border border-fv-neutral-300 focus:ring-fv-primary-
|
|
310
|
+
border border-fv-neutral-300 focus:ring-2 focus:ring-fv-primary-300 focus:border-fv-primary-500
|
|
265
311
|
dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-400
|
|
266
|
-
dark:text-white dark:focus:ring-fv-primary-
|
|
312
|
+
dark:text-white dark:focus:ring-fv-primary-800 dark:focus:border-fv-primary-500
|
|
313
|
+
transition-colors duration-200 shadow-sm"
|
|
267
314
|
@focus="handleFocus"
|
|
268
315
|
@blur="handleBlur"
|
|
269
316
|
/>
|
|
270
317
|
</div>
|
|
271
318
|
<div
|
|
272
319
|
v-if="dpOptions.counterMax && model"
|
|
273
|
-
class="text-sm text-fv-neutral-500 dark:text-fv-neutral-400"
|
|
320
|
+
class="text-sm text-fv-neutral-500 dark:text-fv-neutral-400 mt-1 text-right"
|
|
274
321
|
:class="{
|
|
275
322
|
'text-red-500 dark:text-red-300':
|
|
276
323
|
model?.toString().length > dpOptions.counterMax,
|
|
@@ -304,16 +351,16 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
304
351
|
:required="req"
|
|
305
352
|
:aria-invalid="checkErrors ? 'true' : 'false'"
|
|
306
353
|
class="block p-2.5 w-full text-sm text-fv-neutral-900 bg-fv-neutral-50
|
|
307
|
-
rounded-lg border border-fv-neutral-300 focus:ring-fv-primary-500
|
|
308
|
-
|
|
309
|
-
dark:
|
|
310
|
-
|
|
354
|
+
rounded-lg border border-fv-neutral-300 focus:ring-2 focus:ring-fv-primary-300 focus:border-fv-primary-500
|
|
355
|
+
dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-400
|
|
356
|
+
dark:text-white dark:focus:ring-fv-primary-800 dark:focus:border-fv-primary-500
|
|
357
|
+
transition-colors duration-200 shadow-sm min-h-[100px]"
|
|
311
358
|
@focus="handleFocus"
|
|
312
359
|
@blur="handleBlur"
|
|
313
360
|
/>
|
|
314
361
|
<div
|
|
315
362
|
v-if="dpOptions.counterMax && model"
|
|
316
|
-
class="text-sm text-fv-neutral-500 dark:text-fv-neutral-400"
|
|
363
|
+
class="text-sm text-fv-neutral-500 dark:text-fv-neutral-400 mt-1 text-right"
|
|
317
364
|
:class="{
|
|
318
365
|
'text-red-500 dark:text-red-300':
|
|
319
366
|
model?.toString().length > dpOptions.counterMax,
|
|
@@ -332,34 +379,42 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
332
379
|
>
|
|
333
380
|
{{ label }}
|
|
334
381
|
</label>
|
|
335
|
-
<
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
<option
|
|
356
|
-
v-for="opt in options"
|
|
357
|
-
:key="opt[0]?.toString()"
|
|
358
|
-
:value="opt[0]"
|
|
382
|
+
<div class="relative">
|
|
383
|
+
<select
|
|
384
|
+
:id="id"
|
|
385
|
+
ref="inputRef"
|
|
386
|
+
v-model="model"
|
|
387
|
+
:name="id"
|
|
388
|
+
:disabled="disabled"
|
|
389
|
+
:aria-describedby="help ? `${id}-help` : undefined"
|
|
390
|
+
:required="req"
|
|
391
|
+
:class="{
|
|
392
|
+
error: checkErrors,
|
|
393
|
+
}"
|
|
394
|
+
:aria-invalid="checkErrors ? 'true' : 'false'"
|
|
395
|
+
class="appearance-none bg-fv-neutral-50 border border-fv-neutral-300 text-fv-neutral-900 text-sm
|
|
396
|
+
rounded-lg focus:ring-2 focus:ring-fv-primary-300 focus:border-fv-primary-500
|
|
397
|
+
block w-full p-2.5 dark:bg-fv-neutral-700 dark:border-fv-neutral-600
|
|
398
|
+
dark:placeholder-fv-neutral-400 dark:text-white dark:focus:ring-fv-primary-800
|
|
399
|
+
dark:focus:border-fv-primary-500 shadow-sm transition-colors duration-200 pr-10"
|
|
400
|
+
@focus="handleFocus"
|
|
401
|
+
@blur="handleBlur"
|
|
359
402
|
>
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
403
|
+
<option
|
|
404
|
+
v-for="opt in options"
|
|
405
|
+
:key="opt[0]?.toString()"
|
|
406
|
+
:value="opt[0]"
|
|
407
|
+
>
|
|
408
|
+
{{ opt[1] }}
|
|
409
|
+
</option>
|
|
410
|
+
</select>
|
|
411
|
+
<!-- Dropdown arrow icon -->
|
|
412
|
+
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-fv-neutral-700 dark:text-fv-neutral-300">
|
|
413
|
+
<svg class="h-4 w-4 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
|
414
|
+
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
415
|
+
</svg>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
363
418
|
</div>
|
|
364
419
|
</div>
|
|
365
420
|
</template>
|
|
@@ -367,9 +422,10 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
367
422
|
<!-- TOGGLE (switch) -->
|
|
368
423
|
<template v-else-if="type === 'toggle'">
|
|
369
424
|
<label
|
|
370
|
-
class="inline-flex items-center mb-5 cursor-pointer"
|
|
425
|
+
class="inline-flex items-center mb-5 cursor-pointer group"
|
|
371
426
|
:class="{
|
|
372
|
-
error: checkErrors,
|
|
427
|
+
'error': checkErrors,
|
|
428
|
+
'opacity-70 cursor-not-allowed': disabled,
|
|
373
429
|
}"
|
|
374
430
|
>
|
|
375
431
|
<input
|
|
@@ -394,7 +450,7 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
394
450
|
after:content-[''] after:absolute after:top-[2px] after:start-[2px]
|
|
395
451
|
after:bg-white after:border-fv-neutral-300 after:border after:rounded-full
|
|
396
452
|
after:w-5 after:h-5 after:transition-all dark:border-fv-neutral-600
|
|
397
|
-
peer-checked:bg-fv-primary-600"
|
|
453
|
+
peer-checked:bg-fv-primary-600 shadow-sm group-hover:shadow-md transition-all duration-200"
|
|
398
454
|
/>
|
|
399
455
|
<span
|
|
400
456
|
class="ms-3 text-sm font-medium text-fv-neutral-900 dark:text-fv-neutral-300"
|
|
@@ -414,14 +470,16 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
414
470
|
|
|
415
471
|
<!-- CHECKBOX / RADIO -->
|
|
416
472
|
<template v-else-if="type === 'checkbox' || type === 'radio'">
|
|
417
|
-
<div class="flex mb-4">
|
|
473
|
+
<div class="flex mb-4" :class="{ 'opacity-70 cursor-not-allowed': disabled }">
|
|
418
474
|
<div class="flex items-center h-5">
|
|
419
475
|
<input
|
|
420
476
|
:id="id"
|
|
421
477
|
ref="inputRef"
|
|
422
478
|
v-model="modelCheckbox"
|
|
423
479
|
:class="{
|
|
424
|
-
error: checkErrors,
|
|
480
|
+
'error': checkErrors,
|
|
481
|
+
'cursor-not-allowed': disabled,
|
|
482
|
+
'cursor-pointer': !disabled,
|
|
425
483
|
}"
|
|
426
484
|
:aria-describedby="help ? `${id}-help` : undefined"
|
|
427
485
|
:type="type"
|
|
@@ -432,7 +490,8 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
432
490
|
class="w-4 h-4 text-fv-primary-600 bg-fv-neutral-100 border-fv-neutral-300
|
|
433
491
|
rounded focus:ring-fv-primary-500 dark:focus:ring-fv-primary-600
|
|
434
492
|
dark:ring-offset-fv-neutral-800 dark:focus:ring-offset-fv-neutral-800
|
|
435
|
-
focus:ring-2 dark:bg-fv-neutral-700 dark:border-fv-neutral-600
|
|
493
|
+
focus:ring-2 dark:bg-fv-neutral-700 dark:border-fv-neutral-600
|
|
494
|
+
transition-colors duration-200 shadow-sm"
|
|
436
495
|
@focus="handleFocus"
|
|
437
496
|
@blur="handleBlur"
|
|
438
497
|
>
|
|
@@ -440,7 +499,8 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
440
499
|
<div class="ms-2 text-sm">
|
|
441
500
|
<label
|
|
442
501
|
:for="id"
|
|
443
|
-
class="font-medium text-fv-neutral-900 dark:text-fv-neutral-300"
|
|
502
|
+
class="font-medium text-fv-neutral-900 dark:text-fv-neutral-300 cursor-pointer"
|
|
503
|
+
:class="{ 'cursor-not-allowed': disabled }"
|
|
444
504
|
>
|
|
445
505
|
{{ label }}
|
|
446
506
|
</label>
|
|
@@ -458,10 +518,13 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
458
518
|
<!-- Error message -->
|
|
459
519
|
<p
|
|
460
520
|
v-if="checkErrors"
|
|
461
|
-
class="mt-0.5 text-sm text-red-600 dark:text-red-300"
|
|
521
|
+
class="mt-0.5 text-sm text-red-600 dark:text-red-300 flex items-center"
|
|
462
522
|
role="alert"
|
|
463
523
|
aria-live="assertive"
|
|
464
524
|
>
|
|
525
|
+
<svg class="w-4 h-4 mr-1.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
|
526
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
|
527
|
+
</svg>
|
|
465
528
|
{{ checkErrors }}
|
|
466
529
|
</p>
|
|
467
530
|
|
|
@@ -481,7 +544,60 @@ input,
|
|
|
481
544
|
textarea,
|
|
482
545
|
select {
|
|
483
546
|
&.error {
|
|
484
|
-
@apply border-red-500 dark:border-red-400;
|
|
547
|
+
@apply border-red-500 dark:border-red-400 focus:border-red-500 dark:focus:border-red-400 focus:ring-red-200 dark:focus:ring-red-800;
|
|
485
548
|
}
|
|
486
549
|
}
|
|
550
|
+
|
|
551
|
+
/* Range input styling */
|
|
552
|
+
input[type="range"] {
|
|
553
|
+
@apply appearance-none bg-transparent;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
input[type="range"]::-webkit-slider-thumb {
|
|
557
|
+
@apply appearance-none w-4 h-4 rounded-full bg-fv-primary-500 dark:bg-fv-primary-600 cursor-pointer border-0 shadow-md;
|
|
558
|
+
margin-top: -0.5rem;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
input[type="range"]::-moz-range-thumb {
|
|
562
|
+
@apply w-4 h-4 rounded-full bg-fv-primary-500 dark:bg-fv-primary-600 cursor-pointer border-0 shadow-md;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
input[type="range"]:focus::-webkit-slider-thumb {
|
|
566
|
+
@apply ring-2 ring-fv-primary-300 dark:ring-fv-primary-800;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
input[type="range"]:focus::-moz-range-thumb {
|
|
570
|
+
@apply ring-2 ring-fv-primary-300 dark:ring-fv-primary-800;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/* Textarea auto-grow */
|
|
574
|
+
.grow-wrap {
|
|
575
|
+
@apply grid;
|
|
576
|
+
}
|
|
577
|
+
.grow-wrap::after {
|
|
578
|
+
content: attr(data-replicated-value) " ";
|
|
579
|
+
white-space: pre-wrap;
|
|
580
|
+
visibility: hidden;
|
|
581
|
+
}
|
|
582
|
+
.grow-wrap > textarea {
|
|
583
|
+
resize: none;
|
|
584
|
+
overflow: hidden;
|
|
585
|
+
}
|
|
586
|
+
.grow-wrap > textarea,
|
|
587
|
+
.grow-wrap::after {
|
|
588
|
+
grid-area: 1 / 1 / 2 / 2;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/* Add smooth transitions */
|
|
592
|
+
input, select, textarea, input[type="range"]::-webkit-slider-thumb, input[type="range"]::-moz-range-thumb {
|
|
593
|
+
@apply transition-all duration-200;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/* Floating label styling */
|
|
597
|
+
.input-with-floating-label::placeholder {
|
|
598
|
+
@apply opacity-0;
|
|
599
|
+
}
|
|
600
|
+
.input-with-floating-label:focus::placeholder {
|
|
601
|
+
@apply opacity-100 transition-opacity duration-300;
|
|
602
|
+
}
|
|
487
603
|
</style>
|
|
@@ -45,6 +45,7 @@ const props = withDefaults(
|
|
|
45
45
|
*/
|
|
46
46
|
const textInput = ref<HTMLElement>()
|
|
47
47
|
const isMaxReached = ref(false)
|
|
48
|
+
const inputContainer = ref<HTMLElement>()
|
|
48
49
|
|
|
49
50
|
const emit = defineEmits(['update:modelValue'])
|
|
50
51
|
|
|
@@ -62,7 +63,7 @@ const model = computed({
|
|
|
62
63
|
* Compute aria-describedby IDs if help or error exist
|
|
63
64
|
*/
|
|
64
65
|
const describedByIds = computed(() => {
|
|
65
|
-
const ids:
|
|
66
|
+
const ids: string[] = []
|
|
66
67
|
if (props.help) {
|
|
67
68
|
ids.push(`help_tags_${props.id}`)
|
|
68
69
|
}
|
|
@@ -95,7 +96,7 @@ onMounted(() => {
|
|
|
95
96
|
})
|
|
96
97
|
|
|
97
98
|
/**
|
|
98
|
-
* Event Bus
|
|
99
|
+
* Event Bus for notifications
|
|
99
100
|
*/
|
|
100
101
|
const eventBus = useEventBus()
|
|
101
102
|
|
|
@@ -104,14 +105,23 @@ const eventBus = useEventBus()
|
|
|
104
105
|
*/
|
|
105
106
|
async function copyText() {
|
|
106
107
|
const text = model.value.join(', ')
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await navigator.clipboard.writeText(text)
|
|
111
|
+
|
|
112
|
+
eventBus.emit('SendNotif', {
|
|
113
|
+
title: 'Tags copied!',
|
|
114
|
+
type: 'success',
|
|
115
|
+
time: 2500,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
eventBus.emit('SendNotif', {
|
|
120
|
+
title: 'Failed to copy tags',
|
|
121
|
+
type: 'error',
|
|
122
|
+
time: 2500,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
115
125
|
}
|
|
116
126
|
|
|
117
127
|
/**
|
|
@@ -153,7 +163,7 @@ function addTag() {
|
|
|
153
163
|
filteredTags.splice(slotsAvailable)
|
|
154
164
|
}
|
|
155
165
|
|
|
156
|
-
model.value.
|
|
166
|
+
model.value = [...model.value, ...filteredTags]
|
|
157
167
|
textInput.value.textContent = ''
|
|
158
168
|
}
|
|
159
169
|
|
|
@@ -161,7 +171,9 @@ function addTag() {
|
|
|
161
171
|
* Remove a tag by index
|
|
162
172
|
*/
|
|
163
173
|
function removeTag(index: number) {
|
|
164
|
-
model.value
|
|
174
|
+
const newTags = [...model.value]
|
|
175
|
+
newTags.splice(index, 1)
|
|
176
|
+
model.value = newTags
|
|
165
177
|
focusInput()
|
|
166
178
|
}
|
|
167
179
|
|
|
@@ -172,7 +184,11 @@ function removeLastTag() {
|
|
|
172
184
|
if (!textInput.value) return
|
|
173
185
|
if (textInput.value.textContent === '') {
|
|
174
186
|
// If input is empty, remove the last tag
|
|
175
|
-
model.value.
|
|
187
|
+
if (model.value.length > 0) {
|
|
188
|
+
const newTags = [...model.value]
|
|
189
|
+
newTags.pop()
|
|
190
|
+
model.value = newTags
|
|
191
|
+
}
|
|
176
192
|
}
|
|
177
193
|
else {
|
|
178
194
|
// Otherwise, remove the last character in the input
|
|
@@ -223,33 +239,49 @@ function handlePaste(e: ClipboardEvent) {
|
|
|
223
239
|
e.preventDefault()
|
|
224
240
|
addTag()
|
|
225
241
|
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Handle keyboard navigation between tags
|
|
245
|
+
*/
|
|
246
|
+
function handleKeyNavigation(e: KeyboardEvent, index: number) {
|
|
247
|
+
if (e.key === 'ArrowLeft' && index > 0) {
|
|
248
|
+
const prevTag = inputContainer.value?.querySelector(`[data-index="${index - 1}"] button`) as HTMLElement
|
|
249
|
+
if (prevTag) prevTag.focus()
|
|
250
|
+
}
|
|
251
|
+
else if (e.key === 'ArrowRight' && index < model.value.length - 1) {
|
|
252
|
+
const nextTag = inputContainer.value?.querySelector(`[data-index="${index + 1}"] button`) as HTMLElement
|
|
253
|
+
if (nextTag) nextTag.focus()
|
|
254
|
+
}
|
|
255
|
+
}
|
|
226
256
|
</script>
|
|
227
257
|
|
|
228
258
|
<template>
|
|
229
|
-
<div class="space-y-
|
|
230
|
-
<!-- Optional label -->
|
|
231
|
-
<label
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
259
|
+
<div class="space-y-2 w-full">
|
|
260
|
+
<!-- Optional label with help text -->
|
|
261
|
+
<div v-if="label" class="flex items-center flex-wrap gap-1">
|
|
262
|
+
<label
|
|
263
|
+
:id="`label_tags_${id}`"
|
|
264
|
+
:for="`tags_${id}`"
|
|
265
|
+
class="block text-sm font-medium dark:text-white"
|
|
266
|
+
>
|
|
267
|
+
{{ label }}
|
|
268
|
+
</label>
|
|
238
269
|
<!-- Optional help text -->
|
|
239
270
|
<span
|
|
240
271
|
v-if="help"
|
|
241
272
|
:id="`help_tags_${id}`"
|
|
242
|
-
class="
|
|
273
|
+
class="text-xs text-fv-neutral-500 dark:text-fv-neutral-300"
|
|
243
274
|
>
|
|
244
275
|
{{ help }}
|
|
245
276
|
</span>
|
|
246
|
-
</
|
|
277
|
+
</div>
|
|
247
278
|
|
|
248
279
|
<div
|
|
280
|
+
ref="inputContainer"
|
|
249
281
|
class="tags-input"
|
|
250
282
|
:class="[
|
|
251
283
|
$props.error ? 'error' : '',
|
|
252
|
-
isMaxReached ? '
|
|
284
|
+
isMaxReached ? 'max-reached' : '',
|
|
253
285
|
]"
|
|
254
286
|
role="textbox"
|
|
255
287
|
:aria-labelledby="`label_tags_${id}`"
|
|
@@ -264,31 +296,34 @@ function handlePaste(e: ClipboardEvent) {
|
|
|
264
296
|
v-for="(tag, index) in model"
|
|
265
297
|
:key="`${tag}-${index}`"
|
|
266
298
|
role="listitem"
|
|
299
|
+
:data-index="index"
|
|
267
300
|
class="tag"
|
|
268
301
|
:class="{
|
|
269
|
-
|
|
270
|
-
[color]: maxLenghtPerTag === 0 || tag.length <= maxLenghtPerTag,
|
|
302
|
+
'tag-error': maxLenghtPerTag > 0 && tag.length > maxLenghtPerTag,
|
|
303
|
+
[`tag-${color}`]: maxLenghtPerTag === 0 || tag.length <= maxLenghtPerTag,
|
|
271
304
|
}"
|
|
272
305
|
>
|
|
273
|
-
{{ tag }}
|
|
306
|
+
<span class="tag-text">{{ tag }}</span>
|
|
274
307
|
<button
|
|
275
308
|
type="button"
|
|
276
|
-
class="
|
|
309
|
+
class="tag-remove"
|
|
277
310
|
:aria-label="`Remove tag ${tag}`"
|
|
278
311
|
@click.prevent="removeTag(index)"
|
|
312
|
+
@keydown="(e) => handleKeyNavigation(e, index)"
|
|
279
313
|
>
|
|
280
314
|
<svg
|
|
281
|
-
class="w-
|
|
315
|
+
class="w-3.5 h-3.5"
|
|
282
316
|
xmlns="http://www.w3.org/2000/svg"
|
|
283
317
|
fill="none"
|
|
284
318
|
viewBox="0 0 24 24"
|
|
285
|
-
stroke-width="2"
|
|
319
|
+
stroke-width="2.5"
|
|
286
320
|
stroke="currentColor"
|
|
321
|
+
aria-hidden="true"
|
|
287
322
|
>
|
|
288
323
|
<path
|
|
289
324
|
stroke-linecap="round"
|
|
290
325
|
stroke-linejoin="round"
|
|
291
|
-
d="
|
|
326
|
+
d="M6 18L18 6M6 6l12 12"
|
|
292
327
|
/>
|
|
293
328
|
</svg>
|
|
294
329
|
</button>
|
|
@@ -301,7 +336,7 @@ function handlePaste(e: ClipboardEvent) {
|
|
|
301
336
|
contenteditable="true"
|
|
302
337
|
tabindex="0"
|
|
303
338
|
class="input"
|
|
304
|
-
:placeholder="isMaxReached
|
|
339
|
+
:data-placeholder="isMaxReached
|
|
305
340
|
? 'Max tags reached'
|
|
306
341
|
: 'Type or paste and press Enter...'"
|
|
307
342
|
:aria-placeholder="isMaxReached
|
|
@@ -312,6 +347,11 @@ function handlePaste(e: ClipboardEvent) {
|
|
|
312
347
|
/>
|
|
313
348
|
</div>
|
|
314
349
|
|
|
350
|
+
<!-- Tag counter when maxTags is set -->
|
|
351
|
+
<div v-if="maxTags > 0" class="tag-counter">
|
|
352
|
+
<span>{{ model.length }}/{{ maxTags }}</span>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
315
355
|
<!-- Inline error display if needed -->
|
|
316
356
|
<p
|
|
317
357
|
v-if="$props.error"
|
|
@@ -323,12 +363,28 @@ function handlePaste(e: ClipboardEvent) {
|
|
|
323
363
|
</p>
|
|
324
364
|
|
|
325
365
|
<!-- Copy button / or any additional actions -->
|
|
326
|
-
<div v-if="copyButton" class="
|
|
366
|
+
<div v-if="copyButton" class="copy-button-container">
|
|
327
367
|
<button
|
|
328
|
-
class="
|
|
368
|
+
class="copy-button"
|
|
329
369
|
type="button"
|
|
370
|
+
:disabled="model.length === 0"
|
|
330
371
|
@click.prevent="copyText"
|
|
331
372
|
>
|
|
373
|
+
<svg
|
|
374
|
+
class="w-4 h-4 mr-1"
|
|
375
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
376
|
+
fill="none"
|
|
377
|
+
viewBox="0 0 24 24"
|
|
378
|
+
stroke-width="2"
|
|
379
|
+
stroke="currentColor"
|
|
380
|
+
aria-hidden="true"
|
|
381
|
+
>
|
|
382
|
+
<path
|
|
383
|
+
stroke-linecap="round"
|
|
384
|
+
stroke-linejoin="round"
|
|
385
|
+
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75"
|
|
386
|
+
/>
|
|
387
|
+
</svg>
|
|
332
388
|
Copy tags
|
|
333
389
|
</button>
|
|
334
390
|
</div>
|
|
@@ -338,49 +394,126 @@ function handlePaste(e: ClipboardEvent) {
|
|
|
338
394
|
<style scoped>
|
|
339
395
|
/* Container for all tags plus input */
|
|
340
396
|
.tags-input {
|
|
341
|
-
@apply w-full flex flex-wrap gap-2 items-center
|
|
342
|
-
border border-fv-neutral-300 text-fv-neutral-900 text-sm rounded-
|
|
343
|
-
focus-within:ring-fv-primary-500 focus-within:border-fv-primary-500
|
|
344
|
-
dark:bg-fv-neutral-
|
|
345
|
-
dark:placeholder-fv-neutral-400 dark:text-white p-
|
|
346
|
-
dark:focus-within:ring-fv-primary-500 dark:focus-within:border-fv-primary-500
|
|
397
|
+
@apply w-full flex flex-wrap gap-2 items-center bg-white
|
|
398
|
+
border border-fv-neutral-300 text-fv-neutral-900 text-sm rounded-md
|
|
399
|
+
focus-within:ring-2 focus-within:ring-fv-primary-500 focus-within:border-fv-primary-500
|
|
400
|
+
dark:bg-fv-neutral-800 dark:border-fv-neutral-600
|
|
401
|
+
dark:placeholder-fv-neutral-400 dark:text-white p-2
|
|
402
|
+
dark:focus-within:ring-fv-primary-500 dark:focus-within:border-fv-primary-500
|
|
403
|
+
transition-all duration-200 ease-in-out shadow-sm;
|
|
347
404
|
cursor: text;
|
|
405
|
+
min-height: 2.5rem;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/* Max tags reached state */
|
|
409
|
+
.tags-input.max-reached {
|
|
410
|
+
@apply opacity-80 pointer-events-none bg-fv-neutral-100 dark:bg-fv-neutral-900;
|
|
348
411
|
}
|
|
349
412
|
|
|
350
413
|
/* Error border */
|
|
351
414
|
.tags-input.error {
|
|
352
|
-
@apply border-red-500 dark:border-red-400
|
|
415
|
+
@apply border-red-500 dark:border-red-400 ring-1 ring-red-500 dark:ring-red-400 !important;
|
|
353
416
|
}
|
|
354
417
|
|
|
355
|
-
/* Tag styling */
|
|
418
|
+
/* Tag base styling */
|
|
356
419
|
.tag {
|
|
357
|
-
@apply inline-flex
|
|
358
|
-
font-medium px-
|
|
359
|
-
dark:text-white
|
|
420
|
+
@apply inline-flex items-center justify-between
|
|
421
|
+
text-sm font-medium rounded-full px-3 py-1
|
|
422
|
+
dark:text-white transition-all duration-200 ease-in-out;
|
|
360
423
|
}
|
|
361
424
|
|
|
362
|
-
|
|
363
|
-
.
|
|
364
|
-
@apply bg-blue-400 dark:bg-blue-800;
|
|
425
|
+
.tag-text {
|
|
426
|
+
@apply mr-1.5 truncate max-w-[200px];
|
|
365
427
|
}
|
|
366
|
-
|
|
367
|
-
|
|
428
|
+
|
|
429
|
+
/* Tag remove button */
|
|
430
|
+
.tag-remove {
|
|
431
|
+
@apply rounded-full p-0.5 flex items-center justify-center
|
|
432
|
+
hover:bg-black/10 dark:hover:bg-white/20
|
|
433
|
+
focus:outline-none focus:ring-2 focus:ring-offset-1
|
|
434
|
+
transition-colors duration-200;
|
|
435
|
+
height: 18px;
|
|
436
|
+
width: 18px;
|
|
368
437
|
}
|
|
369
|
-
|
|
370
|
-
|
|
438
|
+
|
|
439
|
+
/* Color variants with modern styling */
|
|
440
|
+
.tag-blue {
|
|
441
|
+
@apply bg-blue-100 text-blue-800
|
|
442
|
+
dark:bg-blue-900/70 dark:text-blue-100
|
|
443
|
+
ring-1 ring-blue-400/30 dark:ring-blue-700/30;
|
|
371
444
|
}
|
|
372
|
-
|
|
373
|
-
|
|
445
|
+
|
|
446
|
+
.tag-red, .tag-error {
|
|
447
|
+
@apply bg-red-100 text-red-800
|
|
448
|
+
dark:bg-red-900/70 dark:text-red-100
|
|
449
|
+
ring-1 ring-red-400/30 dark:ring-red-700/30;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.tag-green {
|
|
453
|
+
@apply bg-green-100 text-green-800
|
|
454
|
+
dark:bg-green-900/70 dark:text-green-100
|
|
455
|
+
ring-1 ring-green-400/30 dark:ring-green-700/30;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.tag-purple {
|
|
459
|
+
@apply bg-purple-100 text-purple-800
|
|
460
|
+
dark:bg-purple-900/70 dark:text-purple-100
|
|
461
|
+
ring-1 ring-purple-400/30 dark:ring-purple-700/30;
|
|
374
462
|
}
|
|
375
|
-
|
|
376
|
-
|
|
463
|
+
|
|
464
|
+
.tag-orange {
|
|
465
|
+
@apply bg-orange-100 text-orange-800
|
|
466
|
+
dark:bg-orange-900/70 dark:text-orange-100
|
|
467
|
+
ring-1 ring-orange-400/30 dark:ring-orange-700/30;
|
|
377
468
|
}
|
|
378
|
-
|
|
379
|
-
|
|
469
|
+
|
|
470
|
+
.tag-neutral {
|
|
471
|
+
@apply bg-fv-neutral-100 text-fv-neutral-800
|
|
472
|
+
dark:bg-fv-neutral-700/70 dark:text-fv-neutral-100
|
|
473
|
+
ring-1 ring-fv-neutral-400/30 dark:ring-fv-neutral-500/30;
|
|
380
474
|
}
|
|
381
475
|
|
|
382
476
|
/* The editable input area for new tags */
|
|
383
477
|
.input {
|
|
384
|
-
@apply flex-grow min-w-[100px] outline-none border-none break-words;
|
|
478
|
+
@apply flex-grow min-w-[100px] outline-none border-none break-words p-1;
|
|
479
|
+
min-height: 1.5rem;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/* Placeholder styling */
|
|
483
|
+
.input:empty:before {
|
|
484
|
+
content: attr(data-placeholder);
|
|
485
|
+
@apply text-fv-neutral-400 dark:text-fv-neutral-500;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/* Copy button styling */
|
|
489
|
+
.copy-button-container {
|
|
490
|
+
@apply flex justify-end mt-2;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.copy-button {
|
|
494
|
+
@apply inline-flex items-center justify-center
|
|
495
|
+
bg-fv-neutral-100 hover:bg-fv-neutral-200
|
|
496
|
+
text-fv-neutral-700 dark:text-fv-neutral-200
|
|
497
|
+
dark:bg-fv-neutral-700 dark:hover:bg-fv-neutral-600
|
|
498
|
+
px-3 py-1.5 rounded-md text-sm font-medium
|
|
499
|
+
transition-colors duration-200 ease-in-out
|
|
500
|
+
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-fv-primary-500
|
|
501
|
+
disabled:opacity-50 disabled:cursor-not-allowed;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/* Tag counter styling */
|
|
505
|
+
.tag-counter {
|
|
506
|
+
@apply text-xs text-right text-fv-neutral-500 dark:text-fv-neutral-400 mt-1;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/* Responsive adjustments */
|
|
510
|
+
@media (max-width: 640px) {
|
|
511
|
+
.tag-text {
|
|
512
|
+
@apply max-w-[120px];
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.tags-input {
|
|
516
|
+
@apply p-1.5;
|
|
517
|
+
}
|
|
385
518
|
}
|
|
386
519
|
</style>
|