sprintify-ui 0.0.182 → 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/BaseTable.vue +1 -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
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<BaseModalCenter
|
|
3
|
+
:model-value="modelValue"
|
|
4
|
+
clipped
|
|
5
|
+
:max-width="cropperSize + 'px'"
|
|
6
|
+
@update:model-value="$emit('update:modelValue', $event)"
|
|
7
|
+
>
|
|
8
|
+
<div v-if="ready" class="flex items-center justify-center">
|
|
9
|
+
<BaseCropper
|
|
10
|
+
v-if="cropperInternal.source"
|
|
11
|
+
ref="baseCropperRef"
|
|
12
|
+
:disabled="loading"
|
|
13
|
+
v-bind="cropperInternal"
|
|
14
|
+
>
|
|
15
|
+
<template #footer="{ initializing }">
|
|
16
|
+
<div class="mt-5 px-4 pb-5">
|
|
17
|
+
<div class="flex justify-center space-x-2">
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
class="btn btn-lg btn-white"
|
|
21
|
+
:disabled="initializing"
|
|
22
|
+
@click="close()"
|
|
23
|
+
>
|
|
24
|
+
{{ $t('cancel') }}
|
|
25
|
+
</button>
|
|
26
|
+
<BaseButton
|
|
27
|
+
type="button"
|
|
28
|
+
class="btn btn-lg btn-primary"
|
|
29
|
+
:loading="loading"
|
|
30
|
+
:disabled="initializing"
|
|
31
|
+
@click="save()"
|
|
32
|
+
>
|
|
33
|
+
{{ $t('save') }}
|
|
34
|
+
</BaseButton>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
</BaseCropper>
|
|
39
|
+
</div>
|
|
40
|
+
</BaseModalCenter>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<script lang="ts" setup>
|
|
44
|
+
import BaseModalCenter from './BaseModalCenter.vue';
|
|
45
|
+
import BaseCropper from './BaseCropper.vue';
|
|
46
|
+
import { BaseCropperConfig } from '@/types';
|
|
47
|
+
import { debounce } from 'lodash';
|
|
48
|
+
import { BaseButton } from '.';
|
|
49
|
+
|
|
50
|
+
const props = defineProps<{
|
|
51
|
+
modelValue?: boolean;
|
|
52
|
+
cropper: BaseCropperConfig;
|
|
53
|
+
}>();
|
|
54
|
+
|
|
55
|
+
const emits = defineEmits<{
|
|
56
|
+
(event: 'update:modelValue', value: boolean): void;
|
|
57
|
+
(event: 'cropped', value: HTMLCanvasElement | string | Blob): void;
|
|
58
|
+
}>();
|
|
59
|
+
|
|
60
|
+
const ready = ref(false);
|
|
61
|
+
const loading = ref(false);
|
|
62
|
+
const baseCropperRef = ref<InstanceType<typeof BaseCropper> | null>(null);
|
|
63
|
+
|
|
64
|
+
const maxWidth = computed(() => {
|
|
65
|
+
if (props.cropper.config?.maxWidth) {
|
|
66
|
+
return props.cropper.config.maxWidth;
|
|
67
|
+
}
|
|
68
|
+
return 384;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const cropperSize = ref(maxWidth.value);
|
|
72
|
+
|
|
73
|
+
const onResizeDebounced = debounce(() => {
|
|
74
|
+
onResize();
|
|
75
|
+
}, 100);
|
|
76
|
+
|
|
77
|
+
function onResize() {
|
|
78
|
+
cropperSize.value = Math.min(window.innerWidth, maxWidth.value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const cropperInternal = computed(() => {
|
|
82
|
+
return {
|
|
83
|
+
...props.cropper,
|
|
84
|
+
config: {
|
|
85
|
+
...props.cropper.config,
|
|
86
|
+
maxWidth: cropperSize.value,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
onMounted(() => {
|
|
92
|
+
onResize();
|
|
93
|
+
|
|
94
|
+
// To avoid the cropper to be initialized with a wrong size
|
|
95
|
+
ready.value = true;
|
|
96
|
+
|
|
97
|
+
window.addEventListener('resize', onResizeDebounced);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
onBeforeUnmount(() => {
|
|
101
|
+
window.removeEventListener('resize', onResizeDebounced);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
async function save() {
|
|
105
|
+
if (!baseCropperRef.value) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
loading.value = true;
|
|
110
|
+
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
baseCropperRef.value
|
|
114
|
+
?.save()
|
|
115
|
+
.then((result) => {
|
|
116
|
+
resolve(result);
|
|
117
|
+
if (result) {
|
|
118
|
+
emits('cropped', result);
|
|
119
|
+
}
|
|
120
|
+
close();
|
|
121
|
+
})
|
|
122
|
+
.catch((error) => {
|
|
123
|
+
reject(error);
|
|
124
|
+
})
|
|
125
|
+
.finally(() => {
|
|
126
|
+
loading.value = false;
|
|
127
|
+
});
|
|
128
|
+
}, 50);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function close() {
|
|
133
|
+
emits('update:modelValue', false);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
defineExpose({
|
|
137
|
+
save,
|
|
138
|
+
});
|
|
139
|
+
</script>
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import BaseFilePicker from './BaseFilePicker.vue';
|
|
2
2
|
import { Icon as BaseIcon } from '@iconify/vue';
|
|
3
|
+
import ShowValue from '../../.storybook/components/ShowValue.vue';
|
|
4
|
+
import BaseAppNotifications from '@/components/BaseAppNotifications.vue';
|
|
3
5
|
|
|
4
6
|
export default {
|
|
5
7
|
title: 'Form/BaseFilePicker',
|
|
6
8
|
component: BaseFilePicker,
|
|
9
|
+
args: {},
|
|
7
10
|
};
|
|
8
11
|
|
|
9
12
|
const Template = (args) => ({
|
|
10
|
-
components: { BaseFilePicker, BaseIcon },
|
|
13
|
+
components: { BaseFilePicker, BaseIcon, ShowValue, BaseAppNotifications },
|
|
11
14
|
setup() {
|
|
12
|
-
|
|
15
|
+
const file = ref(null);
|
|
16
|
+
|
|
17
|
+
function onSelect(f) {
|
|
18
|
+
file.value = f;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { args, file, onSelect };
|
|
13
22
|
},
|
|
14
23
|
template: `
|
|
15
|
-
<BaseFilePicker v-bind="args">
|
|
24
|
+
<BaseFilePicker v-bind="args" @select="onSelect">
|
|
16
25
|
<template #default="{ dragging, disabled }">
|
|
17
26
|
<div
|
|
18
27
|
class="flex w-full items-center space-x-4 rounded-lg border-2 border-dashed border-slate-200 p-5 duration-100"
|
|
@@ -35,16 +44,34 @@ const Template = (args) => ({
|
|
|
35
44
|
>
|
|
36
45
|
{{ $t("sui.drop_or_click_to_upload") }}
|
|
37
46
|
</p>
|
|
47
|
+
<div class="mt-1">
|
|
48
|
+
<p v-if="args.maxSize" class="text-sm text-slate-500">Max {{ args.maxSize }} bytes</p>
|
|
49
|
+
<p v-if="args.acceptedExtensions" class="text-sm text-slate-500">{{ args.acceptedExtensions?.join(', ') }}</p>
|
|
50
|
+
</div>
|
|
38
51
|
</div>
|
|
39
52
|
</div>
|
|
40
53
|
</template>
|
|
41
54
|
</BaseFilePicker>
|
|
55
|
+
|
|
56
|
+
<ShowValue :value="file" />
|
|
57
|
+
|
|
58
|
+
<BaseAppNotifications></BaseAppNotifications>
|
|
42
59
|
`,
|
|
43
60
|
});
|
|
44
61
|
|
|
45
62
|
export const Demo = Template.bind({});
|
|
46
63
|
Demo.args = {};
|
|
47
64
|
|
|
65
|
+
export const MaxSize = Template.bind({});
|
|
66
|
+
MaxSize.args = {
|
|
67
|
+
maxSize: 1024 * 10, // 10kb
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const FileExtension = Template.bind({});
|
|
71
|
+
FileExtension.args = {
|
|
72
|
+
acceptedExtensions: ['xlsx', 'xls'],
|
|
73
|
+
};
|
|
74
|
+
|
|
48
75
|
export const Disabled = Template.bind({});
|
|
49
76
|
Disabled.args = {
|
|
50
77
|
disabled: true,
|
|
@@ -23,79 +23,111 @@
|
|
|
23
23
|
/>
|
|
24
24
|
</template>
|
|
25
25
|
|
|
26
|
-
<script lang="ts">
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
26
|
+
<script lang="ts" setup>
|
|
27
|
+
import { useNotificationsStore } from '@/stores/notifications';
|
|
28
|
+
import { fileSizeFormat, toHumanList } from '@/utils';
|
|
29
|
+
import { maxSize, validExtension } from '@/utils/fileValidations';
|
|
30
|
+
|
|
31
|
+
const props = withDefaults(
|
|
32
|
+
defineProps<{
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
buttonClass?: string;
|
|
35
|
+
maxSize?: number;
|
|
36
|
+
accept?: string;
|
|
37
|
+
acceptedExtensions?: string[];
|
|
38
|
+
}>(),
|
|
39
|
+
{
|
|
40
|
+
disabled: false,
|
|
41
|
+
buttonClass: '',
|
|
42
|
+
maxSize: 1024 * 1024 * 20, // 20 MB,
|
|
43
|
+
accept: undefined,
|
|
44
|
+
acceptedExtensions: undefined,
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const emit = defineEmits(['select']);
|
|
49
|
+
|
|
50
|
+
const notifications = useNotificationsStore();
|
|
51
|
+
|
|
52
|
+
const i18n = useI18n();
|
|
53
|
+
|
|
54
|
+
const selecting = ref(false);
|
|
55
|
+
const dragging = ref(false);
|
|
56
|
+
const input = ref<HTMLInputElement | undefined>();
|
|
57
|
+
|
|
58
|
+
async function pickFile() {
|
|
59
|
+
if (props.disabled) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
input.value?.click();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function onInputChange() {
|
|
67
|
+
const files = (input.value?.files ?? []) as File[];
|
|
68
|
+
select(files);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function handleDrop(e: any) {
|
|
72
|
+
if (props.disabled) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const files = e?.dataTransfer?.files ?? [];
|
|
77
|
+
|
|
78
|
+
select(files);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function select(files: File[]) {
|
|
82
|
+
if (props.disabled) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!files || files.length == 0 || !(files[0] instanceof File)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
selecting.value = true;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const file = files[0];
|
|
94
|
+
|
|
95
|
+
if (!maxSize(file, props.maxSize)) {
|
|
96
|
+
notifications.push({
|
|
97
|
+
color: 'danger',
|
|
98
|
+
title: i18n.t('sui.error'),
|
|
99
|
+
text: i18n.t('sui.the_file_size_must_not_exceed_x', {
|
|
100
|
+
x: fileSizeFormat(props.maxSize),
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
selecting.value = false;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!validExtension(file, props.acceptedExtensions)) {
|
|
109
|
+
notifications.push({
|
|
110
|
+
color: 'danger',
|
|
111
|
+
title: i18n.t('sui.error'),
|
|
112
|
+
text:
|
|
113
|
+
i18n.t('sui.the_file_type_is_invalid') +
|
|
114
|
+
' ' +
|
|
115
|
+
i18n.t('sui.file_must_be_of_type') +
|
|
116
|
+
' ' +
|
|
117
|
+
toHumanList(props.acceptedExtensions as string[], i18n.t('sui.or')) +
|
|
118
|
+
'.',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
selecting.value = false;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
emit('select', file);
|
|
126
|
+
} finally {
|
|
127
|
+
if (input.value) {
|
|
128
|
+
input.value.value = '';
|
|
129
|
+
}
|
|
130
|
+
selecting.value = false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
101
133
|
</script>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import BaseFilePickerCrop from '@/components/BaseFilePickerCrop.vue';
|
|
2
|
+
import BaseLoadingCover from '@/components/BaseLoadingCover.vue';
|
|
3
|
+
import BaseAppNotifications from '@/components/BaseAppNotifications.vue';
|
|
4
|
+
import BaseCropper from '@/components/BaseCropper.vue';
|
|
5
|
+
import BaseModalCenter from '@/components/BaseModalCenter.vue';
|
|
6
|
+
import BaseButton from '@/components/BaseButton.vue';
|
|
7
|
+
import { Icon as BaseIcon } from '@iconify/vue';
|
|
8
|
+
import ShowValue from '../../.storybook/components/ShowValue.vue';
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
title: 'Form/BaseFilePickerCrop',
|
|
12
|
+
component: BaseFilePickerCrop,
|
|
13
|
+
args: {
|
|
14
|
+
buttonClass: 'w-full',
|
|
15
|
+
acceptedExtensions: ['jpg', 'jpeg', 'png'],
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const Template = (args) => ({
|
|
20
|
+
components: {
|
|
21
|
+
BaseFilePickerCrop,
|
|
22
|
+
BaseIcon,
|
|
23
|
+
BaseLoadingCover,
|
|
24
|
+
BaseAppNotifications,
|
|
25
|
+
BaseCropper,
|
|
26
|
+
BaseModalCenter,
|
|
27
|
+
BaseButton,
|
|
28
|
+
ShowValue,
|
|
29
|
+
},
|
|
30
|
+
setup() {
|
|
31
|
+
const file = ref(null);
|
|
32
|
+
|
|
33
|
+
function onSelect(f) {
|
|
34
|
+
file.value = f;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { args, file, onSelect };
|
|
38
|
+
},
|
|
39
|
+
template: `
|
|
40
|
+
<BaseFilePickerCrop v-bind="args" @select="onSelect">
|
|
41
|
+
<template #default="{ dragging, disabled, uploading, selecting }">
|
|
42
|
+
<div
|
|
43
|
+
class="flex w-full items-center space-x-4 rounded-lg border-2 border-dashed border-slate-200 p-5 duration-100"
|
|
44
|
+
:class="[
|
|
45
|
+
dragging ? 'bg-slate-100' : 'bg-white',
|
|
46
|
+
disabled ? 'bg-slate-100 cursor-not-allowed' : 'hover:bg-slate-50',
|
|
47
|
+
]"
|
|
48
|
+
>
|
|
49
|
+
<div class="rounded-full bg-slate-200 p-2">
|
|
50
|
+
<BaseIcon
|
|
51
|
+
icon="heroicons:arrow-up-on-square"
|
|
52
|
+
class="h-6 w-6"
|
|
53
|
+
:class="[disabled ? 'text-slate-400' : 'text-slate-500']"
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="text-left" :class="[disabled ? 'opacity-50' : '']">
|
|
57
|
+
<p class="mb-0 text-sm font-medium leading-tight">
|
|
58
|
+
{{ $t("sui.drop_or_click_to_upload") }}
|
|
59
|
+
</p>
|
|
60
|
+
<div class="mt-1">
|
|
61
|
+
<p v-if="args.maxSize" class="text-sm text-slate-500">Max {{ args.maxSize }} bytes</p>
|
|
62
|
+
<p v-if="args.acceptedExtensions" class="text-sm text-slate-500">{{ args.acceptedExtensions?.join(', ') }}</p>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
67
|
+
<template #loading="{ dragging, disabled, uploading, selecting }">
|
|
68
|
+
<BaseLoadingCover
|
|
69
|
+
:model-value="args.loading || uploading || selecting"
|
|
70
|
+
:delay="0"
|
|
71
|
+
icon-class="text-primary-600 w-6 h-6"
|
|
72
|
+
backdrop-class="bg-white opacity-60"
|
|
73
|
+
/>
|
|
74
|
+
</template>
|
|
75
|
+
</BaseFilePickerCrop>
|
|
76
|
+
|
|
77
|
+
<ShowValue :value="file" />
|
|
78
|
+
|
|
79
|
+
<BaseAppNotifications></BaseAppNotifications>
|
|
80
|
+
`,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const Demo = Template.bind({});
|
|
84
|
+
Demo.args = {};
|
|
85
|
+
|
|
86
|
+
export const Avatar = Template.bind({});
|
|
87
|
+
Avatar.args = {
|
|
88
|
+
cropper: {
|
|
89
|
+
preset: 'avatar',
|
|
90
|
+
presetOptions: {
|
|
91
|
+
size: 300,
|
|
92
|
+
},
|
|
93
|
+
config: {
|
|
94
|
+
maxWidth: 300,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const Cover = Template.bind({});
|
|
100
|
+
Cover.args = {
|
|
101
|
+
cropper: {
|
|
102
|
+
preset: 'cover',
|
|
103
|
+
presetOptions: {
|
|
104
|
+
size: 600,
|
|
105
|
+
},
|
|
106
|
+
config: {
|
|
107
|
+
maxWidth: 600,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const Resize = Template.bind({});
|
|
113
|
+
Resize.args = {
|
|
114
|
+
cropper: {
|
|
115
|
+
maxSize: 600,
|
|
116
|
+
config: {
|
|
117
|
+
initialResize: 100,
|
|
118
|
+
},
|
|
119
|
+
preset: 'cover',
|
|
120
|
+
presetOptions: {
|
|
121
|
+
size: 600,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const MaxSize = Template.bind({});
|
|
127
|
+
MaxSize.args = {
|
|
128
|
+
maxSize: 10 * 1024,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const Disabled = Template.bind({});
|
|
132
|
+
Disabled.args = {
|
|
133
|
+
disabled: true,
|
|
134
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<BaseFilePicker
|
|
3
|
+
:button-class="buttonClass"
|
|
4
|
+
:disabled="disabled"
|
|
5
|
+
accept="image/*"
|
|
6
|
+
:max-size="maxSize"
|
|
7
|
+
:accepted-extensions="acceptedExtensions"
|
|
8
|
+
@select="launchCropper"
|
|
9
|
+
>
|
|
10
|
+
<template #default="slotProps">
|
|
11
|
+
<slot name="default" v-bind="slotProps" />
|
|
12
|
+
|
|
13
|
+
<BaseCropperModal
|
|
14
|
+
v-if="cropperInternal"
|
|
15
|
+
ref="baseCropperModalRef"
|
|
16
|
+
v-model="showCropperModal"
|
|
17
|
+
:cropper="cropperInternal"
|
|
18
|
+
@cropped="onCropped"
|
|
19
|
+
/>
|
|
20
|
+
</template>
|
|
21
|
+
</BaseFilePicker>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script lang="ts" setup>
|
|
25
|
+
import BaseFilePicker from '@/components/BaseFilePicker.vue';
|
|
26
|
+
import BaseCropperModal from '@/components/BaseCropperModal.vue';
|
|
27
|
+
import { blobToBase64 } from '@/utils';
|
|
28
|
+
import { BaseCropperConfig } from '@/types';
|
|
29
|
+
import { isObject, reject } from 'lodash';
|
|
30
|
+
|
|
31
|
+
const props = withDefaults(
|
|
32
|
+
defineProps<{
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
buttonClass?: string;
|
|
35
|
+
maxSize?: number;
|
|
36
|
+
acceptedExtensions?: string[];
|
|
37
|
+
cropper: BaseCropperConfig | null;
|
|
38
|
+
}>(),
|
|
39
|
+
{
|
|
40
|
+
disabled: false,
|
|
41
|
+
buttonClass: '',
|
|
42
|
+
maxSize: 1024 * 1024 * 20, // 20 MB
|
|
43
|
+
acceptedExtensions: undefined,
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const baseCropperModalRef = ref<InstanceType<typeof BaseCropperModal> | null>(
|
|
48
|
+
null
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const emit = defineEmits(['select']);
|
|
52
|
+
|
|
53
|
+
const showCropperModal = ref(false);
|
|
54
|
+
const cropperSource = ref('');
|
|
55
|
+
|
|
56
|
+
const cropperInternal = computed<BaseCropperConfig | null>(() => {
|
|
57
|
+
if (!cropperSource.value) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (isObject(props.cropper)) {
|
|
62
|
+
return {
|
|
63
|
+
...props.cropper,
|
|
64
|
+
source: cropperSource.value,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
source: cropperSource.value,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
async function launchCropper(file: File) {
|
|
74
|
+
if (!file) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
showCropperModal.value = false;
|
|
79
|
+
cropperSource.value = await blobToBase64(file);
|
|
80
|
+
|
|
81
|
+
if (!cropperSource.value) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
showCropperModal.value = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function onCropped(cropped: HTMLCanvasElement | string | Blob) {
|
|
89
|
+
if (cropped instanceof Blob) {
|
|
90
|
+
emit('select', cropped);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (cropped instanceof HTMLCanvasElement) {
|
|
95
|
+
const blob = await new Promise<Blob>((resolve) => {
|
|
96
|
+
cropped.toBlob((blob) => {
|
|
97
|
+
if (blob) {
|
|
98
|
+
resolve(blob);
|
|
99
|
+
} else {
|
|
100
|
+
reject(new Error('Failed to convert canvas to blob'));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
emit('select', blob);
|
|
106
|
+
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (typeof cropped === 'string') {
|
|
111
|
+
const blob = await fetch(cropped).then((r) => r.blob());
|
|
112
|
+
emit('select', blob);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
</script>
|
|
@@ -31,21 +31,17 @@ const Template = (args) => ({
|
|
|
31
31
|
class="flex w-full items-center space-x-4 rounded-lg border-2 border-dashed border-slate-200 p-5 duration-100"
|
|
32
32
|
:class="[
|
|
33
33
|
dragging ? 'bg-slate-100' : 'bg-white',
|
|
34
|
-
disabled ? 'bg-slate-100 cursor-not-allowed' : 'hover:bg-slate-50',
|
|
34
|
+
disabled ? 'bg-slate-100 cursor-not-allowed opacity-50' : 'hover:bg-slate-50',
|
|
35
35
|
]"
|
|
36
36
|
>
|
|
37
37
|
<div class="rounded-full bg-slate-200 p-2">
|
|
38
38
|
<BaseIcon
|
|
39
39
|
icon="heroicons:arrow-up-on-square"
|
|
40
40
|
class="h-6 w-6"
|
|
41
|
-
:class="[disabled ? 'text-slate-400' : 'text-slate-500']"
|
|
42
41
|
/>
|
|
43
42
|
</div>
|
|
44
43
|
<div class="text-left">
|
|
45
|
-
<p
|
|
46
|
-
class="mb-0 text-sm font-medium leading-tight"
|
|
47
|
-
:class="[disabled ? 'text-slate-400' : 'text-slate-900']"
|
|
48
|
-
>
|
|
44
|
+
<p class="mb-0 text-sm font-medium leading-tight">
|
|
49
45
|
{{ $t("sui.drop_or_click_to_upload") }}
|
|
50
46
|
</p>
|
|
51
47
|
<p class="text-sm text-slate-500 mt-1">Max 200kb</p>
|
|
@@ -57,7 +53,7 @@ const Template = (args) => ({
|
|
|
57
53
|
<BaseLoadingCover
|
|
58
54
|
:model-value="args.loading || uploading || selecting"
|
|
59
55
|
:delay="0"
|
|
60
|
-
icon-class="text-
|
|
56
|
+
icon-class="text-red-600 w-6 h-6"
|
|
61
57
|
backdrop-class="bg-white opacity-60"
|
|
62
58
|
/>
|
|
63
59
|
</template>
|
|
@@ -69,6 +65,14 @@ const Template = (args) => ({
|
|
|
69
65
|
export const Demo = Template.bind({});
|
|
70
66
|
Demo.args = {};
|
|
71
67
|
|
|
68
|
+
export const ImagePicker = Template.bind({});
|
|
69
|
+
ImagePicker.args = {
|
|
70
|
+
component: 'BaseFilePickerCrop',
|
|
71
|
+
cropper: {
|
|
72
|
+
preset: 'avatar',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
72
76
|
export const Disabled = Template.bind({});
|
|
73
77
|
Disabled.args = {
|
|
74
78
|
disabled: true,
|