@wishbone-media/spark 0.27.0 → 0.28.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/dist/index.js +1503 -1326
- package/package.json +1 -1
- package/src/components/SparkImageUpload.vue +202 -0
- package/src/components/index.js +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<label v-if="label" class="block text-sm font-medium text-gray-700 mb-2">{{ label }}</label>
|
|
4
|
+
<div v-if="modelValue" class="mb-2 relative inline-block">
|
|
5
|
+
<a :href="modelValue" target="_blank" rel="noopener noreferrer">
|
|
6
|
+
<img
|
|
7
|
+
:src="modelValue"
|
|
8
|
+
:alt="label || 'Image preview'"
|
|
9
|
+
:class="[height, 'w-auto object-contain border border-gray-300 rounded-md cursor-pointer hover:opacity-80 transition-opacity']"
|
|
10
|
+
/>
|
|
11
|
+
</a>
|
|
12
|
+
<button
|
|
13
|
+
type="button"
|
|
14
|
+
class="absolute -top-2 -right-2 w-5 h-5 bg-white/80 hover:bg-white text-gray-500 hover:text-gray-700 rounded-full flex items-center justify-center shadow-sm border border-gray-200"
|
|
15
|
+
:disabled="disabled"
|
|
16
|
+
@click="clearImage"
|
|
17
|
+
>
|
|
18
|
+
<font-awesome-icon :icon="Icons.farXmark" class="text-xs" />
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
<div
|
|
22
|
+
v-if="!modelValue"
|
|
23
|
+
class="relative rounded-md transition-colors"
|
|
24
|
+
:class="{
|
|
25
|
+
'ring-2 ring-blue-500 ring-offset-2 bg-blue-50': isDragging,
|
|
26
|
+
}"
|
|
27
|
+
@dragover.prevent="onDragOver"
|
|
28
|
+
@dragenter.prevent="onDragOver"
|
|
29
|
+
@dragleave.prevent="onDragLeave"
|
|
30
|
+
@drop.prevent="onDrop"
|
|
31
|
+
>
|
|
32
|
+
<FormKit
|
|
33
|
+
type="file"
|
|
34
|
+
:name="name"
|
|
35
|
+
:accept="accept"
|
|
36
|
+
:disabled="disabled || uploading"
|
|
37
|
+
@input="handleFormKitInput"
|
|
38
|
+
/>
|
|
39
|
+
<div
|
|
40
|
+
v-if="isDragging"
|
|
41
|
+
class="absolute inset-0 flex items-center justify-center bg-blue-50/80 rounded-md pointer-events-none"
|
|
42
|
+
>
|
|
43
|
+
<span class="text-sm text-blue-600 font-medium">Drop image here</span>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<p v-if="uploading" class="text-sm text-gray-500 mt-1">Uploading...</p>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<script setup>
|
|
51
|
+
import { ref, inject } from 'vue'
|
|
52
|
+
import { Icons } from '@/plugins/fontawesome'
|
|
53
|
+
import { sparkNotificationService } from '@/composables/sparkNotificationService'
|
|
54
|
+
|
|
55
|
+
const props = defineProps({
|
|
56
|
+
modelValue: {
|
|
57
|
+
type: String,
|
|
58
|
+
default: '',
|
|
59
|
+
},
|
|
60
|
+
uploadUrl: {
|
|
61
|
+
type: String,
|
|
62
|
+
required: true,
|
|
63
|
+
},
|
|
64
|
+
fieldName: {
|
|
65
|
+
type: String,
|
|
66
|
+
default: 'file',
|
|
67
|
+
},
|
|
68
|
+
extraFields: {
|
|
69
|
+
type: Object,
|
|
70
|
+
default: () => ({}),
|
|
71
|
+
},
|
|
72
|
+
label: {
|
|
73
|
+
type: String,
|
|
74
|
+
default: '',
|
|
75
|
+
},
|
|
76
|
+
name: {
|
|
77
|
+
type: String,
|
|
78
|
+
default: 'image_file',
|
|
79
|
+
},
|
|
80
|
+
height: {
|
|
81
|
+
type: String,
|
|
82
|
+
default: 'h-16',
|
|
83
|
+
},
|
|
84
|
+
accept: {
|
|
85
|
+
type: String,
|
|
86
|
+
default: 'image/*',
|
|
87
|
+
},
|
|
88
|
+
disabled: {
|
|
89
|
+
type: Boolean,
|
|
90
|
+
default: false,
|
|
91
|
+
},
|
|
92
|
+
responseParser: {
|
|
93
|
+
type: Function,
|
|
94
|
+
default: (response) => response.data.url || response.data,
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const emit = defineEmits(['update:modelValue', 'upload-start', 'upload-success', 'upload-error'])
|
|
99
|
+
|
|
100
|
+
const axios = inject('axios')
|
|
101
|
+
const uploading = ref(false)
|
|
102
|
+
const isDragging = ref(false)
|
|
103
|
+
|
|
104
|
+
function clearImage() {
|
|
105
|
+
emit('update:modelValue', '')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function onDragOver() {
|
|
109
|
+
if (props.disabled || uploading.value) return
|
|
110
|
+
isDragging.value = true
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function onDragLeave() {
|
|
114
|
+
isDragging.value = false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function onDrop(event) {
|
|
118
|
+
isDragging.value = false
|
|
119
|
+
if (props.disabled || uploading.value) return
|
|
120
|
+
|
|
121
|
+
const file = event.dataTransfer?.files?.[0]
|
|
122
|
+
if (!file) return
|
|
123
|
+
|
|
124
|
+
// Validate file type against accept prop
|
|
125
|
+
if (!isFileTypeAccepted(file)) {
|
|
126
|
+
sparkNotificationService.show({
|
|
127
|
+
type: 'danger',
|
|
128
|
+
message: 'File type not accepted.',
|
|
129
|
+
})
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
uploadFile(file)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isFileTypeAccepted(file) {
|
|
137
|
+
// Parse accept prop (e.g., "image/*", "image/png,image/jpeg", ".png,.jpg")
|
|
138
|
+
const acceptTypes = props.accept.split(',').map((t) => t.trim())
|
|
139
|
+
|
|
140
|
+
for (const acceptType of acceptTypes) {
|
|
141
|
+
if (acceptType === '*/*') return true
|
|
142
|
+
if (acceptType.endsWith('/*')) {
|
|
143
|
+
// Wildcard like "image/*"
|
|
144
|
+
const category = acceptType.slice(0, -2)
|
|
145
|
+
if (file.type.startsWith(category + '/')) return true
|
|
146
|
+
} else if (acceptType.startsWith('.')) {
|
|
147
|
+
// Extension like ".png"
|
|
148
|
+
if (file.name.toLowerCase().endsWith(acceptType.toLowerCase())) return true
|
|
149
|
+
} else {
|
|
150
|
+
// Exact MIME type like "image/png"
|
|
151
|
+
if (file.type === acceptType) return true
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return false
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Handle FormKit file input
|
|
159
|
+
function handleFormKitInput(files) {
|
|
160
|
+
if (!files || files.length === 0) return
|
|
161
|
+
const file = files[0].file
|
|
162
|
+
if (!file) return
|
|
163
|
+
uploadFile(file)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Core upload logic
|
|
167
|
+
async function uploadFile(file) {
|
|
168
|
+
uploading.value = true
|
|
169
|
+
emit('upload-start')
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const formData = new FormData()
|
|
173
|
+
formData.append(props.fieldName, file)
|
|
174
|
+
|
|
175
|
+
// Add any extra fields
|
|
176
|
+
Object.entries(props.extraFields).forEach(([key, value]) => {
|
|
177
|
+
formData.append(key, value)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const response = await axios.post(props.uploadUrl, formData, {
|
|
181
|
+
headers: { 'Content-Type': 'multipart/form-data' },
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
const url = props.responseParser(response)
|
|
185
|
+
emit('update:modelValue', url)
|
|
186
|
+
emit('upload-success', response)
|
|
187
|
+
|
|
188
|
+
sparkNotificationService.show({
|
|
189
|
+
type: 'success',
|
|
190
|
+
message: 'Image uploaded!',
|
|
191
|
+
})
|
|
192
|
+
} catch (error) {
|
|
193
|
+
emit('upload-error', error)
|
|
194
|
+
sparkNotificationService.show({
|
|
195
|
+
type: 'danger',
|
|
196
|
+
message: error.response?.data?.message || 'Failed to upload image.',
|
|
197
|
+
})
|
|
198
|
+
} finally {
|
|
199
|
+
uploading.value = false
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
</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 SparkImageUpload } from './SparkImageUpload.vue'
|
|
9
10
|
export { default as SparkModalContainer } from './SparkModalContainer.vue'
|
|
10
11
|
export { default as SparkModalDialog } from './SparkModalDialog.vue'
|
|
11
12
|
export { default as SparkOverlay } from './SparkOverlay.vue'
|