@a-vision-software/vue-input-components 1.4.21 → 1.4.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a-vision-software/vue-input-components",
3
- "version": "1.4.21",
3
+ "version": "1.4.23",
4
4
  "description": "A collection of reusable Vue 3 input components with TypeScript support",
5
5
  "author": "A-Vision Software",
6
6
  "license": "MIT",
@@ -106,7 +106,7 @@ const formatFileSize = (bytes: number): string => {
106
106
  const k = 1024
107
107
  const sizes = ['Bytes', 'KB', 'MB', 'GB']
108
108
  const i = Math.floor(Math.log(bytes) / Math.log(k))
109
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
109
+ return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
110
110
  }
111
111
 
112
112
  const validateFileSize = (file: File): boolean => {
@@ -33,7 +33,7 @@
33
33
  </div>
34
34
 
35
35
  <!-- Actions -->
36
- <div v-if="actions?.length" class="list__actions">
36
+ <div v-if="actions?.length || props.CSVDownload" class="list__actions">
37
37
  <Action
38
38
  v-for="(action, actionIndex) in [
39
39
  ...(props.CSVDownload !== undefined
@@ -47,7 +47,7 @@
47
47
  },
48
48
  ]
49
49
  : []),
50
- ...actions,
50
+ ...(actions || []),
51
51
  ]"
52
52
  :key="actionIndex"
53
53
  v-bind="action"
@@ -1,10 +1,12 @@
1
1
  <template>
2
- <nav :class="[
3
- 'navigation',
4
- `navigation--${type}`,
5
- `navigation--${orientation}`,
6
- { 'navigation--large-icons': iconSize === 'large' },
7
- ]" :style="{
2
+ <nav
3
+ :class="[
4
+ 'navigation',
5
+ `navigation--${type}`,
6
+ `navigation--${orientation}`,
7
+ { 'navigation--large-icons': iconSize === 'large' },
8
+ ]"
9
+ :style="{
8
10
  '--navigation-color': color,
9
11
  '--navigation-hover-color': hoverColor,
10
12
  '--navigation-active-color': activeColor,
@@ -21,61 +23,101 @@
21
23
  : 'none',
22
24
  '--navigation-tiles-grid': navigationGrid,
23
25
  'max-height': height,
24
- }">
26
+ }"
27
+ >
25
28
  <template v-if="type === 'tiles'">
26
29
  <div class="navigation__tiles">
27
- <div v-for="(item, index) in sortedItems" :key="item.id" class="navigation__tile" :class="[
28
- { 'navigation__tile--active': item.id === activeItem },
29
- { 'navigation__tile--disabled': item.disabled },
30
- { 'navigation__tile--right': item.alignment === 'right' },
31
- { 'navigation__tile--open': isDropdownOpen(item.id) },
32
- { 'navigation__tile--spacer': item.id.includes('spacer') },
33
- ]" :style="{
34
- '--item-alignment': item.alignment || activeItemAlignment,
35
- width: item.width || '150px',
36
- 'min-width': item.width || '150px',
37
- 'max-width': item.width || '150px',
38
- 'grid-column': item.alignment === 'right' ? `${index - items.length}` : `auto`,
39
- }" @click="(e) => !item.id.includes('spacer') && handleItemClick(item, e)">
40
- <div class="navigation__tile-content" :class="{
41
- 'navigation__tile-content--icon-only': !item.label,
42
- 'navigation__tile-content--large-icon': iconSize === 'large' && item.icon,
43
- }">
30
+ <div
31
+ v-for="(item, index) in sortedItems"
32
+ :key="item.id"
33
+ class="navigation__tile"
34
+ :class="[
35
+ { 'navigation__tile--active': item.id === activeItem },
36
+ { 'navigation__tile--disabled': item.disabled },
37
+ { 'navigation__tile--right': item.alignment === 'right' },
38
+ { 'navigation__tile--open': isDropdownOpen(item.id) },
39
+ { 'navigation__tile--spacer': item.id.includes('spacer') },
40
+ ]"
41
+ :style="{
42
+ '--item-alignment': item.alignment || activeItemAlignment,
43
+ width: item.width || '150px',
44
+ 'min-width': item.width || '150px',
45
+ 'max-width': item.width || '150px',
46
+ 'grid-column': item.alignment === 'right' ? `${index - items.length}` : `auto`,
47
+ }"
48
+ @click="(e) => !item.id.includes('spacer') && handleItemClick(item, e)"
49
+ >
50
+ <div
51
+ class="navigation__tile-content"
52
+ :class="{
53
+ 'navigation__tile-content--icon-only': !item.label,
54
+ 'navigation__tile-content--large-icon': iconSize === 'large' && item.icon,
55
+ }"
56
+ >
44
57
  <div v-if="item.icon" class="navigation__icon">
45
- <img v-if="item.icon.startsWith('img:')" :src="item.icon.substring(4)" :alt="item.label || 'Icon'"
46
- class="navigation__icon-image" />
58
+ <img
59
+ v-if="item.icon.startsWith('img:')"
60
+ :src="item.icon.substring(4)"
61
+ :alt="item.label || 'Icon'"
62
+ class="navigation__icon-image"
63
+ />
47
64
  <font-awesome-icon v-else :icon="item.icon" />
48
65
  </div>
49
- <div v-if="item.label" class="navigation__label" :class="{
50
- 'navigation__label--small': item.labelSize === 'small',
51
- 'navigation__label--large': item.labelSize === 'large',
52
- }">
66
+ <div
67
+ v-if="item.label"
68
+ class="navigation__label"
69
+ :class="{
70
+ 'navigation__label--small': item.labelSize === 'small',
71
+ 'navigation__label--large': item.labelSize === 'large',
72
+ }"
73
+ >
53
74
  <span>{{ item.label }}</span>
54
75
  <div v-if="item.children" class="navigation__dropdown-arrow">
55
76
  <font-awesome-icon icon="chevron-down" />
56
77
  </div>
57
78
  </div>
58
79
  </div>
59
- <div v-if="item.url && parseInt(height || '0') >= 80 && !item.hideExternalOpen"
60
- class="navigation__external-link" @click.stop="openUrl(item.url)">
80
+ <div
81
+ v-if="item.url && parseInt(height || '0') >= 80 && !item.hideExternalOpen"
82
+ class="navigation__external-link"
83
+ @click.stop="openUrl(item.url)"
84
+ >
61
85
  <font-awesome-icon icon="square-up-right" />
62
86
  </div>
63
- <div v-if="item.children && isDropdownOpen(item.id)" class="navigation__dropdown-content" :class="{
64
- 'navigation__dropdown-content--start': item.alignment === 'start',
65
- 'navigation__dropdown-content--end': item.alignment === 'end',
66
- }">
67
- <div v-for="child in item.children" :key="child.id" class="navigation__dropdown-item" :class="{
68
- 'navigation__dropdown-item--disabled': child.disabled,
69
- }" @click="(e) => handleItemClick(child, e)">
87
+ <div
88
+ v-if="item.children && isDropdownOpen(item.id)"
89
+ class="navigation__dropdown-content"
90
+ :class="{
91
+ 'navigation__dropdown-content--start': item.alignment === 'start',
92
+ 'navigation__dropdown-content--end': item.alignment === 'end',
93
+ }"
94
+ >
95
+ <div
96
+ v-for="child in item.children"
97
+ :key="child.id"
98
+ class="navigation__dropdown-item"
99
+ :class="{
100
+ 'navigation__dropdown-item--disabled': child.disabled,
101
+ }"
102
+ @click="(e) => handleItemClick(child, e)"
103
+ >
70
104
  <div v-if="child.icon" class="navigation__icon">
71
- <img v-if="child.icon.startsWith('img:')" :src="child.icon.substring(4)" :alt="child.label || 'Icon'"
72
- class="navigation__icon-image" />
105
+ <img
106
+ v-if="child.icon.startsWith('img:')"
107
+ :src="child.icon.substring(4)"
108
+ :alt="child.label || 'Icon'"
109
+ class="navigation__icon-image"
110
+ />
73
111
  <font-awesome-icon v-else :icon="child.icon" />
74
112
  </div>
75
- <div v-if="child.label" class="navigation__label" :class="{
76
- 'navigation__label--small': child.labelSize === 'small',
77
- 'navigation__label--large': child.labelSize === 'large',
78
- }">
113
+ <div
114
+ v-if="child.label"
115
+ class="navigation__label"
116
+ :class="{
117
+ 'navigation__label--small': child.labelSize === 'small',
118
+ 'navigation__label--large': child.labelSize === 'large',
119
+ }"
120
+ >
79
121
  {{ child.label }}
80
122
  </div>
81
123
  </div>
@@ -86,54 +128,93 @@
86
128
 
87
129
  <template v-else>
88
130
  <div class="navigation__dropdowns">
89
- <div v-for="item in items" :key="item.id" class="navigation__dropdown" :class="[
90
- { 'navigation__dropdown--active': item.id === activeItem },
91
- { 'navigation__dropdown--disabled': item.disabled },
92
- { 'navigation__dropdown--start': item.alignment === 'start' },
93
- { 'navigation__dropdown--end': item.alignment === 'end' },
94
- { 'navigation__dropdown--open': isDropdownOpen(item.id) },
95
- ]" :style="{
96
- '--item-alignment': item.alignment || activeItemAlignment,
97
- }">
98
- <div class="navigation__dropdown-header" :class="{
99
- 'navigation__dropdown-header--icon-only': !item.label,
100
- 'navigation__dropdown-header--large-icon': iconSize === 'large' && item.icon,
101
- }" @click="(e) => handleItemClick(item, e)">
131
+ <div
132
+ v-for="item in items"
133
+ :key="item.id"
134
+ class="navigation__dropdown"
135
+ :class="[
136
+ { 'navigation__dropdown--active': item.id === activeItem },
137
+ { 'navigation__dropdown--disabled': item.disabled },
138
+ { 'navigation__dropdown--start': item.alignment === 'start' },
139
+ { 'navigation__dropdown--end': item.alignment === 'end' },
140
+ { 'navigation__dropdown--open': isDropdownOpen(item.id) },
141
+ ]"
142
+ :style="{
143
+ '--item-alignment': item.alignment || activeItemAlignment,
144
+ }"
145
+ >
146
+ <div
147
+ class="navigation__dropdown-header"
148
+ :class="{
149
+ 'navigation__dropdown-header--icon-only': !item.label,
150
+ 'navigation__dropdown-header--large-icon': iconSize === 'large' && item.icon,
151
+ }"
152
+ @click="(e) => handleItemClick(item, e)"
153
+ >
102
154
  <div v-if="item.icon" class="navigation__icon">
103
- <img v-if="item.icon.startsWith('img:')" :src="item.icon.substring(4)" :alt="item.label || 'Icon'"
104
- class="navigation__icon-image" />
155
+ <img
156
+ v-if="item.icon.startsWith('img:')"
157
+ :src="item.icon.substring(4)"
158
+ :alt="item.label || 'Icon'"
159
+ class="navigation__icon-image"
160
+ />
105
161
  <font-awesome-icon v-else :icon="item.icon" />
106
162
  </div>
107
- <div v-if="item.label" class="navigation__label" :class="{
108
- 'navigation__label--small': item.labelSize === 'small',
109
- 'navigation__label--large': item.labelSize === 'large',
110
- }">
163
+ <div
164
+ v-if="item.label"
165
+ class="navigation__label"
166
+ :class="{
167
+ 'navigation__label--small': item.labelSize === 'small',
168
+ 'navigation__label--large': item.labelSize === 'large',
169
+ }"
170
+ >
111
171
  <span>{{ item.label }}</span>
112
172
  <div v-if="item.children" class="navigation__dropdown-arrow">
113
173
  <font-awesome-icon icon="chevron-down" />
114
174
  </div>
115
175
  </div>
116
176
  </div>
117
- <div v-if="item.url && parseInt(height || '0') >= 80 && !item.hideExternalOpen"
118
- class="navigation__external-link" @click.stop="openUrl(item.url)">
177
+ <div
178
+ v-if="item.url && parseInt(height || '0') >= 80 && !item.hideExternalOpen"
179
+ class="navigation__external-link"
180
+ @click.stop="openUrl(item.url)"
181
+ >
119
182
  <font-awesome-icon icon="square-up-right" />
120
183
  </div>
121
- <div v-if="item.children && isDropdownOpen(item.id)" class="navigation__dropdown-content" :class="{
122
- 'navigation__dropdown-content--start': item.alignment === 'start',
123
- 'navigation__dropdown-content--end': item.alignment === 'end',
124
- }">
125
- <div v-for="child in item.children" :key="child.id" class="navigation__dropdown-item" :class="{
126
- 'navigation__dropdown-item--disabled': child.disabled,
127
- }" @click="(e) => handleItemClick(child, e)">
184
+ <div
185
+ v-if="item.children && isDropdownOpen(item.id)"
186
+ class="navigation__dropdown-content"
187
+ :class="{
188
+ 'navigation__dropdown-content--start': item.alignment === 'start',
189
+ 'navigation__dropdown-content--end': item.alignment === 'end',
190
+ }"
191
+ >
192
+ <div
193
+ v-for="child in item.children"
194
+ :key="child.id"
195
+ class="navigation__dropdown-item"
196
+ :class="{
197
+ 'navigation__dropdown-item--disabled': child.disabled,
198
+ }"
199
+ @click="(e) => handleItemClick(child, e)"
200
+ >
128
201
  <div v-if="child.icon" class="navigation__icon">
129
- <img v-if="child.icon.startsWith('img:')" :src="child.icon.substring(4)" :alt="child.label || 'Icon'"
130
- class="navigation__icon-image" />
202
+ <img
203
+ v-if="child.icon.startsWith('img:')"
204
+ :src="child.icon.substring(4)"
205
+ :alt="child.label || 'Icon'"
206
+ class="navigation__icon-image"
207
+ />
131
208
  <font-awesome-icon v-else :icon="child.icon" />
132
209
  </div>
133
- <div v-if="child.label" class="navigation__label" :class="{
134
- 'navigation__label--small': child.labelSize === 'small',
135
- 'navigation__label--large': child.labelSize === 'large',
136
- }">
210
+ <div
211
+ v-if="child.label"
212
+ class="navigation__label"
213
+ :class="{
214
+ 'navigation__label--small': child.labelSize === 'small',
215
+ 'navigation__label--large': child.labelSize === 'large',
216
+ }"
217
+ >
137
218
  {{ child.label }}
138
219
  </div>
139
220
  </div>
@@ -319,6 +400,7 @@ onUnmounted(() => {
319
400
  .navigation__dropdown-header {
320
401
  display: flex;
321
402
  align-items: center;
403
+ width: 100%;
322
404
  padding: var(--navigation-padding);
323
405
  cursor: pointer;
324
406
  transition: all 0.2s ease;
@@ -392,7 +474,6 @@ onUnmounted(() => {
392
474
  }
393
475
 
394
476
  .navigation__dropdown-item .navigation__icon {
395
- margin-left: 0;
396
477
  margin: 0.5rem !important;
397
478
  font-size: 1.2rem !important;
398
479
  }
@@ -468,26 +549,6 @@ onUnmounted(() => {
468
549
  }
469
550
 
470
551
  /* Update content layouts */
471
- .navigation__tile-content,
472
- .navigation__dropdown-header {
473
- display: flex;
474
- align-items: center;
475
- width: 100%;
476
- justify-content: flex-start;
477
- }
478
-
479
- .navigation__tile-content {
480
- display: flex;
481
- align-items: center;
482
- width: 100%;
483
- flex-direction: row;
484
- justify-content: center;
485
- }
486
-
487
- .navigation__dropdown-header {
488
- flex-direction: row;
489
- justify-content: flex-start;
490
- }
491
552
 
492
553
  .navigation__tile--spacer {
493
554
  cursor: default;
@@ -538,11 +599,6 @@ onUnmounted(() => {
538
599
  text-align: center;
539
600
  }
540
601
 
541
- .navigation__dropdown-item .navigation__label {
542
- text-align: center;
543
- justify-content: center;
544
- }
545
-
546
602
  .navigation__tile--open .navigation__dropdown-arrow,
547
603
  .navigation__dropdown--open .navigation__dropdown-arrow {
548
604
  transform: rotate(180deg);
@@ -560,6 +616,7 @@ onUnmounted(() => {
560
616
  min-width: 100%;
561
617
  background: white;
562
618
  border: 1px solid var(--navigation-color);
619
+ border-radius: var(--navigation-border-radius);
563
620
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
564
621
  z-index: 10;
565
622
  }
@@ -569,11 +626,6 @@ onUnmounted(() => {
569
626
  right: 0;
570
627
  }
571
628
 
572
- .navigation__tile--open .navigation__dropdown-content,
573
- .navigation__dropdown--open .navigation__dropdown-content {
574
- border-radius: var(--navigation-border-radius);
575
- }
576
-
577
629
  .navigation__tile .navigation__dropdown-item {
578
630
  min-height: 3rem;
579
631
  display: flex;
@@ -4,7 +4,7 @@
4
4
  :class="{
5
5
  'text-input--disabled': disabled,
6
6
  'text-input--has-error': error,
7
- 'text-input--date': type === 'date',
7
+ 'text-input--date': type === 'date' || type === 'datetime',
8
8
  [`label-${labelPosition}`]: label,
9
9
  [`label-align-${labelAlign}`]: label,
10
10
  'text-input--has-icon': icon,
@@ -34,7 +34,7 @@
34
34
  <font-awesome-icon :icon="icon" />
35
35
  </div>
36
36
  <Datepicker
37
- v-if="type === 'date'"
37
+ v-if="type === 'date' || type === 'datetime'"
38
38
  :id="id"
39
39
  v-model="dateValue"
40
40
  :placeholder="placeholder"
@@ -43,7 +43,7 @@
43
43
  :min-date="min"
44
44
  :max-date="max"
45
45
  :format="dateFormat"
46
- :enable-time-picker="false"
46
+ :enable-time-picker="type === 'datetime'"
47
47
  :auto-apply="true"
48
48
  :close-on-auto-apply="true"
49
49
  :clearable="true"
@@ -104,6 +104,9 @@
104
104
  :disabled="disabled"
105
105
  class="text-input__input"
106
106
  @input="handleInput"
107
+ @focus="handleFocus"
108
+ @blur="handleBlur"
109
+ @keydown="handleKeydown"
107
110
  ref="inputRef"
108
111
  :readonly="readonly"
109
112
  ></textarea>
@@ -156,6 +159,7 @@ const props = withDefaults(defineProps<TextInputProps>(), {
156
159
  maxHeight: '14rem',
157
160
  bgColor: 'var(--input-color, #ffffffee)',
158
161
  width: '100%',
162
+ autosaveOnBlur: true,
159
163
  })
160
164
 
161
165
  const emit = defineEmits<{
@@ -171,11 +175,12 @@ const id = ref<string>('')
171
175
  const showSaved = ref(false)
172
176
  const showChanged = ref(false)
173
177
  const isChanged = ref(false)
174
- const debounceTimer = ref<number | null>(null)
175
178
  const changedTimer = ref<number | null>(null)
176
179
  const inputRef = ref<HTMLInputElement | null>(null)
177
180
  const dateValue = ref<Date | null>(null)
178
181
  const isFocused = ref(false)
182
+ const originalValue = ref<string | number>('')
183
+ const originalDateValue = ref<Date | null>(null)
179
184
 
180
185
  const defaultCurrencyFormatter = new Intl.NumberFormat('en-NZ', {
181
186
  style: 'currency',
@@ -194,14 +199,18 @@ const currencyFormatter = computed(() => {
194
199
  const formattedMoney = computed(() => {
195
200
  if (props.type !== 'money') return ''
196
201
  const numericValue =
197
- typeof props.modelValue === 'number' ? props.modelValue : parseFloat(String(props.modelValue))
202
+ typeof props.modelValue === 'number'
203
+ ? props.modelValue
204
+ : Number.parseFloat(String(props.modelValue))
198
205
  if (!Number.isFinite(numericValue)) {
199
206
  return ''
200
207
  }
201
208
  return currencyFormatter.value.format(numericValue)
202
209
  })
203
210
 
204
- const dateFormat = 'dd/MM/yyyy'
211
+ const dateFormat = computed(() => {
212
+ return props.type === 'datetime' ? 'dd/MM/yyyy HH:mm' : 'dd/MM/yyyy'
213
+ })
205
214
 
206
215
  const labelStyle = computed(() => {
207
216
  if (!props.label) return {}
@@ -218,12 +227,35 @@ const formatDateForModel = (date: Date | null): string => {
218
227
  const day = String(date.getDate()).padStart(2, '0')
219
228
  const month = String(date.getMonth() + 1).padStart(2, '0')
220
229
  const year = date.getFullYear()
230
+
231
+ if (props.type === 'datetime') {
232
+ const hours = String(date.getHours()).padStart(2, '0')
233
+ const minutes = String(date.getMinutes()).padStart(2, '0')
234
+ return `${year}-${month}-${day}T${hours}:${minutes}`
235
+ }
236
+
221
237
  return `${year}-${month}-${day}`
222
238
  }
223
239
 
224
240
  const parseDateFromModel = (dateStr: string): Date | null => {
225
241
  if (!dateStr) return null
226
- if (dateStr.includes('T')) dateStr = dateStr.split('T')[0]
242
+
243
+ // Handle ISO format with T separator (e.g., "2023-06-22T13:29:00")
244
+ if (dateStr.includes('T')) {
245
+ const [datePart, timePart] = dateStr.split('T')
246
+ const [year, month, day] = datePart.split('-').map(Number)
247
+ const [hours, minutes] = timePart.split(':').map(Number)
248
+ return new Date(year, month - 1, day, hours, minutes)
249
+ }
250
+
251
+ // Handle space-separated format from backend (e.g., "2023-06-22 13:29:00")
252
+ if (dateStr.includes(' ')) {
253
+ const [datePart, timePart] = dateStr.split(' ')
254
+ const [year, month, day] = datePart.split('-').map(Number)
255
+ const [hours, minutes] = timePart.split(':').map(Number)
256
+ return new Date(year, month - 1, day, hours, minutes)
257
+ }
258
+
227
259
  const [year, month, day] = dateStr.split('-').map(Number)
228
260
  return new Date(year, month - 1, day)
229
261
  }
@@ -246,10 +278,7 @@ const handleAutosave = async (value: string) => {
246
278
  }
247
279
  }
248
280
 
249
- const debounceAutosave = (value: string) => {
250
- if (debounceTimer.value) {
251
- clearTimeout(debounceTimer.value)
252
- }
281
+ const showChangedIndicator = () => {
253
282
  if (changedTimer.value) {
254
283
  clearTimeout(changedTimer.value)
255
284
  }
@@ -262,10 +291,6 @@ const debounceAutosave = (value: string) => {
262
291
  emit('changed')
263
292
  isChanged.value = true
264
293
  }, 500)
265
-
266
- debounceTimer.value = window.setTimeout(() => {
267
- handleAutosave(value)
268
- }, 1500)
269
294
  }
270
295
 
271
296
  const focusInput = () => {
@@ -287,7 +312,9 @@ const adjustHeight = (element: HTMLTextAreaElement) => {
287
312
  const inputValue = computed(() => {
288
313
  if (props.type === 'money') {
289
314
  const numericValue =
290
- typeof props.modelValue === 'number' ? props.modelValue : parseFloat(String(props.modelValue))
315
+ typeof props.modelValue === 'number'
316
+ ? props.modelValue
317
+ : Number.parseFloat(String(props.modelValue))
291
318
  return Number.isFinite(numericValue) ? numericValue.toString() : ''
292
319
  }
293
320
  return String(props.modelValue)
@@ -297,14 +324,14 @@ const handleInput = (event: Event) => {
297
324
  const value = (event.target as HTMLTextAreaElement).value
298
325
 
299
326
  if (props.type === 'money') {
300
- const parsed = parseFloat(value)
327
+ const parsed = Number.parseFloat(value)
301
328
  if (!Number.isNaN(parsed)) {
302
329
  emit('update:modelValue', parsed)
303
- debounceAutosave(String(parsed))
330
+ showChangedIndicator()
304
331
  }
305
332
  } else {
306
333
  emit('update:modelValue', value)
307
- debounceAutosave(value)
334
+ showChangedIndicator()
308
335
  if (props.type === 'textarea' && (event.target as HTMLTextAreaElement).tagName === 'TEXTAREA') {
309
336
  adjustHeight(event.target as HTMLTextAreaElement)
310
337
  }
@@ -313,6 +340,10 @@ const handleInput = (event: Event) => {
313
340
 
314
341
  const handleFocus = () => {
315
342
  isFocused.value = true
343
+ originalValue.value = props.modelValue
344
+ if (props.type === 'date' || props.type === 'datetime') {
345
+ originalDateValue.value = dateValue.value
346
+ }
316
347
  emit('focus')
317
348
  }
318
349
 
@@ -320,7 +351,7 @@ const handleBlur = (event?: Event) => {
320
351
  if (props.type === 'money') {
321
352
  const target = event && event.target instanceof HTMLInputElement ? event.target : inputRef.value
322
353
  if (target) {
323
- const parsed = parseFloat(target.value.replace(/,/g, ''))
354
+ const parsed = Number.parseFloat(target.value.replace(/,/g, ''))
324
355
  if (!Number.isNaN(parsed)) {
325
356
  emit('update:modelValue', parsed)
326
357
  }
@@ -328,9 +359,41 @@ const handleBlur = (event?: Event) => {
328
359
  }
329
360
  isFocused.value = false
330
361
  emit('blur')
362
+
363
+ if (props.autosaveOnBlur && props.autosave) {
364
+ const value = String(props.modelValue)
365
+ handleAutosave(value)
366
+ }
331
367
  }
332
368
 
333
369
  const handleKeydown = (event: KeyboardEvent) => {
370
+ if (event.key === 'Escape') {
371
+ event.preventDefault()
372
+ emit('update:modelValue', originalValue.value)
373
+ if (props.type === 'date' || props.type === 'datetime') {
374
+ dateValue.value = originalDateValue.value
375
+ }
376
+ if (changedTimer.value) {
377
+ clearTimeout(changedTimer.value)
378
+ changedTimer.value = null
379
+ }
380
+ showSaved.value = false
381
+ showChanged.value = false
382
+ inputRef.value?.blur()
383
+ }
384
+
385
+ if (event.key === 'Enter') {
386
+ if (props.type === 'textarea') {
387
+ if (event.ctrlKey || event.shiftKey) {
388
+ event.preventDefault()
389
+ inputRef.value?.blur()
390
+ }
391
+ } else {
392
+ event.preventDefault()
393
+ inputRef.value?.blur()
394
+ }
395
+ }
396
+
334
397
  emit('keydown', event)
335
398
  }
336
399
 
@@ -339,7 +402,7 @@ const handlePaste = (event: ClipboardEvent) => {
339
402
  event.preventDefault()
340
403
  const text = event.clipboardData?.getData('text') || ''
341
404
  const cleaned = text.replace(/[^0-9+\-\.]/g, '')
342
- const parsed = parseFloat(cleaned)
405
+ const parsed = Number.parseFloat(cleaned)
343
406
  if (!Number.isNaN(parsed) && inputRef.value) {
344
407
  inputRef.value.value = parsed.toString()
345
408
  emit('update:modelValue', parsed)
@@ -349,13 +412,10 @@ const handlePaste = (event: ClipboardEvent) => {
349
412
  const handleDateChange = (date: Date | null) => {
350
413
  const formattedDate = formatDateForModel(date)
351
414
  emit('update:modelValue', formattedDate)
352
- debounceAutosave(formattedDate)
415
+ showChangedIndicator()
353
416
  }
354
417
 
355
418
  onUnmounted(() => {
356
- if (debounceTimer.value) {
357
- clearTimeout(debounceTimer.value)
358
- }
359
419
  if (changedTimer.value) {
360
420
  clearTimeout(changedTimer.value)
361
421
  }
@@ -363,15 +423,15 @@ onUnmounted(() => {
363
423
 
364
424
  onMounted(() => {
365
425
  id.value = `text-input-${Math.random().toString(36).substring(2, 9)}`
366
- if (props.type === 'date' && props.modelValue) {
367
- dateValue.value = parseDateFromModel(props.modelValue)
426
+ if ((props.type === 'date' || props.type === 'datetime') && props.modelValue) {
427
+ dateValue.value = parseDateFromModel(props.modelValue as string)
368
428
  }
369
429
  })
370
430
 
371
431
  watch(
372
432
  () => props.modelValue,
373
433
  (newValue) => {
374
- if (props.type === 'date' && newValue) {
434
+ if ((props.type === 'date' || props.type === 'datetime') && newValue) {
375
435
  dateValue.value = parseDateFromModel(newValue as string)
376
436
  }
377
437
  },