inertia-bootstrap-forms 2.1.21 → 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
@@ -0,0 +1,96 @@
1
+ <script>
2
+ import { computed, defineComponent, nextTick, watch, ref } from 'vue';
3
+ import { fieldProps, useField } from './composables/useField.js';
4
+ import { useBsInstance } from './composables/useBsInstance.js';
5
+
6
+ /** Bootstrap 6 range slider (`.form-range` + `.form-range-input`) with optional value bubble and ticks. */
7
+ export default defineComponent({
8
+ name: 'RangeInput',
9
+ emits: ['update:modelValue', 'change', 'input', 'focus', 'blur'],
10
+ props: {
11
+ ...fieldProps,
12
+ min: { type: Number, default: 0 },
13
+ max: { type: Number, default: 100 },
14
+ step: { type: Number, default: 1 },
15
+ /** Show the current value above the thumb. */
16
+ bubble: { type: Boolean, default: false },
17
+ /** (value:number) => string — formats the bubble and the tick labels. */
18
+ formatter: { type: Function, default: null },
19
+ /** Tick marks: numbers or { value, label } objects. */
20
+ ticks: { type: Array, default: () => [] },
21
+ },
22
+ setup(props, { emit }) {
23
+ const field = useField(props, emit);
24
+ const el = ref(null);
25
+ const input = ref(null);
26
+
27
+ const current = computed(() => {
28
+ const n = Number(field.model.value);
29
+ return field.model.value === null || field.model.value === undefined || Number.isNaN(n) ? props.min : n;
30
+ });
31
+
32
+ const listId = computed(() => (props.ticks.length ? `${field.inputId.value}-ticks` : undefined));
33
+ const tickItems = computed(() =>
34
+ props.ticks.map((t) => (typeof t === 'object' && t !== null ? t : { value: t, label: props.formatter ? props.formatter(t) : String(t) }))
35
+ );
36
+
37
+ function onInput(event) {
38
+ const n = Number(event.target.value);
39
+ field.model.value = n;
40
+ emit('input', n, event);
41
+ }
42
+
43
+ const { instance, create } = useBsInstance(
44
+ 'Range',
45
+ () => el.value,
46
+ () => ({ bubble: props.bubble, formatter: props.formatter })
47
+ );
48
+
49
+ // The thumb / fill / bubble are recalculated when the value is changed from the outside
50
+ watch(
51
+ () => [field.model.value, props.min, props.max],
52
+ () => nextTick(() => instance.value?.update()),
53
+ { flush: 'post' }
54
+ );
55
+
56
+ // Ticks, bubble and formatter are read when the plugin is constructed
57
+ watch(
58
+ () => [props.bubble, props.formatter, props.ticks, props.min, props.max],
59
+ async () => {
60
+ await nextTick();
61
+ create();
62
+ },
63
+ { deep: true }
64
+ );
65
+
66
+ return { ...field, el, input, current, listId, tickItems, onInput };
67
+ },
68
+ });
69
+ </script>
70
+
71
+ <template>
72
+ <div ref="el" class="form-range">
73
+ <input
74
+ ref="input"
75
+ type="range"
76
+ class="form-range-input"
77
+ :class="{ 'is-invalid': isInvalid }"
78
+ :name="name"
79
+ :id="inputId"
80
+ :min="min"
81
+ :max="max"
82
+ :step="step"
83
+ :list="listId"
84
+ :disabled="isDisabled || readonly"
85
+ :aria-invalid="isInvalid || undefined"
86
+ :value="current"
87
+ @input="onInput"
88
+ @change="emitChange(Number($event.target.value), $event)"
89
+ @focus="$emit('focus', $event)"
90
+ @blur="$emit('blur', $event)"
91
+ />
92
+ <datalist v-if="listId" :id="listId">
93
+ <option v-for="tick in tickItems" :key="tick.value" :value="tick.value" :label="tick.label"></option>
94
+ </datalist>
95
+ </div>
96
+ </template>
@@ -1,148 +1,13 @@
1
1
  <script>
2
- import {defineComponent, inject} from "vue";
2
+ import { defineComponent, h } from 'vue';
3
+ import FileInput from './FileInput.vue';
3
4
 
5
+ /** @deprecated Kept for backwards compatibility — identical to <FileInput>. */
4
6
  export default defineComponent({
5
- props: {
6
- modelValue: '',
7
- name: {
8
- type: String,
9
- required: true,
7
+ name: 'SimpleUploader',
8
+ inheritAttrs: true,
9
+ setup(_, { attrs, slots }) {
10
+ return () => h(FileInput, attrs, slots);
10
11
  },
11
- multiple: {
12
- type: Boolean,
13
- default: false,
14
- },
15
- endpoint: {
16
- type: String,
17
- default: '/upload',
18
- },
19
- },
20
- setup(props) {
21
- let form = inject('form');
22
-
23
- if (form === undefined) {
24
- form = {
25
- errors: {},
26
- getID(name) {
27
- }
28
- };
29
- }
30
-
31
- return {form};
32
- },
33
- computed: {
34
- allFiles() {
35
- return this.files
36
- },
37
- },
38
- methods: {
39
- addFile(file) {
40
- let uploadedFile = {
41
- id: crypto.randomUUID(),
42
- percent: 0,
43
- error: 0,
44
- file: file,
45
- };
46
-
47
- if (this.multiple) {
48
- this.files.push(uploadedFile);
49
- } else {
50
- this.files = [uploadedFile];
51
- }
52
-
53
- this.uploadFile(uploadedFile);
54
- this.$refs.input.value = '';
55
- },
56
- calculatePercent(event, file) {
57
- if (event.lengthComputable) {
58
- this.files.find(item => item.id === file.id)['percent'] = Math.round((event.loaded / event.total) * 100);
59
- }
60
- },
61
- addFileToInputValue(file, res) {
62
- this.files.find(item => item.id === file.id).path = res.data?.path || res.path;
63
-
64
- this.form[this.name] = this.multiple ? this.files.map(item => item.path) : this.files[0].path;
65
- this.$emit('update:modelValue', this.form[this.name]);
66
- },
67
- async uploadFile(file) {
68
- let _this = this;
69
- let formData = new FormData();
70
- formData.append('file', file.file);
71
- _this.files.find(item => item.id === file.id)['error'] = '';
72
-
73
- await fetch(this.endpoint,
74
- {
75
- method: 'POST',
76
- headers: {
77
- 'Accept': 'application/json',
78
- },
79
- body: formData,
80
- },
81
- )
82
- .then(response => response.json())
83
- .then(function (res) {
84
- if(res.errors) {
85
- throw res;
86
- }
87
-
88
- _this.addFileToInputValue(file, res)
89
- }).catch(function (err) {
90
- console.error('error on uploader', err);
91
- _this.files.find(item => item.id === file.id)['error'] = (err.response?.data?.message || err.message);
92
- });
93
- },
94
- async deleteFile(file) {
95
- this.files = this.files.filter(item => item.id !== file.id);
96
- await fetch(this.endpoint, {
97
- method: 'DELETE',
98
- body: file.path
99
- });
100
- },
101
- fileSelected(e) {
102
- Array.from(e.target.files).forEach(file => this.addFile(file));
103
- }
104
- },
105
- data() {
106
- return {
107
- files: [],
108
- }
109
- }
110
- })
12
+ });
111
13
  </script>
112
- <template>
113
- <div class="file-input-uploader">
114
- <input
115
- ref="input"
116
- :name="name"
117
- :class="{'is-invalid': form?.errors[name] !== undefined}"
118
- :disabled="form?.processing"
119
- type="file"
120
- :multiple="multiple"
121
- @change="fileSelected"
122
- class="form-control">
123
- <div class="file-input-uploader--list">
124
- <div :id="'file-'+file.id" class="file-input-uploader--list-item" v-for="file in allFiles">
125
- <div class="file-input-uploader--progress" v-if="file.percent && file.percent< 100">
126
- <div :style="{width: file.percent + '%'}"></div>
127
- </div>
128
- <div>{{ file.file.name }}</div>
129
- <div class="file-input-uploader--list-error" v-if="file.error">{{ file.error }}</div>
130
- <div class="file-input-uploader--list-item--size" dir="ltr">{{ Math.round(file.file.size / 1024) }}Kb</div>
131
- <div class="file-input-uploader--list-item--actions">
132
- <button @click="uploadFile(file)" type="button" class="file-input-uploader--list-item--btn" v-if="file.error">
133
- <svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
134
- <path
135
- d="M1,12A11,11,0,0,1,17.882,2.7l1.411-1.41A1,1,0,0,1,21,2V6a1,1,0,0,1-1,1H16a1,1,0,0,1-.707-1.707l1.128-1.128A8.994,8.994,0,0,0,3,12a1,1,0,0,1-2,0Zm21-1a1,1,0,0,0-1,1,9.01,9.01,0,0,1-9,9,8.9,8.9,0,0,1-4.42-1.166l1.127-1.127A1,1,0,0,0,8,17H4a1,1,0,0,0-1,1v4a1,1,0,0,0,.617.924A.987.987,0,0,0,4,23a1,1,0,0,0,.707-.293L6.118,21.3A10.891,10.891,0,0,0,12,23,11.013,11.013,0,0,0,23,12,1,1,0,0,0,22,11Z"/>
136
- </svg>
137
- </button>
138
- <button @click="deleteFile(file)" type="button" class="file-input-uploader--list-item--btn" v-if="file.path || file.error">
139
- <svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
140
- <path
141
- d="M12,23A11,11,0,1,0,1,12,11.013,11.013,0,0,0,12,23ZM12,3a9,9,0,1,1-9,9A9.01,9.01,0,0,1,12,3ZM8.293,14.293,10.586,12,8.293,9.707A1,1,0,0,1,9.707,8.293L12,10.586l2.293-2.293a1,1,0,0,1,1.414,1.414L13.414,12l2.293,2.293a1,1,0,1,1-1.414,1.414L12,13.414,9.707,15.707a1,1,0,0,1-1.414-1.414Z"/>
142
- </svg>
143
- </button>
144
- </div>
145
- </div>
146
- </div>
147
- </div>
148
- </template>
@@ -1,158 +1,134 @@
1
1
  <script>
2
- import {defineComponent, inject} from 'vue';
2
+ import { computed, defineComponent, onMounted, ref } from 'vue';
3
+ import { fieldProps, useField } from './composables/useField.js';
3
4
 
5
+ /** Star rating with fractional precision, pointer + keyboard support and RTL awareness. */
4
6
  export default defineComponent({
5
- emits: ['update:modelValue'],
7
+ name: 'StarRatingInput',
8
+ emits: ['update:modelValue', 'change', 'hover'],
6
9
  props: {
7
- name: {
8
- type: String,
9
- default: null,
10
- },
11
- modelValue: {
12
- type: Number,
13
- default: 0
14
- },
15
- value: {
16
- type: Number,
17
- default: 0
18
- },
19
- maxStars: {
20
- type: Number,
21
- default: 5
22
- },
23
- precision: {
24
- type: Number,
25
- default: 2
26
- },
27
- showValue: {
28
- type: Boolean,
29
- default: true
30
- },
31
- readonly: {
32
- type: Boolean,
33
- default: false
34
- },
10
+ ...fieldProps,
11
+ /** Initial rating used when the model is empty. */
12
+ value: { type: Number, default: 0 },
13
+ maxStars: { type: Number, default: 5 },
14
+ /** Steps per star: 1 = whole stars, 2 = half stars, 4 = quarter stars… */
15
+ precision: { type: Number, default: 2 },
16
+ showValue: { type: Boolean, default: true },
17
+ /** Allow clicking the current rating again to reset it to 0. */
18
+ clearable: { type: Boolean, default: false },
35
19
  },
36
- setup(props) {
37
- let form = inject('form');
38
- if (form === undefined) {
39
- form = {
40
- errors: {},
41
- getID(name) {
42
- }
43
- };
44
- }
45
- return {form};
46
- },
47
- computed: {
48
- displayRating() {
49
- return (this.hoveredRating ?? (this.modelValue ?? (this.name ? this.form[this.name] : null)) ?? this.value).toFixed(1);
50
- }
51
- },
52
- methods: {
53
- getStarFill(star) {
54
- const rating = this.hoveredRating ?? (this.modelValue || this.form[this.name]);
55
- const fullStars = Math.floor(rating);
56
- const remainder = rating - fullStars;
20
+ setup(props, { emit }) {
21
+ const field = useField(props, emit);
22
+ const container = ref(null);
23
+ const hovered = ref(null);
24
+ const isRTL = ref(false);
57
25
 
58
- if (star <= fullStars) return 100;
59
- if (star === fullStars + 1) return remainder * 100;
60
- return 0;
61
- },
62
- setHover(event) {
63
- if (this.readonly) return;
26
+ onMounted(() => {
27
+ if (container.value) isRTL.value = getComputedStyle(container.value).direction === 'rtl';
28
+ });
64
29
 
65
- const rect = this.$refs.ratingContainer.getBoundingClientRect();
66
- let offsetX = event.clientX - rect.left;
30
+ const locked = computed(() => props.readonly || field.isDisabled.value);
31
+ const stored = computed(() => {
32
+ const n = Number(field.model.value);
33
+ return field.model.value === null || field.model.value === undefined || Number.isNaN(n) ? props.value : n;
34
+ });
35
+ const rating = computed(() => hovered.value ?? stored.value);
36
+ const step = computed(() => 1 / props.precision);
67
37
 
68
- if (this.isRTL) {
69
- offsetX = rect.width - (event.clientX - rect.left); // اصلاح برای حالت راست‌چین
70
- }
38
+ const fillOf = (star) => Math.max(0, Math.min(1, rating.value - (star - 1))) * 100;
71
39
 
72
- const starWidth = rect.width / this.maxStars;
73
- let newRating = Math.round((offsetX / starWidth) * this.precision) / this.precision;
74
- newRating = Math.min(newRating, this.maxStars);
40
+ function ratingFromPointer(event) {
41
+ const rect = container.value.getBoundingClientRect();
42
+ const offset = isRTL.value ? rect.right - event.clientX : event.clientX - rect.left;
43
+ const starWidth = rect.width / props.maxStars;
44
+ const raw = Math.ceil((offset / starWidth) * props.precision) / props.precision;
45
+ return Math.min(props.maxStars, Math.max(step.value, raw));
46
+ }
75
47
 
76
- this.hoveredRating = newRating;
77
- },
78
- updateRating() {
79
- if (this.readonly) return;
48
+ function onMove(event) {
49
+ if (locked.value) return;
50
+ hovered.value = ratingFromPointer(event);
51
+ emit('hover', hovered.value);
52
+ }
80
53
 
81
- if (this.name) this.form[this.name] = this.hoveredRating.toFixed(1);
82
- this.$emit('update:modelValue', this.hoveredRating.toFixed(1));
83
- },
84
- resetHover() {
85
- if (this.readonly) return;
54
+ function leave() {
55
+ hovered.value = null;
56
+ }
86
57
 
87
- this.hoveredRating = null;
58
+ function commit(next, event) {
59
+ if (locked.value) return;
60
+ const value = props.clearable && next === stored.value ? 0 : next;
61
+ field.model.value = value;
62
+ field.emitChange(value, event);
88
63
  }
89
- },
90
- mounted() {
91
- if (typeof window !== 'undefined' && this.$refs.ratingContainer) {
92
- this.isRTL = getComputedStyle(this.$refs.ratingContainer).direction === 'rtl';
64
+
65
+ function onClick(event) {
66
+ if (locked.value) return;
67
+ commit(ratingFromPointer(event), event);
93
68
  }
94
- },
95
- data() {
96
- return {
97
- hoveredRating: parseFloat(this.modelValue || ((this.name ? this.form[this.name] : null) ?? this.value)),
98
- isRTL: false,
69
+
70
+ function onKeydown(event) {
71
+ if (locked.value) return;
72
+ const forward = isRTL.value ? 'ArrowLeft' : 'ArrowRight';
73
+ const backward = isRTL.value ? 'ArrowRight' : 'ArrowLeft';
74
+ let next = null;
75
+
76
+ if (event.key === forward || event.key === 'ArrowUp') next = stored.value + step.value;
77
+ else if (event.key === backward || event.key === 'ArrowDown') next = stored.value - step.value;
78
+ else if (event.key === 'Home') next = 0;
79
+ else if (event.key === 'End') next = props.maxStars;
80
+
81
+ if (next !== null) {
82
+ event.preventDefault();
83
+ commit(Math.min(props.maxStars, Math.max(0, Number(next.toFixed(4)))), event);
84
+ }
99
85
  }
100
- }
101
- })
86
+
87
+ return {
88
+ ...field,
89
+ container,
90
+ rating,
91
+ stored,
92
+ fillOf,
93
+ onMove,
94
+ leave,
95
+ onClick,
96
+ onKeydown,
97
+ locked,
98
+ display: computed(() => rating.value.toFixed(1)),
99
+ };
100
+ },
101
+ });
102
102
  </script>
103
+
103
104
  <template>
104
- <div class="star-rating">
105
- <div class="star-rating-container" ref="ratingContainer" @mouseleave="resetHover" @mousemove="setHover" @click="updateRating">
106
- <div v-for="star in maxStars" :key="star" class="star-wrapper">
107
- <div class="star-bg">★</div>
108
- <span class="star-overlay" :style="{ width: getStarFill(star) + '%' }">★</span>
109
- </div>
105
+ <div class="star-rating d-flex align-items-center gap-2" :class="{ 'is-invalid': isInvalid }">
106
+ <div
107
+ ref="container"
108
+ class="star-rating-container d-flex position-relative"
109
+ role="slider"
110
+ :tabindex="locked ? -1 : 0"
111
+ :aria-valuemin="0"
112
+ :aria-valuemax="maxStars"
113
+ :aria-valuenow="stored"
114
+ :aria-readonly="locked || undefined"
115
+ :aria-invalid="isInvalid || undefined"
116
+ :style="{ cursor: locked ? 'default' : 'pointer' }"
117
+ @pointermove="onMove"
118
+ @pointerleave="leave"
119
+ @click="onClick"
120
+ @keydown="onKeydown"
121
+ >
122
+ <span v-for="star in maxStars" :key="star" class="star-wrapper position-relative d-inline-block fs-2xl lh-1">
123
+ <span class="star-bg fg-secondary opacity-50" aria-hidden="true">★</span>
124
+ <span
125
+ class="star-overlay fg-warning"
126
+ aria-hidden="true"
127
+ :style="{ position: 'absolute', top: 0, insetInlineStart: 0, overflow: 'hidden', whiteSpace: 'nowrap', width: fillOf(star) + '%' }"
128
+ >★</span>
129
+ </span>
110
130
  </div>
111
- <div class="rating-value fanum" v-if="showValue && displayRating> 0">{{ displayRating }}</div>
131
+ <input v-if="name" type="hidden" :name="name" :value="stored" />
132
+ <span v-if="showValue && rating > 0" class="rating-value fanum">{{ display }}</span>
112
133
  </div>
113
134
  </template>
114
-
115
- <style>
116
- .star-rating,
117
- .star-rating-container {
118
- display: flex;
119
- align-items: center;
120
- }
121
-
122
- .star-rating {
123
- gap: 8px;
124
- }
125
-
126
- .star-rating .star-rating-container {
127
- cursor: pointer;
128
- position: relative;
129
- }
130
-
131
- .star-rating .star-wrapper {
132
- position: relative;
133
- font-size: 28px;
134
- line-height: 1;
135
- display: inline-block;
136
- }
137
-
138
- .star-rating .star-bg {
139
- color: #ccc;
140
- }
141
-
142
- .star-rating .star-overlay {
143
- position: absolute;
144
- top: 0;
145
- left: 0; /* 👈 اصلاح برای RTL */
146
- overflow: hidden;
147
- color: gold;
148
- white-space: nowrap;
149
- width: 0;
150
- transition: width 0.2s ease-in-out;
151
- }
152
-
153
- .star-rating .rating-value {
154
- font-size: 18px;
155
- color: #444;
156
- margin-right: 8px;
157
- }
158
- </style>
@@ -1,30 +1,45 @@
1
1
  <script>
2
- import {Spinner} from 'vue3-bootstrap-components';
3
- import {defineComponent} from "vue";
2
+ import { computed, defineComponent, inject } from 'vue';
3
+ import { useI18n } from './i18n.js';
4
4
 
5
5
  export default defineComponent({
6
- components: {
7
- Spinner
8
- },
9
- inject: ['form'],
10
- props: {
11
- disabled: {
12
- type: Boolean,
13
- default: false
14
- }
15
- },
16
- })
6
+ name: 'SubmitButton',
7
+ emits: ['click'],
8
+ props: {
9
+ disabled: { type: Boolean, default: false },
10
+ /** Force the loading state (otherwise it follows form.processing / form.uploading). */
11
+ loading: { type: Boolean, default: null },
12
+ /** Bootstrap 6 button variant: solid | outline | subtle | text | styled | link. Use null to supply your own classes. */
13
+ variant: { type: String, default: 'solid' },
14
+ /** Bootstrap 6 theme: primary | secondary | accent | success | danger | warning | info | inverse. */
15
+ theme: { type: String, default: 'primary' },
16
+ locale: { type: String, default: null },
17
+ },
18
+ setup(props, { emit }) {
19
+ const formRef = inject('form', null);
20
+ const form = computed(() => formRef?.value ?? null);
21
+ const { t } = useI18n(() => props.locale);
22
+
23
+ const busy = computed(() =>
24
+ props.loading !== null ? props.loading : !!(form.value?.processing || form.value?.uploading)
25
+ );
26
+
27
+ const classes = computed(() => [
28
+ props.variant ? `btn-${props.variant}` : null,
29
+ props.theme && props.variant ? `theme-${props.theme}` : null,
30
+ ]);
31
+
32
+ return { form, busy, classes, t, onClick: (e) => emit('click', e) };
33
+ },
34
+ });
17
35
  </script>
36
+
18
37
  <template>
19
- <button
20
- type="submit"
21
- :disabled="(disabled || form?.processing || form?.uploading)">
22
- <Spinner v-if="form?.processing || form?.uploading"/>
23
- <slot :form="form">
24
- تایید و ثبت اطلاعات
25
- </slot>
26
- <slot name="progress">
27
- <span class="submit-upload-percent" v-if="form?.uploading">{{ form?.uploading }}%</span>
28
- </slot>
29
- </button>
38
+ <button type="submit" :class="classes" :disabled="disabled || busy" :aria-busy="busy || undefined" @click="onClick">
39
+ <span v-if="busy" class="spinner-border spinner-sm me-2" role="status" aria-hidden="true"></span>
40
+ <slot :form="form" :loading="busy">{{ t('submit.default') }}</slot>
41
+ <slot name="progress" :progress="form?.uploading">
42
+ <span v-if="form?.uploading" class="fanum">{{ form.uploading }}%</span>
43
+ </slot>
44
+ </button>
30
45
  </template>