inertia-bootstrap-forms 2.1.20 → 3.0.0

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 (59) hide show
  1. package/README.fa.md +468 -0
  2. package/README.md +322 -714
  3. package/dist/inertia-bootstrap-forms.cjs.js +1 -0
  4. package/dist/inertia-bootstrap-forms.es.js +3524 -30
  5. package/index.d.ts +143 -507
  6. package/package.json +89 -70
  7. package/src/AmountInput.vue +81 -76
  8. package/src/CaptchaInput.vue +87 -88
  9. package/src/CheckboxButtonInput.vue +56 -69
  10. package/src/CheckboxInput.vue +60 -79
  11. package/src/CheckboxToggle.vue +53 -136
  12. package/src/ChipsInput.vue +157 -0
  13. package/src/ComboboxInput.vue +556 -0
  14. package/src/DropzoneInput.vue +125 -103
  15. package/src/EditorInput.vue +47 -106
  16. package/src/EmailInput.vue +16 -15
  17. package/src/FileInput.vue +189 -179
  18. package/src/FormAdornInput.vue +75 -0
  19. package/src/FormContainer.vue +288 -251
  20. package/src/FormField.vue +59 -0
  21. package/src/FormLabel.vue +27 -27
  22. package/src/GroupControl.vue +55 -53
  23. package/src/LocationInput.vue +79 -72
  24. package/src/MobileInput.vue +21 -13
  25. package/src/MultiQuantityInput.vue +109 -181
  26. package/src/OTPInput.vue +144 -76
  27. package/src/PasswordInput.vue +151 -17
  28. package/src/PersianDatePickerInput.vue +63 -166
  29. package/src/QuantityInput.vue +100 -119
  30. package/src/RangeInput.vue +96 -0
  31. package/src/SimpleUploader.vue +8 -143
  32. package/src/StarRatingInput.vue +114 -138
  33. package/src/SubmitButton.vue +39 -24
  34. package/src/SwitchInput.vue +58 -83
  35. package/src/TelInput.vue +86 -120
  36. package/src/TextAreaInput.vue +29 -33
  37. package/src/TextInput.vue +42 -51
  38. package/src/UppyInput.vue +237 -324
  39. package/src/composables/useBsInstance.js +66 -0
  40. package/src/composables/useChoice.js +34 -0
  41. package/src/composables/useDisposables.js +59 -0
  42. package/src/composables/useField.js +121 -0
  43. package/src/composables/useFormApi.js +12 -0
  44. package/src/i18n.js +120 -0
  45. package/src/index.js +123 -42
  46. package/src/utils/bootstrap.js +48 -0
  47. package/src/utils/digits.js +21 -0
  48. package/src/utils/http.js +15 -0
  49. package/dist/choices-KiM-I_0P.js +0 -2955
  50. package/dist/dropdown-yh2aQrbw-XuCU1v1F.js +0 -1543
  51. package/dist/index-BRtKc2RA.js +0 -604
  52. package/dist/index-_x4UqcXz.js +0 -4938
  53. package/dist/inertia-bootstrap-forms.umd.js +0 -19
  54. package/dist/range-slider-pips-DTDOg3Q9.js +0 -3506
  55. package/dist/style.css +0 -1
  56. package/src/css/from-select.scss +0 -384
  57. package/src/css/loader.gif +0 -0
  58. package/src/css/location.scss +0 -44
  59. package/src/css/simple-upload.scss +0 -54
package/src/UppyInput.vue CHANGED
@@ -1,324 +1,237 @@
1
- <template>
2
- <div class="uppy-input-area" ref="inputEl" v-if="uppy">
3
- <UppyContextProvider :name="'uu' + name" :uppy="uppy">
4
- <slot :uppy="uppy">
5
- <FilesList class="uppy-file-lists"/>
6
- <Dropzone class="uppy-drop-zone"/>
7
- </slot>
8
- </UppyContextProvider>
9
- <div class="uppy-input-area--caption small text-body-secondary fst-italic" v-if="restrictionCaption">{{ restrictionCaption }}</div>
10
- </div>
11
- </template>
12
-
13
- <script setup>
14
- import {computed, inject, onBeforeUnmount, onMounted, shallowRef, ref, watch} from 'vue'
15
- import {
16
- Dropzone,
17
- FilesList,
18
- UppyContextProvider,
19
- } from '@uppy/vue';
20
- import Uppy from '@uppy/core';
21
-
22
- import '@uppy/vue/css/style.css';
23
-
24
- const props = defineProps({
25
- name: {type: String, required: true},
26
- multiple: {type: Boolean, default: false},
27
- useXHR: {type: Boolean, default: true},
28
- XHRConfig: {type: Object, default: {}},
29
- url: {type: String, default: '/upload'},
30
- config: {type: Object, default: () => ({})},
31
- errorHandler: {type: Function, default: null},
32
- showRestrictionCaption: {type: Boolean, default: true},
33
- })
34
-
35
- const emits = defineEmits([
36
- 'update:modelValue', 'file-added', 'file-removed', 'beforeUpload',
37
- 'progress', 'upload', 'upload-progress', 'upload-error',
38
- 'upload-success', 'upload-pause', 'complete', 'error',
39
- 'upload-retry', 'upload-stalled', 'retry-all', 'cancel-all',
40
- 'restriction-failed',
41
- ])
42
-
43
- const restrictionCaption = ref(null);
44
- const inputEl = ref(null);
45
- const uppy = shallowRef(null);
46
- const isResetting = ref(false);
47
- const isUnmounting = ref(false);
48
- const uploadingFiles = ref(0);
49
-
50
- // Inject form and group contexts
51
- const form = inject("form", {value: {}, errors: {}, getID: n => n});
52
- const group = inject("group", null);
53
-
54
- const modelValue = computed({
55
- get() {
56
- return group?.value?.name
57
- ? group.value.getData(props.name)
58
- : form.value[props.name];
59
- },
60
- set(val) {
61
- if (group?.value?.name) {
62
- group.value.setData(props.name, val);
63
- } else {
64
- form.value[props.name] = val;
65
- }
66
- emits("update:modelValue", val);
67
- }
68
- });
69
-
70
- uppy.value = new Uppy({
71
- id: props.name,
72
- autoProceed: true,
73
- ...props.config,
74
- restrictions: {
75
- maxNumberOfFiles: props.multiple ? null : 1,
76
- ...(props.config?.restrictions || {})
77
- },
78
- });
79
-
80
- uppy.value.on('upload', () => {
81
- uploadingFiles.value = uppy.value.getFiles().filter(f => !f.progress.uploadComplete).length;
82
- emits('upload');
83
- });
84
-
85
- uppy.value.on('before-upload', (files) => {
86
- emits('beforeUpload', files);
87
- });
88
-
89
- uppy.value.on('upload-success', (file, response) => {
90
- uploadingFiles.value = Math.max(0, uploadingFiles.value - 1);
91
-
92
- const result = response.body ?? response;
93
- if (props.multiple) {
94
- const currentValues = Array.isArray(modelValue.value) ? modelValue.value : [];
95
- modelValue.value = [...currentValues, result];
96
- } else {
97
- modelValue.value = result;
98
- }
99
- emits('upload-success', file, response);
100
- });
101
-
102
- uppy.value.on('file-added', (file) => emits('file-added', file));
103
-
104
- uppy.value.on('file-removed', (file) => {
105
- const serverResponse = file.response?.body ?? file.response;
106
-
107
- if (props.multiple && Array.isArray(modelValue.value)) {
108
- modelValue.value = modelValue.value.filter(item => {
109
- return JSON.stringify(item) !== JSON.stringify(serverResponse);
110
- });
111
- } else {
112
- modelValue.value = null;
113
- }
114
-
115
- if (serverResponse && !isResetting.value && !isUnmounting.value) {
116
- fetch(props.url, {
117
- method: 'DELETE',
118
- headers: {
119
- 'X-Requested-With': 'XMLHttpRequest',
120
- 'Content-Type': 'application/json',
121
- },
122
- body: JSON.stringify(serverResponse),
123
- });
124
- }
125
-
126
- emits('file-removed', file);
127
- });
128
-
129
- uppy.value.on('progress', (progress) => {
130
- if (progress <= 0) {
131
- form.value['uploading'] = null;
132
- } else if (progress >= 100) {
133
- // هنوز منتظر جواب سرور - uploading رو null نکن
134
- form.value['uploading'] = uploadingFiles.value > 0 ? 99 : null;
135
- } else {
136
- form.value['uploading'] = progress;
137
- }
138
-
139
- emits('progress', progress)
140
- });
141
- uppy.value.on('upload-progress', (file, progress) => emits('upload-progress', file, progress));
142
- uppy.value.on('upload-pause', (file, progress) => emits('upload-pause', file, progress));
143
- uppy.value.on('cancel-all', () => emits('cancel-all'));
144
- uppy.value.on('retry-all', () => emits('retry-all'));
145
- uppy.value.on('upload-stalled', (error, files) => emits('upload-stalled', error, files));
146
- uppy.value.on('upload-retry', (file) => emits('upload-retry', file));
147
-
148
- uppy.value.on('complete', (result) => {
149
- uploadingFiles.value = 0;
150
- form.value['uploading'] = null;
151
- emits('complete', result);
152
- });
153
-
154
- uppy.value.on('error', (error) => {
155
- emits('error', error)
156
- });
157
-
158
- uppy.value.on('upload-error', (file, error, response) => {
159
- uploadingFiles.value = Math.max(0, uploadingFiles.value - 1);
160
-
161
- const errorMessage = JSON.parse(response.response)?.message ?? error;
162
- handleError(errorMessage);
163
- emits('upload-error', file, error, response, errorMessage)
164
- });
165
-
166
- uppy.value.on('restriction-failed', (file, error) => {
167
- handleError(error.message);
168
- emits('restriction-failed', file, error);
169
- });
170
-
171
- watch(() => form.value?.wasSuccessful, (newVal, oldVal) => {
172
- if (newVal === true && oldVal === false) {
173
- resetUppy();
174
- }
175
- });
176
-
177
- onMounted(() => {
178
- if (props.useXHR) {
179
- import('@uppy/xhr-upload').then(module => {
180
- const XHR = module.default;
181
- uppy.value.use(XHR, {
182
- method: 'POST',
183
- endpoint: props.url,
184
- headers: {
185
- 'accept': 'application/json',
186
- },
187
- ...props.XHRConfig
188
- });
189
- });
190
- }
191
-
192
- if (uppy.value?.opts?.restrictions) {
193
- restrictionCaption.value = buildRestrictionsCaption(uppy.value.opts.restrictions)
194
- }
195
-
196
-
197
- if (inputEl && uppy.value?.opts?.locale?.strings?.dropzoneText) {
198
- inputEl.value?.querySelectorAll('[data-uppy-element="dropzone"]>div>div>p')?.forEach(el => {
199
- el.innerHTML = uppy.value?.opts?.locale?.strings?.dropzoneText;
200
- });
201
- }
202
- });
203
-
204
- onBeforeUnmount(() => {
205
- isUnmounting.value = true;
206
- if (uppy.value) {
207
- uppy.value.destroy();
208
- }
209
- });
210
-
211
- function resetUppy() {
212
- if (uppy.value) {
213
- isResetting.value = true;
214
- uppy.value.cancelAll();
215
- isResetting.value = false;
216
- modelValue.value = props.multiple ? [] : null;
217
- }
218
- }
219
-
220
- function handleError(error) {
221
- if (props.errorHandler) {
222
- props.errorHandler(error);
223
- } else if (error) {
224
- showError(error);
225
- }
226
- }
227
-
228
- function showError(message) {
229
- if(typeof window !== 'undefined'){
230
- const errorEl = document?.createElement('div');
231
- errorEl.textContent = message;
232
- errorEl.className = 'uppy-error';
233
-
234
- if (inputEl.value) {
235
- inputEl.value.prepend(errorEl);
236
- setTimeout(() => errorEl.remove(), 3000);
237
- }
238
- }
239
- }
240
-
241
- const niceBytesUnits = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
242
-
243
- function niceBytes(x) {
244
- let l = 0, n = parseInt(x, 10) || 0;
245
- while (n >= 1024 && ++l) {
246
- n = n / 1024;
247
- }
248
- return (n.toFixed(n < 10 && l > 0 ? 1 : 0) + niceBytesUnits[l]);
249
- }
250
-
251
- function buildRestrictionsCaption(restrictions) {
252
- if (!restrictions) return '';
253
-
254
- const {
255
- allowedFileTypes,
256
- maxFileSize,
257
- minFileSize,
258
- maxNumberOfFiles,
259
- minNumberOfFiles,
260
- maxTotalFileSize,
261
- } = restrictions;
262
-
263
- const parts = [];
264
-
265
- if (allowedFileTypes && allowedFileTypes.length) {
266
- const types = allowedFileTypes
267
- .map(type => type.replace('.', ''))
268
- .join('، ');
269
- parts.push(`فقط فایل‌های ${types}`);
270
- }
271
-
272
- if (maxNumberOfFiles && maxNumberOfFiles > 1) {
273
- parts.push(`امکان انتخاب حداکثر ${maxNumberOfFiles} فایل`);
274
- }
275
-
276
- if (minNumberOfFiles) {
277
- parts.push(`حداقل ${minNumberOfFiles} فایل نیاز است`);
278
- }
279
-
280
- if (maxFileSize) {
281
- parts.push(`با حداکثر حجم ${niceBytes(maxFileSize)} برای هر فایل`);
282
- }
283
-
284
- if (minFileSize) {
285
- parts.push(`با حداقل حجم ${niceBytes(minFileSize)} برای هر فایل`);
286
- }
287
-
288
- if (maxTotalFileSize) {
289
- parts.push(`و مجموع حجم کل حداکثر ${niceBytes(maxTotalFileSize)}`);
290
- }
291
-
292
- if (!parts.length) return '';
293
-
294
- return parts.join('، ') + ' مجاز است.';
295
- }
296
- </script>
297
-
298
- <style>
299
- .uppy-reset p {
300
- margin-bottom: 0;
301
- }
302
-
303
- .uppy-file-lists > ul {
304
- list-style: none;
305
- padding: 0;
306
- margin: 0;
307
- }
308
-
309
- .uppy-input-area .uppy-error {
310
- display: block;
311
- padding: 5px;
312
- background-color: #f8d7da;
313
- border: 1px solid #f5c2c7;
314
- color: #842029;
315
- text-align: center;
316
- border-radius: 0.5rem;
317
- margin-bottom: 5px;
318
- }
319
-
320
- .uppy-input-area .uppy-input-area--caption {
321
- text-align: center;
322
- margin: 5px 0;
323
- }
324
- </style>
1
+ <script>
2
+ import { computed, defineAsyncComponent, defineComponent, onMounted, ref, shallowRef, watch } from 'vue';
3
+ import { fieldProps, useField } from './composables/useField.js';
4
+ import { useDisposables } from './composables/useDisposables.js';
5
+ import { useI18n } from './i18n.js';
6
+ import { laravelHeaders } from './utils/http.js';
7
+
8
+ const UppyContextProvider = defineAsyncComponent(() => import('@uppy/vue').then((m) => m.UppyContextProvider));
9
+ const FilesList = defineAsyncComponent(() => import('@uppy/vue').then((m) => m.FilesList));
10
+ const UppyDropzone = defineAsyncComponent(() => import('@uppy/vue').then((m) => m.Dropzone));
11
+
12
+ const UNITS = ['bytes', 'KB', 'MB', 'GB', 'TB'];
13
+ function niceBytes(x) {
14
+ let l = 0;
15
+ let n = parseInt(x, 10) || 0;
16
+ while (n >= 1024 && l < UNITS.length - 1) {
17
+ n /= 1024;
18
+ l++;
19
+ }
20
+ return `${n.toFixed(n < 10 && l > 0 ? 1 : 0)}${UNITS[l]}`;
21
+ }
22
+
23
+ /**
24
+ * Uppy based uploader. Import `@uppy/vue/css/style.css` in your application.
25
+ * Uppy is created after mount and destroyed on unmount.
26
+ */
27
+ export default defineComponent({
28
+ name: 'UppyInput',
29
+ components: { UppyContextProvider, FilesList, UppyDropzone },
30
+ emits: [
31
+ 'update:modelValue', 'change', 'ready', 'file-added', 'file-removed', 'beforeUpload', 'progress', 'upload',
32
+ 'upload-progress', 'upload-error', 'upload-success', 'upload-pause', 'complete', 'error', 'upload-retry',
33
+ 'upload-stalled', 'retry-all', 'cancel-all', 'restriction-failed',
34
+ ],
35
+ props: {
36
+ ...fieldProps,
37
+ multiple: { type: Boolean, default: false },
38
+ useXHR: { type: Boolean, default: true },
39
+ XHRConfig: { type: Object, default: () => ({}) },
40
+ url: { type: String, default: '/upload' },
41
+ /** Uppy core options. */
42
+ config: { type: Object, default: () => ({}) },
43
+ errorHandler: { type: Function, default: null },
44
+ showRestrictionCaption: { type: Boolean, default: true },
45
+ locale: { type: String, default: null },
46
+ },
47
+ setup(props, { emit, expose }) {
48
+ const field = useField(props, emit);
49
+ const { t } = useI18n(() => props.locale);
50
+ const d = useDisposables();
51
+
52
+ const uppy = shallowRef(null);
53
+ const area = ref(null);
54
+ const caption = ref(null);
55
+ const errorMessage = ref(null);
56
+ const uploadingFiles = ref(0);
57
+ let resetting = false;
58
+ let unmounting = false;
59
+
60
+ const setUploading = (v) => {
61
+ if (field.form.value) field.form.value.uploading = v;
62
+ };
63
+
64
+ function showError(message) {
65
+ if (props.errorHandler) return props.errorHandler(message);
66
+ if (!message) return;
67
+ errorMessage.value = message;
68
+ d.timeout(() => (errorMessage.value = null), 3000);
69
+ }
70
+
71
+ function buildCaption(r) {
72
+ if (!r) return '';
73
+ const parts = [];
74
+
75
+ if (r.allowedFileTypes?.length) {
76
+ parts.push(t('uppy.types', { types: r.allowedFileTypes.map((x) => x.replace('.', '')).join(', ') }));
77
+ }
78
+ if (r.maxNumberOfFiles && r.maxNumberOfFiles > 1) parts.push(t('uppy.maxFiles', { count: r.maxNumberOfFiles }));
79
+ if (r.minNumberOfFiles) parts.push(t('uppy.minFiles', { count: r.minNumberOfFiles }));
80
+ if (r.maxFileSize) parts.push(t('uppy.maxSize', { size: niceBytes(r.maxFileSize) }));
81
+ if (r.minFileSize) parts.push(t('uppy.minSize', { size: niceBytes(r.minFileSize) }));
82
+ if (r.maxTotalFileSize) parts.push(t('uppy.maxTotal', { size: niceBytes(r.maxTotalFileSize) }));
83
+
84
+ return parts.length ? `${parts.join(', ')} ${t('uppy.suffix')}` : '';
85
+ }
86
+
87
+ onMounted(async () => {
88
+ const { default: Uppy } = await import('@uppy/core');
89
+ if (d.isDisposed()) return;
90
+
91
+ const instance = new Uppy({
92
+ id: props.name ?? 'uppy',
93
+ autoProceed: true,
94
+ ...props.config,
95
+ restrictions: {
96
+ maxNumberOfFiles: props.multiple ? null : 1,
97
+ ...(props.config?.restrictions || {}),
98
+ },
99
+ });
100
+
101
+ instance.on('upload', () => {
102
+ uploadingFiles.value = instance.getFiles().filter((f) => !f.progress.uploadComplete).length;
103
+ emit('upload');
104
+ });
105
+ instance.on('before-upload', (files) => emit('beforeUpload', files));
106
+
107
+ instance.on('upload-success', (file, response) => {
108
+ uploadingFiles.value = Math.max(0, uploadingFiles.value - 1);
109
+ const result = response.body ?? response;
110
+
111
+ if (props.multiple) {
112
+ const current = Array.isArray(field.model.value) ? field.model.value : [];
113
+ field.model.value = [...current, result];
114
+ } else {
115
+ field.model.value = result;
116
+ }
117
+ field.emitChange(field.model.value);
118
+ emit('upload-success', file, response);
119
+ });
120
+
121
+ instance.on('file-added', (file) => emit('file-added', file));
122
+
123
+ instance.on('file-removed', (file) => {
124
+ const serverResponse = file.response?.body ?? file.response;
125
+
126
+ if (props.multiple && Array.isArray(field.model.value)) {
127
+ field.model.value = field.model.value.filter((item) => JSON.stringify(item) !== JSON.stringify(serverResponse));
128
+ } else {
129
+ field.model.value = null;
130
+ }
131
+ field.emitChange(field.model.value);
132
+
133
+ if (serverResponse && !resetting && !unmounting) {
134
+ fetch(props.url, {
135
+ method: 'DELETE',
136
+ headers: laravelHeaders({ 'Content-Type': 'application/json' }),
137
+ credentials: 'same-origin',
138
+ body: JSON.stringify(serverResponse),
139
+ }).catch(() => {});
140
+ }
141
+ emit('file-removed', file);
142
+ });
143
+
144
+ instance.on('progress', (progress) => {
145
+ if (progress <= 0) setUploading(null);
146
+ else if (progress >= 100) setUploading(uploadingFiles.value > 0 ? 99 : null); // still waiting for the server
147
+ else setUploading(progress);
148
+ emit('progress', progress);
149
+ });
150
+
151
+ instance.on('upload-progress', (file, progress) => emit('upload-progress', file, progress));
152
+ instance.on('upload-pause', (file, progress) => emit('upload-pause', file, progress));
153
+ instance.on('cancel-all', () => emit('cancel-all'));
154
+ instance.on('retry-all', () => emit('retry-all'));
155
+ instance.on('upload-stalled', (error, files) => emit('upload-stalled', error, files));
156
+ instance.on('upload-retry', (file) => emit('upload-retry', file));
157
+ instance.on('complete', (result) => {
158
+ uploadingFiles.value = 0;
159
+ setUploading(null);
160
+ emit('complete', result);
161
+ });
162
+ instance.on('error', (error) => emit('error', error));
163
+
164
+ instance.on('upload-error', (file, error, response) => {
165
+ uploadingFiles.value = Math.max(0, uploadingFiles.value - 1);
166
+ let message = error;
167
+ try {
168
+ message = JSON.parse(response.response)?.message ?? error;
169
+ } catch (e) { /* not json */ }
170
+ showError(message?.message ?? message);
171
+ emit('upload-error', file, error, response, message);
172
+ });
173
+
174
+ instance.on('restriction-failed', (file, error) => {
175
+ showError(error.message);
176
+ emit('restriction-failed', file, error);
177
+ });
178
+
179
+ if (props.useXHR) {
180
+ const { default: XHR } = await import('@uppy/xhr-upload');
181
+ if (d.isDisposed()) {
182
+ instance.destroy?.();
183
+ return;
184
+ }
185
+ instance.use(XHR, {
186
+ method: 'POST',
187
+ endpoint: props.url,
188
+ headers: laravelHeaders(),
189
+ ...props.XHRConfig,
190
+ });
191
+ }
192
+
193
+ d.add(() => {
194
+ unmounting = true;
195
+ setUploading(null);
196
+ try { instance.destroy?.(); } catch (e) { /* noop */ }
197
+ uppy.value = null;
198
+ });
199
+
200
+ uppy.value = instance;
201
+ if (props.showRestrictionCaption) caption.value = buildCaption(instance.opts?.restrictions);
202
+ emit('ready', instance);
203
+ });
204
+
205
+ // Form was submitted successfully → forget the files without touching the server
206
+ watch(
207
+ () => field.form.value?.wasSuccessful,
208
+ (now, before) => {
209
+ if (now === true && before === false && uppy.value) {
210
+ resetting = true;
211
+ uppy.value.cancelAll();
212
+ resetting = false;
213
+ field.model.value = props.multiple ? [] : null;
214
+ }
215
+ }
216
+ );
217
+
218
+ expose({ uppy: () => uppy.value });
219
+
220
+ return { ...field, uppy, area, caption, errorMessage };
221
+ },
222
+ });
223
+ </script>
224
+
225
+ <template>
226
+ <div v-if="uppy" ref="area" class="uppy-input-area" :id="inputId">
227
+ <div v-if="errorMessage" class="alert theme-danger text-center py-1 mb-2" role="alert">{{ errorMessage }}</div>
228
+ <UppyContextProvider :name="'uu' + (name ?? 'uppy')" :uppy="uppy">
229
+ <slot :uppy="uppy">
230
+ <FilesList class="uppy-file-lists" />
231
+ <UppyDropzone class="uppy-drop-zone" />
232
+ </slot>
233
+ </UppyContextProvider>
234
+ <div v-if="caption" class="uppy-input-area--caption form-text text-center fst-italic">{{ caption }}</div>
235
+ <div v-if="isInvalid" class="invalid-feedback d-block">{{ error }}</div>
236
+ </div>
237
+ </template>
@@ -0,0 +1,66 @@
1
+ import { onBeforeUnmount, onMounted, shallowRef } from 'vue';
2
+ import { loadBootstrap } from '../utils/bootstrap.js';
3
+
4
+ /**
5
+ * Creates a Bootstrap 6 plugin instance after the component is mounted and
6
+ * disposes it when the component unmounts (or when it is re-created).
7
+ *
8
+ * @param {string} name Plugin name: Combobox | Chips | OtpInput | Range
9
+ * @param {Function} getElement () => HTMLElement (usually reads a template ref)
10
+ * @param {Function} getConfig () => object (plugin options)
11
+ * @param {object} hooks { onCreate(instance, el), onDestroy(instance, el) }
12
+ */
13
+ export function useBsInstance(name, getElement, getConfig, hooks = {}) {
14
+ const instance = shallowRef(null);
15
+ let sequence = 0;
16
+ let unmounted = false;
17
+ let currentEl = null;
18
+
19
+ function destroy() {
20
+ sequence++;
21
+ const inst = instance.value;
22
+ instance.value = null;
23
+
24
+ if (inst) {
25
+ try { hooks.onDestroy?.(inst, currentEl); } catch (e) { /* noop */ }
26
+ try { inst.dispose(); } catch (e) { /* noop */ }
27
+ }
28
+ currentEl = null;
29
+ }
30
+
31
+ async function create() {
32
+ destroy();
33
+ const ticket = sequence;
34
+ const Plugin = await loadBootstrap(name);
35
+
36
+ // The component may have been unmounted / re-created while the plugin was loading
37
+ if (unmounted || ticket !== sequence) return null;
38
+
39
+ const el = getElement();
40
+ if (!el) return null;
41
+
42
+ // A data-api click can already have created an instance for this element
43
+ Plugin.getInstance?.(el)?.dispose();
44
+
45
+ currentEl = el;
46
+ const inst = new Plugin(el, getConfig());
47
+ instance.value = inst;
48
+ hooks.onCreate?.(inst, el);
49
+ return inst;
50
+ }
51
+
52
+ onMounted(() => {
53
+ unmounted = false;
54
+ create().catch((error) => {
55
+ // eslint-disable-next-line no-console
56
+ console.error(`[inertia-bootstrap-forms] Could not start Bootstrap ${name}.`, error);
57
+ });
58
+ });
59
+
60
+ onBeforeUnmount(() => {
61
+ unmounted = true;
62
+ destroy();
63
+ });
64
+
65
+ return { instance, create, destroy };
66
+ }