sprintify-ui 0.0.183 → 0.0.184
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 +16888 -14992
- package/dist/style.css +1 -1
- package/dist/types/src/components/BaseCropper.vue.d.ts +57 -0
- package/dist/types/src/components/BaseCropperModal.vue.d.ts +27 -0
- package/dist/types/src/components/BaseDisplayRelativeTime.vue.d.ts +3 -3
- package/dist/types/src/components/BaseFilePicker.vue.d.ts +52 -37
- package/dist/types/src/components/BaseFilePickerCrop.vue.d.ts +57 -0
- package/dist/types/src/components/BaseFileUploader.vue.d.ts +65 -81
- package/dist/types/src/components/BaseMediaLibrary.vue.d.ts +20 -10
- package/dist/types/src/components/BaseTableColumn.vue.d.ts +1 -1
- package/dist/types/src/components/index.d.ts +4 -1
- package/dist/types/src/index.d.ts +24 -4
- package/dist/types/src/svg/BaseEmptyState.vue.d.ts +1 -1
- package/dist/types/src/types/ImagePickerResult.d.ts +5 -0
- package/dist/types/src/types/index.d.ts +28 -0
- package/dist/types/src/utils/blob.d.ts +3 -0
- package/dist/types/src/utils/cropper/avatar.d.ts +5 -0
- package/dist/types/src/utils/cropper/cover.d.ts +5 -0
- package/dist/types/src/utils/cropper/presetInterface.d.ts +7 -0
- package/dist/types/src/utils/cropper/presets.d.ts +6 -0
- package/dist/types/src/utils/fileValidations.d.ts +2 -0
- package/dist/types/src/utils/index.d.ts +3 -1
- package/dist/types/src/utils/resizeImageFromURI.d.ts +1 -0
- package/package.json +35 -32
- package/src/components/BaseCropper.stories.js +113 -0
- package/src/components/BaseCropper.vue +451 -0
- package/src/components/BaseCropperModal.stories.js +54 -0
- package/src/components/BaseCropperModal.vue +139 -0
- package/src/components/BaseFilePicker.stories.js +30 -3
- package/src/components/BaseFilePicker.vue +107 -75
- package/src/components/BaseFilePickerCrop.stories.js +134 -0
- package/src/components/BaseFilePickerCrop.vue +116 -0
- package/src/components/BaseFileUploader.stories.js +11 -7
- package/src/components/BaseFileUploader.vue +57 -86
- package/src/components/BaseMediaLibrary.stories.js +24 -5
- package/src/components/BaseMediaLibrary.vue +17 -2
- package/src/components/index.ts +6 -0
- package/src/lang/en.json +6 -1
- package/src/lang/fr.json +6 -1
- package/src/types/ImagePickerResult.ts +5 -0
- package/src/types/index.ts +31 -0
- package/src/utils/blob.ts +30 -0
- package/src/utils/cropper/avatar.ts +33 -0
- package/src/utils/cropper/cover.ts +41 -0
- package/src/utils/cropper/presetInterface.ts +16 -0
- package/src/utils/cropper/presets.ts +7 -0
- package/src/utils/fileValidations.ts +26 -0
- package/src/utils/index.ts +12 -1
- package/src/utils/resizeImageFromURI.ts +118 -0
|
@@ -1,86 +1,75 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="relative">
|
|
3
|
-
<
|
|
4
|
-
:
|
|
5
|
-
|
|
6
|
-
:accept="accept"
|
|
3
|
+
<component
|
|
4
|
+
:is="componentInternal"
|
|
5
|
+
v-bind="pickerProps"
|
|
7
6
|
@select="onFileSelect"
|
|
8
7
|
>
|
|
9
8
|
<template #default="slotProps">
|
|
10
9
|
<slot
|
|
11
10
|
name="default"
|
|
12
11
|
:uploading="uploading"
|
|
13
|
-
:
|
|
14
|
-
|
|
15
|
-
:disabled="slotProps.disabled"
|
|
12
|
+
:loading="loading"
|
|
13
|
+
v-bind="slotProps"
|
|
16
14
|
/>
|
|
17
15
|
<slot
|
|
18
16
|
name="loading"
|
|
19
17
|
:uploading="uploading"
|
|
20
|
-
:
|
|
21
|
-
|
|
22
|
-
:disabled="slotProps.disabled"
|
|
18
|
+
:loading="loading"
|
|
19
|
+
v-bind="slotProps"
|
|
23
20
|
>
|
|
24
21
|
<BaseLoadingCover
|
|
25
22
|
:delay="0"
|
|
26
|
-
icon-class="
|
|
23
|
+
icon-class="w-6 h-6 text-primary-600"
|
|
27
24
|
:model-value="loading || uploading || slotProps.selecting"
|
|
28
25
|
/>
|
|
29
26
|
</slot>
|
|
30
27
|
</template>
|
|
31
|
-
</
|
|
28
|
+
</component>
|
|
32
29
|
</div>
|
|
33
30
|
</template>
|
|
34
31
|
|
|
35
32
|
<script lang="ts" setup>
|
|
36
33
|
import { config } from '@/index';
|
|
37
|
-
import { PropType } from 'vue';
|
|
38
34
|
import { UploadedFile } from '@/types/UploadedFile';
|
|
39
|
-
import { toHumanList, fileSizeFormat } from '@/utils';
|
|
40
35
|
import { useNotificationsStore } from '@/stores/notifications';
|
|
41
36
|
import BaseLoadingCover from '@/components/BaseLoadingCover.vue';
|
|
37
|
+
import { BaseCropperConfig } from '@/types';
|
|
42
38
|
import BaseFilePicker from './BaseFilePicker.vue';
|
|
39
|
+
import BaseFilePickerCrop from './BaseFilePickerCrop.vue';
|
|
43
40
|
|
|
44
41
|
const http = config.http;
|
|
45
42
|
const i18n = useI18n();
|
|
46
43
|
const notifications = useNotificationsStore();
|
|
47
44
|
|
|
48
|
-
const props =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
const props = withDefaults(
|
|
46
|
+
defineProps<{
|
|
47
|
+
component?: 'BaseFilePicker' | 'BaseFilePickerCrop';
|
|
48
|
+
url?: string;
|
|
49
|
+
disabled?: boolean;
|
|
50
|
+
loading?: boolean;
|
|
51
|
+
beforeUpload?: () => boolean;
|
|
52
|
+
buttonClass?: string;
|
|
53
|
+
maxSize?: number;
|
|
54
|
+
accept?: string;
|
|
55
|
+
acceptedExtensions?: string[];
|
|
56
|
+
cropper?: BaseCropperConfig | boolean | null;
|
|
57
|
+
}>(),
|
|
58
|
+
{
|
|
59
|
+
component: 'BaseFilePicker',
|
|
60
|
+
url: undefined,
|
|
61
|
+
disabled: false,
|
|
62
|
+
loading: false,
|
|
63
|
+
beforeUpload: (): boolean => {
|
|
63
64
|
return true;
|
|
64
65
|
},
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
default: 1024 * 1024 * 20, // 20 MB,
|
|
73
|
-
type: Number,
|
|
74
|
-
},
|
|
75
|
-
accept: {
|
|
76
|
-
default: undefined,
|
|
77
|
-
type: String,
|
|
78
|
-
},
|
|
79
|
-
acceptedExtensions: {
|
|
80
|
-
default: undefined,
|
|
81
|
-
type: Array as PropType<string[]>,
|
|
82
|
-
},
|
|
83
|
-
});
|
|
66
|
+
buttonClass: '',
|
|
67
|
+
maxSize: undefined,
|
|
68
|
+
accept: undefined,
|
|
69
|
+
acceptedExtensions: undefined,
|
|
70
|
+
cropper: true,
|
|
71
|
+
}
|
|
72
|
+
);
|
|
84
73
|
|
|
85
74
|
const emit = defineEmits([
|
|
86
75
|
'upload:start',
|
|
@@ -89,6 +78,25 @@ const emit = defineEmits([
|
|
|
89
78
|
'upload:end',
|
|
90
79
|
]);
|
|
91
80
|
|
|
81
|
+
const componentInternal = computed(() => {
|
|
82
|
+
if (props.component == 'BaseFilePickerCrop') {
|
|
83
|
+
return BaseFilePickerCrop;
|
|
84
|
+
}
|
|
85
|
+
return BaseFilePicker;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const pickerProps = computed(() => {
|
|
89
|
+
return {
|
|
90
|
+
disabled: props.disabled || props.loading || uploading.value,
|
|
91
|
+
buttonClass: props.buttonClass,
|
|
92
|
+
maxSize: props.maxSize,
|
|
93
|
+
accept: props.component == 'BaseFilePickerCrop' ? undefined : props.accept,
|
|
94
|
+
acceptedExtensions: props.acceptedExtensions,
|
|
95
|
+
cropper:
|
|
96
|
+
props.component == 'BaseFilePickerCrop' ? props.cropper : undefined,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
|
|
92
100
|
const uploading = ref(false);
|
|
93
101
|
|
|
94
102
|
async function onFileSelect(file: File) {
|
|
@@ -104,44 +112,6 @@ async function onFileSelect(file: File) {
|
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
try {
|
|
107
|
-
if (file.size > props.maxSize) {
|
|
108
|
-
notifications.push({
|
|
109
|
-
color: 'danger',
|
|
110
|
-
title: i18n.t('sui.error'),
|
|
111
|
-
text: i18n.t('sui.the_file_size_must_not_exceed_x', {
|
|
112
|
-
x: fileSizeFormat(props.maxSize),
|
|
113
|
-
}),
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
uploading.value = false;
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const extension = file.name.split('.').pop();
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
extension &&
|
|
124
|
-
props.acceptedExtensions &&
|
|
125
|
-
props.acceptedExtensions.length
|
|
126
|
-
) {
|
|
127
|
-
if (!props.acceptedExtensions.includes(extension)) {
|
|
128
|
-
notifications.push({
|
|
129
|
-
color: 'danger',
|
|
130
|
-
title: i18n.t('sui.error'),
|
|
131
|
-
text:
|
|
132
|
-
i18n.t('sui.the_file_type_is_invalid') +
|
|
133
|
-
' ' +
|
|
134
|
-
i18n.t('sui.file_must_be_of_type') +
|
|
135
|
-
' ' +
|
|
136
|
-
toHumanList(props.acceptedExtensions, i18n.t('sui.or')) +
|
|
137
|
-
'.',
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
uploading.value = false;
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
115
|
const formData = new FormData();
|
|
146
116
|
|
|
147
117
|
formData.append('file', file);
|
|
@@ -170,6 +140,7 @@ async function onFileSelect(file: File) {
|
|
|
170
140
|
onSuccess(payload);
|
|
171
141
|
}
|
|
172
142
|
} catch (e: unknown) {
|
|
143
|
+
console.error(e);
|
|
173
144
|
emit('upload:fail');
|
|
174
145
|
notifications.push({
|
|
175
146
|
color: 'danger',
|
|
@@ -16,9 +16,8 @@ export default {
|
|
|
16
16
|
args: {
|
|
17
17
|
max: null,
|
|
18
18
|
min: 2,
|
|
19
|
-
acceptedExtensions: ['jpg', 'png'],
|
|
20
|
-
uploadUrl: 'https://faker.witify.io/upload',
|
|
21
|
-
maxSize: 500 * 1024,
|
|
19
|
+
acceptedExtensions: ['jpg', 'jpeg', 'png', 'webp'],
|
|
20
|
+
uploadUrl: 'https://faker.witify.io/api/todos/upload',
|
|
22
21
|
currentMedia: [
|
|
23
22
|
mediaModel,
|
|
24
23
|
{
|
|
@@ -59,10 +58,11 @@ const Template = (args) => ({
|
|
|
59
58
|
BaseMediaLibrary,
|
|
60
59
|
},
|
|
61
60
|
setup() {
|
|
62
|
-
|
|
61
|
+
const value = ref(null);
|
|
62
|
+
return { args, value };
|
|
63
63
|
},
|
|
64
64
|
template: `
|
|
65
|
-
<BaseMediaLibrary v-bind="args" />
|
|
65
|
+
<BaseMediaLibrary v-model="value" v-bind="args" />
|
|
66
66
|
<BaseApp></BaseApp>
|
|
67
67
|
`,
|
|
68
68
|
});
|
|
@@ -70,6 +70,25 @@ const Template = (args) => ({
|
|
|
70
70
|
export const Demo = Template.bind({});
|
|
71
71
|
Demo.args = {};
|
|
72
72
|
|
|
73
|
+
export const Crop = Template.bind({});
|
|
74
|
+
Crop.args = {
|
|
75
|
+
pickerComponent: 'BaseFilePickerCrop',
|
|
76
|
+
cropper: {
|
|
77
|
+
config: {
|
|
78
|
+
maxWidth: 300,
|
|
79
|
+
},
|
|
80
|
+
preset: 'avatar',
|
|
81
|
+
presetOptions: {
|
|
82
|
+
size: 300,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const MaxSize = Template.bind({});
|
|
88
|
+
MaxSize.args = {
|
|
89
|
+
maxSize: 10 * 1024,
|
|
90
|
+
};
|
|
91
|
+
|
|
73
92
|
export const Disabled = Template.bind({});
|
|
74
93
|
Disabled.args = {
|
|
75
94
|
disabled: true,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
3
|
<BaseFileUploader
|
|
4
|
+
:component="pickerComponent"
|
|
4
5
|
:max-size="maxSize"
|
|
5
6
|
:disabled="disabled"
|
|
6
7
|
class="w-full"
|
|
@@ -8,6 +9,7 @@
|
|
|
8
9
|
:accept="accept"
|
|
9
10
|
:accepted-extensions="acceptedExtensions"
|
|
10
11
|
:url="uploadUrl"
|
|
12
|
+
:cropper="pickerComponent == 'BaseFilePickerCrop' ? cropper : undefined"
|
|
11
13
|
@upload:start="onUploadStart"
|
|
12
14
|
@upload:end="onUploadEnd"
|
|
13
15
|
@upload:fail="$emit('upload:fail', $event)"
|
|
@@ -92,13 +94,14 @@ import { PropType } from 'vue';
|
|
|
92
94
|
import { cloneDeep, isArray, isObject, capitalize } from 'lodash';
|
|
93
95
|
import { UploadedFile } from '@/types/UploadedFile';
|
|
94
96
|
import { Media } from '@/types/Media';
|
|
95
|
-
import { MediaLibraryPayload } from '@/types';
|
|
97
|
+
import { BaseCropperConfig, MediaLibraryPayload } from '@/types';
|
|
96
98
|
import { fileSizeFormat } from '@/utils';
|
|
97
99
|
import { useDialogsStore } from '@/stores/dialogs';
|
|
98
100
|
import { useNotificationsStore } from '@/stores/notifications';
|
|
99
101
|
import BaseMediaItem from '@/components/BaseMediaItem.vue';
|
|
100
102
|
import BaseFileUploader from './BaseFileUploader.vue';
|
|
101
103
|
import { useField } from '@/composables/field';
|
|
104
|
+
import { BaseIcon } from '.';
|
|
102
105
|
|
|
103
106
|
const i18n = useI18n();
|
|
104
107
|
|
|
@@ -152,6 +155,14 @@ const props = defineProps({
|
|
|
152
155
|
default: false,
|
|
153
156
|
type: Boolean,
|
|
154
157
|
},
|
|
158
|
+
pickerComponent: {
|
|
159
|
+
default: 'BaseFilePicker',
|
|
160
|
+
type: String as PropType<'BaseFilePicker' | 'BaseFilePickerCrop'>,
|
|
161
|
+
},
|
|
162
|
+
cropper: {
|
|
163
|
+
default: undefined,
|
|
164
|
+
type: Object as PropType<BaseCropperConfig | boolean | null>,
|
|
165
|
+
},
|
|
155
166
|
});
|
|
156
167
|
|
|
157
168
|
const emit = defineEmits([
|
|
@@ -211,7 +222,11 @@ function onUploadSuccess(file: UploadedFile) {
|
|
|
211
222
|
return;
|
|
212
223
|
}
|
|
213
224
|
|
|
214
|
-
if (
|
|
225
|
+
if (
|
|
226
|
+
normalizedMax.value &&
|
|
227
|
+
numberOfFiles.value >= normalizedMax.value &&
|
|
228
|
+
normalizedMax.value > 1
|
|
229
|
+
) {
|
|
215
230
|
notifications.push({
|
|
216
231
|
title: i18n.t('sui.whoops'),
|
|
217
232
|
text: i18n.t('sui.you_can_upload_up_to_n_files', {
|
package/src/components/index.ts
CHANGED
|
@@ -22,6 +22,8 @@ import BaseClipboard from './BaseClipboard.vue';
|
|
|
22
22
|
import BaseColor from './BaseColor.vue';
|
|
23
23
|
import BaseContainer from './BaseContainer.vue';
|
|
24
24
|
import BaseCounter from './BaseCounter.vue';
|
|
25
|
+
import BaseCropper from './BaseCropper.vue';
|
|
26
|
+
import BaseCropperModal from './BaseCropperModal.vue';
|
|
25
27
|
import BaseDataIterator from './BaseDataIterator.vue';
|
|
26
28
|
import BaseDataTable from './BaseDataTable.vue';
|
|
27
29
|
import BaseDatePicker from './BaseDatePicker.vue';
|
|
@@ -36,6 +38,7 @@ import BaseEmptyState from '@/svg/BaseEmptyState.vue';
|
|
|
36
38
|
import BaseField from './BaseField.vue';
|
|
37
39
|
import BaseFieldI18n from './BaseFieldI18n.vue';
|
|
38
40
|
import BaseFilePicker from './BaseFilePicker.vue';
|
|
41
|
+
import BaseFilePickerCrop from './BaseFilePickerCrop.vue';
|
|
39
42
|
import BaseFileUploader from './BaseFileUploader.vue';
|
|
40
43
|
import BaseForm from './BaseForm.vue';
|
|
41
44
|
import BaseHasMany from './BaseHasMany.vue';
|
|
@@ -112,6 +115,8 @@ export {
|
|
|
112
115
|
BaseColor,
|
|
113
116
|
BaseContainer,
|
|
114
117
|
BaseCounter,
|
|
118
|
+
BaseCropper,
|
|
119
|
+
BaseCropperModal,
|
|
115
120
|
BaseDataIterator,
|
|
116
121
|
BaseDataTable,
|
|
117
122
|
BaseDatePicker,
|
|
@@ -126,6 +131,7 @@ export {
|
|
|
126
131
|
BaseField,
|
|
127
132
|
BaseFieldI18n,
|
|
128
133
|
BaseFilePicker,
|
|
134
|
+
BaseFilePickerCrop,
|
|
129
135
|
BaseFileUploader,
|
|
130
136
|
BaseForm,
|
|
131
137
|
BaseHasMany,
|
package/src/lang/en.json
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
+
"cancel": "Cancel",
|
|
3
|
+
"cropper": "Crop",
|
|
4
|
+
"drag_to_reposition": "Drag to reposition",
|
|
5
|
+
"save": "Save",
|
|
2
6
|
"sui": {
|
|
3
7
|
"address": "Address",
|
|
4
8
|
"address_1_placeholder": "Postal address",
|
|
@@ -22,14 +26,15 @@
|
|
|
22
26
|
"delete_record": "Delete the record",
|
|
23
27
|
"delete_record_description": "Are you sure to delete this record? This action is irreversible.",
|
|
24
28
|
"deselect_all": "Deselect all",
|
|
29
|
+
"drag_to_reposition": "Drag to reposition",
|
|
25
30
|
"drop_or_click_to_upload": "Drop or click to upload",
|
|
26
31
|
"edit": "Edit",
|
|
27
32
|
"error": "Error",
|
|
28
33
|
"file_must_be_of_type": "The file must be of type",
|
|
29
34
|
"filters": "Filters",
|
|
30
35
|
"go_to_page": "Go to ",
|
|
31
|
-
"invalid_value": "Invalid value",
|
|
32
36
|
"just_now": "Just now",
|
|
37
|
+
"invalid_value": "Invalid value",
|
|
33
38
|
"maximum_x_decimal_places": "Maximum 1 decimal place|Maximum {count} decimal places",
|
|
34
39
|
"min_x_characters": "{x} characters minimum",
|
|
35
40
|
"month": "Month",
|
package/src/lang/fr.json
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
+
"cancel": "Fermer",
|
|
3
|
+
"cropper": "Recadrer",
|
|
4
|
+
"drag_to_reposition": "Faites glisser pour repositionner",
|
|
5
|
+
"save": "Sauvegarder",
|
|
2
6
|
"sui": {
|
|
3
7
|
"address": "Adresse",
|
|
4
8
|
"address_1_placeholder": "Adresse postale",
|
|
@@ -22,14 +26,15 @@
|
|
|
22
26
|
"delete_record": "Supprimer l'item",
|
|
23
27
|
"delete_record_description": "Voulez-vous vraiment supprimer cet item ? \nCette action est irréversible.",
|
|
24
28
|
"deselect_all": "Tout déselectionner",
|
|
29
|
+
"drag_to_reposition": "Faites glisser pour repositionner",
|
|
25
30
|
"drop_or_click_to_upload": "Déposez ou cliquez pour télécharger",
|
|
26
31
|
"edit": "Modifier",
|
|
27
32
|
"error": "Erreur",
|
|
28
33
|
"file_must_be_of_type": "Le fichier doit être de type",
|
|
29
34
|
"filters": "Filtres",
|
|
30
35
|
"go_to_page": "Page",
|
|
31
|
-
"invalid_value": "Valeur invalide",
|
|
32
36
|
"just_now": "à l’instant",
|
|
37
|
+
"invalid_value": "Valeur invalide",
|
|
33
38
|
"maximum_x_decimal_places": "Maximum 1 décimale|Maximum {count} décimales",
|
|
34
39
|
"min_x_characters": "{x} caractères minimum",
|
|
35
40
|
"month": "Mois",
|
package/src/types/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { RouteLocationRaw } from 'vue-router';
|
|
4
4
|
import { UploadedFile } from './UploadedFile';
|
|
5
5
|
import { Notification as AppNotification } from './Notification';
|
|
6
|
+
import { CropType, ResultOptions } from 'croppie';
|
|
6
7
|
|
|
7
8
|
export type Locales = { [locale: string]: string };
|
|
8
9
|
|
|
@@ -226,3 +227,33 @@ export interface RowAction {
|
|
|
226
227
|
to?: (row: CollectionItem) => RouteLocationRaw;
|
|
227
228
|
disabled?: (row: CollectionItem) => boolean;
|
|
228
229
|
}
|
|
230
|
+
|
|
231
|
+
export interface CropperConfig {
|
|
232
|
+
height?: number;
|
|
233
|
+
width?: number;
|
|
234
|
+
viewport?: {
|
|
235
|
+
type?: CropType;
|
|
236
|
+
width: number;
|
|
237
|
+
height: number;
|
|
238
|
+
};
|
|
239
|
+
boundary?: {
|
|
240
|
+
width: number;
|
|
241
|
+
height: number;
|
|
242
|
+
};
|
|
243
|
+
enableResize?: boolean;
|
|
244
|
+
enableZoom?: boolean;
|
|
245
|
+
enableOrientation?: boolean;
|
|
246
|
+
showZoomer?: boolean;
|
|
247
|
+
initialResize?: number;
|
|
248
|
+
maxWidth?: number;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export type CropperPreset = 'avatar' | 'cover';
|
|
252
|
+
|
|
253
|
+
export interface BaseCropperConfig {
|
|
254
|
+
source: string;
|
|
255
|
+
config?: CropperConfig;
|
|
256
|
+
preset?: CropperPreset;
|
|
257
|
+
presetOptions?: Record<string, any>;
|
|
258
|
+
saveOptions?: ResultOptions;
|
|
259
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function blobToBase64(file: Blob): Promise<string> {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
const reader = new FileReader();
|
|
4
|
+
reader.readAsDataURL(file);
|
|
5
|
+
reader.onload = () => resolve(reader.result as string);
|
|
6
|
+
reader.onerror = (error) => reject(error);
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function validateBase64(
|
|
11
|
+
base64: string,
|
|
12
|
+
prefix = 'data:application/octet-stream'
|
|
13
|
+
): string {
|
|
14
|
+
if (!base64.match(/^data:.*\/.*;base64,/)) {
|
|
15
|
+
base64 = `${prefix};base64,${base64}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return base64;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function base64ToBlob(
|
|
22
|
+
base64: string,
|
|
23
|
+
prefix = 'data:application/octet-stream'
|
|
24
|
+
): Promise<Blob> {
|
|
25
|
+
base64 = validateBase64(base64, prefix);
|
|
26
|
+
|
|
27
|
+
const response = await fetch(base64);
|
|
28
|
+
|
|
29
|
+
return response.blob();
|
|
30
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CropperConfig } from '@/types';
|
|
2
|
+
import { isNumber, merge } from 'lodash';
|
|
3
|
+
import { PresetInterface } from './presetInterface';
|
|
4
|
+
|
|
5
|
+
export class AvatarPreset extends PresetInterface {
|
|
6
|
+
public handle(): CropperConfig {
|
|
7
|
+
const size = this.presetOptions?.size;
|
|
8
|
+
|
|
9
|
+
let defaultSize = isNumber(size) ? size : 200;
|
|
10
|
+
|
|
11
|
+
if (this.config?.maxWidth) {
|
|
12
|
+
defaultSize = Math.min(this.config.maxWidth, defaultSize);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return merge(this.config, {
|
|
16
|
+
width: defaultSize,
|
|
17
|
+
height: defaultSize,
|
|
18
|
+
enableOrientation: true,
|
|
19
|
+
enableResize: false,
|
|
20
|
+
enableZoom: true,
|
|
21
|
+
showZoomer: true,
|
|
22
|
+
viewport: {
|
|
23
|
+
width: defaultSize,
|
|
24
|
+
height: defaultSize,
|
|
25
|
+
type: 'circle',
|
|
26
|
+
},
|
|
27
|
+
boundary: {
|
|
28
|
+
width: defaultSize,
|
|
29
|
+
height: defaultSize,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CropperConfig } from '@/types';
|
|
2
|
+
import { isNumber, merge } from 'lodash';
|
|
3
|
+
import { PresetInterface } from './presetInterface';
|
|
4
|
+
|
|
5
|
+
export class CoverPreset extends PresetInterface {
|
|
6
|
+
public handle(): CropperConfig {
|
|
7
|
+
const size = this.presetOptions?.size;
|
|
8
|
+
const ratio = this.presetOptions?.ratio;
|
|
9
|
+
|
|
10
|
+
const defaultRatio = isNumber(ratio) ? ratio : 16 / 9;
|
|
11
|
+
let width = isNumber(size) ? size : 300;
|
|
12
|
+
let height = width / defaultRatio;
|
|
13
|
+
const padding = height * 0.05;
|
|
14
|
+
|
|
15
|
+
if (this.config?.maxWidth) {
|
|
16
|
+
const safeWidth = Math.min(this.config.maxWidth, width);
|
|
17
|
+
if (safeWidth !== width) {
|
|
18
|
+
height = safeWidth / defaultRatio;
|
|
19
|
+
width = safeWidth;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return merge(this.config, {
|
|
24
|
+
width: width,
|
|
25
|
+
height: height,
|
|
26
|
+
enableOrientation: true,
|
|
27
|
+
enableResize: false,
|
|
28
|
+
enableZoom: true,
|
|
29
|
+
showZoomer: true,
|
|
30
|
+
viewport: {
|
|
31
|
+
width: width - padding,
|
|
32
|
+
height: height - padding,
|
|
33
|
+
type: 'square',
|
|
34
|
+
},
|
|
35
|
+
boundary: {
|
|
36
|
+
width: width,
|
|
37
|
+
height: height,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CropperConfig } from '@/types';
|
|
2
|
+
|
|
3
|
+
export abstract class PresetInterface {
|
|
4
|
+
protected config: CropperConfig | null = null;
|
|
5
|
+
protected presetOptions: Record<string, any> | null = null;
|
|
6
|
+
|
|
7
|
+
constructor(
|
|
8
|
+
config: CropperConfig | null = null,
|
|
9
|
+
presetOptions: Record<string, any> | null = null
|
|
10
|
+
) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.presetOptions = presetOptions;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public abstract handle(): CropperConfig;
|
|
16
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function maxSize(blob: Blob, maxSize: number): boolean {
|
|
2
|
+
if (blob.size > maxSize) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function validExtension(
|
|
10
|
+
blob: Blob | File,
|
|
11
|
+
extensions?: string[] | undefined
|
|
12
|
+
): boolean {
|
|
13
|
+
let extension = blob.type.split('/').pop();
|
|
14
|
+
|
|
15
|
+
if (blob instanceof File) {
|
|
16
|
+
extension = blob.name.split('.').pop();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (extension && extensions && extensions.length) {
|
|
20
|
+
if (!extensions.includes(extension)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return true;
|
|
26
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import toHumanList from './toHumanList';
|
|
2
2
|
import fileSizeFormat from './fileSizeFormat';
|
|
3
|
+
import resizeImageFromURI from './resizeImageFromURI';
|
|
3
4
|
import { disableScroll, enableScroll } from './scrollPreventer';
|
|
5
|
+
import { blobToBase64, validateBase64, base64ToBlob } from './blob';
|
|
4
6
|
|
|
5
|
-
export {
|
|
7
|
+
export {
|
|
8
|
+
toHumanList,
|
|
9
|
+
fileSizeFormat,
|
|
10
|
+
disableScroll,
|
|
11
|
+
enableScroll,
|
|
12
|
+
resizeImageFromURI,
|
|
13
|
+
blobToBase64,
|
|
14
|
+
validateBase64,
|
|
15
|
+
base64ToBlob,
|
|
16
|
+
};
|