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,23 +1,157 @@
1
1
  <script>
2
- import TextInput from "./TextInput.vue";
3
- import {defineComponent} from "vue";
2
+ import { computed, defineComponent, nextTick, onMounted, ref, watch } from 'vue';
3
+ import TextInput from './TextInput.vue';
4
+ import FormAdornInput from './FormAdornInput.vue';
5
+ import { fieldProps, useField } from './composables/useField.js';
6
+ import { useBsInstance } from './composables/useBsInstance.js';
7
+ import { useI18n } from './i18n.js';
4
8
 
9
+ const STRENGTH_MESSAGES = {
10
+ en: { weak: 'Weak', fair: 'Fair', good: 'Good', strong: 'Strong' },
11
+ fa: { weak: 'ضعیف', fair: 'متوسط', good: 'خوب', strong: 'قوی' },
12
+ };
13
+
14
+ /**
15
+ * Password field.
16
+ * - `toggle` adds a show / hide button (Bootstrap 6 form-adorn)
17
+ * - `strength` adds the Bootstrap 6 strength meter under the field (optional)
18
+ *
19
+ * Every other prop / listener (placeholder, size, autocomplete, @change, …) is forwarded to the input.
20
+ */
5
21
  export default defineComponent({
6
- components: {
7
- TextInput
8
- },
9
- props: {
10
- name: {
11
- type: String,
12
- default: 'password',
13
- required: true,
22
+ name: 'PasswordInput',
23
+ components: { TextInput, FormAdornInput },
24
+ inheritAttrs: false,
25
+ emits: ['toggle', 'strength'],
26
+ props: {
27
+ ...fieldProps,
28
+ name: { type: String, default: 'password' },
29
+ placeholder: { type: String, default: null },
30
+ /** Show a "reveal password" button. */
31
+ toggle: { type: Boolean, default: false },
32
+ /** Show the password strength meter. */
33
+ strength: { type: Boolean, default: false },
34
+ /** Show the text feedback ("Weak", "Strong", …) under the meter. */
35
+ strengthText: { type: Boolean, default: true },
36
+ /** Override the labels: { weak, fair, good, strong }. */
37
+ strengthMessages: { type: Object, default: null },
38
+ /** Bootstrap Strength options: { minLength, thresholds: [2, 4, 6], weights, scorer(password) => number }. */
39
+ strengthOptions: { type: Object, default: () => ({}) },
40
+ size: { type: String, default: null },
41
+ locale: { type: String, default: null },
42
+ },
43
+ setup(props, { emit, expose }) {
44
+ const { t, locale } = useI18n(() => props.locale);
45
+ // read-only access to the value (so the meter follows form resets / external changes)
46
+ const field = useField(props, () => {});
47
+
48
+ const root = ref(null);
49
+ const meter = ref(null);
50
+ const visible = ref(false);
51
+
52
+ const messages = computed(() => ({
53
+ ...(STRENGTH_MESSAGES[locale.value] || STRENGTH_MESSAGES.en),
54
+ ...(props.strengthMessages || {}),
55
+ }));
56
+
57
+ function toggleVisible() {
58
+ visible.value = !visible.value;
59
+ emit('toggle', visible.value);
60
+ }
61
+
62
+ let offs = [];
63
+ const { instance, create } = useBsInstance(
64
+ 'Strength',
65
+ () => (props.strength ? meter.value : null),
66
+ () => ({
67
+ ...props.strengthOptions,
68
+ input: root.value?.querySelector('input'),
69
+ messages: messages.value,
70
+ }),
71
+ {
72
+ onCreate(inst, el) {
73
+ const handler = (e) => emit('strength', e.strength ?? null, e.score ?? 0);
74
+ el.addEventListener('strengthChange.bs.strength', handler);
75
+ offs.push(() => el.removeEventListener('strengthChange.bs.strength', handler));
76
+ },
77
+ onDestroy() {
78
+ offs.forEach((off) => off());
79
+ offs = [];
80
+ },
81
+ }
82
+ );
83
+
84
+ // the plugin only listens to typing: re-evaluate when the value changes from the outside (reset, server data)
85
+ watch(
86
+ () => field.model.value,
87
+ () => nextTick(() => instance.value?.evaluate())
88
+ );
89
+
90
+ // things the plugin reads when it is created
91
+ watch(
92
+ () => [props.strength, props.toggle, props.strengthText, messages.value, props.strengthOptions],
93
+ async () => {
94
+ await nextTick();
95
+ if (props.strength) create();
14
96
  },
15
- },
16
- })
97
+ { deep: true }
98
+ );
99
+
100
+ expose({ strength: () => instance.value?.getStrength?.() ?? null });
101
+
102
+ // props that are forwarded to the real input component
103
+ const forwarded = computed(() => ({
104
+ name: props.name,
105
+ modelValue: props.modelValue,
106
+ id: props.id,
107
+ disabled: props.disabled,
108
+ readonly: props.readonly,
109
+ required: props.required,
110
+ invalid: props.invalid,
111
+ size: props.size,
112
+ placeholder: props.placeholder ?? t('password.placeholder'),
113
+ }));
114
+
115
+ return { t, root, meter, visible, toggleVisible, forwarded };
116
+ },
117
+ });
17
118
  </script>
119
+
18
120
  <template>
19
- <TextInput
20
- :name="name"
21
- placeholder="گذرواژه خود را وارد کنید"
22
- type="password"/>
23
- </template>
121
+ <div ref="root" class="password-input">
122
+ <FormAdornInput
123
+ v-if="toggle"
124
+ v-bind="{ ...$attrs, ...forwarded }"
125
+ :type="visible ? 'text' : 'password'"
126
+ autocomplete="current-password"
127
+ >
128
+ <template #end>
129
+ <button
130
+ type="button"
131
+ class="btn-text theme-secondary btn-icon"
132
+ :aria-label="visible ? t('password.hide') : t('password.show')"
133
+ :aria-pressed="visible"
134
+ @click="toggleVisible"
135
+ >
136
+ <svg v-if="!visible" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
137
+ <path d="M1 8s2.5-4.5 7-4.5S15 8 15 8s-2.5 4.5-7 4.5S1 8 1 8Z" /><circle cx="8" cy="8" r="2" />
138
+ </svg>
139
+ <svg v-else width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
140
+ <path d="M2 2l12 12M6.6 3.7A7 7 0 0 1 8 3.5C12.5 3.5 15 8 15 8a11 11 0 0 1-2.2 2.6M4 5.4A11 11 0 0 0 1 8s2.5 4.5 7 4.5c1 0 1.9-.2 2.7-.6" />
141
+ </svg>
142
+ </button>
143
+ </template>
144
+ </FormAdornInput>
145
+ <TextInput v-else v-bind="{ ...$attrs, ...forwarded }" type="password" autocomplete="current-password" />
146
+
147
+ <template v-if="strength">
148
+ <div ref="meter" class="strength" aria-hidden="true">
149
+ <div class="strength-segment"></div>
150
+ <div class="strength-segment"></div>
151
+ <div class="strength-segment"></div>
152
+ <div class="strength-segment"></div>
153
+ </div>
154
+ <div v-if="strengthText" class="strength-text" aria-live="polite"></div>
155
+ </template>
156
+ </div>
157
+ </template>
@@ -1,183 +1,80 @@
1
1
  <script>
2
- import {computed, defineAsyncComponent, defineComponent, inject} from "vue";
2
+ import { defineAsyncComponent, defineComponent, onMounted, ref, watch } from 'vue';
3
+ import { fieldProps, useField } from './composables/useField.js';
3
4
 
4
- const Vue3PersianDatetimePicker = defineAsyncComponent(() =>
5
- import('vue3-persian-datetime-picker')
6
- );
5
+ const Vue3PersianDatetimePicker = defineAsyncComponent(() => import('vue3-persian-datetime-picker'));
7
6
 
8
- let _this;
7
+ /**
8
+ * Jalali (Persian) date / time picker based on vue3-persian-datetime-picker.
9
+ * Bootstrap 6's own Datepicker does not support the Jalali calendar, so this library is kept on purpose.
10
+ */
9
11
  export default defineComponent({
10
- components: {Vue3PersianDatetimePicker},
11
- emits: ['update:modelValue', 'change'],
12
- props: {
13
- name: {
14
- type: String,
15
- required: true,
12
+ name: 'PersianDatePickerInput',
13
+ components: { Vue3PersianDatetimePicker },
14
+ emits: ['update:modelValue', 'change', 'open', 'close'],
15
+ props: {
16
+ ...fieldProps,
17
+ /** date | datetime | time | year-month | month */
18
+ calendar: { type: String, default: 'date' },
19
+ view: { type: String, default: 'date' },
20
+ min: { type: String, default: null },
21
+ max: { type: String, default: null },
22
+ range: { type: Boolean, default: false },
23
+ locale: { type: String, default: 'fa' },
24
+ /** Format of the stored value. */
25
+ format: { type: String, default: 'jYYYY/jMM/jDD' },
26
+ /** Format shown in the input (defaults to `format`). */
27
+ inputFormat: { type: String, default: null },
28
+ placeholder: { type: String, default: null },
29
+ inputClass: { type: String, default: 'form-control fanum' },
30
+ /** Extra props forwarded to vue3-persian-datetime-picker. */
31
+ pickerOptions: { type: Object, default: () => ({}) },
16
32
  },
17
- calendar: {
18
- type: String,
19
- default: 'date',
20
- },
21
- view: {
22
- type: String,
23
- default: 'date',
24
- },
25
- disabled: Boolean,
26
- min: String,
27
- max: String,
28
- range: Boolean,
29
- locale: {
30
- default: 'fa',
31
- },
32
- format: {
33
- type: String,
34
- default: 'jYYYY/jMM/jDD',
35
- },
36
- inputFormat: {
37
- type: String,
38
- default: null,
39
- },
40
- simple: {
41
- type: Boolean,
42
- default: false,
43
- },
44
- placeholder: String,
45
- },
46
- setup(props) {
47
- let form = inject('form', {
48
- errors: {},
49
- getID(name) {
50
- return name;
51
- }
52
- });
53
- let group = inject('group', {});
33
+ setup(props, { emit, expose }) {
34
+ const field = useField(props, emit);
35
+ const ready = ref(false);
54
36
 
55
- const modelValue = computed({
56
- get() {
57
- return (group.value && form.value[group.value.name]) ? group.value?.getData(props.name) : form.value[props.name];
58
- },
59
- set(value) {
60
- if (group?.value?.name) {
61
- group.value.setData(props.name, value);
62
- } else {
63
- form.value[props.name] = value;
64
- }
65
- }
66
- });
37
+ onMounted(() => {
38
+ ready.value = true; // the picker needs the DOM, so it is never rendered during SSR
39
+ });
67
40
 
68
- return {modelValue, form, group};
69
- },
70
- watch: {
71
- modelValue: (newVal, oldVal) => {
72
- _this?.$emit('change', newVal, oldVal);
73
- }
74
- },
75
- methods: {
76
- clear() {
77
- this.modelValue = null;
78
- },
79
- },
80
- mounted() {
81
- _this = this;
82
- },
83
- computed: {
84
- isSsr() {
85
- return !window || typeof window === 'undefined';
41
+ watch(
42
+ () => field.model.value,
43
+ (now, before) => emit('change', now, before)
44
+ );
45
+
46
+ expose({ clear: () => (field.model.value = null) });
47
+
48
+ return { ...field, ready };
86
49
  },
87
- inputID() {
88
- return this.form.getID(this)
89
- }
90
- },
91
- data() {
92
- return {}
93
- }
94
- })
50
+ });
95
51
  </script>
96
52
 
97
53
  <template>
98
- <template v-if="!isSsr">
99
-
100
54
  <Vue3PersianDatetimePicker
101
- v-model="modelValue"
55
+ v-if="ready"
56
+ v-model="model"
57
+ :name="name"
58
+ :id="inputId"
102
59
  :type="calendar"
60
+ :view="view"
61
+ :min="min"
62
+ :max="max"
63
+ :range="range"
64
+ :locale="locale"
65
+ :format="format"
66
+ :input-format="inputFormat || format"
67
+ :placeholder="placeholder"
68
+ :disabled="isDisabled || readonly"
69
+ :input-class="[inputClass, { 'is-invalid': isInvalid }]"
103
70
  editable
104
71
  jumpminute="15"
105
- :disabled="form?.processing || disabled"
106
72
  :roundminute="true"
107
- :showNowBtn="true"
108
- :autoSubmit="true"
109
- :inputFormat="(inputFormat ? inputFormat : format)"
110
- input-class="form-control fanum"
111
- v-bind="$props"/>
112
- </template>
113
-
73
+ :show-now-btn="true"
74
+ :auto-submit="true"
75
+ v-bind="pickerOptions"
76
+ @open="$emit('open')"
77
+ @close="$emit('close')"
78
+ />
79
+ <input v-else type="text" class="form-control" :class="{ 'is-invalid': isInvalid }" :placeholder="placeholder" disabled />
114
80
  </template>
115
- <style>
116
- .vpd-main .vpd-input-group {
117
- display: block !important;
118
- }
119
-
120
- .vpd-main .vpd-input-group input {
121
- padding: 0.375rem 0.75rem !important;
122
- border-radius: var(--bs-border-radius, .5rem) !important;
123
- line-height: var(--bs-body-line-height, 1.5) !important;
124
- border: 1px solid var(--bs-border-color, #dadada) !important
125
- }
126
-
127
- .vpd-main .vpd-icon-btn {
128
- position: absolute;
129
- right: 5px;
130
- left: auto;
131
- top: 0;
132
- bottom: 0;
133
- height: 30px;
134
- min-width: 40px;
135
- border: 1px solid var(--bs-secondary) !important;
136
- background-color: var(--bs-secondary-bg) !important;
137
- color: var(--bs-body-color, #000) !important;
138
- border-radius: var(--bs-border-radius, .5rem);
139
- margin: auto;
140
- transition: all ease 0.3s;
141
- }
142
-
143
- .vpd-main .vpd-icon-btn svg {
144
- fill: var(--bs-body-color, #000) !important;
145
- transition: all ease 0.3s;
146
- }
147
-
148
- .vpd-main .vpd-icon-btn:hover {
149
- background-color: var(--bs-secondary) !important;
150
- color: var(--bs-body-color) !important;
151
- }
152
-
153
- .vpd-main .vpd-icon-btn:hover svg {
154
- fill: var(--bs-body-bg, #000) !important;
155
- }
156
-
157
- .vpd-content {
158
- color: var(--bs-body-color) !important;
159
- background-color: var(--bs-body-bg) !important;
160
- -moz-font-feature-settings: "ss01";
161
- -webkit-font-feature-settings: "ss01";
162
- font-feature-settings: "ss01";
163
- }
164
-
165
- .vpd-time .vpd-down-arrow-btn, .vpd-time .vpd-up-arrow-btn {
166
- text-align: center;
167
- }
168
-
169
- .vpd-time .vpd-down-arrow-btn svg,
170
- .vpd-time .vpd-up-arrow-btn svg {
171
- margin: 0 auto;
172
- }
173
-
174
- .vpd-controls button {
175
- display: flex;
176
- align-items: center;
177
- justify-content: center;
178
- }
179
-
180
- .vpd-month-label {
181
- width: 110px;
182
- }
183
- </style>
@@ -1,133 +1,114 @@
1
1
  <script>
2
- import {InputGroup, InputGroupText} from 'vue3-bootstrap-components';
3
- import {computed, defineComponent, inject} from "vue";
2
+ import { computed, defineComponent, onMounted } from 'vue';
3
+ import { fieldProps, useField } from './composables/useField.js';
4
+ import { useI18n } from './i18n.js';
5
+ import { formatNumber, parseDigits } from './utils/digits.js';
4
6
 
7
+ /** Numeric stepper: [ − ] [ value ] [ + ] unit. */
5
8
  export default defineComponent({
6
- components: {InputGroup, InputGroupText},
7
- props: {
8
- name: {
9
- type: String,
10
- required: true,
9
+ name: 'QuantityInput',
10
+ emits: ['update:modelValue', 'change', 'input', 'increment', 'decrement', 'focus', 'blur'],
11
+ props: {
12
+ ...fieldProps,
13
+ min: { type: Number, default: 0 },
14
+ max: { type: Number, default: null },
15
+ step: { type: Number, default: 1 },
16
+ unit: { type: String, default: null },
17
+ size: { type: String, default: null },
18
+ locale: { type: String, default: null },
11
19
  },
12
- required: Boolean,
13
- disabled: Boolean,
14
- readonly: Boolean,
15
- unit: {
16
- type: String,
17
- default: 'عدد'
18
- },
19
- min: {
20
- type: Number,
21
- default: 0
22
- },
23
- max: {
24
- type: Number,
25
- default: null
26
- },
27
- },
28
- setup(props) {
29
- let form = inject('form');
30
- let group = inject('group', {});
20
+ setup(props, { emit }) {
21
+ const field = useField(props, emit);
22
+ const { t } = useI18n(() => props.locale);
31
23
 
32
- if (form === undefined) {
33
- form = {
34
- errors: {},
35
- getID(name) {
36
- }
37
- };
38
- }
24
+ const current = computed(() => {
25
+ const n = Number(field.model.value);
26
+ return Number.isFinite(n) ? n : props.min;
27
+ });
39
28
 
40
- if (!form[props.name]) {
41
- form[props.name] = props.min;
42
- }
29
+ const unitText = computed(() => props.unit ?? t('quantity.unit'));
30
+ const display = computed(() => formatNumber(current.value));
31
+ const lock = computed(() => props.readonly || field.isDisabled.value);
32
+ const canIncrement = computed(() => !lock.value && (props.max === null || current.value + props.step <= props.max));
33
+ const canDecrement = computed(() => !lock.value && current.value - props.step >= props.min);
43
34
 
44
- const modelValue = computed({
45
- get() {
46
- return ((group.value && form.value[group.value.name]) ? group.value?.getData(props.name) : form.value[props.name]);
47
- },
48
- set(value) {
49
- if (group?.value?.name) {
50
- group.value.setData(props.name, value);
51
- } else {
52
- form.value[props.name] = value;
35
+ function set(next, event, kind) {
36
+ let n = next;
37
+ if (props.max !== null) n = Math.min(n, props.max);
38
+ n = Math.max(n, props.min);
39
+ field.model.value = n;
40
+ if (kind) emit(kind, n);
41
+ field.emitChange(n, event);
53
42
  }
54
- }
55
- });
56
43
 
57
- if (!modelValue.value) {
58
- modelValue.value = props.min;
59
- }
44
+ function onInput(event) {
45
+ const parsed = parseDigits(event.target.value);
46
+ field.model.value = parsed === null ? props.min : parsed;
47
+ event.target.value = formatNumber(field.model.value);
48
+ emit('input', field.model.value, event);
49
+ }
60
50
 
61
- return {modelValue, group, form};
62
- },
63
- methods: {
64
- onChange(event) {
65
- let value = parseFloat(this.$number.toEnglish(event.detail?.unmasked)) || 1;
66
- this.$emit('update:modelValue', value);
67
- this.modelValue = value;
68
- },
69
- increment() {
70
- if (!this.max || (this.max < this.modelValue)) {
71
- this.modelValue = this.modelValue + 1;
72
- }
51
+ onMounted(() => {
52
+ // A quantity is never empty: initialise it with the minimum
53
+ const v = field.model.value;
54
+ if (v === null || v === undefined || v === '') field.model.value = props.min;
55
+ });
56
+
57
+ return {
58
+ ...field,
59
+ t,
60
+ unitText,
61
+ display,
62
+ canIncrement,
63
+ canDecrement,
64
+ lock,
65
+ increment: (e) => canIncrement.value && set(current.value + props.step, e, 'increment'),
66
+ decrement: (e) => canDecrement.value && set(current.value - props.step, e, 'decrement'),
67
+ onInput,
68
+ clampOnBlur: (e) => set(current.value, e),
69
+ };
73
70
  },
74
- decrement() {
75
- if ((!this.min || (this.min < this.modelValue)) && this.modelValue > 1) {
76
- this.modelValue = this.modelValue - 1;
77
- }
78
- }
79
- },
80
- data() {
81
- return {}
82
- }
83
- })
71
+ });
84
72
  </script>
73
+
85
74
  <template>
86
- <InputGroup class="input-group-quantity">
87
- <button @click="increment" type="button" class="btn">
88
- <svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
89
- <path
90
- d="M13 3C13 2.44772 12.5523 2 12 2C11.4477 2 11 2.44772 11 3V11H3C2.44772 11 2 11.4477 2 12C2 12.5523 2.44772 13 3 13H11V21C11 21.5523 11.4477 22 12 22C12.5523 22 13 21.5523 13 21V13H21C21.5523 13 22 12.5523 22 12C22 11.4477 21.5523 11 21 11H13V3Z"
91
- fill="#0F0F0F"/>
92
- </svg>
93
- </button>
94
- <input
95
- :name="name"
96
- :value="this.modelValue"
97
- v-maska
98
- data-maska="###,###,###,###,###,###,###"
99
- data-maska-reversed
100
- @maska="onChange"
101
- :class="{'is-invalid': form.errors[name]}"
102
- :disabled="disabled || form.processing"
103
- :readonly="readonly"
104
- type="tel"
105
- class="form-control fanum text-center">
106
- <button @click="decrement" type="button" class="btn">
107
- <svg width="800px" height="800px" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" fill="none">
108
- <path fill="#000000" fill-rule="evenodd" d="M18 10a1 1 0 01-1 1H3a1 1 0 110-2h14a1 1 0 011 1z"/>
109
- </svg>
110
- </button>
111
- <InputGroupText class="fanum">{{ unit }}</InputGroupText>
112
- <slot name="suffix"/>
113
- </InputGroup>
75
+ <div class="input-group input-group-quantity" :class="{ 'is-invalid': isInvalid }">
76
+ <button
77
+ type="button"
78
+ class="btn-outline theme-secondary btn-icon"
79
+ :disabled="!canIncrement"
80
+ :aria-label="t('quantity.increase')"
81
+ @click="increment"
82
+ >
83
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M8 3v10M3 8h10" /></svg>
84
+ </button>
85
+ <input
86
+ :name="name"
87
+ :id="inputId"
88
+ :value="display"
89
+ :readonly="readonly"
90
+ :disabled="isDisabled"
91
+ :aria-invalid="isInvalid || undefined"
92
+ type="tel"
93
+ inputmode="numeric"
94
+ autocomplete="off"
95
+ class="form-control fanum text-center"
96
+ :class="{ 'is-invalid': isInvalid }"
97
+ @input="onInput"
98
+ @change="clampOnBlur"
99
+ @focus="$emit('focus', $event)"
100
+ @blur="$emit('blur', $event)"
101
+ />
102
+ <button
103
+ type="button"
104
+ class="btn-outline theme-secondary btn-icon"
105
+ :disabled="!canDecrement"
106
+ :aria-label="t('quantity.decrease')"
107
+ @click="decrement"
108
+ >
109
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M3 8h10" /></svg>
110
+ </button>
111
+ <span v-if="unitText" class="input-group-text fanum">{{ unitText }}</span>
112
+ <slot name="suffix" />
113
+ </div>
114
114
  </template>
115
- <style>
116
-
117
- .input-group-quantity .input-group-text,
118
- .input-group-quantity .btn {
119
- background-color: var(--bs-body-bg);
120
- --bs-btn-hover-bg: var(--bs-secondary-bg);
121
- --bs-btn-active-bg: var(--bs-secondary-bg);
122
- }
123
-
124
- .input-group-quantity input.form-control {
125
- border: 0 !important;
126
- box-shadow: none !important;
127
- }
128
-
129
- .input-group-quantity .btn svg {
130
- width: 14px;
131
- height: 14px;
132
- }
133
- </style>