@wishbone-media/spark 0.31.0 → 0.32.0
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/README.md +7 -21
- package/dist/index.js +1663 -1399
- package/package.json +1 -1
- package/src/components/SparkButton.vue +1 -1
- package/src/components/SparkFileDragUpload.vue +275 -0
- package/src/components/index.js +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<label v-if="label" class="block text-sm font-medium text-gray-700 mb-2">{{ label }}</label>
|
|
4
|
+
|
|
5
|
+
<!-- Dashed variant -->
|
|
6
|
+
<div
|
|
7
|
+
v-if="variant === 'dashed'"
|
|
8
|
+
class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center transition-colors"
|
|
9
|
+
:class="{
|
|
10
|
+
'border-blue-500 bg-blue-50': isDragging,
|
|
11
|
+
'opacity-50 pointer-events-none': disabled || uploading,
|
|
12
|
+
}"
|
|
13
|
+
@dragover.prevent="onDragOver"
|
|
14
|
+
@dragenter.prevent="onDragOver"
|
|
15
|
+
@dragleave.prevent="onDragLeave"
|
|
16
|
+
@drop.prevent="onDrop"
|
|
17
|
+
>
|
|
18
|
+
<FormKit
|
|
19
|
+
ref="formkitFileRef"
|
|
20
|
+
type="file"
|
|
21
|
+
:name="name"
|
|
22
|
+
:accept="accept"
|
|
23
|
+
:disabled="disabled || uploading"
|
|
24
|
+
outer-class="hidden"
|
|
25
|
+
@input="handleFormKitInput"
|
|
26
|
+
/>
|
|
27
|
+
<div v-if="!selectedFile">
|
|
28
|
+
<font-awesome-icon :icon="icon" class="text-4xl text-gray-400 mb-3" />
|
|
29
|
+
<p class="text-sm text-gray-600 mb-2">{{ dropText }}</p>
|
|
30
|
+
<SparkButton variant="secondary" @click="triggerFileDialog">
|
|
31
|
+
{{ browseText }}
|
|
32
|
+
</SparkButton>
|
|
33
|
+
</div>
|
|
34
|
+
<div v-else class="flex items-center justify-center gap-3">
|
|
35
|
+
<font-awesome-icon :icon="Icons.farFile" class="text-2xl text-gray-500" />
|
|
36
|
+
<span class="text-gray-700">{{ selectedFile.name }}</span>
|
|
37
|
+
<SparkButton variant="secondary" size="sm" @click="clear">
|
|
38
|
+
<font-awesome-icon :icon="Icons.farXmark" />
|
|
39
|
+
</SparkButton>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- Minimal variant -->
|
|
44
|
+
<div
|
|
45
|
+
v-else
|
|
46
|
+
class="relative rounded-md transition-colors"
|
|
47
|
+
:class="{
|
|
48
|
+
'ring-2 ring-blue-500 ring-offset-2 bg-blue-50': isDragging,
|
|
49
|
+
}"
|
|
50
|
+
@dragover.prevent="onDragOver"
|
|
51
|
+
@dragenter.prevent="onDragOver"
|
|
52
|
+
@dragleave.prevent="onDragLeave"
|
|
53
|
+
@drop.prevent="onDrop"
|
|
54
|
+
>
|
|
55
|
+
<FormKit
|
|
56
|
+
type="file"
|
|
57
|
+
:name="name"
|
|
58
|
+
:accept="accept"
|
|
59
|
+
:disabled="disabled || uploading"
|
|
60
|
+
@input="handleFormKitInput"
|
|
61
|
+
/>
|
|
62
|
+
<div
|
|
63
|
+
v-if="isDragging"
|
|
64
|
+
class="absolute inset-0 flex items-center justify-center bg-blue-50/80 rounded-md pointer-events-none"
|
|
65
|
+
>
|
|
66
|
+
<span class="text-sm text-blue-600 font-medium">Drop file here</span>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- Upload button -->
|
|
71
|
+
<div v-if="showUploadButton" class="flex justify-end mt-4">
|
|
72
|
+
<SparkButton size="lg" :disabled="!selectedFile || uploading" @click="upload">
|
|
73
|
+
<font-awesome-icon
|
|
74
|
+
v-if="uploading"
|
|
75
|
+
:icon="Icons.farSpinner"
|
|
76
|
+
class="mr-2 animate-spin"
|
|
77
|
+
/>
|
|
78
|
+
{{ uploading ? uploadingText : uploadText }}
|
|
79
|
+
</SparkButton>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<p v-if="uploading && !showUploadButton" class="text-sm text-gray-500 mt-1">{{ uploadingText }}</p>
|
|
83
|
+
</div>
|
|
84
|
+
</template>
|
|
85
|
+
|
|
86
|
+
<script setup>
|
|
87
|
+
import { ref, inject } from 'vue'
|
|
88
|
+
import { Icons } from '@/plugins/fontawesome'
|
|
89
|
+
import { sparkNotificationService } from '@/composables/sparkNotificationService'
|
|
90
|
+
import SparkButton from './SparkButton.vue'
|
|
91
|
+
|
|
92
|
+
const props = defineProps({
|
|
93
|
+
uploadUrl: {
|
|
94
|
+
type: String,
|
|
95
|
+
required: true,
|
|
96
|
+
},
|
|
97
|
+
fieldName: {
|
|
98
|
+
type: String,
|
|
99
|
+
default: 'file',
|
|
100
|
+
},
|
|
101
|
+
extraFields: {
|
|
102
|
+
type: Object,
|
|
103
|
+
default: () => ({}),
|
|
104
|
+
},
|
|
105
|
+
extraParams: {
|
|
106
|
+
type: Object,
|
|
107
|
+
default: () => ({}),
|
|
108
|
+
},
|
|
109
|
+
accept: {
|
|
110
|
+
type: String,
|
|
111
|
+
default: '*/*',
|
|
112
|
+
},
|
|
113
|
+
disabled: {
|
|
114
|
+
type: Boolean,
|
|
115
|
+
default: false,
|
|
116
|
+
},
|
|
117
|
+
label: {
|
|
118
|
+
type: String,
|
|
119
|
+
default: '',
|
|
120
|
+
},
|
|
121
|
+
name: {
|
|
122
|
+
type: String,
|
|
123
|
+
default: 'file',
|
|
124
|
+
},
|
|
125
|
+
icon: {
|
|
126
|
+
type: Object,
|
|
127
|
+
default: () => Icons.farCloudArrowUp,
|
|
128
|
+
},
|
|
129
|
+
browseText: {
|
|
130
|
+
type: String,
|
|
131
|
+
default: 'Browse Files',
|
|
132
|
+
},
|
|
133
|
+
dropText: {
|
|
134
|
+
type: String,
|
|
135
|
+
default: 'Drag and drop your file here, or',
|
|
136
|
+
},
|
|
137
|
+
uploadText: {
|
|
138
|
+
type: String,
|
|
139
|
+
default: 'Upload',
|
|
140
|
+
},
|
|
141
|
+
uploadingText: {
|
|
142
|
+
type: String,
|
|
143
|
+
default: 'Uploading...',
|
|
144
|
+
},
|
|
145
|
+
showUploadButton: {
|
|
146
|
+
type: Boolean,
|
|
147
|
+
default: true,
|
|
148
|
+
},
|
|
149
|
+
variant: {
|
|
150
|
+
type: String,
|
|
151
|
+
default: 'dashed',
|
|
152
|
+
validator: (value) => ['dashed', 'minimal'].includes(value),
|
|
153
|
+
},
|
|
154
|
+
responseParser: {
|
|
155
|
+
type: Function,
|
|
156
|
+
default: (response) => response.data,
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const emit = defineEmits(['upload-start', 'upload-success', 'upload-error', 'file-selected', 'file-cleared'])
|
|
161
|
+
|
|
162
|
+
const axios = inject('axios')
|
|
163
|
+
const selectedFile = ref(null)
|
|
164
|
+
const uploading = ref(false)
|
|
165
|
+
const isDragging = ref(false)
|
|
166
|
+
const formkitFileRef = ref(null)
|
|
167
|
+
|
|
168
|
+
function onDragOver() {
|
|
169
|
+
if (props.disabled || uploading.value) return
|
|
170
|
+
isDragging.value = true
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function onDragLeave() {
|
|
174
|
+
isDragging.value = false
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function onDrop(event) {
|
|
178
|
+
isDragging.value = false
|
|
179
|
+
if (props.disabled || uploading.value) return
|
|
180
|
+
|
|
181
|
+
const file = event.dataTransfer?.files?.[0]
|
|
182
|
+
if (!file) return
|
|
183
|
+
|
|
184
|
+
if (!isFileTypeAccepted(file)) {
|
|
185
|
+
sparkNotificationService.show({
|
|
186
|
+
type: 'danger',
|
|
187
|
+
message: 'File type not accepted.',
|
|
188
|
+
})
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
selectedFile.value = file
|
|
193
|
+
emit('file-selected', file)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isFileTypeAccepted(file) {
|
|
197
|
+
const acceptTypes = props.accept.split(',').map((t) => t.trim())
|
|
198
|
+
|
|
199
|
+
for (const acceptType of acceptTypes) {
|
|
200
|
+
if (acceptType === '*/*') return true
|
|
201
|
+
if (acceptType.endsWith('/*')) {
|
|
202
|
+
const category = acceptType.slice(0, -2)
|
|
203
|
+
if (file.type.startsWith(category + '/')) return true
|
|
204
|
+
} else if (acceptType.startsWith('.')) {
|
|
205
|
+
if (file.name.toLowerCase().endsWith(acceptType.toLowerCase())) return true
|
|
206
|
+
} else {
|
|
207
|
+
if (file.type === acceptType) return true
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function handleFormKitInput(files) {
|
|
215
|
+
if (!files || files.length === 0) return
|
|
216
|
+
const file = files[0].file
|
|
217
|
+
if (!file) return
|
|
218
|
+
selectedFile.value = file
|
|
219
|
+
emit('file-selected', file)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function triggerFileDialog() {
|
|
223
|
+
const el = formkitFileRef.value?.$el
|
|
224
|
+
if (!el) return
|
|
225
|
+
const inputEl = el.querySelector('input[type="file"]')
|
|
226
|
+
|| el.parentElement?.querySelector('input[type="file"]')
|
|
227
|
+
if (inputEl) {
|
|
228
|
+
inputEl.click()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function clear() {
|
|
233
|
+
selectedFile.value = null
|
|
234
|
+
emit('file-cleared')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function upload() {
|
|
238
|
+
if (!selectedFile.value) return
|
|
239
|
+
|
|
240
|
+
uploading.value = true
|
|
241
|
+
emit('upload-start')
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const formData = new FormData()
|
|
245
|
+
formData.append(props.fieldName, selectedFile.value)
|
|
246
|
+
|
|
247
|
+
Object.entries(props.extraFields).forEach(([key, value]) => {
|
|
248
|
+
formData.append(key, value)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
const response = await axios.post(props.uploadUrl, formData, {
|
|
252
|
+
headers: { 'Content-Type': 'multipart/form-data' },
|
|
253
|
+
params: props.extraParams,
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
const result = props.responseParser(response)
|
|
257
|
+
emit('upload-success', result)
|
|
258
|
+
} catch (error) {
|
|
259
|
+
emit('upload-error', error)
|
|
260
|
+
sparkNotificationService.show({
|
|
261
|
+
type: 'danger',
|
|
262
|
+
message: error.response?.data?.message || 'Failed to upload file.',
|
|
263
|
+
})
|
|
264
|
+
} finally {
|
|
265
|
+
uploading.value = false
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
defineExpose({
|
|
270
|
+
upload,
|
|
271
|
+
clear,
|
|
272
|
+
selectedFile,
|
|
273
|
+
uploading,
|
|
274
|
+
})
|
|
275
|
+
</script>
|
package/src/components/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export { default as SparkBrandSelector } from './SparkBrandSelector.vue'
|
|
|
6
6
|
export { default as SparkButton } from './SparkButton.vue'
|
|
7
7
|
export { default as SparkButtonGroup } from './SparkButtonGroup.vue'
|
|
8
8
|
export { default as SparkCard } from './SparkCard.vue'
|
|
9
|
+
export { default as SparkFileDragUpload } from './SparkFileDragUpload.vue'
|
|
9
10
|
export { default as SparkImageUpload } from './SparkImageUpload.vue'
|
|
10
11
|
export { default as SparkModalContainer } from './SparkModalContainer.vue'
|
|
11
12
|
export { default as SparkModalDialog } from './SparkModalDialog.vue'
|