sprintify-ui 0.11.27 → 0.11.29
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/sprintify-ui.es.js +4716 -4582
- package/dist/types/components/BaseFileUploader.vue.d.ts +2 -0
- package/dist/types/components/BaseMediaLibrary.vue.d.ts +9 -0
- package/dist/types/utils/index.d.ts +2 -1
- package/dist/types/utils/resizeImageForUpload.d.ts +7 -0
- package/package.json +1 -1
- package/src/components/BaseFileUploader.vue +77 -3
- package/src/components/BaseMediaLibrary.vue +5 -0
- package/src/components/BaseModalSide.vue +1 -1
- package/src/utils/index.ts +2 -0
- package/src/utils/resizeImageForUpload.ts +181 -0
|
@@ -7,6 +7,7 @@ type __VLS_Props = {
|
|
|
7
7
|
beforeUpload?: () => boolean;
|
|
8
8
|
twButton?: string;
|
|
9
9
|
maxSize?: number;
|
|
10
|
+
maxImageSizeBeforeResize?: number;
|
|
10
11
|
accept?: string;
|
|
11
12
|
acceptedExtensions?: string[];
|
|
12
13
|
cropper?: BaseCropperConfig | Record<string, any> | boolean | null;
|
|
@@ -64,6 +65,7 @@ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}
|
|
|
64
65
|
accept: string;
|
|
65
66
|
acceptedExtensions: string[];
|
|
66
67
|
beforeUpload: () => boolean;
|
|
68
|
+
maxImageSizeBeforeResize: number;
|
|
67
69
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
68
70
|
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
69
71
|
export default _default;
|
|
@@ -52,6 +52,10 @@ declare const __VLS_component: import("vue").DefineComponent<import("vue").Extra
|
|
|
52
52
|
default: number;
|
|
53
53
|
type: NumberConstructor;
|
|
54
54
|
};
|
|
55
|
+
maxImageSizeBeforeResize: {
|
|
56
|
+
default: undefined;
|
|
57
|
+
type: NumberConstructor;
|
|
58
|
+
};
|
|
55
59
|
accept: {
|
|
56
60
|
default: undefined;
|
|
57
61
|
type: StringConstructor;
|
|
@@ -127,6 +131,10 @@ declare const __VLS_component: import("vue").DefineComponent<import("vue").Extra
|
|
|
127
131
|
default: number;
|
|
128
132
|
type: NumberConstructor;
|
|
129
133
|
};
|
|
134
|
+
maxImageSizeBeforeResize: {
|
|
135
|
+
default: undefined;
|
|
136
|
+
type: NumberConstructor;
|
|
137
|
+
};
|
|
130
138
|
accept: {
|
|
131
139
|
default: undefined;
|
|
132
140
|
type: StringConstructor;
|
|
@@ -195,6 +203,7 @@ declare const __VLS_component: import("vue").DefineComponent<import("vue").Extra
|
|
|
195
203
|
maxSize: number;
|
|
196
204
|
accept: string;
|
|
197
205
|
acceptedExtensions: string[];
|
|
206
|
+
maxImageSizeBeforeResize: number;
|
|
198
207
|
currentMedia: Media[];
|
|
199
208
|
uploadUrl: string;
|
|
200
209
|
pickerComponent: "BaseFilePicker" | "BaseFilePickerCrop";
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import toHumanList from './toHumanList';
|
|
2
2
|
import fileSizeFormat from './fileSizeFormat';
|
|
3
3
|
import resizeImageFromURI from './resizeImageFromURI';
|
|
4
|
+
import resizeImageForUpload from './resizeImageForUpload';
|
|
4
5
|
import { disableScroll, enableScroll } from './scrollPreventer';
|
|
5
6
|
import { blobToBase64, validateBase64, base64ToBlob } from './blob';
|
|
6
7
|
import { getColorConfig } from './colors';
|
|
7
8
|
import { getInitials, getAvatarColor } from './avatar';
|
|
8
|
-
export { toHumanList, fileSizeFormat, disableScroll, enableScroll, resizeImageFromURI, blobToBase64, validateBase64, base64ToBlob, getColorConfig, getInitials, getAvatarColor, };
|
|
9
|
+
export { toHumanList, fileSizeFormat, disableScroll, enableScroll, resizeImageFromURI, resizeImageForUpload, blobToBase64, validateBase64, base64ToBlob, getColorConfig, getInitials, getAvatarColor, };
|
package/package.json
CHANGED
|
@@ -35,6 +35,7 @@ import { UploadedFile } from '@/types/UploadedFile';
|
|
|
35
35
|
import { useSnackbarsStore } from '@/stores/snackbars';
|
|
36
36
|
import BaseLoadingCover from '@/components/BaseLoadingCover.vue';
|
|
37
37
|
import { BaseCropperConfig } from '@/types';
|
|
38
|
+
import { resizeImageForUpload } from '@/utils';
|
|
38
39
|
import BaseFilePicker from './BaseFilePicker.vue';
|
|
39
40
|
import BaseFilePickerCrop from './BaseFilePickerCrop.vue';
|
|
40
41
|
import { t } from '@/i18n';
|
|
@@ -51,6 +52,7 @@ const props = withDefaults(
|
|
|
51
52
|
beforeUpload?: () => boolean;
|
|
52
53
|
twButton?: string;
|
|
53
54
|
maxSize?: number;
|
|
55
|
+
maxImageSizeBeforeResize?: number;
|
|
54
56
|
accept?: string;
|
|
55
57
|
acceptedExtensions?: string[];
|
|
56
58
|
cropper?: BaseCropperConfig | Record<string, any> | boolean | null;
|
|
@@ -66,6 +68,7 @@ const props = withDefaults(
|
|
|
66
68
|
},
|
|
67
69
|
twButton: '',
|
|
68
70
|
maxSize: undefined,
|
|
71
|
+
maxImageSizeBeforeResize: undefined,
|
|
69
72
|
accept: undefined,
|
|
70
73
|
acceptedExtensions: undefined,
|
|
71
74
|
cropper: true,
|
|
@@ -149,15 +152,16 @@ async function onFileSelect(files: File | File[]) {
|
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
async function processFileUpload(file: File): Promise<UploadedFile> {
|
|
155
|
+
const preparedFile = await processFileBeforeUpload(file);
|
|
152
156
|
|
|
153
157
|
const formData = new FormData();
|
|
154
158
|
|
|
155
|
-
formData.append('file',
|
|
159
|
+
formData.append('file', preparedFile);
|
|
156
160
|
|
|
157
161
|
const response = await http.post(props.url ?? config.upload_url, formData);
|
|
158
162
|
|
|
159
163
|
const payload = response.data as UploadedFile;
|
|
160
|
-
payload.original_file =
|
|
164
|
+
payload.original_file = preparedFile;
|
|
161
165
|
|
|
162
166
|
// Read file if image, add add data_url to payload
|
|
163
167
|
|
|
@@ -175,11 +179,81 @@ async function processFileUpload(file: File): Promise<UploadedFile> {
|
|
|
175
179
|
};
|
|
176
180
|
|
|
177
181
|
if (payload.mime_type.includes('image')) {
|
|
178
|
-
reader.readAsDataURL(
|
|
182
|
+
reader.readAsDataURL(preparedFile);
|
|
179
183
|
} else {
|
|
180
184
|
resolve(payload);
|
|
181
185
|
}
|
|
182
186
|
});
|
|
183
187
|
}
|
|
184
188
|
|
|
189
|
+
async function processFileBeforeUpload(file: File): Promise<File> {
|
|
190
|
+
if (!shouldAutoResize(file)) {
|
|
191
|
+
return file;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return await autoResizeImage(file);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function shouldAutoResize(file: File): boolean {
|
|
198
|
+
if (!props.maxImageSizeBeforeResize || props.maxImageSizeBeforeResize <= 0) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!file.type.includes('image')) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (['image/gif', 'image/svg+xml'].includes(file.type)) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (file.size <= props.maxImageSizeBeforeResize) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function autoResizeImage(file: File): Promise<File> {
|
|
218
|
+
if (!props.maxImageSizeBeforeResize) {
|
|
219
|
+
return file;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const resizedBlob = await resizeImageForUpload(file, {
|
|
224
|
+
maxBytes: props.maxImageSizeBeforeResize,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (resizedBlob.size >= file.size) {
|
|
228
|
+
return file;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const mimeType = resizedBlob.type || file.type;
|
|
232
|
+
|
|
233
|
+
return new File(
|
|
234
|
+
[resizedBlob],
|
|
235
|
+
getResizedFileName(file.name, mimeType),
|
|
236
|
+
{
|
|
237
|
+
type: mimeType,
|
|
238
|
+
lastModified: file.lastModified,
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error(error);
|
|
243
|
+
return file;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function getResizedFileName(filename: string, mimeType: string): string {
|
|
248
|
+
if (mimeType != 'image/jpeg') {
|
|
249
|
+
return filename;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (filename.includes('.')) {
|
|
253
|
+
return filename.replace(/\.[^.]+$/, '.jpg');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return `${filename}.jpg`;
|
|
257
|
+
}
|
|
258
|
+
|
|
185
259
|
</script>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<BaseFileUploader
|
|
4
4
|
:component="pickerComponent"
|
|
5
5
|
:max-size="maxSize"
|
|
6
|
+
:max-image-size-before-resize="maxImageSizeBeforeResize"
|
|
6
7
|
:disabled="disabledInternal"
|
|
7
8
|
class="w-full"
|
|
8
9
|
tw-button="w-full"
|
|
@@ -142,6 +143,10 @@ const props = defineProps({
|
|
|
142
143
|
default: 20 * 1024 * 1024,
|
|
143
144
|
type: Number,
|
|
144
145
|
},
|
|
146
|
+
maxImageSizeBeforeResize: {
|
|
147
|
+
default: undefined,
|
|
148
|
+
type: Number,
|
|
149
|
+
},
|
|
145
150
|
accept: {
|
|
146
151
|
default: undefined,
|
|
147
152
|
type: String,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
class="fixed inset-0 z-modal w-full overflow-y-auto overflow-x-hidden"
|
|
12
12
|
>
|
|
13
13
|
<div class="flex min-h-full w-full pt-20 sm:pt-0">
|
|
14
|
-
<div class="min-h-full grow">
|
|
14
|
+
<div class="min-h-full overflow-hidden grow">
|
|
15
15
|
<transition
|
|
16
16
|
appear
|
|
17
17
|
enter-active-class="duration-200 ease-out"
|
package/src/utils/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import toHumanList from './toHumanList';
|
|
2
2
|
import fileSizeFormat from './fileSizeFormat';
|
|
3
3
|
import resizeImageFromURI from './resizeImageFromURI';
|
|
4
|
+
import resizeImageForUpload from './resizeImageForUpload';
|
|
4
5
|
import { disableScroll, enableScroll } from './scrollPreventer';
|
|
5
6
|
import { blobToBase64, validateBase64, base64ToBlob } from './blob';
|
|
6
7
|
import { getColorConfig } from './colors';
|
|
@@ -12,6 +13,7 @@ export {
|
|
|
12
13
|
disableScroll,
|
|
13
14
|
enableScroll,
|
|
14
15
|
resizeImageFromURI,
|
|
16
|
+
resizeImageForUpload,
|
|
15
17
|
blobToBase64,
|
|
16
18
|
validateBase64,
|
|
17
19
|
base64ToBlob,
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
type ResizeImageForUploadOptions = {
|
|
2
|
+
maxBytes: number;
|
|
3
|
+
outputMimeType?: string;
|
|
4
|
+
quality?: number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type DecodedImage = {
|
|
8
|
+
source: CanvasImageSource;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
cleanup: () => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const DEFAULT_OUTPUT_QUALITY = 0.9;
|
|
15
|
+
|
|
16
|
+
export default async function resizeImageForUpload(
|
|
17
|
+
file: Blob,
|
|
18
|
+
options: ResizeImageForUploadOptions
|
|
19
|
+
): Promise<Blob> {
|
|
20
|
+
if (options.maxBytes <= 0 || file.size <= options.maxBytes) {
|
|
21
|
+
return file;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const decodedImage = await decodeImage(file);
|
|
25
|
+
const outputMimeType =
|
|
26
|
+
options.outputMimeType ?? getOutputMimeType((file as File).type);
|
|
27
|
+
const quality = clamp(options.quality ?? DEFAULT_OUTPUT_QUALITY, 0.1, 1);
|
|
28
|
+
|
|
29
|
+
let canvas: HTMLCanvasElement | null = null;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const resizeRatio = Math.min(1, Math.sqrt(options.maxBytes / file.size));
|
|
33
|
+
const targetWidth = Math.max(1, Math.floor(decodedImage.width * resizeRatio));
|
|
34
|
+
const targetHeight = Math.max(
|
|
35
|
+
1,
|
|
36
|
+
Math.floor(decodedImage.height * resizeRatio)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
canvas = createScaledCanvas(
|
|
40
|
+
decodedImage.source,
|
|
41
|
+
decodedImage.width,
|
|
42
|
+
decodedImage.height,
|
|
43
|
+
targetWidth,
|
|
44
|
+
targetHeight
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (!supportsQualityEncoding(outputMimeType)) {
|
|
48
|
+
return await canvasToBlob(canvas, outputMimeType);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return await canvasToBlob(canvas, outputMimeType, quality);
|
|
52
|
+
} finally {
|
|
53
|
+
decodedImage.cleanup();
|
|
54
|
+
|
|
55
|
+
if (canvas) {
|
|
56
|
+
releaseCanvas(canvas);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getOutputMimeType(inputMimeType: string): string {
|
|
62
|
+
if (inputMimeType === 'image/webp') {
|
|
63
|
+
return 'image/webp';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return 'image/jpeg';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function decodeImage(file: Blob): Promise<DecodedImage> {
|
|
70
|
+
if (typeof createImageBitmap === 'function') {
|
|
71
|
+
try {
|
|
72
|
+
const bitmap = await createImageBitmap(file);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
source: bitmap,
|
|
76
|
+
width: bitmap.width,
|
|
77
|
+
height: bitmap.height,
|
|
78
|
+
cleanup: () => bitmap.close(),
|
|
79
|
+
};
|
|
80
|
+
} catch {
|
|
81
|
+
// Fallback to Image decode when createImageBitmap is not supported.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const objectUrl = URL.createObjectURL(file);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const image = await loadImage(objectUrl);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
source: image,
|
|
92
|
+
width: image.naturalWidth || image.width,
|
|
93
|
+
height: image.naturalHeight || image.height,
|
|
94
|
+
cleanup: () => URL.revokeObjectURL(objectUrl),
|
|
95
|
+
};
|
|
96
|
+
} catch (error) {
|
|
97
|
+
URL.revokeObjectURL(objectUrl);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function loadImage(src: string): Promise<HTMLImageElement> {
|
|
103
|
+
const image = new Image();
|
|
104
|
+
image.decoding = 'async';
|
|
105
|
+
image.src = src;
|
|
106
|
+
|
|
107
|
+
if (typeof image.decode === 'function') {
|
|
108
|
+
try {
|
|
109
|
+
await image.decode();
|
|
110
|
+
return image;
|
|
111
|
+
} catch {
|
|
112
|
+
// decode() may reject if called before enough data is available.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return await new Promise((resolve, reject) => {
|
|
117
|
+
image.onload = () => resolve(image);
|
|
118
|
+
image.onerror = () => reject(new Error('Failed to decode image'));
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function createScaledCanvas(
|
|
123
|
+
source: CanvasImageSource,
|
|
124
|
+
sourceWidth: number,
|
|
125
|
+
sourceHeight: number,
|
|
126
|
+
width: number,
|
|
127
|
+
height: number
|
|
128
|
+
): HTMLCanvasElement {
|
|
129
|
+
const canvas = document.createElement('canvas');
|
|
130
|
+
canvas.width = width;
|
|
131
|
+
canvas.height = height;
|
|
132
|
+
|
|
133
|
+
const context = canvas.getContext('2d', {
|
|
134
|
+
alpha: false,
|
|
135
|
+
desynchronized: true,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (!context) {
|
|
139
|
+
throw new Error('Could not acquire context 2d');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
context.imageSmoothingEnabled = true;
|
|
143
|
+
context.imageSmoothingQuality = 'high';
|
|
144
|
+
context.drawImage(source, 0, 0, sourceWidth, sourceHeight, 0, 0, width, height);
|
|
145
|
+
|
|
146
|
+
return canvas;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function supportsQualityEncoding(mimeType: string): boolean {
|
|
150
|
+
return mimeType === 'image/jpeg' || mimeType === 'image/webp';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function canvasToBlob(
|
|
154
|
+
canvas: HTMLCanvasElement,
|
|
155
|
+
mimeType: string,
|
|
156
|
+
quality?: number
|
|
157
|
+
): Promise<Blob> {
|
|
158
|
+
return await new Promise((resolve, reject) => {
|
|
159
|
+
canvas.toBlob(
|
|
160
|
+
(blob) => {
|
|
161
|
+
if (blob) {
|
|
162
|
+
resolve(blob);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
reject(new Error('Could not create resized image blob'));
|
|
167
|
+
},
|
|
168
|
+
mimeType,
|
|
169
|
+
quality
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function releaseCanvas(canvas: HTMLCanvasElement) {
|
|
175
|
+
canvas.width = 0;
|
|
176
|
+
canvas.height = 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function clamp(value: number, min: number, max: number): number {
|
|
180
|
+
return Math.min(max, Math.max(min, value));
|
|
181
|
+
}
|