@a-vision-software/vue-input-components 1.4.17 → 1.4.19
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/dist/src/components/Dropdown.vue.d.ts +3 -0
- package/dist/src/config.d.ts +8 -0
- package/dist/src/types/dropdown.d.ts +1 -0
- package/dist/src/types/fileupload.d.ts +1 -0
- package/dist/src/types/list.d.ts +1 -1
- package/dist/vue-input-components.cjs.js +2 -2
- package/dist/vue-input-components.css +1 -1
- package/dist/vue-input-components.es.js +3229 -3293
- package/dist/vue-input-components.umd.js +2 -2
- package/package.json +1 -1
- package/src/components/Dropdown.vue +86 -42
- package/src/components/FileUpload.vue +105 -40
- package/src/components/List.vue +185 -87
- package/src/components/TextInput.vue +4 -2
- package/src/config.ts +17 -0
- package/src/types/dropdown.ts +1 -0
- package/src/types/fileupload.ts +1 -0
- package/src/types/list.ts +40 -40
- package/src/views/DropdownTestView.vue +76 -15
- package/src/views/FileUploadTestView.vue +55 -18
package/package.json
CHANGED
|
@@ -1,33 +1,46 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
2
|
+
<div
|
|
3
|
+
class="dropdown"
|
|
4
|
+
:class="{
|
|
5
|
+
'dropdown--open': isOpen,
|
|
6
|
+
'dropdown--disabled': disabled,
|
|
7
|
+
'dropdown--multiple': multiple,
|
|
8
|
+
'dropdown--large-icon': iconSize === 'large',
|
|
9
|
+
'dropdown--has-error': error,
|
|
10
|
+
[`label-${labelPosition}`]: label,
|
|
11
|
+
[`label-align-${labelAlign}`]: label,
|
|
12
|
+
}"
|
|
13
|
+
:style="[
|
|
14
|
+
{ width: width || '100%' },
|
|
15
|
+
labelStyle,
|
|
16
|
+
{
|
|
17
|
+
'--dropdown-color': error ? 'var(--danger-color)' : color,
|
|
18
|
+
'--dropdown-hover-color': hoverColor ? hoverColor : 'var(--dropdown-color)',
|
|
19
|
+
'--dropdown-active-color': activeColor ? activeColor : 'var(--dropdown-color)',
|
|
20
|
+
'--dropdown-disabled-color': disabledColor,
|
|
21
|
+
'--dropdown-background-color': backgroundColor,
|
|
22
|
+
'--dropdown-border-radius': borderRadius,
|
|
23
|
+
'--dropdown-padding': padding,
|
|
24
|
+
'--dropdown-max-height': maxHeight,
|
|
25
|
+
},
|
|
26
|
+
]"
|
|
27
|
+
@click="toggleDropdown"
|
|
28
|
+
@keydown.esc="closeDropdown"
|
|
29
|
+
@keydown.space.prevent="toggleDropdown"
|
|
30
|
+
@keydown.enter.prevent="toggleDropdown"
|
|
31
|
+
tabindex="0"
|
|
32
|
+
>
|
|
25
33
|
<label v-if="label" :for="id" class="label">
|
|
26
34
|
{{ label }}
|
|
27
35
|
</label>
|
|
28
36
|
<div class="dropdown__selected">
|
|
29
37
|
<div v-if="icon" class="dropdown__icon">
|
|
30
|
-
<img
|
|
38
|
+
<img
|
|
39
|
+
v-if="icon.startsWith('img:')"
|
|
40
|
+
:src="icon.substring(4)"
|
|
41
|
+
:alt="placeholder"
|
|
42
|
+
class="dropdown__icon-image"
|
|
43
|
+
/>
|
|
31
44
|
<font-awesome-icon v-else :icon="icon" />
|
|
32
45
|
</div>
|
|
33
46
|
<div v-if="!hasSelection" class="dropdown__placeholder">
|
|
@@ -35,8 +48,12 @@
|
|
|
35
48
|
</div>
|
|
36
49
|
<div v-else class="dropdown__selected-items">
|
|
37
50
|
<template v-if="multiple">
|
|
38
|
-
<div
|
|
39
|
-
|
|
51
|
+
<div
|
|
52
|
+
v-for="option in selectedOptions"
|
|
53
|
+
:key="option.id"
|
|
54
|
+
class="dropdown__selected-tag"
|
|
55
|
+
@click.stop="deselectOption(option)"
|
|
56
|
+
>
|
|
40
57
|
{{ option.label }}
|
|
41
58
|
<font-awesome-icon icon="xmark" class="dropdown__selected-tag-remove" />
|
|
42
59
|
</div>
|
|
@@ -46,12 +63,23 @@
|
|
|
46
63
|
</template>
|
|
47
64
|
</div>
|
|
48
65
|
<div class="dropdown__icons">
|
|
49
|
-
<font-awesome-icon
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
<font-awesome-icon
|
|
67
|
+
v-if="hasSelection && !multiple"
|
|
68
|
+
icon="xmark"
|
|
69
|
+
class="dropdown__clear"
|
|
70
|
+
@click.stop="clearSelection"
|
|
71
|
+
/>
|
|
72
|
+
<font-awesome-icon
|
|
73
|
+
icon="chevron-down"
|
|
74
|
+
class="dropdown__arrow"
|
|
75
|
+
:class="{ 'dropdown__arrow--open': isOpen && !readonly }"
|
|
76
|
+
/>
|
|
52
77
|
</div>
|
|
53
|
-
<span
|
|
54
|
-
|
|
78
|
+
<span
|
|
79
|
+
v-if="required && !showSaved && !showChanged && !error"
|
|
80
|
+
class="status-indicator required-indicator"
|
|
81
|
+
>required</span
|
|
82
|
+
>
|
|
55
83
|
<transition name="fade">
|
|
56
84
|
<span v-if="showSaved && !error" class="status-indicator saved-indicator">saved</span>
|
|
57
85
|
</transition>
|
|
@@ -63,17 +91,34 @@
|
|
|
63
91
|
</transition>
|
|
64
92
|
</div>
|
|
65
93
|
|
|
66
|
-
<div v-if="isOpen" class="dropdown__content">
|
|
94
|
+
<div v-if="isOpen && !readonly" class="dropdown__content">
|
|
67
95
|
<div v-if="filterable" class="dropdown__filter">
|
|
68
|
-
<input
|
|
69
|
-
|
|
96
|
+
<input
|
|
97
|
+
ref="filterInput"
|
|
98
|
+
v-model="filterText"
|
|
99
|
+
type="text"
|
|
100
|
+
class="dropdown__filter-input"
|
|
101
|
+
placeholder="Type to filter..."
|
|
102
|
+
@click.stop
|
|
103
|
+
@keydown.stop
|
|
104
|
+
/>
|
|
70
105
|
</div>
|
|
71
106
|
<div class="dropdown__options">
|
|
72
|
-
<div
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
107
|
+
<div
|
|
108
|
+
v-for="option in filteredOptions"
|
|
109
|
+
:key="option.id"
|
|
110
|
+
class="dropdown__option"
|
|
111
|
+
:class="{
|
|
112
|
+
'dropdown__option--selected': isSelected(option),
|
|
113
|
+
'dropdown__option--disabled': option.disabled,
|
|
114
|
+
}"
|
|
115
|
+
@click.stop="toggleOption(option)"
|
|
116
|
+
>
|
|
117
|
+
<font-awesome-icon
|
|
118
|
+
v-if="multiple && isSelected(option)"
|
|
119
|
+
icon="check"
|
|
120
|
+
class="dropdown__option-check"
|
|
121
|
+
/>
|
|
77
122
|
{{ option.label }}
|
|
78
123
|
</div>
|
|
79
124
|
</div>
|
|
@@ -107,6 +152,7 @@ const props = withDefaults(defineProps<DropdownProps>(), {
|
|
|
107
152
|
labelPosition: 'top',
|
|
108
153
|
labelAlign: 'left',
|
|
109
154
|
labelWidth: '',
|
|
155
|
+
readonly: false,
|
|
110
156
|
})
|
|
111
157
|
|
|
112
158
|
const emit = defineEmits<{
|
|
@@ -136,9 +182,7 @@ const hasSelection = computed(() => {
|
|
|
136
182
|
const filteredOptions = computed(() => {
|
|
137
183
|
if (!filterText.value) return props.options
|
|
138
184
|
const searchText = filterText.value.toLowerCase()
|
|
139
|
-
return props.options.filter((option) =>
|
|
140
|
-
option.label.toLowerCase().includes(searchText)
|
|
141
|
-
)
|
|
185
|
+
return props.options.filter((option) => option.label.toLowerCase().includes(searchText))
|
|
142
186
|
})
|
|
143
187
|
|
|
144
188
|
const isSelected = (option: DropdownOption) => {
|
|
@@ -1,28 +1,53 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
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
|
|
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':
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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"
|
|
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
|
|
43
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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(
|
|
239
|
-
|
|
240
|
-
|
|
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) {
|