@fy-/fws-vue 2.2.57 → 2.2.59
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 +181 -86
- package/components/ui/DefaultTagInput.vue +193 -60
- package/package.json +1 -1
|
@@ -91,6 +91,13 @@ function handleBlur() {
|
|
|
91
91
|
emit('blur', props.id)
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// Copy input value to clipboard
|
|
95
|
+
function copyToClipboard() {
|
|
96
|
+
if (props.modelValue) {
|
|
97
|
+
navigator.clipboard.writeText(props.modelValue.toString())
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
94
101
|
const model = computed<modelValueType>({
|
|
95
102
|
get: () => props.modelValue,
|
|
96
103
|
set: (value) => {
|
|
@@ -155,10 +162,14 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
155
162
|
"
|
|
156
163
|
class="relative"
|
|
157
164
|
>
|
|
165
|
+
<!-- Static label for all types -->
|
|
158
166
|
<label
|
|
159
167
|
v-if="showLabel && label"
|
|
160
168
|
:for="id"
|
|
161
169
|
class="block mb-2 text-sm font-medium text-fv-neutral-900 dark:text-white"
|
|
170
|
+
:class="{
|
|
171
|
+
'text-red-600 dark:text-red-400': checkErrors,
|
|
172
|
+
}"
|
|
162
173
|
>
|
|
163
174
|
{{ label }}
|
|
164
175
|
<template v-if="type === 'range'">
|
|
@@ -166,51 +177,66 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
166
177
|
</template>
|
|
167
178
|
</label>
|
|
168
179
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
180
|
+
<!-- Input element -->
|
|
181
|
+
<div class="relative">
|
|
182
|
+
<input
|
|
183
|
+
:id="id"
|
|
184
|
+
ref="inputRef"
|
|
185
|
+
v-model="model"
|
|
186
|
+
:type="type === 'datepicker' ? 'date' : type === 'phone' ? 'tel' : type"
|
|
187
|
+
:name="id"
|
|
188
|
+
:class="{
|
|
189
|
+
'error': checkErrors,
|
|
190
|
+
'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',
|
|
191
|
+
'p-2.5': type !== 'range',
|
|
192
|
+
'focus:border-fv-primary-500 dark:focus:border-fv-primary-500': !checkErrors,
|
|
193
|
+
'focus:ring-2 focus:ring-fv-primary-300 dark:focus:ring-fv-primary-800 focus:ring-opacity-50': type !== 'range',
|
|
194
|
+
'w-full h-3 bg-fv-neutral-200 rounded-lg appearance-none cursor-pointer dark:bg-fv-neutral-700': type === 'range',
|
|
195
|
+
'pr-10': copyButton,
|
|
196
|
+
}"
|
|
197
|
+
:autocomplete="autocomplete"
|
|
198
|
+
:min="type === 'range' ? minRange : undefined"
|
|
199
|
+
:max="type === 'range' ? maxRange : undefined"
|
|
200
|
+
:placeholder="placeholder"
|
|
201
|
+
:disabled="disabled"
|
|
202
|
+
:aria-describedby="help ? `${id}-help` : undefined"
|
|
203
|
+
:required="req"
|
|
204
|
+
:aria-invalid="checkErrors ? 'true' : 'false'"
|
|
205
|
+
@focus="handleFocus"
|
|
206
|
+
@blur="handleBlur"
|
|
207
|
+
>
|
|
208
|
+
|
|
209
|
+
<!-- Copy button -->
|
|
210
|
+
<button
|
|
211
|
+
v-if="copyButton && model"
|
|
212
|
+
type="button"
|
|
213
|
+
aria-label="Copy to clipboard"
|
|
214
|
+
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"
|
|
215
|
+
@click="copyToClipboard"
|
|
216
|
+
>
|
|
217
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
218
|
+
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
|
|
219
|
+
<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" />
|
|
220
|
+
</svg>
|
|
221
|
+
</button>
|
|
222
|
+
</div>
|
|
191
223
|
|
|
192
224
|
<!-- Range Input Extra Labels -->
|
|
193
225
|
<template v-if="type === 'range'">
|
|
194
|
-
<
|
|
195
|
-
class="text-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
</span>
|
|
209
|
-
<span
|
|
210
|
-
class="text-sm text-gray-500 dark:text-gray-400 absolute end-0 -bottom-6"
|
|
211
|
-
>
|
|
212
|
-
Max ({{ maxRange }})
|
|
213
|
-
</span>
|
|
226
|
+
<div class="relative pt-6 mt-2">
|
|
227
|
+
<div class="flex justify-between gap-1 text-xs text-fv-neutral-500 dark:text-fv-neutral-400 px-1">
|
|
228
|
+
<span>{{ minRange }}</span>
|
|
229
|
+
<span>{{ ((maxRange - minRange) / 3 + minRange).toFixed(0) }}</span>
|
|
230
|
+
<span>{{ (((maxRange - minRange) / 3) * 2 + minRange).toFixed(0) }}</span>
|
|
231
|
+
<span>{{ maxRange }}</span>
|
|
232
|
+
</div>
|
|
233
|
+
<div class="w-full h-1 bg-fv-neutral-200 dark:bg-fv-neutral-700 rounded-full mt-1">
|
|
234
|
+
<div
|
|
235
|
+
class="h-full bg-fv-primary-500 dark:bg-fv-primary-600 rounded-full"
|
|
236
|
+
:style="{ width: `${((model as number || minRange) - minRange) / (maxRange - minRange) * 100}%` }"
|
|
237
|
+
/>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
214
240
|
</template>
|
|
215
241
|
</div>
|
|
216
242
|
|
|
@@ -261,16 +287,17 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
261
287
|
:required="req"
|
|
262
288
|
:aria-invalid="checkErrors ? 'true' : 'false'"
|
|
263
289
|
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-
|
|
290
|
+
border border-fv-neutral-300 focus:ring-2 focus:ring-fv-primary-300 focus:border-fv-primary-500
|
|
265
291
|
dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-400
|
|
266
|
-
dark:text-white dark:focus:ring-fv-primary-
|
|
292
|
+
dark:text-white dark:focus:ring-fv-primary-800 dark:focus:border-fv-primary-500
|
|
293
|
+
transition-colors duration-200 shadow-sm"
|
|
267
294
|
@focus="handleFocus"
|
|
268
295
|
@blur="handleBlur"
|
|
269
296
|
/>
|
|
270
297
|
</div>
|
|
271
298
|
<div
|
|
272
299
|
v-if="dpOptions.counterMax && model"
|
|
273
|
-
class="text-sm text-fv-neutral-500 dark:text-fv-neutral-400"
|
|
300
|
+
class="text-sm text-fv-neutral-500 dark:text-fv-neutral-400 mt-1 text-right"
|
|
274
301
|
:class="{
|
|
275
302
|
'text-red-500 dark:text-red-300':
|
|
276
303
|
model?.toString().length > dpOptions.counterMax,
|
|
@@ -304,16 +331,16 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
304
331
|
:required="req"
|
|
305
332
|
:aria-invalid="checkErrors ? 'true' : 'false'"
|
|
306
333
|
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
|
-
|
|
334
|
+
rounded-lg border border-fv-neutral-300 focus:ring-2 focus:ring-fv-primary-300 focus:border-fv-primary-500
|
|
335
|
+
dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-400
|
|
336
|
+
dark:text-white dark:focus:ring-fv-primary-800 dark:focus:border-fv-primary-500
|
|
337
|
+
transition-colors duration-200 shadow-sm min-h-[100px]"
|
|
311
338
|
@focus="handleFocus"
|
|
312
339
|
@blur="handleBlur"
|
|
313
340
|
/>
|
|
314
341
|
<div
|
|
315
342
|
v-if="dpOptions.counterMax && model"
|
|
316
|
-
class="text-sm text-fv-neutral-500 dark:text-fv-neutral-400"
|
|
343
|
+
class="text-sm text-fv-neutral-500 dark:text-fv-neutral-400 mt-1 text-right"
|
|
317
344
|
:class="{
|
|
318
345
|
'text-red-500 dark:text-red-300':
|
|
319
346
|
model?.toString().length > dpOptions.counterMax,
|
|
@@ -332,34 +359,42 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
332
359
|
>
|
|
333
360
|
{{ label }}
|
|
334
361
|
</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]"
|
|
362
|
+
<div class="relative">
|
|
363
|
+
<select
|
|
364
|
+
:id="id"
|
|
365
|
+
ref="inputRef"
|
|
366
|
+
v-model="model"
|
|
367
|
+
:name="id"
|
|
368
|
+
:disabled="disabled"
|
|
369
|
+
:aria-describedby="help ? `${id}-help` : undefined"
|
|
370
|
+
:required="req"
|
|
371
|
+
:class="{
|
|
372
|
+
error: checkErrors,
|
|
373
|
+
}"
|
|
374
|
+
:aria-invalid="checkErrors ? 'true' : 'false'"
|
|
375
|
+
class="appearance-none bg-fv-neutral-50 border border-fv-neutral-300 text-fv-neutral-900 text-sm
|
|
376
|
+
rounded-lg focus:ring-2 focus:ring-fv-primary-300 focus:border-fv-primary-500
|
|
377
|
+
block w-full p-2.5 dark:bg-fv-neutral-700 dark:border-fv-neutral-600
|
|
378
|
+
dark:placeholder-fv-neutral-400 dark:text-white dark:focus:ring-fv-primary-800
|
|
379
|
+
dark:focus:border-fv-primary-500 shadow-sm transition-colors duration-200 pr-10"
|
|
380
|
+
@focus="handleFocus"
|
|
381
|
+
@blur="handleBlur"
|
|
359
382
|
>
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
383
|
+
<option
|
|
384
|
+
v-for="opt in options"
|
|
385
|
+
:key="opt[0]?.toString()"
|
|
386
|
+
:value="opt[0]"
|
|
387
|
+
>
|
|
388
|
+
{{ opt[1] }}
|
|
389
|
+
</option>
|
|
390
|
+
</select>
|
|
391
|
+
<!-- Dropdown arrow icon -->
|
|
392
|
+
<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">
|
|
393
|
+
<svg class="h-4 w-4 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
|
394
|
+
<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" />
|
|
395
|
+
</svg>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
363
398
|
</div>
|
|
364
399
|
</div>
|
|
365
400
|
</template>
|
|
@@ -367,9 +402,10 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
367
402
|
<!-- TOGGLE (switch) -->
|
|
368
403
|
<template v-else-if="type === 'toggle'">
|
|
369
404
|
<label
|
|
370
|
-
class="inline-flex items-center mb-5 cursor-pointer"
|
|
405
|
+
class="inline-flex items-center mb-5 cursor-pointer group"
|
|
371
406
|
:class="{
|
|
372
|
-
error: checkErrors,
|
|
407
|
+
'error': checkErrors,
|
|
408
|
+
'opacity-70 cursor-not-allowed': disabled,
|
|
373
409
|
}"
|
|
374
410
|
>
|
|
375
411
|
<input
|
|
@@ -394,7 +430,7 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
394
430
|
after:content-[''] after:absolute after:top-[2px] after:start-[2px]
|
|
395
431
|
after:bg-white after:border-fv-neutral-300 after:border after:rounded-full
|
|
396
432
|
after:w-5 after:h-5 after:transition-all dark:border-fv-neutral-600
|
|
397
|
-
peer-checked:bg-fv-primary-600"
|
|
433
|
+
peer-checked:bg-fv-primary-600 shadow-sm group-hover:shadow-md transition-all duration-200"
|
|
398
434
|
/>
|
|
399
435
|
<span
|
|
400
436
|
class="ms-3 text-sm font-medium text-fv-neutral-900 dark:text-fv-neutral-300"
|
|
@@ -414,14 +450,16 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
414
450
|
|
|
415
451
|
<!-- CHECKBOX / RADIO -->
|
|
416
452
|
<template v-else-if="type === 'checkbox' || type === 'radio'">
|
|
417
|
-
<div class="flex mb-4">
|
|
453
|
+
<div class="flex mb-4" :class="{ 'opacity-70 cursor-not-allowed': disabled }">
|
|
418
454
|
<div class="flex items-center h-5">
|
|
419
455
|
<input
|
|
420
456
|
:id="id"
|
|
421
457
|
ref="inputRef"
|
|
422
458
|
v-model="modelCheckbox"
|
|
423
459
|
:class="{
|
|
424
|
-
error: checkErrors,
|
|
460
|
+
'error': checkErrors,
|
|
461
|
+
'cursor-not-allowed': disabled,
|
|
462
|
+
'cursor-pointer': !disabled,
|
|
425
463
|
}"
|
|
426
464
|
:aria-describedby="help ? `${id}-help` : undefined"
|
|
427
465
|
:type="type"
|
|
@@ -432,7 +470,8 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
432
470
|
class="w-4 h-4 text-fv-primary-600 bg-fv-neutral-100 border-fv-neutral-300
|
|
433
471
|
rounded focus:ring-fv-primary-500 dark:focus:ring-fv-primary-600
|
|
434
472
|
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
|
|
473
|
+
focus:ring-2 dark:bg-fv-neutral-700 dark:border-fv-neutral-600
|
|
474
|
+
transition-colors duration-200 shadow-sm"
|
|
436
475
|
@focus="handleFocus"
|
|
437
476
|
@blur="handleBlur"
|
|
438
477
|
>
|
|
@@ -440,7 +479,8 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
440
479
|
<div class="ms-2 text-sm">
|
|
441
480
|
<label
|
|
442
481
|
:for="id"
|
|
443
|
-
class="font-medium text-fv-neutral-900 dark:text-fv-neutral-300"
|
|
482
|
+
class="font-medium text-fv-neutral-900 dark:text-fv-neutral-300 cursor-pointer"
|
|
483
|
+
:class="{ 'cursor-not-allowed': disabled }"
|
|
444
484
|
>
|
|
445
485
|
{{ label }}
|
|
446
486
|
</label>
|
|
@@ -458,10 +498,13 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
458
498
|
<!-- Error message -->
|
|
459
499
|
<p
|
|
460
500
|
v-if="checkErrors"
|
|
461
|
-
class="mt-0.5 text-sm text-red-600 dark:text-red-300"
|
|
501
|
+
class="mt-0.5 text-sm text-red-600 dark:text-red-300 flex items-center"
|
|
462
502
|
role="alert"
|
|
463
503
|
aria-live="assertive"
|
|
464
504
|
>
|
|
505
|
+
<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">
|
|
506
|
+
<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" />
|
|
507
|
+
</svg>
|
|
465
508
|
{{ checkErrors }}
|
|
466
509
|
</p>
|
|
467
510
|
|
|
@@ -481,7 +524,59 @@ input,
|
|
|
481
524
|
textarea,
|
|
482
525
|
select {
|
|
483
526
|
&.error {
|
|
484
|
-
@apply border-red-500 dark:border-red-400;
|
|
527
|
+
@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
528
|
}
|
|
486
529
|
}
|
|
530
|
+
|
|
531
|
+
/* Range input styling */
|
|
532
|
+
input[type="range"] {
|
|
533
|
+
@apply appearance-none bg-transparent;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
input[type="range"]::-webkit-slider-thumb {
|
|
537
|
+
@apply appearance-none w-4 h-4 rounded-full bg-fv-primary-500 dark:bg-fv-primary-600 cursor-pointer border-0 shadow-md;
|
|
538
|
+
margin-top: -0.5rem;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
input[type="range"]::-moz-range-thumb {
|
|
542
|
+
@apply w-4 h-4 rounded-full bg-fv-primary-500 dark:bg-fv-primary-600 cursor-pointer border-0 shadow-md;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
input[type="range"]:focus::-webkit-slider-thumb {
|
|
546
|
+
@apply ring-2 ring-fv-primary-300 dark:ring-fv-primary-800;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
input[type="range"]:focus::-moz-range-thumb {
|
|
550
|
+
@apply ring-2 ring-fv-primary-300 dark:ring-fv-primary-800;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/* Textarea auto-grow */
|
|
554
|
+
.grow-wrap {
|
|
555
|
+
@apply grid;
|
|
556
|
+
}
|
|
557
|
+
.grow-wrap::after {
|
|
558
|
+
content: attr(data-replicated-value) " ";
|
|
559
|
+
white-space: pre-wrap;
|
|
560
|
+
visibility: hidden;
|
|
561
|
+
}
|
|
562
|
+
.grow-wrap > textarea {
|
|
563
|
+
resize: none;
|
|
564
|
+
overflow: hidden;
|
|
565
|
+
}
|
|
566
|
+
.grow-wrap > textarea,
|
|
567
|
+
.grow-wrap::after {
|
|
568
|
+
grid-area: 1 / 1 / 2 / 2;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/* Add smooth transitions */
|
|
572
|
+
input, select, textarea, input[type="range"]::-webkit-slider-thumb, input[type="range"]::-moz-range-thumb {
|
|
573
|
+
@apply transition-all duration-200;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/* Placeholder styling */
|
|
577
|
+
input::placeholder,
|
|
578
|
+
textarea::placeholder,
|
|
579
|
+
select::placeholder {
|
|
580
|
+
@apply text-fv-neutral-400 dark:text-fv-neutral-500;
|
|
581
|
+
}
|
|
487
582
|
</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-200 text-blue-800
|
|
442
|
+
dark:bg-blue-700 dark:text-blue-50
|
|
443
|
+
ring-1 ring-blue-400 dark:ring-blue-600;
|
|
371
444
|
}
|
|
372
|
-
|
|
373
|
-
|
|
445
|
+
|
|
446
|
+
.tag-red, .tag-error {
|
|
447
|
+
@apply bg-red-200 text-red-800
|
|
448
|
+
dark:bg-red-700 dark:text-red-50
|
|
449
|
+
ring-1 ring-red-400 dark:ring-red-600;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.tag-green {
|
|
453
|
+
@apply bg-green-200 text-green-800
|
|
454
|
+
dark:bg-green-700 dark:text-green-50
|
|
455
|
+
ring-1 ring-green-400 dark:ring-green-600;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.tag-purple {
|
|
459
|
+
@apply bg-purple-200 text-purple-800
|
|
460
|
+
dark:bg-purple-700 dark:text-purple-50
|
|
461
|
+
ring-1 ring-purple-400 dark:ring-purple-600;
|
|
374
462
|
}
|
|
375
|
-
|
|
376
|
-
|
|
463
|
+
|
|
464
|
+
.tag-orange {
|
|
465
|
+
@apply bg-orange-200 text-orange-800
|
|
466
|
+
dark:bg-orange-700 dark:text-orange-50
|
|
467
|
+
ring-1 ring-orange-400 dark:ring-orange-600;
|
|
377
468
|
}
|
|
378
|
-
|
|
379
|
-
|
|
469
|
+
|
|
470
|
+
.tag-neutral {
|
|
471
|
+
@apply bg-fv-neutral-200 text-fv-neutral-800
|
|
472
|
+
dark:bg-fv-neutral-600 dark:text-fv-neutral-50
|
|
473
|
+
ring-1 ring-fv-neutral-400 dark:ring-fv-neutral-500;
|
|
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>
|