@a-vision-software/vue-input-components 1.4.16 → 1.4.18

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,91 +1,91 @@
1
1
  {
2
- "name": "@a-vision-software/vue-input-components",
3
- "version": "1.4.16",
4
- "description": "A collection of reusable Vue 3 input components with TypeScript support",
5
- "author": "A-Vision Software",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/a-vision/vue-input-components.git"
10
- },
11
- "homepage": "https://a-vision.github.io/vue-input-components/",
12
- "keywords": [
13
- "vue",
14
- "vue3",
15
- "components",
16
- "input",
17
- "form",
18
- "typescript",
19
- "file-upload",
20
- "text-input",
21
- "date-picker",
22
- "dropdown",
23
- "textarea"
24
- ],
25
- "type": "module",
26
- "files": [
27
- "src",
28
- "types",
29
- "dist"
30
- ],
31
- "main": "./dist/vue-input-components.cjs.js",
32
- "module": "./dist/vue-input-components.es.js",
33
- "types": "./dist/src/index.d.ts",
34
- "exports": {
35
- ".": {
36
- "types": "./dist/src/index.d.ts",
37
- "import": "./dist/vue-input-components.es.js",
38
- "require": "./dist/vue-input-components.cjs.js"
2
+ "name": "@a-vision-software/vue-input-components",
3
+ "version": "1.4.18",
4
+ "description": "A collection of reusable Vue 3 input components with TypeScript support",
5
+ "author": "A-Vision Software",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/a-vision/vue-input-components.git"
39
10
  },
40
- "./global": {
41
- "types": "./dist/src/global.d.ts"
11
+ "homepage": "https://a-vision.github.io/vue-input-components/",
12
+ "keywords": [
13
+ "vue",
14
+ "vue3",
15
+ "components",
16
+ "input",
17
+ "form",
18
+ "typescript",
19
+ "file-upload",
20
+ "text-input",
21
+ "date-picker",
22
+ "dropdown",
23
+ "textarea"
24
+ ],
25
+ "type": "module",
26
+ "files": [
27
+ "src",
28
+ "types",
29
+ "dist"
30
+ ],
31
+ "main": "./dist/vue-input-components.cjs.js",
32
+ "module": "./dist/vue-input-components.es.js",
33
+ "types": "./dist/src/index.d.ts",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/src/index.d.ts",
37
+ "import": "./dist/vue-input-components.es.js",
38
+ "require": "./dist/vue-input-components.cjs.js"
39
+ },
40
+ "./global": {
41
+ "types": "./dist/src/global.d.ts"
42
+ },
43
+ "./dist/*": "./dist/*",
44
+ "./styles.css": "./dist/vue-input-components.css",
45
+ "./styles": "./dist/vue-input-components.css"
42
46
  },
43
- "./dist/*": "./dist/*",
44
- "./styles.css": "./dist/vue-input-components.css",
45
- "./styles": "./dist/vue-input-components.css"
46
- },
47
- "sideEffects": [
48
- "**/*.css"
49
- ],
50
- "scripts": {
51
- "dev": "vite",
52
- "build": "vite build",
53
- "preview": "vite preview",
54
- "test": "vitest"
55
- },
56
- "peerDependencies": {
57
- "vue": "^3.5.0"
58
- },
59
- "dependencies": {
60
- "@fortawesome/fontawesome-svg-core": "^6.7.0",
61
- "@fortawesome/free-regular-svg-icons": "^6.7.0",
62
- "@fortawesome/free-solid-svg-icons": "^6.7.0",
63
- "@fortawesome/vue-fontawesome": "^3.0.0",
64
- "@vuepic/vue-datepicker": "^11.0.2"
65
- },
66
- "devDependencies": {
67
- "@tsconfig/node20": "^20.1.2",
68
- "@types/node": "^20.11.0",
69
- "@vitejs/plugin-vue": "^5.0.0",
70
- "@vue/eslint-config-prettier": "^9.0.0",
71
- "@vue/eslint-config-typescript": "^12.0.0",
72
- "@vue/test-utils": "^2.4.0",
73
- "@vue/tsconfig": "^0.5.1",
74
- "eslint": "^8.56.0",
75
- "eslint-plugin-vue": "^9.21.0",
76
- "npm-run-all": "^4.1.5",
77
- "prettier": "^3.2.0",
78
- "typescript": "~5.3.0",
79
- "vite": "^5.0.0",
80
- "vite-plugin-dts": "^3.7.0",
81
- "vitest": "^1.2.0",
82
- "vue-router": "^4.2.0",
83
- "vue-tsc": "^1.8.0"
84
- },
85
- "engines": {
86
- "node": ">=18.0.0"
87
- },
88
- "publishConfig": {
89
- "access": "public"
90
- }
91
- }
47
+ "sideEffects": [
48
+ "**/*.css"
49
+ ],
50
+ "scripts": {
51
+ "dev": "vite",
52
+ "build": "vite build",
53
+ "preview": "vite preview",
54
+ "test": "vitest"
55
+ },
56
+ "peerDependencies": {
57
+ "vue": "^3.5.0"
58
+ },
59
+ "dependencies": {
60
+ "@fortawesome/fontawesome-svg-core": "^6.7.0",
61
+ "@fortawesome/free-regular-svg-icons": "^6.7.0",
62
+ "@fortawesome/free-solid-svg-icons": "^6.7.0",
63
+ "@fortawesome/vue-fontawesome": "^3.0.0",
64
+ "@vuepic/vue-datepicker": "^11.0.2"
65
+ },
66
+ "devDependencies": {
67
+ "@tsconfig/node20": "^20.1.2",
68
+ "@types/node": "^20.11.0",
69
+ "@vitejs/plugin-vue": "^5.0.0",
70
+ "@vue/eslint-config-prettier": "^9.0.0",
71
+ "@vue/eslint-config-typescript": "^12.0.0",
72
+ "@vue/test-utils": "^2.4.0",
73
+ "@vue/tsconfig": "^0.5.1",
74
+ "eslint": "^8.56.0",
75
+ "eslint-plugin-vue": "^9.21.0",
76
+ "npm-run-all": "^4.1.5",
77
+ "prettier": "^3.2.0",
78
+ "typescript": "~5.3.0",
79
+ "vite": "^5.0.0",
80
+ "vite-plugin-dts": "^3.7.0",
81
+ "vitest": "^1.2.0",
82
+ "vue-router": "^4.2.0",
83
+ "vue-tsc": "^1.8.0"
84
+ },
85
+ "engines": {
86
+ "node": ">=18.0.0"
87
+ },
88
+ "publishConfig": {
89
+ "access": "public"
90
+ }
91
+ }
@@ -1,28 +1,53 @@
1
1
  <template>
2
- <div class="file-upload"
3
- :class="{ 'label-position-top': labelPosition === 'top', 'label-position-left': labelPosition === 'left' }" :style="{
2
+ <div
3
+ class="file-upload"
4
+ :class="{
5
+ 'label-position-top': labelPosition === 'top',
6
+ 'label-position-left': labelPosition === 'left',
7
+ }"
8
+ :style="{
4
9
  '--file-upload-width': width || 'auto',
5
10
  '--file-upload-height': height || 'auto',
6
- '--file-upload-color': error ? 'var(--ui-error-text-color)' : (color || 'var(--ui-upload-text-color, #888888)'),
11
+ '--file-upload-color': error
12
+ ? 'var(--ui-error-text-color)'
13
+ : color || 'var(--ui-upload-text-color, #888888)',
7
14
  '--file-upload-disabled-color': 'color-mix(in srgb, var(--file-upload-color), white 70%)',
8
15
  '--file-upload-background-color': bgColor || 'var(--ui-upload-bg-color, #ffffff)',
9
- '--file-upload-background-active-color': activeColor || 'var(--ui-upload-active-bg-color, #cccccc)',
16
+ '--file-upload-background-active-color':
17
+ activeColor || 'var(--ui-upload-active-bg-color, #cccccc)',
10
18
  '--file-upload-border-color': borderColor || 'var(--ui-upload-border-color, #888888)',
11
19
  '--file-upload-icon-color': iconColor || 'var(--file-upload-color, #888888)',
12
20
  '--file-upload-progress-color': progressColor || 'var(--ui-progress-color, #4444aa)',
13
21
  '--file-upload-error-color': 'var(--ui-error-text-color, #aa0000)',
14
22
  '--file-upload-success-color': 'var(--ui-success-text-color, #00aa00)',
15
- }">
23
+ }"
24
+ >
16
25
  <label class="file-upload-label" v-if="label" for="fileInput">{{ label }}</label>
17
- <div class="upload-area"
18
- :class="{ 'is-dragging': isDragging, 'has-files': files.length > 0, 'is-disabled': disabled }">
19
- <input ref="fileInput" type="file" :multiple="multiple" class="file-input" @change="handleFileSelect"
20
- :accept="accept" :required="required" :disabled="disabled" :placeholder="placeholder"
21
- @dragenter.prevent="handleDragEnter" @dragleave.prevent="handleDragLeave" @dragover.prevent
22
- @drop.prevent="handleDrop" />
26
+ <div
27
+ class="upload-area"
28
+ :class="{ 'is-dragging': isDragging, 'has-files': files.length > 0, 'is-disabled': disabled }"
29
+ >
30
+ <input
31
+ ref="fileInput"
32
+ type="file"
33
+ :multiple="multiple"
34
+ class="file-input"
35
+ @change="handleFileSelect"
36
+ :accept="accept"
37
+ :required="required"
38
+ :disabled="disabled"
39
+ :placeholder="placeholder"
40
+ @dragenter.prevent="handleDragEnter"
41
+ @dragleave.prevent="handleDragLeave"
42
+ @dragover.prevent
43
+ @drop.prevent="handleDrop"
44
+ />
23
45
  <div class="upload-content">
24
46
  <font-awesome-icon v-if="icon" :icon="['fas', icon]" />
25
- <p v-if="files.length === 0" v-text="placeholder || 'Drag & drop files here or click to select'"></p>
47
+ <p v-if="files.length === 0">
48
+ {{ placeholder || 'Drag & drop files here or click to select' }}
49
+ <div v-if="maxSize" class="max-size">(max size: {{ maxSize }}MB)</div>
50
+ </p>
26
51
  <div v-else class="selected-files">
27
52
  <p>{{ files.length }} file(s) selected</p>
28
53
  <div v-for="(file, index) in files" :key="index" class="file-info">
@@ -39,8 +64,11 @@
39
64
  <div class="progress" :style="{ width: `${uploadProgress}%` }"></div>
40
65
  </div>
41
66
  </div>
42
- <button v-if="files.length > 0 && !uploadUrl && !disabled && showUploadButton" class="upload-button"
43
- @click="handleStartUpload">
67
+ <button
68
+ v-if="files.length > 0 && !uploadUrl && !disabled && showUploadButton"
69
+ class="upload-button"
70
+ @click="handleStartUpload"
71
+ >
44
72
  Upload Files
45
73
  </button>
46
74
  </div>
@@ -48,7 +76,12 @@
48
76
 
49
77
  <script setup lang="ts">
50
78
  import { ref, watch, onMounted } from 'vue'
51
- import type { FileUploadProps, FileUploadEmits, UploadStatus, UploadCallbackResponse } from '@/types/fileupload'
79
+ import type {
80
+ FileUploadProps,
81
+ FileUploadEmits,
82
+ UploadStatus,
83
+ UploadCallbackResponse,
84
+ } from '@/types/fileupload'
52
85
 
53
86
  const props = defineProps<FileUploadProps>()
54
87
  const emit = defineEmits<FileUploadEmits>()
@@ -65,7 +98,7 @@ const uploadStatus = ref<UploadStatus>({
65
98
  })
66
99
 
67
100
  onMounted(() => {
68
- startUpload.value = props.doUpload;
101
+ startUpload.value = props.doUpload
69
102
  })
70
103
 
71
104
  const formatFileSize = (bytes: number): string => {
@@ -77,7 +110,7 @@ const formatFileSize = (bytes: number): string => {
77
110
  }
78
111
 
79
112
  const validateFileSize = (file: File): boolean => {
80
- if (props.maxSize && file.size > props.maxSize) {
113
+ if (props.maxSize && file.size > props.maxSize * 1024 * 1024) {
81
114
  error.value = `File "${file.name}" exceeds the maximum size of ${props.maxSize}MB`
82
115
  return false
83
116
  }
@@ -129,12 +162,13 @@ const uploadFiles = () => {
129
162
  const formData = new FormData()
130
163
  files.value.forEach((file) => {
131
164
  formData.append('files', file)
132
- });
165
+ })
133
166
 
134
167
  if (props.uploadCallback) {
135
- props.uploadCallback(formData)
168
+ props
169
+ .uploadCallback(formData)
136
170
  .then((response: UploadCallbackResponse) => {
137
- const { formData, headers } = response;
171
+ const { formData, headers } = response
138
172
  doUploadFiles(formData, headers)
139
173
  })
140
174
  .catch((err) => {
@@ -143,10 +177,11 @@ const uploadFiles = () => {
143
177
  uploadStatus.value = {
144
178
  type: 'error',
145
179
  message: errorMessage,
180
+ response: err,
146
181
  }
147
182
  emit('upload-complete', files.value)
148
183
  emit('upload-error', uploadStatus.value)
149
- });
184
+ })
150
185
  } else {
151
186
  doUploadFiles(formData, {})
152
187
  }
@@ -172,24 +207,43 @@ const doUploadFiles = (formData: FormData, headers: Record<string, string>) => {
172
207
 
173
208
  xhr.addEventListener('load', () => {
174
209
  if (xhr.status >= 200 && xhr.status < 300) {
210
+ const response = JSON.parse(xhr.response)
175
211
  uploadStatus.value = {
176
212
  type: 'success',
177
213
  message: 'Upload completed successfully',
214
+ response: response,
178
215
  }
179
216
 
180
- const response = JSON.parse(xhr.response)
181
- emit('upload-complete', response)
217
+ emit('upload-complete', files.value)
182
218
  emit('upload-success', uploadStatus.value)
183
219
  files.value = []
184
220
  uploadProgress.value = 0
185
- startUpload.value = false;
221
+ startUpload.value = false
186
222
  } else {
187
- throw new Error(`Upload failed with status ${xhr.status}`)
223
+ const errorMessage = xhr.statusText || 'Upload failed'
224
+ error.value = errorMessage
225
+ uploadStatus.value = {
226
+ type: 'error',
227
+ message: errorMessage,
228
+ response: null,
229
+ }
230
+ emit('upload-complete', files.value)
231
+ emit('upload-error', uploadStatus.value)
232
+ files.value = []
233
+ uploadProgress.value = 0
234
+ startUpload.value = false
188
235
  }
189
236
  })
190
237
 
191
238
  xhr.addEventListener('error', () => {
192
- throw new Error('Upload failed')
239
+ const errorMessage = xhr.statusText || 'Upload error'
240
+ uploadStatus.value = {
241
+ type: 'error',
242
+ message: errorMessage,
243
+ response: null,
244
+ }
245
+ emit('upload-complete', files.value)
246
+ emit('upload-error', uploadStatus.value)
193
247
  })
194
248
 
195
249
  xhr.open('POST', props.uploadUrl || '')
@@ -205,6 +259,7 @@ const doUploadFiles = (formData: FormData, headers: Record<string, string>) => {
205
259
  uploadStatus.value = {
206
260
  type: 'error',
207
261
  message: errorMessage,
262
+ response: err,
208
263
  }
209
264
  emit('upload-complete', files.value)
210
265
  emit('upload-error', uploadStatus.value)
@@ -226,20 +281,26 @@ watch(files, (newFiles) => {
226
281
  }
227
282
  })
228
283
 
229
- watch(() => props.doUpload, (newDoUpload) => {
230
- if (files.value && newDoUpload) {
231
- startUpload.value = newDoUpload;
232
- } else {
233
- emit('files-selected', files.value)
234
- startUpload.value = false;
235
- }
236
- })
284
+ watch(
285
+ () => props.doUpload,
286
+ (newDoUpload) => {
287
+ if (files.value && newDoUpload) {
288
+ startUpload.value = newDoUpload
289
+ } else {
290
+ emit('files-selected', files.value)
291
+ startUpload.value = false
292
+ }
293
+ },
294
+ )
237
295
 
238
- watch(() => startUpload.value, (newStartUpload) => {
239
- if (newStartUpload) {
240
- uploadFiles()
241
- }
242
- })
296
+ watch(
297
+ () => startUpload.value,
298
+ (newStartUpload) => {
299
+ if (newStartUpload) {
300
+ uploadFiles()
301
+ }
302
+ },
303
+ )
243
304
  </script>
244
305
 
245
306
  <style scoped>
@@ -253,7 +314,6 @@ watch(() => startUpload.value, (newStartUpload) => {
253
314
  grid-template-columns: 10em calc(100% - 10em);
254
315
  gap: 1rem;
255
316
  }
256
-
257
317
  }
258
318
 
259
319
  .upload-area {
@@ -299,6 +359,11 @@ watch(() => startUpload.value, (newStartUpload) => {
299
359
  grid-template-rows: 1fr 1fr;
300
360
  text-align: center;
301
361
  gap: 1rem;
362
+
363
+ .max-size {
364
+ font-size: 0.75rem;
365
+ color: var(--file-upload-color);
366
+ }
302
367
  }
303
368
 
304
369
  .upload-content :deep(svg) {
@@ -22,7 +22,16 @@
22
22
  <!-- Actions -->
23
23
  <div v-if="actions?.length" class="list__actions">
24
24
 
25
- <Action v-for="(action, actionIndex) in actions" :key="actionIndex" v-bind="action"
25
+ <Action v-for="(action, actionIndex) in [
26
+ ...(props.CSVDownload !== undefined ? [{
27
+ id: 'csv-download',
28
+ label: 'Download',
29
+ icon: 'file-csv',
30
+ color: 'var(--primary-color)',
31
+ onActionClick: handleCSVDownload
32
+ }] : []),
33
+ ...actions
34
+ ]" :key="actionIndex" v-bind="action"
26
35
  :href="presentation === 'minimal' && !action.href ? `#/${action.id ? action.id : action.label?.toLowerCase()}` : action.href"
27
36
  @click.prevent="action.onActionClick ? action.onActionClick(undefined, action) : null"
28
37
  :size="presentation === 'minimal' ? 'small' : 'regular'" />
@@ -85,6 +94,11 @@
85
94
  <template v-if="column.type === 'text'">
86
95
  {{ row[column.key] }}
87
96
  </template>
97
+ <template v-else-if="column.type === 'email'">
98
+ <a v-if="row['name']" :href="`mailto:${row['name']} <${row[column.key]}>`" @click.stop>{{
99
+ row[column.key] }}</a>
100
+ <a v-else :href="`mailto:${row[column.key]}`" @click.stop>{{ row[column.key] }}</a>
101
+ </template>
88
102
  <template v-else-if="column.type === 'number'">
89
103
  {{ row[column.key] }}
90
104
  </template>
@@ -154,6 +168,43 @@ watch(filterValue, (newValue) => {
154
168
  return () => clearTimeout(timeout)
155
169
  })
156
170
 
171
+ const handleCSVDownload = () => {
172
+ const csv = [];
173
+ const headerRow = [];
174
+ const headerKeys = []
175
+ for (const column of props.columns) {
176
+ const dataType = typeof props.data[0][column.key];
177
+ if (dataType !== 'object') {
178
+ const columnName = column.label || column.key;
179
+ headerRow.push(columnName);
180
+ headerKeys.push(column.key);
181
+ }
182
+ }
183
+ csv.push(headerRow.join(','));
184
+
185
+ for (const row of props.data) {
186
+ const rowData: string[] = [];
187
+ headerKeys.forEach(key => {
188
+ const value = row[key];
189
+ if (value == null) {
190
+ rowData.push('');
191
+ } else {
192
+ rowData.push(value);
193
+ }
194
+ })
195
+ csv.push('"' + rowData.join('","') + '"');
196
+ }
197
+
198
+ const blob = new Blob([csv.join('\n')], { type: 'text/csv;charset=utf-8;' })
199
+ const url = URL.createObjectURL(blob)
200
+ const a = document.createElement('a')
201
+ a.href = url
202
+ const downloadName = props.CSVDownload || 'data';
203
+ a.download = downloadName.toLowerCase().endsWith('.csv') ? downloadName : downloadName + '.csv'
204
+ a.click()
205
+ URL.revokeObjectURL(url)
206
+ }
207
+
157
208
  const handleFilter = () => {
158
209
  // No need to do anything here as the watch handles the filtering
159
210
  }
@@ -203,16 +254,17 @@ const filteredData = computed(() => {
203
254
  const value = row[column.key]
204
255
  if (value == null) return false
205
256
 
257
+ const dateStr = formatDate(value)
258
+ const checkboxValue = value?.modelValue ? 'yes' : 'no'
259
+
206
260
  switch (column.type) {
207
261
  case 'text':
208
262
  return String(value).toLowerCase().includes(effectiveFilterValue.value)
209
263
  case 'number':
210
264
  return String(value).includes(effectiveFilterValue.value)
211
265
  case 'date':
212
- const dateStr = formatDate(value)
213
266
  return dateStr.toLowerCase().includes(effectiveFilterValue.value)
214
267
  case 'checkbox':
215
- const checkboxValue = value?.modelValue ? 'yes' : 'no'
216
268
  return checkboxValue.includes(effectiveFilterValue.value)
217
269
  default:
218
270
  return false
@@ -228,32 +280,29 @@ const sortedAndFilteredData = computed(() => {
228
280
  const column = sortColumn.value!
229
281
  const aValue = a[column.key]
230
282
  const bValue = b[column.key]
283
+ const sortOrder = sortDirection.value === 'asc' ? 1 : -1
231
284
 
232
285
  // Handle different data types
233
286
  if (column.type === 'date') {
234
287
  const dateA = new Date(aValue).getTime()
235
288
  const dateB = new Date(bValue).getTime()
236
- return sortDirection.value === 'asc' ? dateA - dateB : dateB - dateA
289
+ return (dateA - dateB) * sortOrder
237
290
  }
238
291
 
239
292
  if (column.type === 'number') {
240
- return sortDirection.value === 'asc' ? aValue - bValue : bValue - aValue
293
+ return (aValue - bValue) * sortOrder
241
294
  }
242
295
 
243
296
  if (column.type === 'checkbox') {
244
297
  const aChecked = aValue?.modelValue || false
245
298
  const bChecked = bValue?.modelValue || false
246
- return sortDirection.value === 'asc'
247
- ? (aChecked === bChecked ? 0 : aChecked ? 1 : -1)
248
- : (aChecked === bChecked ? 0 : aChecked ? -1 : 1)
299
+ return (aChecked - bChecked) * sortOrder
249
300
  }
250
301
 
251
302
  // Default string comparison for text and other types
252
303
  const stringA = String(aValue || '').toLowerCase()
253
304
  const stringB = String(bValue || '').toLowerCase()
254
- return sortDirection.value === 'asc'
255
- ? stringA.localeCompare(stringB)
256
- : stringB.localeCompare(stringA)
305
+ return stringA.localeCompare(stringB) * sortOrder
257
306
  })
258
307
  })
259
308
 
@@ -31,6 +31,7 @@ interface UploadCallbackResponse {
31
31
  interface UploadStatus {
32
32
  type: 'pending' | 'success' | 'error' | 'processing'
33
33
  message: string
34
+ response?: unknown
34
35
  }
35
36
 
36
37
  interface FileUploadEmits {
package/src/types/list.ts CHANGED
@@ -2,7 +2,7 @@ import { ListActionProps } from './action'
2
2
 
3
3
  type ListPresentation = 'default' | 'minimal'
4
4
 
5
- type ListDataType = 'text' | 'number' | 'date' | 'action' | 'checkbox' | 'icon'
5
+ type ListDataType = 'text' | 'number' | 'date' | 'action' | 'checkbox' | 'icon' | 'email'
6
6
 
7
7
  interface ListAction {
8
8
  id: string
@@ -35,6 +35,7 @@ interface ListProps {
35
35
  columns: ListColumn[]
36
36
  data: any[]
37
37
  actions?: ListActionProps[]
38
+ CSVDownload?: string
38
39
  filter?: {
39
40
  placeholder?: string
40
41
  }