sprintify-ui 0.0.183 → 0.0.185
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 +16895 -14999
- 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 +13 -8
- 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,113 @@
|
|
|
1
|
+
import BaseCropper from '@/components/BaseCropper.vue';
|
|
2
|
+
import BaseLoadingCover from '@/components/BaseLoadingCover.vue';
|
|
3
|
+
import BaseAppNotifications from '@/components/BaseAppNotifications.vue';
|
|
4
|
+
import { Icon as BaseIcon } from '@iconify/vue';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'Form/BaseCropper',
|
|
8
|
+
component: BaseCropper,
|
|
9
|
+
args: {
|
|
10
|
+
source:
|
|
11
|
+
'https://images.unsplash.com/photo-1560250097-0b93528c311a?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&q=80',
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const Template = (args) => ({
|
|
16
|
+
components: {
|
|
17
|
+
BaseCropper,
|
|
18
|
+
BaseIcon,
|
|
19
|
+
BaseLoadingCover,
|
|
20
|
+
BaseAppNotifications,
|
|
21
|
+
},
|
|
22
|
+
setup() {
|
|
23
|
+
const cropperRef = ref(null);
|
|
24
|
+
|
|
25
|
+
function saveAsCanvas() {
|
|
26
|
+
cropperRef.value.save({
|
|
27
|
+
type: 'canvas',
|
|
28
|
+
size: 'original',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { args, saveAsCanvas, cropperRef };
|
|
33
|
+
},
|
|
34
|
+
template: `
|
|
35
|
+
<BaseCropper ref="cropperRef" v-bind="args">
|
|
36
|
+
<template #footer="{cancelCrop, initializing, rotateLeft, rotateRight}">
|
|
37
|
+
<div class="mt-1 flex flex-wrap gap-2">
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
:disabled="initializing"
|
|
41
|
+
class="btn btn-sm"
|
|
42
|
+
@click="rotateLeft()"
|
|
43
|
+
>
|
|
44
|
+
Rotate Left
|
|
45
|
+
</button>
|
|
46
|
+
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
:disabled="initializing"
|
|
50
|
+
class="btn btn-sm"
|
|
51
|
+
@click="rotateRight()"
|
|
52
|
+
>
|
|
53
|
+
Rotate Right
|
|
54
|
+
</button>
|
|
55
|
+
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
:disabled="!initializing"
|
|
59
|
+
class="btn btn-sm"
|
|
60
|
+
@click="cancelCrop()"
|
|
61
|
+
>
|
|
62
|
+
Cancel
|
|
63
|
+
</button>
|
|
64
|
+
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
:disabled="initializing"
|
|
68
|
+
class="btn btn-sm"
|
|
69
|
+
@click="saveAsCanvas()"
|
|
70
|
+
>
|
|
71
|
+
Save as Canvas
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
75
|
+
</BaseCropper>
|
|
76
|
+
|
|
77
|
+
<BaseAppNotifications></BaseAppNotifications>
|
|
78
|
+
`,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export const Demo = Template.bind({});
|
|
82
|
+
|
|
83
|
+
Demo.args = {
|
|
84
|
+
config: {
|
|
85
|
+
height: 200,
|
|
86
|
+
width: 200,
|
|
87
|
+
viewport: {
|
|
88
|
+
width: 200,
|
|
89
|
+
height: 200,
|
|
90
|
+
type: 'rectangle',
|
|
91
|
+
},
|
|
92
|
+
boundary: {
|
|
93
|
+
width: 200,
|
|
94
|
+
height: 200,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const Avatar = Template.bind({});
|
|
100
|
+
|
|
101
|
+
Avatar.args = {
|
|
102
|
+
preset: 'avatar',
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const Cover = Template.bind({});
|
|
106
|
+
|
|
107
|
+
Cover.args = {
|
|
108
|
+
preset: 'cover',
|
|
109
|
+
presetOptions: {
|
|
110
|
+
size: 400,
|
|
111
|
+
ratio: 16 / 9,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="relative"
|
|
4
|
+
:style="{
|
|
5
|
+
width: cropperConfiguration?.boundary?.width + 'px',
|
|
6
|
+
}"
|
|
7
|
+
>
|
|
8
|
+
<slot
|
|
9
|
+
name="header"
|
|
10
|
+
:saving="saving"
|
|
11
|
+
:initializing="initializing"
|
|
12
|
+
v-bind="shared"
|
|
13
|
+
/>
|
|
14
|
+
|
|
15
|
+
<div ref="container" class="base-cropper-wrapper relative">
|
|
16
|
+
<div
|
|
17
|
+
:style="{
|
|
18
|
+
width: cropperConfiguration?.boundary?.width + 'px',
|
|
19
|
+
height: cropperConfiguration?.showZoomer
|
|
20
|
+
? (cropperConfiguration?.boundary?.height ?? 0) +
|
|
21
|
+
ZOOMER_HEIGHT +
|
|
22
|
+
'px'
|
|
23
|
+
: cropperConfiguration?.boundary?.height + 'px',
|
|
24
|
+
}"
|
|
25
|
+
>
|
|
26
|
+
<div
|
|
27
|
+
ref="croppie"
|
|
28
|
+
:style="{
|
|
29
|
+
visibility: initializing ? 'hidden' : 'visible',
|
|
30
|
+
}"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div
|
|
35
|
+
v-show="!initializing"
|
|
36
|
+
class="absolute left-0 z-[1] flex w-full items-center justify-center"
|
|
37
|
+
:style="{
|
|
38
|
+
bottom: cropperConfiguration?.showZoomer
|
|
39
|
+
? ZOOMER_HEIGHT - 13 + 'px'
|
|
40
|
+
: 6 + 'px',
|
|
41
|
+
}"
|
|
42
|
+
>
|
|
43
|
+
<div class="flex overflow-hidden rounded-full shadow-md">
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
:disabled="disabled"
|
|
47
|
+
class="border-r border-slate-300 bg-white px-3 py-1.5 hover:bg-slate-100"
|
|
48
|
+
@click="rotateLeft"
|
|
49
|
+
>
|
|
50
|
+
<BaseIcon icon="mdi:rotate-left" class="h-4 w-4" />
|
|
51
|
+
</button>
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
:disabled="disabled"
|
|
55
|
+
class="bg-white px-3 py-1.5 hover:bg-slate-100"
|
|
56
|
+
@click="rotateRight"
|
|
57
|
+
>
|
|
58
|
+
<BaseIcon icon="mdi:rotate-right" class="h-4 w-4" />
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div
|
|
64
|
+
v-if="showDragHelp"
|
|
65
|
+
class="pointer-events-none absolute left-0 top-14 z-[1] flex w-full animate-pulse justify-center"
|
|
66
|
+
>
|
|
67
|
+
<div
|
|
68
|
+
class="flex items-center rounded-lg bg-black bg-opacity-75 px-3 py-1.5 text-center text-white"
|
|
69
|
+
>
|
|
70
|
+
<BaseIcon icon="ri:drag-move-2-fill" class="mr-1 h-5 w-5" />
|
|
71
|
+
<span>
|
|
72
|
+
{{ $t('drag_to_reposition') }}
|
|
73
|
+
</span>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<BaseLoadingCover
|
|
78
|
+
:delay="40"
|
|
79
|
+
class="z-[1]"
|
|
80
|
+
:model-value="initializing"
|
|
81
|
+
></BaseLoadingCover>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<slot
|
|
85
|
+
name="footer"
|
|
86
|
+
:saving="saving"
|
|
87
|
+
:initializing="initializing"
|
|
88
|
+
v-bind="shared"
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|
|
92
|
+
|
|
93
|
+
<script lang="ts" setup>
|
|
94
|
+
import { onBeforeUnmount, Ref } from 'vue';
|
|
95
|
+
import Croppie, { CroppieOptions, CropType, ResultOptions } from 'croppie';
|
|
96
|
+
import 'croppie/croppie.css';
|
|
97
|
+
import { ref, onMounted } from 'vue';
|
|
98
|
+
import { resizeImageFromURI } from '@/utils';
|
|
99
|
+
import { cloneDeep, debounce } from 'lodash';
|
|
100
|
+
import { CropperConfig } from '../types';
|
|
101
|
+
import { BaseIcon, BaseLoadingCover } from '.';
|
|
102
|
+
import { presets } from '@/utils/cropper/presets';
|
|
103
|
+
|
|
104
|
+
const props = defineProps<{
|
|
105
|
+
source: string;
|
|
106
|
+
config?: CropperConfig;
|
|
107
|
+
preset?: 'avatar' | 'cover';
|
|
108
|
+
presetOptions?: Record<string, any>;
|
|
109
|
+
disabled?: boolean;
|
|
110
|
+
saveOptions?: ResultOptions;
|
|
111
|
+
}>();
|
|
112
|
+
|
|
113
|
+
const RESIZE_MAX_SIZE = 1000;
|
|
114
|
+
const ZOOMER_HEIGHT = 44;
|
|
115
|
+
|
|
116
|
+
const container = ref(null) as Ref<HTMLElement | null>;
|
|
117
|
+
const croppie = ref(null) as Ref<HTMLElement | null>;
|
|
118
|
+
|
|
119
|
+
let lastSource = null as string | null;
|
|
120
|
+
let lastInitialResize = null as number | null;
|
|
121
|
+
let lastSourceResized = null as string | null;
|
|
122
|
+
|
|
123
|
+
let cropper = null as Croppie | null;
|
|
124
|
+
|
|
125
|
+
// Croppie initialization
|
|
126
|
+
const initializing = ref(false);
|
|
127
|
+
|
|
128
|
+
// Croppie saving cropped image
|
|
129
|
+
const saving = ref(false);
|
|
130
|
+
|
|
131
|
+
const showDragHelp = ref(false);
|
|
132
|
+
|
|
133
|
+
const sourceOriginalWidth = ref(0);
|
|
134
|
+
const sourceOriginalHeight = ref(0);
|
|
135
|
+
|
|
136
|
+
const resetViewPortDebounced = debounce(() => {
|
|
137
|
+
resetViewPort();
|
|
138
|
+
}, 100);
|
|
139
|
+
|
|
140
|
+
watch(
|
|
141
|
+
() =>
|
|
142
|
+
JSON.stringify({
|
|
143
|
+
...(props.config ?? {}),
|
|
144
|
+
source: props.source,
|
|
145
|
+
preset: props.preset,
|
|
146
|
+
presetOptions: props.presetOptions,
|
|
147
|
+
}),
|
|
148
|
+
() => {
|
|
149
|
+
resetViewPortDebounced();
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
onMounted(() => {
|
|
154
|
+
init();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
onBeforeUnmount(() => {
|
|
158
|
+
// Give time for animations to finish
|
|
159
|
+
setTimeout(() => {
|
|
160
|
+
destroy();
|
|
161
|
+
}, 400);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const cropperConfiguration = computed<CroppieOptions>(() => {
|
|
165
|
+
let config = cloneDeep(props.config ?? {});
|
|
166
|
+
|
|
167
|
+
if (props.preset) {
|
|
168
|
+
const preset = presets[props.preset] ?? null;
|
|
169
|
+
|
|
170
|
+
if (preset) {
|
|
171
|
+
const presetClass = new preset(config, props.presetOptions);
|
|
172
|
+
config = presetClass.handle();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Put default values
|
|
177
|
+
|
|
178
|
+
config.width = config.width ?? 300;
|
|
179
|
+
config.height = config.height ?? 300;
|
|
180
|
+
config.maxWidth = config.maxWidth ?? undefined;
|
|
181
|
+
config.enableResize = config.enableResize ?? false;
|
|
182
|
+
config.enableZoom = config.enableZoom ?? true;
|
|
183
|
+
config.enableOrientation = config.enableOrientation ?? true;
|
|
184
|
+
config.showZoomer = config.showZoomer ?? true;
|
|
185
|
+
config.viewport = config.viewport ?? {
|
|
186
|
+
width: config.width,
|
|
187
|
+
height: config.height,
|
|
188
|
+
type: 'square' as CropType,
|
|
189
|
+
};
|
|
190
|
+
config.boundary = config.boundary ?? {
|
|
191
|
+
width: config.width,
|
|
192
|
+
height: config.height,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Make sure values respect max width
|
|
196
|
+
|
|
197
|
+
if (config.maxWidth) {
|
|
198
|
+
const ratio = (config.width as number) / (config.height as number);
|
|
199
|
+
config.width = Math.min(config.width as number, config.maxWidth);
|
|
200
|
+
config.height = config.width / ratio;
|
|
201
|
+
config.boundary.width = Math.min(config.boundary.width, config.maxWidth);
|
|
202
|
+
config.boundary.height = config.boundary.width / ratio;
|
|
203
|
+
config.viewport.width = Math.min(config.viewport.width, config.maxWidth);
|
|
204
|
+
config.viewport.height = config.viewport.width / ratio;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
enableExif: true,
|
|
209
|
+
enableResize: config.enableResize,
|
|
210
|
+
enableZoom: config.enableZoom,
|
|
211
|
+
enableOrientation: config.enableOrientation,
|
|
212
|
+
showZoomer: config.showZoomer,
|
|
213
|
+
viewport: config.viewport,
|
|
214
|
+
boundary: config.boundary,
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
function init() {
|
|
219
|
+
console.log('init');
|
|
220
|
+
|
|
221
|
+
if (initializing.value) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Start the loading state...
|
|
226
|
+
initializing.value = true;
|
|
227
|
+
|
|
228
|
+
// ...Give time to the loading state to render before doing CPU intensive tasks
|
|
229
|
+
setTimeout(async () => {
|
|
230
|
+
try {
|
|
231
|
+
await initCropper();
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error(error);
|
|
234
|
+
} finally {
|
|
235
|
+
initializing.value = false;
|
|
236
|
+
}
|
|
237
|
+
}, 10);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function initCropper() {
|
|
241
|
+
if (croppie.value == null) {
|
|
242
|
+
throw new Error('Croppie element not found');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
cropper = new Croppie(croppie.value, cropperConfiguration.value);
|
|
246
|
+
|
|
247
|
+
const sourceWasChanged = lastSource != props.source;
|
|
248
|
+
const initialResizeWasChanged =
|
|
249
|
+
lastInitialResize != props.config?.initialResize;
|
|
250
|
+
|
|
251
|
+
if (sourceWasChanged || initialResizeWasChanged) {
|
|
252
|
+
// Try to resize image to avoid using too much memory
|
|
253
|
+
lastSourceResized = await resizeImage(props.source);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
lastSource = props.source;
|
|
257
|
+
lastInitialResize = props.config?.initialResize ?? null;
|
|
258
|
+
|
|
259
|
+
lastSourceResized = lastSourceResized ?? props.source;
|
|
260
|
+
|
|
261
|
+
await cropper.bind({
|
|
262
|
+
url: lastSourceResized,
|
|
263
|
+
zoom: 0,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
showDragHelp.value = true;
|
|
267
|
+
|
|
268
|
+
croppie.value.addEventListener('update', (a) => {
|
|
269
|
+
if (initializing.value) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
showDragHelp.value = false;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function resizeImage(url: string): Promise<string> {
|
|
278
|
+
console.log('resizeImage');
|
|
279
|
+
|
|
280
|
+
await getOriginalDimensions();
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const initialResize = getInitialResize();
|
|
284
|
+
|
|
285
|
+
if (!initialResize) {
|
|
286
|
+
return url;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return await resizeImageFromURI(
|
|
290
|
+
url,
|
|
291
|
+
initialResize.height,
|
|
292
|
+
initialResize.width
|
|
293
|
+
);
|
|
294
|
+
} catch (e: any) {
|
|
295
|
+
return url;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function save(
|
|
300
|
+
result?: ResultOptions
|
|
301
|
+
): Promise<HTMLCanvasElement | string | Blob | null> {
|
|
302
|
+
if (!cropper) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
saving.value = true;
|
|
307
|
+
|
|
308
|
+
const resultConfig = result ??
|
|
309
|
+
props.saveOptions ?? {
|
|
310
|
+
type: 'blob',
|
|
311
|
+
size: 'original',
|
|
312
|
+
circle: false,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
console.log(resultConfig);
|
|
316
|
+
|
|
317
|
+
const r = await cropper.result(resultConfig);
|
|
318
|
+
|
|
319
|
+
saving.value = false;
|
|
320
|
+
|
|
321
|
+
return r;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function resetViewPort() {
|
|
325
|
+
if (!initializing.value) {
|
|
326
|
+
destroy();
|
|
327
|
+
init();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function rotateLeft() {
|
|
332
|
+
cropper?.rotate(90);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function rotateRight() {
|
|
336
|
+
cropper?.rotate(-90);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const destroy = () => {
|
|
340
|
+
cropper?.destroy();
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
async function getOriginalDimensions() {
|
|
344
|
+
return new Promise((resolve) => {
|
|
345
|
+
const img = new Image();
|
|
346
|
+
img.src = props.source;
|
|
347
|
+
img.onload = () => {
|
|
348
|
+
sourceOriginalWidth.value = img.width;
|
|
349
|
+
sourceOriginalHeight.value = img.height;
|
|
350
|
+
resolve(true);
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function getInitialResize() {
|
|
356
|
+
const resizeMax = props.config?.initialResize ?? RESIZE_MAX_SIZE;
|
|
357
|
+
|
|
358
|
+
const reducedWidth =
|
|
359
|
+
sourceOriginalWidth.value > resizeMax
|
|
360
|
+
? resizeMax
|
|
361
|
+
: sourceOriginalWidth.value;
|
|
362
|
+
|
|
363
|
+
const reducedHeight =
|
|
364
|
+
sourceOriginalHeight.value > resizeMax
|
|
365
|
+
? resizeMax
|
|
366
|
+
: sourceOriginalHeight.value;
|
|
367
|
+
|
|
368
|
+
const ratioWidth = reducedWidth / sourceOriginalWidth.value;
|
|
369
|
+
const ratioHeight = reducedHeight / sourceOriginalHeight.value;
|
|
370
|
+
const ratio = ratioWidth > ratioHeight ? ratioWidth : ratioHeight;
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
width: sourceOriginalWidth.value * ratio,
|
|
374
|
+
height: sourceOriginalHeight.value * ratio,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const shared = {
|
|
379
|
+
save,
|
|
380
|
+
resetViewPort,
|
|
381
|
+
rotateLeft,
|
|
382
|
+
rotateRight,
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
defineExpose(shared);
|
|
386
|
+
</script>
|
|
387
|
+
|
|
388
|
+
<style lang="postcss">
|
|
389
|
+
.base-cropper-wrapper {
|
|
390
|
+
.cr-slider-wrap {
|
|
391
|
+
margin-top: 20px;
|
|
392
|
+
display: flex;
|
|
393
|
+
align-items: center;
|
|
394
|
+
justify-content: center;
|
|
395
|
+
|
|
396
|
+
&:before {
|
|
397
|
+
content: '-';
|
|
398
|
+
flex-shrink: 0;
|
|
399
|
+
font-size: 20px;
|
|
400
|
+
font-weight: 400;
|
|
401
|
+
@apply text-slate-900;
|
|
402
|
+
display: flex;
|
|
403
|
+
align-items: center;
|
|
404
|
+
justify-content: center;
|
|
405
|
+
width: 36px;
|
|
406
|
+
height: 24px;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
&:after {
|
|
410
|
+
content: '+';
|
|
411
|
+
flex-shrink: 0;
|
|
412
|
+
font-size: 20px;
|
|
413
|
+
font-weight: 400;
|
|
414
|
+
@apply text-slate-900;
|
|
415
|
+
display: flex;
|
|
416
|
+
align-items: center;
|
|
417
|
+
justify-content: center;
|
|
418
|
+
width: 36px;
|
|
419
|
+
height: 24px;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
.cr-slider::-webkit-slider-runnable-track {
|
|
423
|
+
@apply bg-gray-300;
|
|
424
|
+
height: 5px;
|
|
425
|
+
}
|
|
426
|
+
.cr-slider::-moz-range-track {
|
|
427
|
+
@apply bg-gray-300;
|
|
428
|
+
height: 5px;
|
|
429
|
+
}
|
|
430
|
+
.cr-slider::-webkit-slider-thumb {
|
|
431
|
+
width: 24px;
|
|
432
|
+
height: 24px;
|
|
433
|
+
cursor: pointer;
|
|
434
|
+
position: relative;
|
|
435
|
+
top: -3px;
|
|
436
|
+
@apply bg-white;
|
|
437
|
+
@apply shadow-md;
|
|
438
|
+
@apply ring-0;
|
|
439
|
+
}
|
|
440
|
+
.cr-slider::-moz-range-thumb {
|
|
441
|
+
width: 24px;
|
|
442
|
+
height: 24px;
|
|
443
|
+
cursor: pointer;
|
|
444
|
+
position: relative;
|
|
445
|
+
top: -3px;
|
|
446
|
+
@apply bg-white;
|
|
447
|
+
@apply shadow-md;
|
|
448
|
+
@apply ring-0;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
</style>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import BaseCropperModal from '@/components/BaseCropperModal.vue';
|
|
2
|
+
import BaseAppNotifications from '@/components/BaseAppNotifications.vue';
|
|
3
|
+
|
|
4
|
+
const source =
|
|
5
|
+
'https://images.unsplash.com/photo-1560250097-0b93528c311a?auto=format&fit=crop&q=80';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: 'Form/BaseCropperModal',
|
|
9
|
+
component: BaseCropperModal,
|
|
10
|
+
args: {},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const Template = (args) => ({
|
|
14
|
+
components: {
|
|
15
|
+
BaseCropperModal,
|
|
16
|
+
BaseAppNotifications,
|
|
17
|
+
},
|
|
18
|
+
setup() {
|
|
19
|
+
const value = ref(true);
|
|
20
|
+
return { args, value };
|
|
21
|
+
},
|
|
22
|
+
template: `
|
|
23
|
+
<BaseCropperModal v-bind="args" v-model="value"></BaseCropperModal>
|
|
24
|
+
<BaseAppNotifications></BaseAppNotifications>
|
|
25
|
+
`,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const Avatar = Template.bind({});
|
|
29
|
+
Avatar.args = {
|
|
30
|
+
cropper: {
|
|
31
|
+
source: source,
|
|
32
|
+
config: {
|
|
33
|
+
maxWidth: 300,
|
|
34
|
+
},
|
|
35
|
+
preset: 'avatar',
|
|
36
|
+
presetOptions: {
|
|
37
|
+
size: 300,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const Cover = Template.bind({});
|
|
43
|
+
Cover.args = {
|
|
44
|
+
cropper: {
|
|
45
|
+
source: source,
|
|
46
|
+
config: {
|
|
47
|
+
maxWidth: 600,
|
|
48
|
+
},
|
|
49
|
+
preset: 'cover',
|
|
50
|
+
presetOptions: {
|
|
51
|
+
size: 600,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|