@adminforth/upload 1.0.5
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/custom/package-lock.json +21 -0
- package/custom/package.json +15 -0
- package/custom/preview.vue +105 -0
- package/custom/uploader.vue +221 -0
- package/dist/custom/package-lock.json +21 -0
- package/dist/custom/package.json +15 -0
- package/dist/custom/preview.vue +105 -0
- package/dist/custom/uploader.vue +221 -0
- package/dist/package-lock.json +21 -0
- package/dist/package.json +15 -0
- package/dist/preview.vue +56 -0
- package/dist/uploader.vue +163 -0
- package/index.ts +349 -0
- package/package.json +16 -0
- package/types.ts +81 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "custom",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "custom",
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"license": "ISC",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"medium-zoom": "^1.1.0"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"node_modules/medium-zoom": {
|
|
16
|
+
"version": "1.1.0",
|
|
17
|
+
"resolved": "https://registry.npmjs.org/medium-zoom/-/medium-zoom-1.1.0.tgz",
|
|
18
|
+
"integrity": "sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ=="
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "custom",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [],
|
|
10
|
+
"author": "",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"medium-zoom": "^1.1.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<template v-if="url">
|
|
4
|
+
<img
|
|
5
|
+
v-if="contentType && contentType.startsWith('image')"
|
|
6
|
+
:src="url"
|
|
7
|
+
class="rounded-md"
|
|
8
|
+
ref="img"
|
|
9
|
+
data-zoomable
|
|
10
|
+
@click.stop="zoom.open()"
|
|
11
|
+
/>
|
|
12
|
+
<video
|
|
13
|
+
v-else-if="contentType && contentType.startsWith('video')"
|
|
14
|
+
:src="url"
|
|
15
|
+
class="rounded-md"
|
|
16
|
+
controls
|
|
17
|
+
@click.stop >
|
|
18
|
+
</video>
|
|
19
|
+
|
|
20
|
+
<a v-else :href="url" target="_blank"
|
|
21
|
+
class="flex gap-1 items-center py-1 px-3 me-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-darkListTable dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
|
|
22
|
+
>
|
|
23
|
+
<!-- download file icon -->
|
|
24
|
+
<svg class="w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
25
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
26
|
+
</svg>
|
|
27
|
+
Download file
|
|
28
|
+
</a>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<style>
|
|
36
|
+
.medium-zoom-image {
|
|
37
|
+
z-index: 999999;
|
|
38
|
+
background: rgba(0, 0, 0, 0.8);
|
|
39
|
+
}
|
|
40
|
+
.medium-zoom-overlay {
|
|
41
|
+
background: rgba(255, 255, 255, 0.8) !important
|
|
42
|
+
}
|
|
43
|
+
body.medium-zoom--opened aside {
|
|
44
|
+
filter: grayscale(1)
|
|
45
|
+
}
|
|
46
|
+
</style>
|
|
47
|
+
|
|
48
|
+
<style scoped>
|
|
49
|
+
img {
|
|
50
|
+
min-width: 200px;
|
|
51
|
+
}
|
|
52
|
+
video {
|
|
53
|
+
min-width: 200px;
|
|
54
|
+
}
|
|
55
|
+
</style>
|
|
56
|
+
<script setup>
|
|
57
|
+
import { ref, computed , onMounted, watch} from 'vue'
|
|
58
|
+
import mediumZoom from 'medium-zoom'
|
|
59
|
+
|
|
60
|
+
const img = ref(null);
|
|
61
|
+
const zoom = ref(null);
|
|
62
|
+
|
|
63
|
+
const props = defineProps({
|
|
64
|
+
record: Object,
|
|
65
|
+
column: Object,
|
|
66
|
+
meta: Object,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const url = computed(() => {
|
|
70
|
+
return props.record[`previewUrl_${props.meta.pluginInstanceId}`];
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
// since we have no way to know the content type of the file, we will try to guess it from extension
|
|
75
|
+
// for better experience probably we should check whether user saves content type in the database and use it here
|
|
76
|
+
const contentType = computed(() => {
|
|
77
|
+
const u = new URL(url.value);
|
|
78
|
+
return guessContentType(u.pathname);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function guessContentType(url) {
|
|
82
|
+
if (!url) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const ext = url.split('.').pop();
|
|
86
|
+
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'tiff'].includes(ext)) {
|
|
87
|
+
return 'image';
|
|
88
|
+
}
|
|
89
|
+
if (['mp4', 'webm', 'ogg', 'avi', 'mov', 'flv', 'wmv', 'mkv'].includes(ext)) {
|
|
90
|
+
return 'video';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
onMounted(async () => {
|
|
96
|
+
|
|
97
|
+
if (contentType.value?.startsWith('image')) {
|
|
98
|
+
zoom.value = mediumZoom(img.value, {
|
|
99
|
+
margin: 24,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
</script>
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex items-center justify-center w-full">
|
|
3
|
+
<label for="dropzone-file" class="flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:hover:bg-gray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600">
|
|
4
|
+
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
|
5
|
+
<img v-if="imgPreview" :src="imgPreview" class="w-100 mt-4 rounded-lg h-40 object-contain" />
|
|
6
|
+
|
|
7
|
+
<svg v-else class="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
|
|
8
|
+
<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"/>
|
|
9
|
+
</svg>
|
|
10
|
+
|
|
11
|
+
<template v-if="!uploaded">
|
|
12
|
+
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400"><span class="font-semibold">Click to upload</span> or drag and drop</p>
|
|
13
|
+
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
14
|
+
{{ allowedExtensionsLabel }} {{ meta.maxFileSize ? `(up to ${maxFileSizeHumanized})` : '' }}
|
|
15
|
+
</p>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<div class="w-full bg-gray-200 rounded-full dark:bg-gray-700 mt-1 mb-2" v-if="progress > 0 && !uploaded">
|
|
19
|
+
<!-- progress bar with smooth progress animation -->
|
|
20
|
+
<div class="bg-blue-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full
|
|
21
|
+
transition-all duration-200 ease-in-out
|
|
22
|
+
"
|
|
23
|
+
:style="{width: `${progress}%`}">{{ progress }}%
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div v-else-if="uploaded" class="flex items-center justify-center w-full mt-1">
|
|
28
|
+
<svg class="w-4 h-4 text-green-600 dark:text-green-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
29
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
30
|
+
</svg>
|
|
31
|
+
<p class="ml-2 text-sm text-green-600 dark:text-green-400 flex items-center">
|
|
32
|
+
File uploaded
|
|
33
|
+
<span class="text-xs text-gray-500 dark:text-gray-400">{{ humanifySize(uploadedSize) }}</span>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<button @click.stop.prevent="clear" class="ml-2 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-500
|
|
37
|
+
hover:underline dark:hover:underline focus:outline-none">Clear</button>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
</div>
|
|
41
|
+
<input id="dropzone-file" type="file" class="hidden" @change="onFileChange" />
|
|
42
|
+
</label>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<script setup>
|
|
48
|
+
import { computed, ref, onMounted, watch } from 'vue'
|
|
49
|
+
import { callAdminForthApi } from '@/utils'
|
|
50
|
+
|
|
51
|
+
const props = defineProps({
|
|
52
|
+
meta: String,
|
|
53
|
+
record: Object,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const emit = defineEmits([
|
|
57
|
+
'update:value',
|
|
58
|
+
'update:inValidity',
|
|
59
|
+
'update:emptiness',
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const imgPreview = ref(null);
|
|
63
|
+
const progress = ref(0);
|
|
64
|
+
|
|
65
|
+
const uploaded = ref(false);
|
|
66
|
+
const uploadedSize = ref(0);
|
|
67
|
+
|
|
68
|
+
watch(() => uploaded, (value) => {
|
|
69
|
+
emit('update:emptiness', !value);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
onMounted(() => {
|
|
73
|
+
const previewColumnName = `previewUrl_${props.meta.pluginInstanceId}`;
|
|
74
|
+
if (props.record[previewColumnName]) {
|
|
75
|
+
imgPreview.value = props.record[previewColumnName];
|
|
76
|
+
uploaded.value = true;
|
|
77
|
+
emit('update:emptiness', false);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const allowedExtensionsLabel = computed(() => {
|
|
82
|
+
const allowedExtensions = props.meta.allowedExtensions || []
|
|
83
|
+
if (allowedExtensions.length === 0) {
|
|
84
|
+
return 'Any file type'
|
|
85
|
+
}
|
|
86
|
+
// make upper case and write in format EXT1, EXT2 or EXT3
|
|
87
|
+
let label = allowedExtensions.map(ext => ext.toUpperCase()).join(', ');
|
|
88
|
+
// last comma to 'or'
|
|
89
|
+
label = label.replace(/,([^,]*)$/, ' or$1')
|
|
90
|
+
return label
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
function clear() {
|
|
94
|
+
imgPreview.value = null;
|
|
95
|
+
progress.value = 0;
|
|
96
|
+
uploaded.value = false;
|
|
97
|
+
uploadedSize.value = 0;
|
|
98
|
+
emit('update:value', null);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function humanifySize(size) {
|
|
102
|
+
if (!size) {
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
106
|
+
let i = 0
|
|
107
|
+
while (size >= 1024 && i < units.length - 1) {
|
|
108
|
+
size /= 1024
|
|
109
|
+
i++
|
|
110
|
+
}
|
|
111
|
+
return `${size.toFixed(1)} ${units[i]}`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
const onFileChange = async (e) => {
|
|
116
|
+
imgPreview.value = null;
|
|
117
|
+
progress.value = 0;
|
|
118
|
+
uploaded.value = false;
|
|
119
|
+
|
|
120
|
+
const file = e.target.files[0]
|
|
121
|
+
|
|
122
|
+
// get filename, extension, size, mimeType
|
|
123
|
+
const { name, size, type } = file;
|
|
124
|
+
|
|
125
|
+
uploadedSize.value = size;
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
const extension = name.split('.').pop();
|
|
129
|
+
const nameNoExtension = name.replace(`.${extension}`, '');
|
|
130
|
+
console.log('File details:', { name, extension, size, type });
|
|
131
|
+
// validate file extension
|
|
132
|
+
const allowedExtensions = props.meta.allowedExtensions || []
|
|
133
|
+
if (allowedExtensions.length > 0 && !allowedExtensions.includes(extension)) {
|
|
134
|
+
window.adminforth.alert({
|
|
135
|
+
message: `Sorry but the file type ${extension} is not allowed. Please upload a file with one of the following extensions: ${allowedExtensionsLabel.value}`,
|
|
136
|
+
variant: 'danger'
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// validate file size
|
|
142
|
+
if (props.meta.maxFileSize && size > props.meta.maxFileSize) {
|
|
143
|
+
window.adminforth.alert({
|
|
144
|
+
message: `Sorry but the file size ${humanifySize(size)} is too large. Please upload a file with a maximum size of ${humanifySize(props.meta.maxFileSize)}`,
|
|
145
|
+
variant: 'danger'
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
emit('update:inValidity', 'Upload in progress...');
|
|
151
|
+
try {
|
|
152
|
+
// supports preview
|
|
153
|
+
if (type.startsWith('image/')) {
|
|
154
|
+
const reader = new FileReader();
|
|
155
|
+
reader.onload = (e) => {
|
|
156
|
+
imgPreview.value = e.target.result;
|
|
157
|
+
}
|
|
158
|
+
reader.readAsDataURL(file);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { uploadUrl, s3Path } = await callAdminForthApi({
|
|
162
|
+
path: `/plugin/${props.meta.pluginInstanceId}/get_s3_upload_url`,
|
|
163
|
+
method: 'POST',
|
|
164
|
+
body: {
|
|
165
|
+
originalFilename: nameNoExtension,
|
|
166
|
+
contentType: type,
|
|
167
|
+
size,
|
|
168
|
+
originalExtension: extension,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const xhr = new XMLHttpRequest();
|
|
173
|
+
const success = await new Promise((resolve) => {
|
|
174
|
+
xhr.upload.onprogress = (e) => {
|
|
175
|
+
if (e.lengthComputable) {
|
|
176
|
+
progress.value = Math.round((e.loaded / e.total) * 100);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
xhr.addEventListener('loadend', () => {
|
|
180
|
+
const success = xhr.readyState === 4 && xhr.status === 200;
|
|
181
|
+
// try to read response
|
|
182
|
+
resolve(success);
|
|
183
|
+
});
|
|
184
|
+
xhr.open('PUT', uploadUrl, true);
|
|
185
|
+
xhr.setRequestHeader('Content-Type', type);
|
|
186
|
+
xhr.setRequestHeader('x-amz-tagging', (new URL(uploadUrl)).searchParams.get('x-amz-tagging'));
|
|
187
|
+
xhr.send(file);
|
|
188
|
+
});
|
|
189
|
+
if (!success) {
|
|
190
|
+
window.adminforth.alert({
|
|
191
|
+
messageHtml: `<div>Sorry but the file was not be uploaded because of S3 Request Error: </div>
|
|
192
|
+
<pre style="white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; max-width: 100%;">${
|
|
193
|
+
xhr.responseText.replace(/</g, '<').replace(/>/g, '>')
|
|
194
|
+
}</pre>`,
|
|
195
|
+
variant: 'danger',
|
|
196
|
+
timeout: 30,
|
|
197
|
+
});
|
|
198
|
+
imgPreview.value = null;
|
|
199
|
+
uploaded.value = false;
|
|
200
|
+
progress.value = 0;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
uploaded.value = true;
|
|
204
|
+
emit('update:value', s3Path);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error('Error uploading file:', error);
|
|
207
|
+
window.adminforth.alert({
|
|
208
|
+
message: `Sorry but the file was not be uploaded. Please try again: ${error.message}`,
|
|
209
|
+
variant: 'danger'
|
|
210
|
+
});
|
|
211
|
+
imgPreview.value = null;
|
|
212
|
+
uploaded.value = false;
|
|
213
|
+
progress.value = 0;
|
|
214
|
+
} finally {
|
|
215
|
+
emit('update:inValidity', false);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
</script>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "custom",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "custom",
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"license": "ISC",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"medium-zoom": "^1.1.0"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"node_modules/medium-zoom": {
|
|
16
|
+
"version": "1.1.0",
|
|
17
|
+
"resolved": "https://registry.npmjs.org/medium-zoom/-/medium-zoom-1.1.0.tgz",
|
|
18
|
+
"integrity": "sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ=="
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "custom",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [],
|
|
10
|
+
"author": "",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"medium-zoom": "^1.1.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<template v-if="url">
|
|
4
|
+
<img
|
|
5
|
+
v-if="contentType && contentType.startsWith('image')"
|
|
6
|
+
:src="url"
|
|
7
|
+
class="rounded-md"
|
|
8
|
+
ref="img"
|
|
9
|
+
data-zoomable
|
|
10
|
+
@click.stop="zoom.open()"
|
|
11
|
+
/>
|
|
12
|
+
<video
|
|
13
|
+
v-else-if="contentType && contentType.startsWith('video')"
|
|
14
|
+
:src="url"
|
|
15
|
+
class="rounded-md"
|
|
16
|
+
controls
|
|
17
|
+
@click.stop >
|
|
18
|
+
</video>
|
|
19
|
+
|
|
20
|
+
<a v-else :href="url" target="_blank"
|
|
21
|
+
class="flex gap-1 items-center py-1 px-3 me-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-darkListTable dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
|
|
22
|
+
>
|
|
23
|
+
<!-- download file icon -->
|
|
24
|
+
<svg class="w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
25
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
26
|
+
</svg>
|
|
27
|
+
Download file
|
|
28
|
+
</a>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<style>
|
|
36
|
+
.medium-zoom-image {
|
|
37
|
+
z-index: 999999;
|
|
38
|
+
background: rgba(0, 0, 0, 0.8);
|
|
39
|
+
}
|
|
40
|
+
.medium-zoom-overlay {
|
|
41
|
+
background: rgba(255, 255, 255, 0.8) !important
|
|
42
|
+
}
|
|
43
|
+
body.medium-zoom--opened aside {
|
|
44
|
+
filter: grayscale(1)
|
|
45
|
+
}
|
|
46
|
+
</style>
|
|
47
|
+
|
|
48
|
+
<style scoped>
|
|
49
|
+
img {
|
|
50
|
+
min-width: 200px;
|
|
51
|
+
}
|
|
52
|
+
video {
|
|
53
|
+
min-width: 200px;
|
|
54
|
+
}
|
|
55
|
+
</style>
|
|
56
|
+
<script setup>
|
|
57
|
+
import { ref, computed , onMounted, watch} from 'vue'
|
|
58
|
+
import mediumZoom from 'medium-zoom'
|
|
59
|
+
|
|
60
|
+
const img = ref(null);
|
|
61
|
+
const zoom = ref(null);
|
|
62
|
+
|
|
63
|
+
const props = defineProps({
|
|
64
|
+
record: Object,
|
|
65
|
+
column: Object,
|
|
66
|
+
meta: Object,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const url = computed(() => {
|
|
70
|
+
return props.record[`previewUrl_${props.meta.pluginInstanceId}`];
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
// since we have no way to know the content type of the file, we will try to guess it from extension
|
|
75
|
+
// for better experience probably we should check whether user saves content type in the database and use it here
|
|
76
|
+
const contentType = computed(() => {
|
|
77
|
+
const u = new URL(url.value);
|
|
78
|
+
return guessContentType(u.pathname);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function guessContentType(url) {
|
|
82
|
+
if (!url) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const ext = url.split('.').pop();
|
|
86
|
+
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'tiff'].includes(ext)) {
|
|
87
|
+
return 'image';
|
|
88
|
+
}
|
|
89
|
+
if (['mp4', 'webm', 'ogg', 'avi', 'mov', 'flv', 'wmv', 'mkv'].includes(ext)) {
|
|
90
|
+
return 'video';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
onMounted(async () => {
|
|
96
|
+
|
|
97
|
+
if (contentType.value?.startsWith('image')) {
|
|
98
|
+
zoom.value = mediumZoom(img.value, {
|
|
99
|
+
margin: 24,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
</script>
|