Skip to content

Commit 520bdbc

Browse files
committed
feat: add ability in dropzone to remove selected files and upload them on by one
1 parent d45c091 commit 520bdbc

File tree

2 files changed

+94
-38
lines changed

2 files changed

+94
-38
lines changed

adminforth/documentation/docs/tutorial/03-Customization/07-alert.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ Next variants are supported:
2424
* `warning`
2525
* `info`
2626

27-
// ...existing code...
2827
### Making alert responsive
2928
You can pass buttons in the alert method and receive a response like:
3029

adminforth/spa/src/afcl/Dropzone.vue

Lines changed: 94 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,78 @@
1616
'h-32': !props.multiple,
1717
}"
1818
>
19-
<div class="flex flex-col items-center justify-center pt-5 pb-6">
20-
21-
22-
<svg v-if="!selectedFiles.length" class="w-8 h-8 mb-4 text-lightDropzoneIcon dark:text-darkDropzoneIcon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
23-
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/>
24-
</svg>
25-
<div v-else class="flex items-center justify-center flex-wrap gap-1 w-full mt-1 mb-4">
26-
<template v-for="file in selectedFiles">
27-
<p class="text-sm text-lightDropzoneIcon dark:text-darkDropzoneIcon flex items-center gap-1">
28-
<IconFileSolid class="w-5 h-5" />
29-
{{ file.name }} ({{ humanifySize(file.size) }})
30-
</p>
31-
</template>
32-
19+
<input
20+
:id="id"
21+
type="file"
22+
class="hidden"
23+
:accept="props.extensions.join(',')"
24+
@change="$event.target && doEmit(($event.target as HTMLInputElement).files!)"
25+
:multiple="props.multiple || false"
26+
/>
27+
28+
<div class="flex flex-col items-center justify-center pt-5 pb-6">
29+
<svg
30+
v-if="!selectedFiles.length"
31+
class="w-8 h-8 mb-4 text-lightDropzoneIcon dark:text-darkDropzoneIcon"
32+
xmlns="http://www.w3.org/2000/svg"
33+
fill="none"
34+
viewBox="0 0 20 16"
35+
>
36+
<path
37+
stroke="currentColor"
38+
stroke-linecap="round"
39+
stroke-linejoin="round"
40+
stroke-width="2"
41+
d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5
42+
5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0
43+
0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"
44+
/>
45+
</svg>
46+
47+
<div
48+
v-else
49+
class="flex items-center justify-center py-1 flex-wrap gap-2 w-full gap-2 mt-1 mb-4 px-4"
50+
>
51+
<template v-for="(file, index) in selectedFiles" :key="index">
52+
<div
53+
class="text-sm text-lightDropzoneIcon dark:text-darkDropzoneIcon bg-lightDropzoneBackgroundHover dark:bg-darkDropzoneBackgroundHover rounded-md
54+
flex items-center gap-1 px-2 py-1 group"
55+
>
56+
<IconFileSolid class="w-4 h-4 flex-shrink-0" />
57+
<span class="truncate max-w-[200px]">{{ file.name }}</span>
58+
<span class="text-xs">({{ humanifySize(file.size) }})</span>
59+
<button
60+
type="button"
61+
@click.prevent.stop="removeFile(index)"
62+
class="text-lightDropzoneIcon dark:text-darkDropzoneIcon hover:text-red-600 dark:hover:text-red-400
63+
opacity-70 hover:opacity-100 transition-all"
64+
:title="$t('Remove file')"
65+
>
66+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
67+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
68+
</svg>
69+
</button>
3370
</div>
34-
35-
<p v-if="!selectedFiles.length" class="mb-2 text-sm text-lightDropzoneText dark:text-darkDropzoneText"><span class="font-semibold">{{ $t('Click to upload') }}</span> {{ $t('or drag and drop') }}</p>
36-
<p class="text-xs text-lightDropzoneText dark:text-darkDropzoneText">
37-
{{ props.extensions.join(', ').toUpperCase().replace(/\./g, '') }}
38-
<template v-if="props.maxSizeBytes">
39-
(Max size: {{ humanifySize(props.maxSizeBytes) }})
40-
</template>
41-
</p>
71+
</template>
4272
</div>
43-
<input :id="id" type="file" class="hidden"
44-
:accept="props.extensions.join(', ')"
45-
@change="$event.target && doEmit(($event.target as HTMLInputElement).files!)"
46-
:multiple="props.multiple || false"
47-
/>
73+
74+
<p
75+
v-if="!selectedFiles.length"
76+
class="mb-2 text-sm text-lightDropzoneText dark:text-darkDropzoneText"
77+
>
78+
<span class="font-semibold">{{ $t('Click to upload') }}</span>
79+
{{ $t('or drag and drop') }}
80+
</p>
81+
82+
<p class="text-xs text-lightDropzoneText dark:text-darkDropzoneText">
83+
{{ props.extensions.join(', ').toUpperCase().replace(/\./g, '') }}
84+
<template v-if="props.maxSizeBytes">
85+
(Max size: {{ humanifySize(props.maxSizeBytes) }})
86+
</template>
87+
</p>
88+
</div>
4889
</label>
49-
</form>
90+
</form>
5091
</template>
5192

5293
<script setup lang="ts">
@@ -73,12 +114,17 @@ const selectedFiles: Ref<{
73114
mime: string,
74115
}[]> = ref([]);
75116
117+
const storedFiles: Ref<File[]> = ref([]);
118+
76119
watch(() => props.modelValue, (files) => {
77-
selectedFiles.value = Array.from(files).map(file => ({
78-
name: file.name,
79-
size: file.size,
80-
mime: file.type,
81-
}));
120+
if (files && files.length > 0) {
121+
selectedFiles.value = Array.from(files).map(file => ({
122+
name: file.name,
123+
size: file.size,
124+
mime: file.type,
125+
}));
126+
storedFiles.value = Array.from(files);
127+
}
82128
});
83129
84130
function doEmit(filesIn: FileList) {
@@ -110,26 +156,37 @@ function doEmit(filesIn: FileList) {
110156
});
111157
return;
112158
}
113-
159+
114160
validFiles.push(file);
115161
});
116162
117163
if (!multiple) {
118-
validFiles.splice(1);
164+
storedFiles.value = validFiles.slice(0, 1);
165+
} else {
166+
storedFiles.value = [...storedFiles.value, ...validFiles];
119167
}
120-
selectedFiles.value = validFiles.map(file => ({
168+
169+
selectedFiles.value = storedFiles.value.map(file => ({
121170
name: file.name,
122171
size: file.size,
123172
mime: file.type,
124173
}));
125174
126-
emit('update:modelValue', validFiles);
175+
emit('update:modelValue', storedFiles.value);
176+
127177
}
128178
129179
const dragging = ref(false);
130180
181+
function removeFile(index: number) {
182+
storedFiles.value = storedFiles.value.filter((_, i) => i !== index);
183+
selectedFiles.value = selectedFiles.value.filter((_, i) => i !== index);
184+
emit('update:modelValue', storedFiles.value);
185+
}
186+
131187
function clear() {
132188
selectedFiles.value = [];
189+
storedFiles.value = [];
133190
emit('update:modelValue', []);
134191
const form = document.getElementById(id)?.closest('form');
135192
form?.reset();

0 commit comments

Comments
 (0)