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
@@ -1,251 +1,288 @@
1
- <script>
2
- import {useForm} from "@inertiajs/vue3";
3
- import {computed, defineComponent, reactive, toRef} from "vue";
4
- import {Alert} from "vue3-bootstrap-components";
5
-
6
- export default defineComponent({
7
- components: {Alert},
8
- emits: ['submit', 'reset', 'onStart', 'onFinish', 'onSuccess', 'onError', 'change'],
9
- props: {
10
- url: {
11
- type: String,
12
- default: '',
13
- required: false,
14
- },
15
- method: {
16
- default: 'post'
17
- },
18
- autoTab: {
19
- type: Boolean,
20
- default: true,
21
- },
22
- only: {
23
- type: Array,
24
- default: [],
25
- required: false,
26
- },
27
- modelValue: {
28
- type: Object,
29
- default: {},
30
- required: false,
31
- },
32
- options: {
33
- type: Object,
34
- default: {},
35
- required: false,
36
- },
37
- resetOnSuccess: {
38
- type: Boolean,
39
- default: false,
40
- },
41
- submitHandler: {
42
- type: Function,
43
- default: null,
44
- required: false,
45
- },
46
- },
47
- setup(props) {
48
- const formEl = toRef('formEl');
49
- const formData = reactive(props.modelValue);
50
- const form = useForm({
51
- hasMessage: false,
52
- successMessage: null,
53
- ...props.options,
54
- ...formData
55
- });
56
-
57
- form.getID = function (el) {
58
-
59
- const output = [];
60
-
61
- if(formEl.value?.id) {
62
- output.push(formEl.value?.id)
63
- }
64
-
65
- if (typeof el === String) {
66
- output.push(el);
67
- }
68
-
69
- if (el.group && (el.group.name || el.group.groupID)) {
70
- if(el.group.name){
71
- output.push(el.group.name);
72
- }
73
-
74
- if(el.group.groupID){
75
- output.push(el.group.groupID);
76
- }
77
- }
78
-
79
- if (el.name) {
80
- output.push(el.name);
81
- }
82
-
83
- if (el.value) {
84
- output.push(el.value);
85
- }
86
-
87
- return output.join('-');
88
- };
89
-
90
- return {form, formData, formEl};
91
- },
92
- provide() {
93
- return {
94
- form: computed(() => this.form)
95
- }
96
- },
97
- watch: {
98
- form: {
99
- handler: function (newVal) {
100
- const {
101
- isDirty,
102
- errors,
103
- hasErrors,
104
- hasMessage,
105
- successMessage,
106
- processing,
107
- progress,
108
- wasSuccessful,
109
- uploading,
110
- recentlySuccessful,
111
- __rememberable,
112
- ...cleanedData
113
- } = newVal;
114
-
115
- this.$emit('change', cleanedData);
116
- this.$emit('update:modelValue', cleanedData)
117
- },
118
- deep: true,
119
- }
120
- },
121
- methods: {
122
- handleAutoTab(event) {
123
- if (this.formEl?.dataset.autotab !== 'true') return;
124
-
125
- const el = event.target;
126
- if (!['INPUT', 'TEXTAREA'].includes(el.tagName)) return;
127
-
128
- const maxLength = el.maxLength;
129
- if (maxLength > 0 && el.value.length >= maxLength) {
130
- this.focusNextField(el);
131
- }
132
- },
133
- handleAutoSubmitLength(event) {
134
- const el = event.target;
135
- if (!['INPUT', 'TEXTAREA'].includes(el.tagName)) return;
136
-
137
- const autoSubmitLength = parseInt(el.dataset.autoSubmitLength, 10);
138
- if (!isNaN(autoSubmitLength) && el.value.length >= autoSubmitLength) {
139
- this.submit();
140
- }
141
- },
142
- focusNextField(current) {
143
- const focusable = Array.from(
144
- this.formEl.querySelectorAll('input, select, textarea')
145
- ).filter(el =>
146
- !el.disabled &&
147
- el.tabIndex !== -1 &&
148
- el.type !== 'hidden' &&
149
- el.offsetParent !== null
150
- );
151
-
152
- const index = focusable.indexOf(current);
153
- const next = focusable[index + 1];
154
-
155
- if (next) {
156
- next.focus();
157
- if (next.tagName !== 'SELECT' && typeof next.select === 'function') {
158
- next.select();
159
- }
160
- }
161
- },
162
- reset() {
163
- this.form.reset();
164
- },
165
- async submit(event) {
166
- const formValues = this.modelValue;
167
-
168
- if (this.submitHandler) {
169
- await this.submitHandler(event);
170
- } else {
171
- this.$emit('submit', event);
172
- await this.form.transform(function (formDataValues) {
173
- delete formDataValues.hasMessage;
174
- delete formDataValues.successMessage;
175
-
176
- let data = JSON.stringify({
177
- ...formValues,
178
- ...formDataValues,
179
- });
180
-
181
- const persianDigits = [/۰/g, /۱/g, /۲/g, /۳/g, /۴/g, /۵/g, /۶/g, /۷/g, /۸/g, /۹/g];
182
- const arabicDigits = [/٠/g, /١/g, /٢/g, /٣/g, /٤/g, /٥/g, /٦/g, /٧/g, /٨/g, /٩/g];
183
-
184
- for (let i = 0; i < 10; i++) {
185
- data = data.replace(persianDigits[i], i).replace(arabicDigits[i], i);
186
- }
187
-
188
- return JSON.parse(data.toLocaleString('en-US'));
189
- }).submit(this.method.toString(), this.url, {
190
- only: this.only,
191
- onStart: () => {
192
- if (this.form.hasMessage) {
193
- this.form.hasMessage = false;
194
- }
195
- if (this.form.successMessage) {
196
- this.form.hasMessage = null;
197
- }
198
-
199
- this.$emit('onStart');
200
- this.form.clearErrors();
201
- },
202
- onFinish: (data) => {
203
- this.$emit('onFinish', data);
204
- },
205
- onError: (errors) => {
206
- this.$emit('onError', errors);
207
- },
208
- onSuccess: (data) => {
209
- if (this.resetOnSuccess) {
210
- this.form?.reset();
211
- }
212
-
213
- if (data?.props?.message) {
214
- this.form.hasMessage = true;
215
- this.form.successMessage = data?.props?.message;
216
- }
217
-
218
- this.$emit('onSuccess', data);
219
- }
220
- });
221
- }
222
- }
223
- },
224
- expose: ['submit', 'reset'],
225
- })
226
-
227
- </script>
228
-
229
- <template>
230
- <form ref="formEl"
231
- :action="url"
232
- :method="method"
233
- :data-autotab="autoTab ? 'true' : ''"
234
- @input="(event) => { handleAutoTab(event); handleAutoSubmitLength(event) }"
235
- @submit.prevent="submit"
236
- @reset="$emit('reset')"
237
- :class="{'form-processing': form.processing}"
238
- :novalidate="!!Object.values(form.errors).length">
239
- <slot name="errors">
240
- <Alert type="danger" v-if="form.hasErrors">
241
- <ul class="list-unstyled p-0 m-0 fanum">
242
- <li v-for="error in form.errors" v-html="error"></li>
243
- </ul>
244
- </Alert>
245
- </slot>
246
- <slot name="message">
247
- <Alert type="success" v-html="form.successMessage" v-if="form.hasMessage && form.successMessage"/>
248
- </slot>
249
- <slot :form="form" :submit="submit"/>
250
- </form>
251
- </template>
1
+ <script>
2
+ import { useForm } from '@inertiajs/vue3';
3
+ import { computed, defineComponent, provide, reactive, ref, watch } from 'vue';
4
+ import { toEnglishDigits } from './utils/digits.js';
5
+
6
+ const isPlainObject = (v) =>
7
+ v !== null && typeof v === 'object' && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
8
+
9
+ /** Recursively converts Persian/Arabic digits inside strings; leaves File/Blob/Date untouched. */
10
+ function normalizeDigits(value) {
11
+ if (typeof value === 'string') return toEnglishDigits(value);
12
+ if (Array.isArray(value)) return value.map(normalizeDigits);
13
+ if (isPlainObject(value)) {
14
+ return Object.keys(value).reduce((carry, key) => {
15
+ carry[key] = normalizeDigits(value[key]);
16
+ return carry;
17
+ }, {});
18
+ }
19
+ return value;
20
+ }
21
+
22
+ function safeEqual(a, b) {
23
+ try {
24
+ return JSON.stringify(a) === JSON.stringify(b);
25
+ } catch (e) {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ export default defineComponent({
31
+ name: 'FormContainer',
32
+ emits: [
33
+ 'update:modelValue',
34
+ 'change',
35
+ 'submit',
36
+ 'reset',
37
+ 'before',
38
+ 'start',
39
+ 'progress',
40
+ 'success',
41
+ 'error',
42
+ 'cancel',
43
+ 'finish',
44
+ // Legacy (v2) event names
45
+ 'onStart',
46
+ 'onFinish',
47
+ 'onSuccess',
48
+ 'onError',
49
+ ],
50
+ props: {
51
+ /** The object that holds the value of every field (v-model). */
52
+ modelValue: { type: Object, default: () => ({}) },
53
+ url: { type: String, default: '' },
54
+ method: { type: String, default: 'post' },
55
+ /** Only reload these props after a successful visit (Inertia `only`). */
56
+ only: { type: Array, default: () => [] },
57
+ /** Extra form fields that are not part of `modelValue`. */
58
+ options: { type: Object, default: () => ({}) },
59
+ /** Extra Inertia visit options (preserveScroll, preserveState, headers, …). */
60
+ visitOptions: { type: Object, default: () => ({}) },
61
+ autoTab: { type: Boolean, default: true },
62
+ resetOnSuccess: { type: Boolean, default: false },
63
+ /** Replace the default submit behaviour. Receives (event, form). */
64
+ submitHandler: { type: Function, default: null },
65
+ /** Convert Persian/Arabic digits to English digits before submitting. */
66
+ normalizeDigits: { type: Boolean, default: true },
67
+ /** Enable Bootstrap 6 client-side validation (`data-bs-validate`). */
68
+ validate: { type: Boolean, default: false },
69
+ /** Render server messages as HTML (only enable for trusted messages). */
70
+ htmlMessages: { type: Boolean, default: false },
71
+ /** Hide the built-in error / success alerts. */
72
+ hideAlerts: { type: Boolean, default: false },
73
+ },
74
+ setup(props, { emit, expose }) {
75
+ const formEl = ref(null);
76
+ const ui = reactive({ hasMessage: false, successMessage: null });
77
+
78
+ const form = useForm({
79
+ ...props.options,
80
+ ...props.modelValue,
81
+ });
82
+
83
+ // Upload components write their progress here; SubmitButton reads it.
84
+ form.uploading = null;
85
+
86
+ form.getID = function (el = {}) {
87
+ const output = [];
88
+
89
+ if (formEl.value?.id) output.push(formEl.value.id);
90
+ if (typeof el === 'string') output.push(el);
91
+
92
+ if (el.group) {
93
+ if (el.group.name) output.push(el.group.name);
94
+ if (el.group.index !== undefined && el.group.index !== null) output.push(el.group.index);
95
+ }
96
+
97
+ if (el.name) output.push(el.name);
98
+ if (el.value !== undefined && el.value !== null && el.value !== '') output.push(el.value);
99
+
100
+ return output.join('-');
101
+ };
102
+
103
+ // Keys that exist on the Inertia form object right now (its own state, methods and the initial fields).
104
+ // Fields that are not part of the initial v-model are added later by the inputs themselves.
105
+ const baseKeys = new Set(Object.keys(form));
106
+ const dynamicKeys = () =>
107
+ Object.keys(form).filter((key) => !baseKeys.has(key) && typeof form[key] !== 'function' && form[key] !== undefined);
108
+
109
+ const currentData = () => {
110
+ const data = typeof form.data === 'function'
111
+ ? { ...form.data() }
112
+ : Object.keys(form).reduce((carry, key) => {
113
+ if (baseKeys.has(key) && typeof form[key] !== 'function') carry[key] = form[key];
114
+ return carry;
115
+ }, {});
116
+
117
+ // v-model objects do not have to declare every field up front
118
+ dynamicKeys().forEach((key) => {
119
+ data[key] = form[key];
120
+ });
121
+
122
+ return data;
123
+ };
124
+
125
+ const resetAll = () => {
126
+ dynamicKeys().forEach((key) => delete form[key]);
127
+ form.reset();
128
+ };
129
+
130
+ // form -> parent (v-model)
131
+ watch(
132
+ () => currentData(),
133
+ (data) => {
134
+ emit('change', data);
135
+ emit('update:modelValue', data);
136
+ },
137
+ { deep: true }
138
+ );
139
+
140
+ // parent -> form (external changes of the v-model object)
141
+ watch(
142
+ () => props.modelValue,
143
+ (incoming) => {
144
+ if (!incoming || safeEqual(incoming, currentData())) return;
145
+ Object.keys(incoming).forEach((key) => {
146
+ if (!safeEqual(form[key], incoming[key])) form[key] = incoming[key];
147
+ });
148
+ },
149
+ { deep: true }
150
+ );
151
+
152
+ function focusNextField(current) {
153
+ const focusable = Array.from(formEl.value.querySelectorAll('input, select, textarea')).filter(
154
+ (el) => !el.disabled && el.tabIndex !== -1 && el.type !== 'hidden' && el.offsetParent !== null
155
+ );
156
+ const next = focusable[focusable.indexOf(current) + 1];
157
+
158
+ if (next) {
159
+ next.focus();
160
+ if (next.tagName !== 'SELECT' && typeof next.select === 'function') next.select();
161
+ }
162
+ }
163
+
164
+ function handleInput(event) {
165
+ const el = event.target;
166
+ if (!['INPUT', 'TEXTAREA'].includes(el.tagName)) return;
167
+
168
+ if (props.autoTab && el.maxLength > 0 && el.value.length >= el.maxLength) {
169
+ focusNextField(el);
170
+ }
171
+
172
+ const autoSubmitLength = parseInt(el.dataset.autoSubmitLength, 10);
173
+ if (!isNaN(autoSubmitLength) && el.value.length >= autoSubmitLength) {
174
+ submit();
175
+ }
176
+ }
177
+
178
+ function reset() {
179
+ resetAll();
180
+ form.clearErrors();
181
+ ui.hasMessage = false;
182
+ ui.successMessage = null;
183
+ emit('reset');
184
+ }
185
+
186
+ async function submit(event) {
187
+ if (props.submitHandler) {
188
+ return props.submitHandler(event, form);
189
+ }
190
+
191
+ emit('submit', event);
192
+
193
+ form
194
+ .transform((data) => {
195
+ const merged = { ...props.modelValue, ...data, ...currentData() };
196
+ return props.normalizeDigits ? normalizeDigits(merged) : merged;
197
+ })
198
+ .submit((props.method || 'post').toString().toLowerCase(), props.url, {
199
+ only: props.only,
200
+ ...props.visitOptions,
201
+ onBefore: (visit) => {
202
+ emit('before', visit);
203
+ return props.visitOptions.onBefore?.(visit);
204
+ },
205
+ onStart: (visit) => {
206
+ ui.hasMessage = false;
207
+ ui.successMessage = null;
208
+ form.clearErrors();
209
+ emit('start', visit);
210
+ emit('onStart', visit);
211
+ props.visitOptions.onStart?.(visit);
212
+ },
213
+ onProgress: (progress) => {
214
+ emit('progress', progress);
215
+ props.visitOptions.onProgress?.(progress);
216
+ },
217
+ onCancel: () => {
218
+ emit('cancel');
219
+ props.visitOptions.onCancel?.();
220
+ },
221
+ onFinish: (visit) => {
222
+ emit('finish', visit);
223
+ emit('onFinish', visit);
224
+ props.visitOptions.onFinish?.(visit);
225
+ },
226
+ onError: (errors) => {
227
+ emit('error', errors);
228
+ emit('onError', errors);
229
+ props.visitOptions.onError?.(errors);
230
+ },
231
+ onSuccess: (page) => {
232
+ if (props.resetOnSuccess) resetAll();
233
+
234
+ if (page?.props?.message) {
235
+ ui.hasMessage = true;
236
+ ui.successMessage = page.props.message;
237
+ }
238
+
239
+ emit('success', page);
240
+ emit('onSuccess', page);
241
+ props.visitOptions.onSuccess?.(page);
242
+ },
243
+ });
244
+ }
245
+
246
+ provide('form', computed(() => form));
247
+ provide('formApi', { submit, reset, el: formEl });
248
+
249
+ expose({ submit, reset, form, el: formEl });
250
+
251
+ return { form, formEl, ui, handleInput, reset, submit };
252
+ },
253
+ });
254
+ </script>
255
+
256
+ <template>
257
+ <form
258
+ ref="formEl"
259
+ :action="url || undefined"
260
+ :method="method"
261
+ :data-bs-validate="validate ? '' : undefined"
262
+ :novalidate="!validate || form.hasErrors ? true : undefined"
263
+ :class="{ 'form-processing': form.processing }"
264
+ @input="handleInput"
265
+ @submit.prevent="submit"
266
+ @reset.prevent="reset"
267
+ >
268
+ <template v-if="!hideAlerts">
269
+ <slot name="errors" :errors="form.errors" :form="form">
270
+ <div v-if="form.hasErrors" class="alert theme-danger" role="alert">
271
+ <ul class="list-unstyled p-0 m-0 fanum">
272
+ <li v-for="(error, key) in form.errors" :key="key">
273
+ <span v-if="htmlMessages" v-html="error"></span>
274
+ <template v-else>{{ error }}</template>
275
+ </li>
276
+ </ul>
277
+ </div>
278
+ </slot>
279
+ <slot name="message" :message="ui.successMessage">
280
+ <div v-if="ui.hasMessage && ui.successMessage" class="alert theme-success" role="status">
281
+ <span v-if="htmlMessages" v-html="ui.successMessage"></span>
282
+ <span v-else>{{ ui.successMessage }}</span>
283
+ </div>
284
+ </slot>
285
+ </template>
286
+ <slot :form="form" :submit="submit" :reset="reset" />
287
+ </form>
288
+ </template>
@@ -0,0 +1,59 @@
1
+ <script>
2
+ import { computed, defineComponent, inject, provide } from 'vue';
3
+ import * as Vue from 'vue';
4
+ import FormLabel from './FormLabel.vue';
5
+
6
+ let uid = 0;
7
+
8
+ /**
9
+ * Bootstrap 6 `.form-field` wrapper: label + control + help text + validation feedback.
10
+ * Child inputs pick up the generated `id` automatically, so the label always points to the control.
11
+ */
12
+ export default defineComponent({
13
+ name: 'FormField',
14
+ components: { FormLabel },
15
+ props: {
16
+ /** Field key. Used to read the server-side error of the field. */
17
+ name: { type: String, default: null },
18
+ label: { type: String, default: null },
19
+ help: { type: String, default: null },
20
+ /** Override the error text (otherwise it is read from the form errors). */
21
+ error: { type: String, default: null },
22
+ required: { type: Boolean, default: false },
23
+ id: { type: String, default: null },
24
+ },
25
+ setup(props) {
26
+ const form = inject('form', null);
27
+ const generated = Vue.useId ? Vue.useId() : `ff-${++uid}`;
28
+ const fieldId = computed(() => props.id || (props.name ? `${props.name}-${generated}` : generated));
29
+
30
+ provide('formField', {
31
+ get id() {
32
+ return fieldId.value;
33
+ },
34
+ get name() {
35
+ return props.name;
36
+ },
37
+ });
38
+
39
+ const errorText = computed(() => props.error ?? (props.name ? form?.value?.errors?.[props.name] : null) ?? null);
40
+
41
+ return { fieldId, errorText };
42
+ },
43
+ });
44
+ </script>
45
+
46
+ <template>
47
+ <div class="form-field">
48
+ <slot name="label">
49
+ <FormLabel v-if="label" :for="fieldId" :required="required">{{ label }}</FormLabel>
50
+ </slot>
51
+ <slot />
52
+ <slot name="help">
53
+ <div v-if="help" :id="fieldId + '-help'" class="form-text">{{ help }}</div>
54
+ </slot>
55
+ <slot name="error" :error="errorText">
56
+ <div v-if="errorText" :id="fieldId + '-error'" class="invalid-feedback d-block" role="alert">{{ errorText }}</div>
57
+ </slot>
58
+ </div>
59
+ </template>