@dejwcake/craftable 0.1.10 → 2.0.1

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 (127) hide show
  1. package/README.md +117 -3
  2. package/dist/buildMatchPatternFn-BV4I8r8j.js +63 -0
  3. package/dist/buildMatchPatternFn-T5KjFTBQ.cjs +1 -0
  4. package/dist/craftable.css +2 -0
  5. package/dist/cs-B5QMxFrl.js +682 -0
  6. package/dist/cs-MAoetkhe.cjs +1 -0
  7. package/dist/de-BlQ2ZKmc.js +526 -0
  8. package/dist/de-BoVLAn6U.cjs +1 -0
  9. package/dist/en-US-BLvEnhs4.js +443 -0
  10. package/dist/en-US-Dz8vkDZ_.cjs +1 -0
  11. package/dist/en-US-X9gX759i.cjs +1 -0
  12. package/dist/es-DBHDeXHI.cjs +1 -0
  13. package/dist/es-YEqfkBY2.js +437 -0
  14. package/dist/fr-B7MHC2AN.js +407 -0
  15. package/dist/fr-DLs3QoNB.cjs +1 -0
  16. package/dist/hu-BKZPI9jH.cjs +1 -0
  17. package/dist/hu-reoJgNxD.js +425 -0
  18. package/dist/index.cjs +1 -0
  19. package/dist/index.mjs +3922 -0
  20. package/dist/isSameWeek-CC6GX_zp.js +40 -0
  21. package/dist/isSameWeek-Mctj6Tdj.cjs +1 -0
  22. package/dist/pl-B5A0Rjp7.js +628 -0
  23. package/dist/pl-BMyrOO-Q.cjs +1 -0
  24. package/dist/sk-B7JF2MoV.cjs +1 -0
  25. package/dist/sk-Btro3W4S.js +602 -0
  26. package/package.json +68 -40
  27. package/scripts/publish-components.js +118 -0
  28. package/scss/_images.scss +1 -1
  29. package/scss/_variables.scss +13 -6
  30. package/scss/app.scss +4 -4
  31. package/scss/overrides/_alerts.scss +0 -2
  32. package/scss/overrides/_badges.scss +1 -17
  33. package/scss/overrides/_buttons.scss +64 -7
  34. package/scss/overrides/_cards.scss +4 -51
  35. package/scss/overrides/_checkboxes.scss +54 -59
  36. package/scss/overrides/_forms.scss +270 -176
  37. package/scss/overrides/_index.scss +18 -18
  38. package/scss/overrides/_keyframes.scss +6 -4
  39. package/scss/overrides/_layout.scss +24 -30
  40. package/scss/overrides/_media-wysiwyg.scss +9 -111
  41. package/scss/overrides/_modal.scss +0 -18
  42. package/scss/overrides/_multiselect.scss +63 -7
  43. package/scss/overrides/_nav.scss +55 -33
  44. package/scss/overrides/_notifications.scss +6 -1
  45. package/scss/overrides/_pagination.scss +3 -3
  46. package/scss/overrides/_sidebar.scss +84 -0
  47. package/scss/overrides/_tables.scss +52 -37
  48. package/scss/overrides/_text.scss +1 -3
  49. package/scss/overrides/_wysiwyg.scss +151 -48
  50. package/scss/styles/_auth.scss +32 -37
  51. package/scss/styles/_custom.scss +44 -1
  52. package/scss/styles/_index.scss +4 -4
  53. package/scss/styles/_legacy.scss +5 -81
  54. package/scss/styles/_others.scss +232 -337
  55. package/scss/vendor/_index.scss +5 -4
  56. package/src/auth/ActivationError.vue +23 -0
  57. package/src/auth/ActivationForm.vue +69 -0
  58. package/src/auth/ForgotPasswordForm.vue +74 -0
  59. package/src/auth/LoginForm.vue +107 -0
  60. package/src/auth/ResetPasswordForm.vue +122 -0
  61. package/src/components/ConfirmModal.vue +65 -0
  62. package/src/components/UserDetailTooltip.vue +88 -0
  63. package/src/components/form/CkeditorEditor.vue +205 -0
  64. package/src/components/form/FormCheckbox.vue +29 -0
  65. package/src/components/form/FormDatePicker.vue +52 -0
  66. package/src/components/form/FormEmail.vue +17 -0
  67. package/src/components/form/FormInput.vue +31 -0
  68. package/src/components/form/FormLocalizedInput.vue +47 -0
  69. package/src/components/form/FormLocalizedWysiwyg.vue +55 -0
  70. package/src/components/form/FormLocalizedWysiwygSimple.vue +55 -0
  71. package/src/components/form/FormMultiSelect.vue +36 -0
  72. package/src/components/form/FormPasswordConfirm.vue +49 -0
  73. package/src/components/form/FormSelect.vue +35 -0
  74. package/src/components/form/FormSubmit.vue +13 -0
  75. package/src/components/form/FormTagInput.vue +36 -0
  76. package/src/components/form/FormTextarea.vue +31 -0
  77. package/src/components/form/FormWysiwyg.vue +31 -0
  78. package/src/components/form/FormWysiwygSimple.vue +31 -0
  79. package/src/components/form/LocalizationBar.vue +90 -0
  80. package/src/components/form/MediaUpload.vue +317 -0
  81. package/src/components/form/TiptapEditor.vue +238 -0
  82. package/src/components/listing/BulkCheckboxHeader.vue +18 -0
  83. package/src/components/listing/BulkCheckboxRow.vue +21 -0
  84. package/src/components/listing/BulkOperationsBar.vue +41 -0
  85. package/src/components/listing/DeleteButton.vue +46 -0
  86. package/src/components/listing/EditButton.vue +17 -0
  87. package/src/components/listing/EmptyState.vue +25 -0
  88. package/src/components/listing/ListingHeader.vue +24 -0
  89. package/src/components/listing/Pagination.vue +158 -0
  90. package/src/components/listing/PerPage.vue +15 -0
  91. package/src/components/listing/PublishedAtColumn.vue +226 -0
  92. package/src/components/listing/Search.vue +81 -0
  93. package/src/components/listing/Sortable.vue +28 -0
  94. package/src/components/listing/ToggleSwitch.vue +47 -0
  95. package/src/composables/useAdmin.js +39 -0
  96. package/src/composables/useBaseAuth.js +81 -0
  97. package/src/composables/useBaseForm.js +265 -0
  98. package/src/composables/useBaseListing.js +316 -0
  99. package/src/composables/useResponsiveColumns.js +62 -0
  100. package/src/index.js +63 -0
  101. package/src/translation/TranslationListing.vue +230 -0
  102. package/src/translation/modals/TranslationEditModal.vue +86 -0
  103. package/src/translation/modals/TranslationExportModal.vue +74 -0
  104. package/src/translation/modals/TranslationImportModal.vue +255 -0
  105. package/src/ui/index.js +99 -0
  106. package/src/utils/ckeditorHelpers.js +36 -0
  107. package/src/utils/dateFnsLocale.js +24 -0
  108. package/src/utils/dateFormatters.js +30 -0
  109. package/src/utils/mediaProps.js +14 -0
  110. package/src/utils/notify.js +22 -0
  111. package/dist/admin/Admin.js +0 -42
  112. package/dist/auth/Form.js +0 -15
  113. package/dist/base-components/Form/BaseForm.js +0 -395
  114. package/dist/base-components/Form/BaseUpload.js +0 -188
  115. package/dist/base-components/Form/bootstrap.js +0 -21
  116. package/dist/base-components/Listing/BaseListing.js +0 -454
  117. package/dist/base-components/Listing/components/Pagination.js +0 -105
  118. package/dist/base-components/Listing/components/Sortable.js +0 -48
  119. package/dist/base-components/Listing/components/UserDetailTooltip.js +0 -55
  120. package/dist/index.js +0 -70
  121. package/dist/overrides/trumbowyg.edit-embed-template.js +0 -72
  122. package/dist/overrides/trumbowyg.reupload.js +0 -146
  123. package/dist/overrides/trumbowyg.template.js +0 -67
  124. package/dist/translation/Form.js +0 -15
  125. package/dist/translation/Listing.js +0 -283
  126. package/dist/ui/index.js +0 -33
  127. package/scss/overrides/_switches.scss +0 -25
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <div class="mb-3 row align-items-center">
3
+ <label :for="name" class="col-md-3 col-form-label text-md-end">{{ label }}</label>
4
+ <div class="col-md-9 col-xl-8">
5
+ <textarea
6
+ :id="name"
7
+ :value="modelValue"
8
+ class="form-control"
9
+ :class="{ 'is-invalid': error }"
10
+ :name="name"
11
+ :placeholder="placeholder || label"
12
+ :rows="rows"
13
+ @input="$emit('update:modelValue', $event.target.value)"
14
+ ></textarea>
15
+ <div v-if="error" class="invalid-feedback form-text">{{ error }}</div>
16
+ </div>
17
+ </div>
18
+ </template>
19
+
20
+ <script setup>
21
+ defineProps({
22
+ modelValue: { type: String, default: '' },
23
+ name: { type: String, required: true },
24
+ label: { type: String, required: true },
25
+ error: { type: String, default: '' },
26
+ placeholder: { type: String, default: '' },
27
+ rows: { type: [Number, String], default: 3 },
28
+ });
29
+
30
+ defineEmits(['update:modelValue']);
31
+ </script>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <div class="mb-3 row align-items-center">
3
+ <label :for="name" class="col-md-3 col-form-label text-md-end">{{ label }}</label>
4
+ <div class="col-md-9 col-xl-8">
5
+ <CkeditorEditor
6
+ :id="name"
7
+ :model-value="modelValue"
8
+ :name="name"
9
+ :upload-url="uploadUrl"
10
+ v-bind="config ? { config } : {}"
11
+ @update:model-value="$emit('update:modelValue', $event)"
12
+ ></CkeditorEditor>
13
+ <div v-if="error" class="invalid-feedback d-block form-text">{{ error }}</div>
14
+ </div>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup>
19
+ import CkeditorEditor from './CkeditorEditor.vue';
20
+
21
+ defineProps({
22
+ modelValue: { type: String, default: '' },
23
+ name: { type: String, required: true },
24
+ label: { type: String, required: true },
25
+ error: { type: String, default: '' },
26
+ config: { type: Object, default: null },
27
+ uploadUrl: { type: String, default: '/admin/wysiwyg-media' },
28
+ });
29
+
30
+ defineEmits(['update:modelValue']);
31
+ </script>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <div class="mb-3 row align-items-center">
3
+ <label :for="name" class="col-md-3 col-form-label text-md-end">{{ label }}</label>
4
+ <div class="col-md-9 col-xl-8">
5
+ <TiptapEditor
6
+ :id="name"
7
+ :model-value="modelValue"
8
+ :name="name"
9
+ :upload-url="uploadUrl"
10
+ v-bind="config ? { config } : {}"
11
+ @update:model-value="$emit('update:modelValue', $event)"
12
+ ></TiptapEditor>
13
+ <div v-if="error" class="invalid-feedback d-block form-text">{{ error }}</div>
14
+ </div>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup>
19
+ import TiptapEditor from './TiptapEditor.vue';
20
+
21
+ defineProps({
22
+ modelValue: { type: String, default: '' },
23
+ name: { type: String, required: true },
24
+ label: { type: String, required: true },
25
+ error: { type: String, default: '' },
26
+ config: { type: Object, default: null },
27
+ uploadUrl: { type: String, default: '/admin/wysiwyg-media' },
28
+ });
29
+
30
+ defineEmits(['update:modelValue']);
31
+ </script>
@@ -0,0 +1,90 @@
1
+ <template>
2
+ <div v-if="locales.length > 0" class="row mb-2 localization-bar">
3
+ <div class="offset-md-3 col-md-9 col-xl-8 text-start" :class="{ 'd-none': onSmallScreen }">
4
+ <small v-if="!isFormLocalized"
5
+ >{{ currentlyEditingText }}<span v-if="otherLocales.length > 1"> {{ moreCanBeManagedText }}</span> |
6
+ <a href="#" @click.prevent="$emit('show-localization')">{{
7
+ translations.manage_translations
8
+ }}</a></small
9
+ >
10
+ <i v-if="!isFormLocalized && showLocalizedValidationError" class="localization-error"></i>
11
+
12
+ <div v-if="isFormLocalized" class="d-flex align-items-center gap-2">
13
+ <small class="text-nowrap">{{ translations.choose_translation_to_edit }}</small>
14
+ <Multiselect
15
+ :model-value="currentLocale"
16
+ :options="localeOptions"
17
+ :custom-label="(l) => l.toUpperCase()"
18
+ :searchable="false"
19
+ :allow-empty="false"
20
+ class="locale-selector"
21
+ @update:model-value="$emit('update:currentLocale', $event)"
22
+ />
23
+ <i v-if="showLocalizedValidationError" class="localization-error"></i>
24
+ <small
25
+ ><a href="#" @click.prevent="$emit('hide-localization')">{{ translations.hide }}</a></small
26
+ >
27
+ </div>
28
+ </div>
29
+
30
+ <div
31
+ v-if="onSmallScreen"
32
+ class="col"
33
+ :class="{ 'language-mobile': true, 'text-danger': !isFormLocalized && showLocalizedValidationError }"
34
+ >
35
+ <small
36
+ >{{ translations.choose_translation_to_edit }}
37
+ <Multiselect
38
+ :model-value="currentLocale"
39
+ :options="[defaultLocale, ...otherLocales]"
40
+ :custom-label="(l) => l.toUpperCase()"
41
+ :searchable="false"
42
+ :allow-empty="false"
43
+ class="locale-selector"
44
+ @update:model-value="$emit('update:currentLocale', $event)"
45
+ />
46
+ <i v-if="showLocalizedValidationError" class="localization-error"></i>
47
+ </small>
48
+ </div>
49
+ </div>
50
+ </template>
51
+
52
+ <script setup>
53
+ import { computed } from 'vue';
54
+ import Multiselect from 'vue-multiselect';
55
+
56
+ const props = defineProps({
57
+ translations: { type: Object, required: true },
58
+ locales: { type: Array, required: true },
59
+ defaultLocale: { type: String, required: true },
60
+ otherLocales: { type: Array, required: true },
61
+ isFormLocalized: { type: Boolean, required: true },
62
+ currentLocale: { type: String, required: true },
63
+ onSmallScreen: { type: Boolean, default: false },
64
+ showLocalizedValidationError: { type: Boolean, default: false },
65
+ });
66
+
67
+ defineEmits(['show-localization', 'hide-localization', 'update:currentLocale']);
68
+
69
+ const currentlyEditingText = computed(() => {
70
+ if (!props.translations.currently_editing_translation) return '';
71
+ return props.translations.currently_editing_translation.replace(':locale', props.defaultLocale.toUpperCase());
72
+ });
73
+
74
+ const moreCanBeManagedText = computed(() => {
75
+ if (!props.translations.more_can_be_managed) return '';
76
+ return props.translations.more_can_be_managed.replace(':count', String(props.otherLocales.length));
77
+ });
78
+
79
+ const localeOptions = computed(() => {
80
+ return props.onSmallScreen ? [props.defaultLocale, ...props.otherLocales] : [...props.otherLocales];
81
+ });
82
+ </script>
83
+
84
+ <style scoped>
85
+ .locale-selector {
86
+ display: inline-block;
87
+ width: 75px;
88
+ vertical-align: middle;
89
+ }
90
+ </style>
@@ -0,0 +1,317 @@
1
+ <template>
2
+ <div class="base-upload">
3
+ <label v-if="label" class="form-label d-flex align-items-center">
4
+ <i :class="labelIcon" class="me-1 text-body-secondary"></i>
5
+ <span class="fw-semibold upload-label-title">{{ label }}</span>
6
+ <small v-for="(c, i) in constraintParts" :key="i" class="text-body-secondary fw-light ms-1"
7
+ >({{ c }})</small
8
+ >
9
+ <i v-if="isPrivate" class="fa fa-lock ms-auto text-body-secondary"></i>
10
+ </label>
11
+ <div v-bind="getRootProps()" class="vue3-dropzone dropzone" :class="{ 'dz-drag-hover': isDragActive }">
12
+ <input v-bind="getInputProps()" />
13
+ <input type="hidden" name="collection" :value="collection" />
14
+
15
+ <div v-if="visibleUploadedMedia.length === 0 && uploadedFiles.length === 0" class="dz-message">
16
+ <i class="fa-solid fa-cloud-arrow-up"></i>
17
+ <p>Drop files here or click to upload</p>
18
+ </div>
19
+
20
+ <div
21
+ v-for="(file, index) in mutableUploadedMedia"
22
+ :key="'existing-' + (file.id || index)"
23
+ class="dz-preview dz-file-preview dz-existing"
24
+ :class="{ 'dz-deleted': file.deleted }"
25
+ >
26
+ <div class="dz-image">
27
+ <img v-if="isImage(file)" :src="file.thumb_url" :alt="file.name" />
28
+ <div v-else class="dz-file-icon">
29
+ <i :class="getIconClass(file)"></i>
30
+ <p>{{ file.name }}</p>
31
+ </div>
32
+ </div>
33
+ <div class="dz-hover-overlay" @click.stop>
34
+ <span class="dz-file-size">{{ formatSize(file.size) }}</span>
35
+ <span class="dz-file-name">{{ file.name }}</span>
36
+ <button type="button" class="dz-remove-btn" @click.stop="removeUploadedFile(index)">REMOVE</button>
37
+ </div>
38
+ </div>
39
+
40
+ <div
41
+ v-for="(entry, index) in uploadedFiles"
42
+ :key="'new-' + index"
43
+ class="dz-preview dz-file-preview"
44
+ :class="{ 'dz-success': entry.showSuccess, 'dz-error': entry.showError }"
45
+ >
46
+ <div class="dz-image">
47
+ <img v-if="entry.previewUrl" :src="entry.previewUrl" :alt="entry.file.name" />
48
+ <div v-else class="dz-file-icon">
49
+ <i :class="getIconClassFromType(entry.file.type)"></i>
50
+ <p>{{ entry.file.name }}</p>
51
+ </div>
52
+ </div>
53
+ <div v-if="entry.uploading" class="dz-progress">
54
+ <span class="dz-upload" :style="{ width: entry.progress + '%' }"></span>
55
+ </div>
56
+ <div v-if="entry.showSuccess" class="dz-success-mark">
57
+ <i class="fa-solid fa-check"></i>
58
+ </div>
59
+ <div v-if="entry.showError" class="dz-error-mark">
60
+ <i class="fa-solid fa-xmark"></i>
61
+ </div>
62
+ <div v-if="!entry.uploading && !entry.showSuccess" class="dz-hover-overlay" @click.stop>
63
+ <span class="dz-file-size">{{ formatSize(entry.file.size) }}</span>
64
+ <span class="dz-file-name">{{ entry.file.name }}</span>
65
+ <button type="button" class="dz-remove-btn" @click.stop="removeNewFile(index)">REMOVE</button>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </template>
71
+
72
+ <script setup>
73
+ import { ref, reactive, computed, onMounted } from 'vue';
74
+ import { useDropzone } from 'vue3-dropzone';
75
+ import { notifyError } from '../../utils/notify.js';
76
+ const axios = window.axios;
77
+
78
+ const props = defineProps({
79
+ url: {
80
+ type: String,
81
+ required: true,
82
+ },
83
+ collection: {
84
+ type: String,
85
+ required: true,
86
+ },
87
+ label: {
88
+ type: String,
89
+ default: '',
90
+ },
91
+ maxNumberOfFiles: {
92
+ type: Number,
93
+ default: 1,
94
+ },
95
+ maxFileSizeInMb: {
96
+ type: Number,
97
+ default: 2,
98
+ },
99
+ acceptedFileTypes: {
100
+ type: String,
101
+ default: undefined,
102
+ },
103
+ isPrivate: {
104
+ type: Boolean,
105
+ default: false,
106
+ },
107
+ thumbnailWidth: {
108
+ type: Number,
109
+ default: 200,
110
+ },
111
+ uploadedMedia: {
112
+ type: Array,
113
+ default: () => [],
114
+ },
115
+ });
116
+
117
+ const labelIcon = computed(() => {
118
+ if (!props.acceptedFileTypes) return 'fa fa-file';
119
+ return props.acceptedFileTypes.includes('image') ? 'fa fa-file-image' : 'fa fa-file';
120
+ });
121
+
122
+ const constraintParts = computed(() => {
123
+ const parts = [];
124
+ if (props.maxNumberOfFiles > 0) {
125
+ parts.push(`max. ${props.maxNumberOfFiles} file${props.maxNumberOfFiles > 1 ? 's' : ''}`);
126
+ }
127
+ if (props.maxFileSizeInMb > 0) {
128
+ parts.push(`max. ${props.maxFileSizeInMb.toFixed(2)} MB`);
129
+ }
130
+ return parts;
131
+ });
132
+
133
+ const uploadedFiles = ref([]);
134
+ const mutableUploadedMedia = ref([]);
135
+
136
+ function getFileObj(file) {
137
+ return {
138
+ id: file.id,
139
+ collection_name: props.collection,
140
+ name: file.custom_properties?.name ?? file.file_name ?? file.name,
141
+ type: file.mime_type ?? file.type,
142
+ size: file.size,
143
+ url: file.original_url ?? file.url,
144
+ thumb_url: file.thumb_url ?? file.original_url ?? file.url,
145
+ };
146
+ }
147
+
148
+ function getMutableUploadedMedia() {
149
+ const files = [];
150
+ props.uploadedMedia.forEach((file) => {
151
+ files.push(getFileObj(file));
152
+ });
153
+ return files;
154
+ }
155
+
156
+ const visibleUploadedMedia = computed(() => mutableUploadedMedia.value.filter((f) => !f.deleted));
157
+
158
+ onMounted(() => {
159
+ mutableUploadedMedia.value = getMutableUploadedMedia();
160
+ });
161
+
162
+ function uploadFile(file) {
163
+ const entry = reactive({
164
+ file,
165
+ response: null,
166
+ previewUrl: file.type && file.type.startsWith('image/') ? URL.createObjectURL(file) : null,
167
+ uploading: true,
168
+ progress: 0,
169
+ showSuccess: false,
170
+ showError: false,
171
+ });
172
+
173
+ uploadedFiles.value.push(entry);
174
+
175
+ const formData = new FormData();
176
+ formData.append('file', file);
177
+
178
+ const csrfToken = document.head.querySelector('meta[name="csrf-token"]');
179
+
180
+ axios
181
+ .post(props.url, formData, {
182
+ headers: {
183
+ 'Content-Type': 'multipart/form-data',
184
+ ...(csrfToken ? { 'X-CSRF-TOKEN': csrfToken.getAttribute('content') } : {}),
185
+ },
186
+ onUploadProgress(progressEvent) {
187
+ if (progressEvent.total) {
188
+ entry.progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
189
+ }
190
+ },
191
+ })
192
+ .then((response) => {
193
+ entry.response = response.data;
194
+ entry.uploading = false;
195
+ entry.showSuccess = true;
196
+ setTimeout(() => {
197
+ entry.showSuccess = false;
198
+ }, 1500);
199
+ })
200
+ .catch((error) => {
201
+ const errorMessage = error.response?.data?.message || error.message || 'Upload failed';
202
+ notifyError(errorMessage);
203
+ entry.uploading = false;
204
+ entry.showError = true;
205
+ setTimeout(() => {
206
+ const idx = uploadedFiles.value.indexOf(entry);
207
+ if (idx !== -1) {
208
+ uploadedFiles.value.splice(idx, 1);
209
+ }
210
+ }, 2000);
211
+ });
212
+ }
213
+
214
+ function onDrop(acceptedFiles, fileRejections) {
215
+ fileRejections.forEach((rejection) => {
216
+ const messages = rejection.errors.map((e) => e?.message || 'Invalid file').join(', ');
217
+ notifyError(`${rejection.file.name}: ${messages}`);
218
+ });
219
+
220
+ if (props.maxNumberOfFiles > 0) {
221
+ const currentCount = visibleUploadedMedia.value.length + uploadedFiles.value.length;
222
+ const available = props.maxNumberOfFiles - currentCount;
223
+ if (available <= 0) {
224
+ notifyError(`Maximum ${props.maxNumberOfFiles} file${props.maxNumberOfFiles > 1 ? 's' : ''} allowed`);
225
+ return;
226
+ }
227
+ if (acceptedFiles.length > available) {
228
+ notifyError(`Only ${available} more file${available > 1 ? 's' : ''} can be added`);
229
+ acceptedFiles = acceptedFiles.slice(0, available);
230
+ }
231
+ }
232
+
233
+ acceptedFiles.forEach((file) => {
234
+ uploadFile(file);
235
+ });
236
+ }
237
+
238
+ const dropzoneOptions = {
239
+ onDrop,
240
+ accept: props.acceptedFileTypes ? props.acceptedFileTypes.split(',').map((t) => t.trim()) : undefined,
241
+ maxSize: props.maxFileSizeInMb > 0 ? props.maxFileSizeInMb * 1024 * 1024 : Infinity,
242
+ multiple: props.maxNumberOfFiles !== 1,
243
+ };
244
+
245
+ const { getRootProps, getInputProps, isDragActive } = useDropzone(dropzoneOptions);
246
+
247
+ function removeUploadedFile(index) {
248
+ if (mutableUploadedMedia.value[index]) {
249
+ mutableUploadedMedia.value[index].deleted = true;
250
+ }
251
+ }
252
+
253
+ function removeNewFile(index) {
254
+ uploadedFiles.value.splice(index, 1);
255
+ }
256
+
257
+ function formatSize(bytes) {
258
+ if (!bytes) return '';
259
+ if (bytes < 1024) return bytes + ' B';
260
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
261
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
262
+ }
263
+
264
+ function isImage(file) {
265
+ return file.type && file.type.includes('image');
266
+ }
267
+
268
+ function getIconClass(file) {
269
+ return getIconClassFromType(file.type || '');
270
+ }
271
+
272
+ function getIconClassFromType(type) {
273
+ if (type.includes('pdf')) return 'fa-solid fa-file-pdf';
274
+ if (type.includes('word')) return 'fa-solid fa-file-word';
275
+ if (type.includes('spreadsheet') || type.includes('csv')) return 'fa-solid fa-file-excel';
276
+ if (type.includes('presentation')) return 'fa-solid fa-file-powerpoint';
277
+ if (type.includes('video')) return 'fa-solid fa-file-video';
278
+ if (type.includes('text')) return 'fa-solid fa-file-lines';
279
+ if (type.includes('zip') || type.includes('rar')) return 'fa-solid fa-file-zipper';
280
+ return 'fa-solid fa-file';
281
+ }
282
+
283
+ function getFiles() {
284
+ const files = [];
285
+
286
+ mutableUploadedMedia.value.forEach((file) => {
287
+ if (file.deleted) {
288
+ files.push({
289
+ id: file.id,
290
+ collection_name: props.collection,
291
+ action: 'delete',
292
+ });
293
+ }
294
+ });
295
+
296
+ uploadedFiles.value.forEach((entry) => {
297
+ if (entry.response?.path) {
298
+ files.push({
299
+ id: entry.file.id,
300
+ collection_name: props.collection,
301
+ path: entry.response.path,
302
+ action: 'add',
303
+ meta_data: {
304
+ name: entry.file.name,
305
+ file_name: entry.file.name,
306
+ width: entry.file.width,
307
+ height: entry.file.height,
308
+ },
309
+ });
310
+ }
311
+ });
312
+
313
+ return files;
314
+ }
315
+
316
+ defineExpose({ getFiles });
317
+ </script>