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,190 +1,118 @@
1
1
  <script>
2
- import {Dropdown, DropdownToggle, DropdownMenu, InputGroup, InputGroupText} from 'vue3-bootstrap-components';
3
- import {computed, defineComponent, inject} from "vue";
4
- import QuantityInput from "./QuantityInput.vue";
5
- import GroupControl from "./GroupControl.vue";
6
-
2
+ import { computed, defineComponent, onMounted, ref } from 'vue';
3
+ import GroupControl from './GroupControl.vue';
4
+ import QuantityInput from './QuantityInput.vue';
5
+ import { fieldProps, useField } from './composables/useField.js';
6
+ import { useBsInstance } from './composables/useBsInstance.js';
7
+ import { useI18n } from './i18n.js';
8
+
9
+ /**
10
+ * A menu (Bootstrap 6 Menu) with one quantity stepper per option, e.g. passengers: adults / children / infants.
11
+ * The value is an object keyed by option index: { 0: 2, 1: 1, 2: 0 }.
12
+ */
7
13
  export default defineComponent({
8
- components: {GroupControl, QuantityInput, InputGroup, InputGroupText, Dropdown, DropdownToggle, DropdownMenu},
9
- props: {
10
- name: {
11
- type: String,
12
- required: true,
13
- },
14
- required: Boolean,
15
- disabled: Boolean,
16
- readonly: Boolean,
17
- options: {
18
- type: Array,
19
- default: [],
20
- required: true,
21
- },
22
- max: {
23
- type: Number,
24
- default: null,
25
- },
26
- totalMax: {
27
- type: Number,
28
- default: null,
29
- },
30
- min: {
31
- type: Number,
32
- default: 0,
33
- },
34
- totalMin: {
35
- type: Number,
36
- default: null,
14
+ name: 'MultiQuantityInput',
15
+ components: { GroupControl, QuantityInput },
16
+ emits: ['update:modelValue', 'change', 'show', 'shown', 'hide', 'hidden'],
17
+ props: {
18
+ ...fieldProps,
19
+ /** Array of strings or { name, min, max, description, html }. */
20
+ options: { type: Array, default: () => [] },
21
+ min: { type: Number, default: 0 },
22
+ max: { type: Number, default: null },
23
+ totalMin: { type: Number, default: null },
24
+ totalMax: { type: Number, default: null },
25
+ unit: { type: String, default: null },
26
+ placement: { type: String, default: 'bottom-start' },
27
+ locale: { type: String, default: null },
37
28
  },
38
- unit: {
39
- type: String,
40
- default: 'Number',
29
+ setup(props, { emit }) {
30
+ const field = useField(props, emit);
31
+ const { t } = useI18n(() => props.locale);
32
+ const toggle = ref(null);
33
+
34
+ const quantities = computed(() => {
35
+ const v = field.model.value;
36
+ return v && typeof v === 'object' ? v : {};
37
+ });
38
+
39
+ const total = computed(() =>
40
+ props.options.reduce((sum, _, i) => sum + (Number(quantities.value[i]) || 0), 0)
41
+ );
42
+
43
+ const unitText = computed(() => props.unit ?? t('quantity.unit'));
44
+ const normalized = computed(() =>
45
+ props.options.map((item, i) => {
46
+ const o = typeof item === 'object' && item !== null ? item : { name: item };
47
+ const own = Number(quantities.value[i]) || 0;
48
+ const itemMax = o.max ?? props.max;
49
+ const room = props.totalMax === null ? Infinity : own + (props.totalMax - total.value);
50
+ return {
51
+ key: i,
52
+ name: o.name ?? o.label ?? String(item),
53
+ description: o.description ?? null,
54
+ html: !!o.html,
55
+ min: o.min ?? props.min,
56
+ max: Math.min(itemMax ?? Infinity, room) === Infinity ? null : Math.min(itemMax ?? Infinity, room),
57
+ };
58
+ })
59
+ );
60
+
61
+ onMounted(() => {
62
+ if (!field.model.value || typeof field.model.value !== 'object') {
63
+ field.model.value = props.options.reduce((carry, item, i) => {
64
+ carry[i] = (typeof item === 'object' && item?.min) || props.min;
65
+ return carry;
66
+ }, {});
67
+ }
68
+ });
69
+
70
+ useBsInstance(
71
+ 'Menu',
72
+ () => toggle.value,
73
+ () => ({ autoClose: 'outside', placement: props.placement }),
74
+ {
75
+ onCreate(_, el) {
76
+ ['show', 'shown', 'hide', 'hidden'].forEach((evt) =>
77
+ el.addEventListener(`${evt}.bs.menu`, (e) => emit(evt, e))
78
+ );
79
+ },
80
+ }
81
+ );
82
+
83
+ return { ...field, t, toggle, total, unitText, normalized };
41
84
  },
42
- },
43
- setup(props) {
44
- let form = inject('form');
45
- let group = inject('group', {});
46
-
47
- if (form === undefined) {
48
- form = {
49
- errors: {},
50
- getID(name) {
51
- }
52
- };
53
- }
54
-
55
- if (!form[props.name]) {
56
- form[props.name] = props.min;
57
- }
58
-
59
- const modelValue = computed({
60
- get() {
61
- return (group.value && form.value[group.value.name]) ? group.value?.getData(props.name) : form.value[props.name];
62
- },
63
- set(value) {
64
- if (group?.value?.name) {
65
- group.value.setData(props.name, value);
66
- } else {
67
- form.value[props.name] = value;
68
- }
69
- }
70
- });
71
-
72
- return {modelValue, form};
73
- },
74
- computed: {
75
- totalQuantity() {
76
- return this.modelValue ? Object.values(this.modelValue).reduce((sum, value) => sum + value, 0) : this.options.length;
77
- }
78
- },
79
- methods: {
80
- show(){
81
- this.showDropdown = !this.showDropdown;
82
- },
83
- clickOnOutside(e){
84
- if(!this.$refs.input.contains(e.target)){
85
- this.showDropdown = false;
86
- }
87
- }
88
- },
89
- mounted() {
90
- if(typeof window !== 'undefined'){
91
- window.addEventListener('click', this.clickOnOutside);
92
- }
93
- },
94
- beforeUnmount() {
95
- if(typeof window !== 'undefined'){
96
- window.removeEventListener('click', this.clickOnOutside);
97
- }
98
- },
99
- data() {
100
- return {
101
- showDropdown : false,
102
- }
103
- }
104
- })
85
+ });
105
86
  </script>
87
+
106
88
  <template>
107
- <div ref="input" class="multi-quantity-input fanum">
108
- <button type="button" class="form-control fanum dropdown-toggle" @click.prevent="show">
109
- {{ totalQuantity }} {{ unit }}
110
- </button>
111
- <div class="multi-quantity-input--dropdown" :class="{'show': showDropdown}">
112
- <GroupControl :name="name">
113
- <div class="row align-items-center justify-content-between g-0" v-for="(item, key) in options">
114
- <div class="col-auto" v-html="item?.name || item">
115
- </div>
116
- <div class="col-auto">
117
- <QuantityInput :name="key" :min="(item.min || min)" :max="(item.max || max)"/>
118
- </div>
89
+ <div class="multi-quantity-input fanum">
90
+ <button
91
+ ref="toggle"
92
+ type="button"
93
+ class="form-control combobox-toggle"
94
+ data-bs-toggle="menu"
95
+ aria-haspopup="true"
96
+ :disabled="isDisabled || readonly"
97
+ :class="{ 'is-invalid': isInvalid }"
98
+ >
99
+ <span class="combobox-value">{{ total }} {{ unitText }}</span>
100
+ </button>
101
+ <div class="menu p-3">
102
+ <GroupControl :name="name">
103
+ <div
104
+ v-for="item in normalized"
105
+ :key="item.key"
106
+ class="d-flex align-items-center justify-content-between gap-3 py-1"
107
+ >
108
+ <div>
109
+ <span v-if="item.html" v-html="item.name"></span>
110
+ <span v-else>{{ item.name }}</span>
111
+ <div v-if="item.description" class="form-text mt-0">{{ item.description }}</div>
112
+ </div>
113
+ <QuantityInput :name="String(item.key)" :min="item.min" :max="item.max" :readonly="readonly" unit="" />
114
+ </div>
115
+ </GroupControl>
119
116
  </div>
120
- </GroupControl>
121
117
  </div>
122
- </div>
123
118
  </template>
124
- <style>
125
-
126
- .multi-quantity-input{
127
- position: relative;
128
- }
129
-
130
- .multi-quantity-input .dropdown-toggle {
131
- text-align: start;
132
- }
133
-
134
- .multi-quantity-input .dropdown-menu small {
135
- color: var(--bs-secondary-color, #cccccc)
136
- }
137
-
138
- .multi-quantity-input .input-group {
139
- align-items: center;
140
- }
141
-
142
- .multi-quantity-input .input-group .input-group-text {
143
- display: none;
144
- }
145
-
146
- .multi-quantity-input .input-group .form-control {
147
- width: 40px;
148
- padding-right: 3px;
149
- padding-left: 3px;
150
- }
151
-
152
- .multi-quantity-input .input-group .btn {
153
- display: flex;
154
- align-content: center;
155
- align-items: center;
156
- width: 25px;
157
- height: 25px;
158
- border-radius: 50% !important;
159
- padding: 5px;
160
- }
161
-
162
- .multi-quantity-input .input-group .btn,
163
- .multi-quantity-input .input-group .btn:active,
164
- .multi-quantity-input .input-group .btn:focus {
165
- background-color: var(--bs-primary, #de0021);
166
- color: #ffffff;
167
- }
168
-
169
- .multi-quantity-input .input-group .btn svg,
170
- .multi-quantity-input .input-group .btn svg path {
171
- fill: #ffffff;
172
- }
173
-
174
- .multi-quantity-input .multi-quantity-input--dropdown {
175
- display: none;
176
- position: absolute;
177
- top: 100%;
178
- left: 0;
179
- right: 0;
180
- z-index: 2;
181
- }
182
-
183
- .multi-quantity-input .multi-quantity-input--dropdown.show {
184
- color: var(--bs-body-color, #333333);
185
- background-color: var(--bs-body-bg, #ffffff);
186
- border-radius: var(--bs-border-radius, 1rem);
187
- display: block;
188
- padding: 10px 15px;
189
- }
190
- </style>
package/src/OTPInput.vue CHANGED
@@ -1,76 +1,144 @@
1
- <script>
2
- import {computed, inject, defineComponent} from "vue";
3
-
4
- export default defineComponent({
5
- props: {
6
- name: {
7
- type: String,
8
- required: true,
9
- },
10
- maxLength: {
11
- type: Number,
12
- default: 5,
13
- },
14
- placeholder: {
15
- type: String,
16
- default: '',
17
- },
18
- },
19
- computed: {
20
- inputID() {
21
- return this.form.getID(this);
22
- },
23
- placeholderLines() {
24
- if (this.placeholder) {
25
- return this.placeholder;
26
- }
27
-
28
- return '_'.repeat(this.maxLength);
29
- }
30
-
31
- },
32
- setup(props) {
33
- let form = inject('form', {
34
- errors: {},
35
- getID(name) {
36
- return name;
37
- }
38
- });
39
- let group = inject('group', {});
40
-
41
- const modelValue = computed({
42
- get() {
43
- return (group.value && form.value[group.value.name]) ? group.value?.getData(props.name) : form.value[props.name];
44
- },
45
- set(value) {
46
- if (group?.value?.name) {
47
- group.value.setData(props.name, value);
48
- } else {
49
- form.value[props.name] = value;
50
- }
51
- }
52
- });
53
-
54
- return {modelValue, form, group};
55
- },
56
- })
57
- </script>
58
- <template>
59
- <input
60
- :name="name"
61
- :id="inputID"
62
- v-model="modelValue"
63
- :class="{'is-invalid': form?.errors[name] !== undefined}"
64
- :disabled="form?.processing"
65
- :placeholder="placeholderLines"
66
- :maxlength="maxLength"
67
- :data-auto-submit-length="maxLength"
68
- type="tel"
69
- class="form-control form-control-otp fanum">
70
- </template>
71
- <style>
72
- .form-control-otp {
73
- letter-spacing: 1rem;
74
- text-align: center;
75
- }
76
- </style>
1
+ <script>
2
+ import { defineComponent, nextTick, onMounted, ref, watch } from 'vue';
3
+ import { fieldProps, useField } from './composables/useField.js';
4
+ import { useBsInstance } from './composables/useBsInstance.js';
5
+
6
+ /**
7
+ * Bootstrap 6 one-time-code input (`data-bs-otp`). A single real <input> backs the visual slots,
8
+ * so password managers and SMS autofill keep working.
9
+ */
10
+ export default defineComponent({
11
+ name: 'OTPInput',
12
+ emits: ['update:modelValue', 'change', 'input', 'complete', 'focus', 'blur'],
13
+ props: {
14
+ ...fieldProps,
15
+ /** Number of characters. */
16
+ length: { type: Number, default: null },
17
+ /** Legacy alias of `length`. */
18
+ maxLength: { type: Number, default: null },
19
+ /** numeric | alphanumeric | alpha */
20
+ type: { type: String, default: 'numeric', validator: (v) => ['numeric', 'alphanumeric', 'alpha'].includes(v) },
21
+ /** Show • instead of the typed character. */
22
+ mask: { type: Boolean, default: false },
23
+ /** Visual grouping, e.g. [3, 3] renders `123 · 456`. */
24
+ groups: { type: Array, default: null },
25
+ separator: { type: String, default: '·' },
26
+ /** Submit the surrounding FormContainer as soon as the code is complete. */
27
+ autoSubmit: { type: Boolean, default: false },
28
+ autofocus: { type: Boolean, default: false },
29
+ },
30
+ setup(props, { emit, expose }) {
31
+ const field = useField(props, emit);
32
+ const el = ref(null);
33
+ const input = ref(null);
34
+ let syncing = false;
35
+ let offs = [];
36
+
37
+ const resolvedLength = () => props.length ?? props.maxLength ?? 6;
38
+ const asText = (v) => (v === null || v === undefined ? '' : String(v));
39
+
40
+ function syncInvalid() {
41
+ // Bootstrap adds its own classes to this input, so the state is toggled through classList
42
+ input.value?.classList.toggle('is-invalid', field.isInvalid.value);
43
+ }
44
+
45
+ function syncFromModel() {
46
+ const inst = instance.value;
47
+ if (!inst) return;
48
+ const text = asText(field.model.value);
49
+ if (inst.getValue() === text) return;
50
+
51
+ syncing = true;
52
+ try {
53
+ inst.setValue(text);
54
+ } finally {
55
+ syncing = false;
56
+ }
57
+ }
58
+
59
+ const { instance, create } = useBsInstance(
60
+ 'OtpInput',
61
+ () => el.value,
62
+ () => ({
63
+ length: resolvedLength(),
64
+ type: props.type,
65
+ mask: props.mask,
66
+ groups: props.groups,
67
+ separator: props.separator,
68
+ }),
69
+ {
70
+ onCreate(inst, element) {
71
+ const on = (type, fn) => {
72
+ element.addEventListener(type, fn);
73
+ offs.push(() => element.removeEventListener(type, fn));
74
+ };
75
+
76
+ on('input.bs.otpInput', (e) => {
77
+ if (syncing) return;
78
+ field.model.value = e.value;
79
+ emit('input', e.value, e);
80
+ field.emitChange(e.value, e);
81
+ });
82
+
83
+ on('complete.bs.otpInput', (e) => {
84
+ if (syncing) return;
85
+ emit('complete', e.value);
86
+ if (props.autoSubmit) field.formApi?.submit?.();
87
+ });
88
+
89
+ syncFromModel();
90
+ syncInvalid();
91
+ if (props.autofocus) inst.focus();
92
+ },
93
+ onDestroy() {
94
+ offs.forEach((off) => off());
95
+ offs = [];
96
+ },
97
+ }
98
+ );
99
+
100
+ onMounted(() => {
101
+ // Pre-fill before the plugin is created so it can sanitise the initial value
102
+ if (input.value) input.value.value = asText(field.model.value);
103
+ });
104
+
105
+ watch(() => field.model.value, syncFromModel);
106
+ watch(field.isInvalid, syncInvalid, { flush: 'post' });
107
+ watch(
108
+ () => [resolvedLength(), props.type, props.mask, props.groups, props.separator],
109
+ async () => {
110
+ await nextTick();
111
+ create();
112
+ },
113
+ { deep: true }
114
+ );
115
+
116
+ expose({
117
+ focus: () => instance.value?.focus(),
118
+ clear: () => {
119
+ instance.value?.clear();
120
+ field.model.value = '';
121
+ },
122
+ });
123
+
124
+ return { ...field, el, input };
125
+ },
126
+ });
127
+ </script>
128
+
129
+ <template>
130
+ <div ref="el" class="otp fanum">
131
+ <input
132
+ ref="input"
133
+ type="text"
134
+ :name="name"
135
+ :id="inputId"
136
+ :disabled="isDisabled"
137
+ :readonly="readonly"
138
+ :required="required"
139
+ :aria-invalid="isInvalid || undefined"
140
+ @focus="$emit('focus', $event)"
141
+ @blur="$emit('blur', $event)"
142
+ />
143
+ </div>
144
+ </template>