@a-vision-software/vue-input-components 1.4.20 → 1.4.22

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.20",
3
+ "version": "1.4.22",
4
4
  "description": "A collection of reusable Vue 3 input components with TypeScript support",
5
5
  "author": "A-Vision Software",
6
6
  "license": "MIT",
@@ -96,7 +96,7 @@
96
96
  }"
97
97
  @click="handleSort(column)"
98
98
  >
99
- <div class="list__column-header">
99
+ <div class="list__column-header" :title="column.headerTooltip">
100
100
  <span>{{ column.label }}</span>
101
101
  <span
102
102
  v-if="column.sortable"
@@ -219,7 +219,7 @@ import { ref, computed, watch } from 'vue'
219
219
  import TextInput from './TextInput.vue'
220
220
  import Action from './Action.vue'
221
221
  import Checkbox from './Checkbox.vue'
222
- import type { ListProps, ListEmits, ListColumn, ListAction } from '../types/list'
222
+ import type { ListProps, ListEmits, ListColumn, ListAction, ListRowData } from '../types/list'
223
223
  import { config } from '../config'
224
224
 
225
225
  const props = withDefaults(defineProps<ListProps>(), {
@@ -341,6 +341,9 @@ const filteredData = computed(() => {
341
341
  if (filterableColumns.length === 0) return props.data
342
342
 
343
343
  return props.data.filter((row) => {
344
+ // Always include rows excluded from filtering
345
+ if (row.excludeFromFilter) return true
346
+
344
347
  return filterableColumns.some((column) => {
345
348
  const value = row[column.key]
346
349
  if (value == null) return false
@@ -364,36 +367,57 @@ const filteredData = computed(() => {
364
367
  })
365
368
 
366
369
  const sortedAndFilteredData = computed(() => {
367
- if (!sortColumn.value) return filteredData.value
368
-
369
- return [...filteredData.value].sort((a, b) => {
370
- const column = sortColumn.value!
371
- const aValue = a[column.key]
372
- const bValue = b[column.key]
373
- const sortOrder = sortDirection.value === 'asc' ? 1 : -1
374
-
375
- // Handle different data types
376
- if (column.type === 'date') {
377
- const dateA = new Date(aValue).getTime()
378
- const dateB = new Date(bValue).getTime()
379
- return (dateA - dateB) * sortOrder
370
+ // Separate fixed rows (excluded from sorting) from sortable rows
371
+ const fixedRows: ListRowData[] = []
372
+ const sortableRows: ListRowData[] = []
373
+
374
+ filteredData.value.forEach((row) => {
375
+ if (row.excludeFromSort) {
376
+ fixedRows.push(row)
377
+ } else {
378
+ sortableRows.push(row)
380
379
  }
380
+ })
381
381
 
382
- if (column.type === 'number') {
383
- return (aValue - bValue) * sortOrder
384
- }
382
+ // Sort only the sortable rows if sorting is active
383
+ let sortedRows = sortableRows
384
+ if (sortColumn.value) {
385
+ sortedRows = [...sortableRows].sort((a, b) => {
386
+ const column = sortColumn.value!
387
+ const aValue = a[column.key]
388
+ const bValue = b[column.key]
389
+ const sortOrder = sortDirection.value === 'asc' ? 1 : -1
390
+
391
+ // Handle different data types
392
+ if (column.type === 'date') {
393
+ const dateA = new Date(aValue).getTime()
394
+ const dateB = new Date(bValue).getTime()
395
+ return (dateA - dateB) * sortOrder
396
+ }
385
397
 
386
- if (column.type === 'checkbox') {
387
- const aChecked = aValue?.modelValue || false
388
- const bChecked = bValue?.modelValue || false
389
- return (aChecked - bChecked) * sortOrder
390
- }
398
+ if (column.type === 'number') {
399
+ return (aValue - bValue) * sortOrder
400
+ }
391
401
 
392
- // Default string comparison for text and other types
393
- const stringA = String(aValue || '').toLowerCase()
394
- const stringB = String(bValue || '').toLowerCase()
395
- return stringA.localeCompare(stringB) * sortOrder
396
- })
402
+ if (column.type === 'checkbox') {
403
+ const aChecked = aValue?.modelValue || false
404
+ const bChecked = bValue?.modelValue || false
405
+ return (aChecked - bChecked) * sortOrder
406
+ }
407
+
408
+ // Default string comparison for text and other types
409
+ const stringA = String(aValue || '').toLowerCase()
410
+ const stringB = String(bValue || '').toLowerCase()
411
+ return stringA.localeCompare(stringB) * sortOrder
412
+ })
413
+ }
414
+
415
+ // Separate fixed rows by their fixed position
416
+ const topFixedRows = fixedRows.filter((row) => row.fixed === 'top')
417
+ const bottomFixedRows = fixedRows.filter((row) => row.fixed === 'bottom')
418
+
419
+ // Return rows in order: top fixed, sorted, bottom fixed
420
+ return [...topFixedRows, ...sortedRows, ...bottomFixedRows]
397
421
  })
398
422
 
399
423
  defineExpose({
@@ -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',
@@ -246,10 +251,7 @@ const handleAutosave = async (value: string) => {
246
251
  }
247
252
  }
248
253
 
249
- const debounceAutosave = (value: string) => {
250
- if (debounceTimer.value) {
251
- clearTimeout(debounceTimer.value)
252
- }
254
+ const showChangedIndicator = () => {
253
255
  if (changedTimer.value) {
254
256
  clearTimeout(changedTimer.value)
255
257
  }
@@ -262,10 +264,6 @@ const debounceAutosave = (value: string) => {
262
264
  emit('changed')
263
265
  isChanged.value = true
264
266
  }, 500)
265
-
266
- debounceTimer.value = window.setTimeout(() => {
267
- handleAutosave(value)
268
- }, 1500)
269
267
  }
270
268
 
271
269
  const focusInput = () => {
@@ -300,11 +298,11 @@ const handleInput = (event: Event) => {
300
298
  const parsed = parseFloat(value)
301
299
  if (!Number.isNaN(parsed)) {
302
300
  emit('update:modelValue', parsed)
303
- debounceAutosave(String(parsed))
301
+ showChangedIndicator()
304
302
  }
305
303
  } else {
306
304
  emit('update:modelValue', value)
307
- debounceAutosave(value)
305
+ showChangedIndicator()
308
306
  if (props.type === 'textarea' && (event.target as HTMLTextAreaElement).tagName === 'TEXTAREA') {
309
307
  adjustHeight(event.target as HTMLTextAreaElement)
310
308
  }
@@ -313,6 +311,10 @@ const handleInput = (event: Event) => {
313
311
 
314
312
  const handleFocus = () => {
315
313
  isFocused.value = true
314
+ originalValue.value = props.modelValue
315
+ if (props.type === 'date') {
316
+ originalDateValue.value = dateValue.value
317
+ }
316
318
  emit('focus')
317
319
  }
318
320
 
@@ -328,9 +330,41 @@ const handleBlur = (event?: Event) => {
328
330
  }
329
331
  isFocused.value = false
330
332
  emit('blur')
333
+
334
+ if (props.autosaveOnBlur && props.autosave) {
335
+ const value = String(props.modelValue)
336
+ handleAutosave(value)
337
+ }
331
338
  }
332
339
 
333
340
  const handleKeydown = (event: KeyboardEvent) => {
341
+ if (event.key === 'Escape') {
342
+ event.preventDefault()
343
+ emit('update:modelValue', originalValue.value)
344
+ if (props.type === 'date') {
345
+ dateValue.value = originalDateValue.value
346
+ }
347
+ if (changedTimer.value) {
348
+ clearTimeout(changedTimer.value)
349
+ changedTimer.value = null
350
+ }
351
+ showSaved.value = false
352
+ showChanged.value = false
353
+ inputRef.value?.blur()
354
+ }
355
+
356
+ if (event.key === 'Enter') {
357
+ if (props.type === 'textarea') {
358
+ if (event.ctrlKey || event.shiftKey) {
359
+ event.preventDefault()
360
+ inputRef.value?.blur()
361
+ }
362
+ } else {
363
+ event.preventDefault()
364
+ inputRef.value?.blur()
365
+ }
366
+ }
367
+
334
368
  emit('keydown', event)
335
369
  }
336
370
 
@@ -349,13 +383,10 @@ const handlePaste = (event: ClipboardEvent) => {
349
383
  const handleDateChange = (date: Date | null) => {
350
384
  const formattedDate = formatDateForModel(date)
351
385
  emit('update:modelValue', formattedDate)
352
- debounceAutosave(formattedDate)
386
+ showChangedIndicator()
353
387
  }
354
388
 
355
389
  onUnmounted(() => {
356
- if (debounceTimer.value) {
357
- clearTimeout(debounceTimer.value)
358
- }
359
390
  if (changedTimer.value) {
360
391
  clearTimeout(changedTimer.value)
361
392
  }
package/src/types/list.ts CHANGED
@@ -38,6 +38,7 @@ interface ListColumn {
38
38
  minWidth?: string
39
39
  maxWidth?: string
40
40
  cellClasses?: conditionalClassList
41
+ headerTooltip?: string
41
42
  }
42
43
 
43
44
  interface ListFilter {
@@ -47,7 +48,7 @@ interface ListFilter {
47
48
 
48
49
  interface ListProps {
49
50
  columns: ListColumn[]
50
- data: any[]
51
+ data: ListRowData[]
51
52
  actions?: ListActionProps[]
52
53
  CSVDownload?: string
53
54
  filter?: {
@@ -76,6 +77,15 @@ interface ListIconProps {
76
77
  color?: string
77
78
  }
78
79
 
80
+ interface ListRowData {
81
+ excludeFromSort?: boolean
82
+ excludeFromFilter?: boolean
83
+ fixed?: 'top' | 'bottom'
84
+ selected?: boolean
85
+ class?: string
86
+ [key: string]: any
87
+ }
88
+
79
89
  export type {
80
90
  ListPresentation,
81
91
  ListProps,
@@ -85,4 +95,5 @@ export type {
85
95
  ListIconProps,
86
96
  ListColumn,
87
97
  ListFilter,
98
+ ListRowData,
88
99
  }
@@ -18,6 +18,7 @@ export type TextInputProps =
18
18
  bgColor?: string
19
19
  currencyFormatter?: Intl.NumberFormat
20
20
  autosave?: (value: string) => Promise<void>
21
+ autosaveOnBlur?: boolean
21
22
  error?: string
22
23
  } & (
23
24
  | {
@@ -65,7 +65,13 @@
65
65
  <script setup lang="ts">
66
66
  import { ref } from 'vue'
67
67
  import List from '@/components/List.vue'
68
- import type { ListColumn, ListActionProps, ListCheckboxProps, ListIconProps } from '@/types'
68
+ import type {
69
+ ListColumn,
70
+ ListActionProps,
71
+ ListCheckboxProps,
72
+ ListIconProps,
73
+ ListRowData,
74
+ } from '@/types'
69
75
 
70
76
  const autosaveActive = (info: any) => {
71
77
  console.log('Autosave active', info)
@@ -130,6 +136,7 @@ const columns: ListColumn[] = [
130
136
  filterable: false,
131
137
  align: 'center',
132
138
  width: '4rem',
139
+ headerTooltip: 'User status indicator',
133
140
  },
134
141
  {
135
142
  key: 'name',
@@ -137,6 +144,7 @@ const columns: ListColumn[] = [
137
144
  type: 'text',
138
145
  sortable: true,
139
146
  filterable: true,
147
+ headerTooltip: 'The full name of the user',
140
148
  },
141
149
  {
142
150
  key: 'email',
@@ -144,6 +152,7 @@ const columns: ListColumn[] = [
144
152
  type: 'email',
145
153
  sortable: true,
146
154
  filterable: true,
155
+ headerTooltip: 'Contact email address',
147
156
  cellClasses: {
148
157
  lightredEmail: (value: any) => value.includes('bob'),
149
158
  },
@@ -154,6 +163,7 @@ const columns: ListColumn[] = [
154
163
  type: 'date',
155
164
  sortable: true,
156
165
  width: '6rem',
166
+ headerTooltip: 'Account creation date',
157
167
  },
158
168
  {
159
169
  key: 'active',
@@ -162,6 +172,7 @@ const columns: ListColumn[] = [
162
172
  align: 'center',
163
173
  sortable: true,
164
174
  width: '4rem',
175
+ headerTooltip: 'Account active status',
165
176
  },
166
177
  {
167
178
  key: 'actions',
@@ -172,7 +183,39 @@ const columns: ListColumn[] = [
172
183
  },
173
184
  ]
174
185
 
175
- const data = ref([
186
+ const data = ref<ListRowData[]>([
187
+ {
188
+ // This row will always appear at the top when sorting
189
+ excludeFromSort: true,
190
+ fixed: 'top',
191
+ selected: false,
192
+ select: <ListCheckboxProps>{
193
+ modelValue: false,
194
+ disabled: false,
195
+ onCheckboxClick: selectClick,
196
+ },
197
+ status: <ListIconProps>{
198
+ icon: 'crown',
199
+ color: 'gold',
200
+ },
201
+ name: 'Current User (Fixed at Top)',
202
+ email: 'current@example.com',
203
+ joined: '2024-01-01',
204
+ active: {
205
+ modelValue: true,
206
+ disabled: false,
207
+ autosave: autosaveActive,
208
+ onCheckboxClick: selectClick,
209
+ },
210
+ actions: <ListActionProps[]>[
211
+ {
212
+ id: 'edit',
213
+ label: 'Edit',
214
+ icon: 'edit',
215
+ onActionClick: rowActionClick,
216
+ },
217
+ ],
218
+ },
176
219
  {
177
220
  class: 'testclass',
178
221
  selected: false,
@@ -390,6 +433,36 @@ const data = ref([
390
433
  },
391
434
  ],
392
435
  },
436
+ {
437
+ // This row will always appear at the bottom when sorting and is excluded from filtering
438
+ excludeFromSort: true,
439
+ excludeFromFilter: true,
440
+ fixed: 'bottom',
441
+ select: <ListCheckboxProps>{
442
+ modelValue: false,
443
+ disabled: false,
444
+ onCheckboxClick: selectClick,
445
+ },
446
+ status: <ListIconProps>{
447
+ icon: 'info-circle',
448
+ color: 'blue',
449
+ },
450
+ name: 'Footer Row (Fixed at Bottom)',
451
+ email: 'footer@example.com',
452
+ joined: '2024-12-31',
453
+ active: {
454
+ modelValue: true,
455
+ disabled: false,
456
+ },
457
+ actions: <ListActionProps[]>[
458
+ {
459
+ id: 'info',
460
+ label: 'Info',
461
+ icon: 'info',
462
+ onActionClick: rowActionClick,
463
+ },
464
+ ],
465
+ },
393
466
  ])
394
467
  </script>
395
468
 
@@ -431,4 +504,8 @@ const data = ref([
431
504
  :deep(.list-test__section .list__cell.lightredEmail) {
432
505
  background-color: rgba(255, 0, 0, 0.1);
433
506
  }
507
+
508
+ :deep(.list-test__section .list__row--testclass) {
509
+ background-color: rgba(155, 155, 0, 0.1);
510
+ }
434
511
  </style>