@a-vision-software/vue-input-components 1.2.3 → 1.2.5

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,503 @@
1
+ <template>
2
+ <div
3
+ class="text-input"
4
+ :class="{
5
+ [`label-${labelPosition}`]: label,
6
+ [`label-align-${labelAlign}`]: label,
7
+ }"
8
+ :style="[
9
+ { width: type === 'date' ? totalWidth || '12rem' : totalWidth || '100%' },
10
+ labelStyle,
11
+ {
12
+ '--max-textarea-height': props.maxHeight || props.height || '14rem',
13
+ '--textarea-height': props.height || '5.5rem',
14
+ },
15
+ ]"
16
+ >
17
+ <label v-if="label" :for="id" class="label">
18
+ {{ label }}
19
+ </label>
20
+ <div
21
+ class="input-wrapper"
22
+ :class="{
23
+ 'has-error': error,
24
+ 'has-icon': icon,
25
+ }"
26
+ >
27
+ <div v-if="icon" class="icon-wrapper" @click="focusInput">
28
+ <font-awesome-icon :icon="icon" class="icon" />
29
+ </div>
30
+ <Datepicker
31
+ v-if="type === 'date'"
32
+ :id="id"
33
+ v-model="dateValue"
34
+ :placeholder="placeholder"
35
+ :disabled="disabled"
36
+ :readonly="readonly"
37
+ :min-date="min"
38
+ :max-date="max"
39
+ :format="dateFormat"
40
+ :enable-time-picker="false"
41
+ :auto-apply="true"
42
+ :close-on-auto-apply="true"
43
+ :clearable="true"
44
+ :input-class-name="['input', { 'has-icon': icon }]"
45
+ @update:model-value="handleDateChange"
46
+ @focus="handleFocus"
47
+ @blur="handleBlur"
48
+ />
49
+ <input
50
+ v-else-if="!isTextarea"
51
+ :id="id"
52
+ :type="type"
53
+ :value="modelValue"
54
+ :placeholder="placeholder"
55
+ :required="required"
56
+ :disabled="disabled"
57
+ :readonly="readonly"
58
+ :maxlength="maxlength"
59
+ class="input"
60
+ @input="handleInput"
61
+ @focus="handleFocus"
62
+ @blur="handleBlur"
63
+ @keydown="handleKeydown"
64
+ ref="inputRef"
65
+ />
66
+ <textarea
67
+ v-else
68
+ :id="id"
69
+ :value="modelValue"
70
+ :placeholder="placeholder"
71
+ :required="required"
72
+ :disabled="disabled"
73
+ class="input"
74
+ @input="handleInput"
75
+ ref="inputRef"
76
+ ></textarea>
77
+ <span
78
+ v-if="required && !showSaved && !showChanged"
79
+ class="status-indicator required-indicator"
80
+ >required</span
81
+ >
82
+ <transition name="fade">
83
+ <span v-if="showSaved && !error" class="status-indicator saved-indicator">saved</span>
84
+ </transition>
85
+ <transition name="fade">
86
+ <span v-if="showChanged && !error" class="status-indicator changed-indicator">changed</span>
87
+ </transition>
88
+ <div v-if="error" class="error-message">{{ error }}</div>
89
+ <span v-if="success" class="message success-message">{{ success }}</span>
90
+ </div>
91
+ </div>
92
+ </template>
93
+
94
+ <script setup lang="ts">
95
+ import { computed, ref, onUnmounted, onMounted } from 'vue'
96
+ import { TextInputProps } from '../types'
97
+ import Datepicker from '@vuepic/vue-datepicker'
98
+ import '@vuepic/vue-datepicker/dist/main.css'
99
+
100
+ const props = withDefaults(defineProps<TextInputProps>(), {
101
+ modelValue: '',
102
+ type: 'text',
103
+ placeholder: '',
104
+ label: '',
105
+ icon: undefined,
106
+ disabled: false,
107
+ readonly: false,
108
+ maxlength: undefined,
109
+ error: '',
110
+ min: undefined,
111
+ max: undefined,
112
+ })
113
+
114
+ const emit = defineEmits<{
115
+ (e: 'update:modelValue', value: string): void
116
+ (e: 'changed'): void
117
+ (e: 'saved'): void
118
+ (e: 'focus'): void
119
+ (e: 'blur'): void
120
+ (e: 'keydown', event: KeyboardEvent): void
121
+ }>()
122
+
123
+ const id = ref<string>('')
124
+ const showSaved = ref(false)
125
+ const showChanged = ref(false)
126
+ const isChanged = ref(false)
127
+ const debounceTimer = ref<number | null>(null)
128
+ const changedTimer = ref<number | null>(null)
129
+ const inputRef = ref<HTMLInputElement | null>(null)
130
+ const dateValue = ref<Date | null>(null)
131
+
132
+ const dateFormat = 'dd/MM/yyyy'
133
+
134
+ const labelStyle = computed(() => {
135
+ if (!props.label) return {}
136
+ if (props.labelPosition === 'left' && props.labelWidth) {
137
+ return {
138
+ 'grid-template-columns': `${props.labelWidth} 1fr`,
139
+ }
140
+ }
141
+ return {}
142
+ })
143
+
144
+ const formatDateForModel = (date: Date | null): string => {
145
+ if (!date) return ''
146
+ const day = String(date.getDate()).padStart(2, '0')
147
+ const month = String(date.getMonth() + 1).padStart(2, '0')
148
+ const year = date.getFullYear()
149
+ return `${day}/${month}/${year}`
150
+ }
151
+
152
+ const parseDateFromModel = (dateStr: string): Date | null => {
153
+ if (!dateStr) return null
154
+ const [day, month, year] = dateStr.split('/').map(Number)
155
+ return new Date(year, month - 1, day)
156
+ }
157
+
158
+ const handleAutosave = async (value: string) => {
159
+ if (props.autosave) {
160
+ try {
161
+ await props.autosave(value)
162
+ if (!props.error) {
163
+ emit('saved')
164
+ showSaved.value = true
165
+ showChanged.value = false
166
+ setTimeout(() => {
167
+ showSaved.value = false
168
+ }, 3000)
169
+ }
170
+ } catch (error) {
171
+ console.error('Autosave failed:', error)
172
+ }
173
+ }
174
+ }
175
+
176
+ const debounceAutosave = (value: string) => {
177
+ // Clear existing timers
178
+ if (debounceTimer.value) {
179
+ clearTimeout(debounceTimer.value)
180
+ }
181
+ if (changedTimer.value) {
182
+ clearTimeout(changedTimer.value)
183
+ }
184
+
185
+ // Show changed indicator immediately
186
+ if (!props.error) {
187
+ showChanged.value = true
188
+ }
189
+
190
+ // Trigger changed event after 500ms
191
+ changedTimer.value = window.setTimeout(() => {
192
+ emit('changed')
193
+ isChanged.value = true
194
+ }, 500)
195
+
196
+ // Trigger autosave after 1500ms
197
+ debounceTimer.value = window.setTimeout(() => {
198
+ handleAutosave(value)
199
+ }, 1500)
200
+ }
201
+
202
+ const focusInput = () => {
203
+ inputRef.value?.focus()
204
+ }
205
+
206
+ const adjustHeight = (element: HTMLTextAreaElement) => {
207
+ element.style.height = 'auto' // Reset height to auto to calculate new height
208
+ element.style.height = `${element.scrollHeight}px` // Set height to scrollHeight
209
+ }
210
+
211
+ const handleInput = (event: Event) => {
212
+ const value = (event.target as HTMLTextAreaElement).value
213
+ emit('update:modelValue', value)
214
+ debounceAutosave(value)
215
+ adjustHeight(event.target as HTMLTextAreaElement) // Adjust height on input
216
+ }
217
+
218
+ const handleFocus = () => {
219
+ emit('focus')
220
+ }
221
+
222
+ const handleBlur = () => {
223
+ emit('blur')
224
+ }
225
+
226
+ const handleKeydown = (event: KeyboardEvent) => {
227
+ emit('keydown', event)
228
+ }
229
+
230
+ const handleDateChange = (date: Date | null) => {
231
+ const formattedDate = formatDateForModel(date)
232
+ emit('update:modelValue', formattedDate)
233
+ debounceAutosave(formattedDate)
234
+ }
235
+
236
+ // Cleanup timers on unmount
237
+ onUnmounted(() => {
238
+ if (debounceTimer.value) {
239
+ clearTimeout(debounceTimer.value)
240
+ }
241
+ if (changedTimer.value) {
242
+ clearTimeout(changedTimer.value)
243
+ }
244
+ })
245
+
246
+ onMounted(() => {
247
+ id.value = `text-input-${Math.random().toString(36).substr(2, 9)}`
248
+ if (props.type === 'date' && props.modelValue) {
249
+ dateValue.value = parseDateFromModel(props.modelValue)
250
+ }
251
+ })
252
+
253
+ defineExpose({
254
+ focus: () => inputRef.value?.focus(),
255
+ blur: () => inputRef.value?.blur(),
256
+ })
257
+ </script>
258
+
259
+ <style scoped>
260
+ .text-input {
261
+ display: grid;
262
+ gap: 0.5rem;
263
+ width: 100%;
264
+ margin-top: 0.7rem;
265
+ }
266
+
267
+ .text-input.label-top {
268
+ grid-template-rows: auto 1fr;
269
+ }
270
+
271
+ .text-input.label-left {
272
+ grid-template-columns: 30% 1fr;
273
+ align-items: start;
274
+ gap: 1rem;
275
+ }
276
+
277
+ .text-input.label-left .label {
278
+ padding-top: 0.75rem;
279
+ width: 100%;
280
+ }
281
+
282
+ .label {
283
+ font-weight: 500;
284
+ color: var(--text-color);
285
+ text-align: left;
286
+ }
287
+
288
+ .label-align-left .label {
289
+ text-align: left;
290
+ }
291
+
292
+ .label-align-right .label {
293
+ text-align: right;
294
+ }
295
+
296
+ .label-align-center .label {
297
+ text-align: center;
298
+ }
299
+
300
+ .required {
301
+ color: var(--danger-color);
302
+ margin-left: 0.25rem;
303
+ }
304
+
305
+ .input-wrapper {
306
+ position: relative;
307
+ display: grid;
308
+ grid-template-columns: 1fr;
309
+ border: 1px solid var(--border-color);
310
+ border-radius: 0.5rem;
311
+ transition: all 0.2s ease;
312
+ width: 100%;
313
+ min-height: 2rem;
314
+ background: var(--input-bg-color);
315
+ }
316
+
317
+ .input-wrapper.has-icon {
318
+ grid-template-columns: auto 1fr;
319
+ }
320
+
321
+ .input-wrapper:focus-within {
322
+ border-color: var(--primary-color);
323
+ box-shadow: 0 0 0 3px var(--shadow-color);
324
+ }
325
+
326
+ .input-wrapper.has-error {
327
+ border-color: var(--danger-color);
328
+ border-bottom-left-radius: 0;
329
+ border-bottom-right-radius: 0;
330
+ }
331
+
332
+ .icon-wrapper {
333
+ display: grid;
334
+ place-items: start;
335
+ padding: 1rem;
336
+ border-right: 1px solid var(--border-color);
337
+ cursor: pointer;
338
+ overflow: hidden;
339
+ }
340
+
341
+ .icon-wrapper:hover {
342
+ background-color: var(--input-bg-hover);
343
+ }
344
+
345
+ .icon {
346
+ color: var(--text-muted);
347
+ font-size: 1rem;
348
+ }
349
+
350
+ .input {
351
+ padding: 0.75rem 1rem;
352
+ border: none;
353
+ outline: none;
354
+ font-size: 1rem;
355
+ color: var(--text-color);
356
+ background: transparent;
357
+ width: 100%;
358
+ line-height: var(--line-height);
359
+ }
360
+
361
+ .input::placeholder {
362
+ color: var(--text-muted);
363
+ }
364
+
365
+ .input:disabled {
366
+ background-color: var(--input-bg-disabled);
367
+ cursor: not-allowed;
368
+ }
369
+
370
+ .message {
371
+ position: absolute;
372
+ bottom: -1.5rem;
373
+ left: 0;
374
+ font-size: 0.75rem;
375
+ white-space: nowrap;
376
+ }
377
+
378
+ .error-message {
379
+ position: absolute;
380
+ bottom: 0;
381
+ left: 0;
382
+ right: 0;
383
+ padding: 0.25rem 0.75rem;
384
+ background-color: var(--danger-color);
385
+ color: white;
386
+ font-size: 0.75rem;
387
+ border-radius: 0 0 0.5rem 0.5rem;
388
+ transform: translateY(100%);
389
+ transition: transform 0.2s ease;
390
+ line-height: var(--line-height);
391
+ z-index: 1;
392
+ }
393
+
394
+ .success-message {
395
+ position: absolute;
396
+ bottom: -1.5rem;
397
+ left: 0;
398
+ color: var(--success-color);
399
+ font-size: 0.75rem;
400
+ line-height: var(--line-height);
401
+ }
402
+
403
+ .status-indicator {
404
+ position: absolute;
405
+ top: -0.1rem;
406
+ right: 0.5rem;
407
+ font-size: 0.75rem;
408
+ color: var(--text-muted);
409
+ line-height: 0px;
410
+ background-color: var(--input-bg-color);
411
+ padding: 0 0.25rem;
412
+ }
413
+
414
+ .saved-indicator {
415
+ color: var(--success-color);
416
+ }
417
+
418
+ .changed-indicator {
419
+ color: var(--warning-color);
420
+ }
421
+
422
+ .fade-enter-active,
423
+ .fade-leave-active {
424
+ transition: opacity 0.2s ease;
425
+ }
426
+
427
+ .fade-enter-from,
428
+ .fade-leave-to {
429
+ opacity: 0;
430
+ }
431
+
432
+ textarea {
433
+ min-height: var(--textarea-height, 5.5rem);
434
+ max-height: var(--max-textarea-height, 14rem);
435
+ overflow-y: auto;
436
+ resize: none;
437
+ }
438
+
439
+ :deep(.dp__input) {
440
+ padding: 0.75rem 1rem;
441
+ border: none;
442
+ outline: none;
443
+ font-size: 1rem;
444
+ color: var(--text-color);
445
+ background: transparent;
446
+ width: 100%;
447
+ line-height: var(--line-height);
448
+ }
449
+
450
+ :deep(.dp__input::placeholder) {
451
+ color: var(--text-muted);
452
+ }
453
+
454
+ :deep(.dp__input:disabled) {
455
+ background-color: var(--input-bg-disabled);
456
+ cursor: not-allowed;
457
+ }
458
+
459
+ :deep(.dp__input.has-icon) {
460
+ padding-left: 2.5rem;
461
+ }
462
+
463
+ :deep(.dp__input_icon) {
464
+ display: none;
465
+ }
466
+
467
+ :deep(.dp__input_icon_pad) {
468
+ padding-right: 0.75rem;
469
+ }
470
+
471
+ :deep(.dp__menu) {
472
+ background-color: var(--input-bg-color);
473
+ border: 1px solid var(--border-color);
474
+ border-radius: 0.5rem;
475
+ box-shadow:
476
+ 0 4px 6px -1px rgba(0, 0, 0, 0.1),
477
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
478
+ }
479
+
480
+ :deep(.dp__cell_inner) {
481
+ color: var(--text-color);
482
+ }
483
+
484
+ :deep(.dp__today) {
485
+ border-color: var(--primary-color);
486
+ }
487
+
488
+ :deep(.dp__active_date) {
489
+ background-color: var(--primary-color);
490
+ color: white;
491
+ }
492
+
493
+ :deep(.dp__range_start),
494
+ :deep(.dp__range_end) {
495
+ background-color: var(--primary-color);
496
+ color: white;
497
+ }
498
+
499
+ :deep(.dp__range_between) {
500
+ background-color: var(--primary-color-light);
501
+ color: var(--text-color);
502
+ }
503
+ </style>