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.
Files changed (49) hide show
  1. package/dist/sprintify-ui.es.js +16895 -14999
  2. package/dist/style.css +1 -1
  3. package/dist/types/src/components/BaseCropper.vue.d.ts +57 -0
  4. package/dist/types/src/components/BaseCropperModal.vue.d.ts +27 -0
  5. package/dist/types/src/components/BaseDisplayRelativeTime.vue.d.ts +3 -3
  6. package/dist/types/src/components/BaseFilePicker.vue.d.ts +52 -37
  7. package/dist/types/src/components/BaseFilePickerCrop.vue.d.ts +57 -0
  8. package/dist/types/src/components/BaseFileUploader.vue.d.ts +65 -81
  9. package/dist/types/src/components/BaseMediaLibrary.vue.d.ts +20 -10
  10. package/dist/types/src/components/BaseTableColumn.vue.d.ts +1 -1
  11. package/dist/types/src/components/index.d.ts +4 -1
  12. package/dist/types/src/index.d.ts +24 -4
  13. package/dist/types/src/svg/BaseEmptyState.vue.d.ts +1 -1
  14. package/dist/types/src/types/ImagePickerResult.d.ts +5 -0
  15. package/dist/types/src/types/index.d.ts +28 -0
  16. package/dist/types/src/utils/blob.d.ts +3 -0
  17. package/dist/types/src/utils/cropper/avatar.d.ts +5 -0
  18. package/dist/types/src/utils/cropper/cover.d.ts +5 -0
  19. package/dist/types/src/utils/cropper/presetInterface.d.ts +7 -0
  20. package/dist/types/src/utils/cropper/presets.d.ts +6 -0
  21. package/dist/types/src/utils/fileValidations.d.ts +2 -0
  22. package/dist/types/src/utils/index.d.ts +3 -1
  23. package/dist/types/src/utils/resizeImageFromURI.d.ts +1 -0
  24. package/package.json +35 -32
  25. package/src/components/BaseCropper.stories.js +113 -0
  26. package/src/components/BaseCropper.vue +451 -0
  27. package/src/components/BaseCropperModal.stories.js +54 -0
  28. package/src/components/BaseCropperModal.vue +139 -0
  29. package/src/components/BaseFilePicker.stories.js +30 -3
  30. package/src/components/BaseFilePicker.vue +107 -75
  31. package/src/components/BaseFilePickerCrop.stories.js +134 -0
  32. package/src/components/BaseFilePickerCrop.vue +116 -0
  33. package/src/components/BaseFileUploader.stories.js +11 -7
  34. package/src/components/BaseFileUploader.vue +57 -86
  35. package/src/components/BaseMediaLibrary.stories.js +24 -5
  36. package/src/components/BaseMediaLibrary.vue +17 -2
  37. package/src/components/index.ts +6 -0
  38. package/src/lang/en.json +6 -1
  39. package/src/lang/fr.json +13 -8
  40. package/src/types/ImagePickerResult.ts +5 -0
  41. package/src/types/index.ts +31 -0
  42. package/src/utils/blob.ts +30 -0
  43. package/src/utils/cropper/avatar.ts +33 -0
  44. package/src/utils/cropper/cover.ts +41 -0
  45. package/src/utils/cropper/presetInterface.ts +16 -0
  46. package/src/utils/cropper/presets.ts +7 -0
  47. package/src/utils/fileValidations.ts +26 -0
  48. package/src/utils/index.ts +12 -1
  49. package/src/utils/resizeImageFromURI.ts +118 -0
@@ -1,86 +1,75 @@
1
1
  <template>
2
2
  <div class="relative">
3
- <BaseFilePicker
4
- :button-class="buttonClass"
5
- :disabled="uploading || disabled"
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
- :selecting="slotProps.selecting"
14
- :dragging="slotProps.dragging"
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
- :selecting="slotProps.selecting"
21
- :dragging="slotProps.dragging"
22
- :disabled="slotProps.disabled"
18
+ :loading="loading"
19
+ v-bind="slotProps"
23
20
  >
24
21
  <BaseLoadingCover
25
22
  :delay="0"
26
- icon-class="text-primary-600 w-6 h-6"
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
- </BaseFilePicker>
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 = defineProps({
49
- url: {
50
- default: undefined,
51
- type: String,
52
- },
53
- disabled: {
54
- default: false,
55
- type: Boolean,
56
- },
57
- loading: {
58
- default: false,
59
- type: Boolean,
60
- },
61
- beforeUpload: {
62
- default: (): boolean => {
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
- type: Function as PropType<() => boolean>,
66
- },
67
- buttonClass: {
68
- default: '',
69
- type: String,
70
- },
71
- maxSize: {
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
- return { args };
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 (numberOfFiles.value >= normalizedMax.value && normalizedMax.value > 1) {
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', {
@@ -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",
@@ -6,12 +10,12 @@
6
10
  "and": "et",
7
11
  "apply": "Appliquer",
8
12
  "apply_filters": "Appliquer les filtres",
9
- "autocomplete_placeholder": "Tapez pour lancer votre recherche",
13
+ "autocomplete_placeholder": "Taper pour lancer votre recherche",
10
14
  "cancel": "Annuler",
11
15
  "city": "Ville",
12
16
  "clear": "Effacer",
13
- "click_or_select_date": "Cliquez ou sélectionnez la date",
14
- "click_to_copy": "Cliquez pour copier",
17
+ "click_or_select_date": "Cliquer ou sélectionner une date",
18
+ "click_to_copy": "Cliquer pour copier",
15
19
  "columns": "Colonnes",
16
20
  "confirm": "Confirmer",
17
21
  "copied": "Copié",
@@ -20,16 +24,17 @@
20
24
  "day": "Jour",
21
25
  "delete": "Supprimer",
22
26
  "delete_record": "Supprimer l'item",
23
- "delete_record_description": "Voulez-vous vraiment supprimer cet item ? \nCette action est irréversible.",
27
+ "delete_record_description": "Supprimer cet item ? \nCette action est irréversible.",
24
28
  "deselect_all": "Tout déselectionner",
25
- "drop_or_click_to_upload": "Déposez ou cliquez pour télécharger",
29
+ "drag_to_reposition": "Faites glisser pour repositionner",
30
+ "drop_or_click_to_upload": "Déposer ou cliquer 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",
@@ -49,7 +54,7 @@
49
54
  "region": "État / Province",
50
55
  "remove": "Retirer",
51
56
  "remove_file": "Retirer le fichier?",
52
- "remove_file_description": "Voulez-vous vraiment supprimer le fichier ? \nCette action est irréversible.",
57
+ "remove_file_description": "Supprimer le fichier ? \nCette action est irréversible.",
53
58
  "search": "Rechercher",
54
59
  "see_all_notifications": "Voir toutes les notifications",
55
60
  "select_an_item": "Sélectionner un élément",
@@ -57,7 +62,7 @@
57
62
  "success": "Succès",
58
63
  "the_file_size_must_not_exceed_x": "La taille du fichier ne doit pas dépasser {x}",
59
64
  "the_file_type_is_invalid": "Le type de fichier n'est pas valide",
60
- "type_to_start_your_search": "Tapez pour lancer votre recherche",
65
+ "type_to_start_your_search": "Taper pour lancer votre recherche",
61
66
  "units": {
62
67
  "b": "o",
63
68
  "gb": "Go",
@@ -0,0 +1,5 @@
1
+ export interface ImagePickerResult {
2
+ mime: string;
3
+ data: string;
4
+ file: File;
5
+ }
@@ -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,7 @@
1
+ import { AvatarPreset } from './avatar';
2
+ import { CoverPreset } from './cover';
3
+
4
+ export const presets = {
5
+ avatar: AvatarPreset,
6
+ cover: CoverPreset,
7
+ };
@@ -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
+ }
@@ -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 { toHumanList, fileSizeFormat, disableScroll, enableScroll };
7
+ export {
8
+ toHumanList,
9
+ fileSizeFormat,
10
+ disableScroll,
11
+ enableScroll,
12
+ resizeImageFromURI,
13
+ blobToBase64,
14
+ validateBase64,
15
+ base64ToBlob,
16
+ };