inertia-bootstrap-forms 1.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.
@@ -0,0 +1,196 @@
1
+ <script>
2
+ import {inject} from "vue";
3
+
4
+ export default {
5
+ props: {
6
+ modelValue: '',
7
+ name: {
8
+ type: String,
9
+ required: true,
10
+ },
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;
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
+
72
+ _this.files.find(item => item.id === file.id)['error'] = '';
73
+
74
+ await axios.post(this.endpoint,
75
+ formData, {
76
+ headers: {
77
+ 'Content-Type': 'multipart/form-data'
78
+ },
79
+ onUploadProgress: progressEvent => _this.calculatePercent(progressEvent, file)
80
+ }
81
+ ).then(function (res) {
82
+ _this.addFileToInputValue(file, res)
83
+ }).catch(function (err) {
84
+ _this.files.find(item => item.id === file.id)['error'] = (err.response?.data?.message || err.message);
85
+ });
86
+ },
87
+ async deleteFile(file) {
88
+ this.files = this.files.filter(item => item.id !== file.id);
89
+ await axios.delete(this.endpoint, {
90
+ data: file.path
91
+ });
92
+ },
93
+ fileSelected(e) {
94
+ Array.from(e.target.files).forEach(file => this.addFile(file));
95
+ }
96
+ },
97
+ data() {
98
+ return {
99
+ files: [],
100
+ }
101
+ }
102
+ }
103
+ </script>
104
+ <template>
105
+ <div class="file-input-uploader">
106
+ <input
107
+ ref="input"
108
+ :name="name"
109
+ :class="{'is-invalid': form?.errors[name] !== undefined}"
110
+ :disabled="form?.processing"
111
+ type="file"
112
+ :multiple="multiple"
113
+ @change="fileSelected"
114
+ class="form-control">
115
+ <div class="file-input-uploader--list">
116
+ <div :id="'file-'+file.id" class="file-input-uploader--list-item" v-for="file in allFiles">
117
+ <div class="file-input-uploader--progress" v-if="file.percent && file.percent< 100">
118
+ <div :style="{width: file.percent + '%'}"></div>
119
+ </div>
120
+ <div>{{ file.file.name }}</div>
121
+ <div class="file-input-uploader--list-error" v-if="file.error">{{ file.error }}</div>
122
+ <div class="file-input-uploader--list-item--size" dir="ltr">{{ Math.round(file.file.size / 1024) }}Kb</div>
123
+ <div class="file-input-uploader--list-item--actions">
124
+ <button @click="uploadFile(file)" type="button" class="file-input-uploader--list-item--btn" v-if="file.error">
125
+ <svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
126
+ <path
127
+ 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"/>
128
+ </svg>
129
+ </button>
130
+ <button @click="deleteFile(file)" type="button" class="file-input-uploader--list-item--btn" v-if="file.path || file.error">
131
+ <svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
132
+ <path
133
+ 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"/>
134
+ </svg>
135
+ </button>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </template>
141
+ <style>
142
+ .file-input-uploader .file-input-uploader--list .file-input-uploader--list-item {
143
+ background-color: var(--bs-secondary-bg);
144
+ border-radius: var(--bs-border-radius);
145
+ position: relative;
146
+ display: flex;
147
+ align-items: center;
148
+ margin: 5px 0;
149
+ padding: 5px 10px;
150
+ }
151
+
152
+ .file-input-uploader .file-input-uploader--list .file-input-uploader--list-item--size {
153
+ margin-left: auto;
154
+ font-size: 90%;
155
+ padding: 0 5px;
156
+ color: var(--bs-secondary-color);
157
+ }
158
+
159
+ .file-input-uploader .file-input-uploader--list-item--btn {
160
+ display: inline-block;
161
+ outline: none;
162
+ border: 0;
163
+ padding: 7px 2px;
164
+ }
165
+
166
+ .file-input-uploader .file-input-uploader--list-item--btn:not(:last-child){
167
+ margin-right: 5px;
168
+ }
169
+
170
+ .file-input-uploader .file-input-uploader--list-item--btn > svg {
171
+ height: 20px;
172
+ width: 20px;
173
+ vertical-align: middle;
174
+ }
175
+
176
+ .file-input-uploader .file-input-uploader--progress {
177
+ position: absolute;
178
+ top: 2px;
179
+ left: 5px;
180
+ right: 5px;
181
+ }
182
+
183
+ .file-input-uploader .file-input-uploader--progress > div {
184
+ background-color: #de0021;
185
+ height: 7px;
186
+ width: 0;
187
+ min-width: 1%;
188
+ border-radius: var(--bs-border-radius)
189
+ }
190
+
191
+ .file-input-uploader .file-input-uploader--list-error {
192
+ font-size: 90%;
193
+ margin: 0 5px;
194
+ color: var(--bs-danger, #de0021);
195
+ }
196
+ </style>
@@ -0,0 +1,129 @@
1
+ <script>
2
+ import {useForm} from "@inertiajs/vue3";
3
+ import {computed, reactive, toRef} from "vue";
4
+ import Numbers from "@/Plugins/Numbers.js";
5
+ import Alert from "@/Bootstrap/Alert.vue";
6
+
7
+ export default {
8
+ components: {Alert},
9
+ emits: ['submit', 'reset', 'onStart', 'onFinish', 'onSuccess', 'change'],
10
+ props: {
11
+ url: {
12
+ type: String,
13
+ default: '',
14
+ required: false,
15
+ },
16
+ method: {
17
+ default: 'post'
18
+ },
19
+ only: {
20
+ type: Array,
21
+ default: [],
22
+ required: false,
23
+ },
24
+ modelValue: {
25
+ type: Object,
26
+ default: {},
27
+ required: false,
28
+ },
29
+ submitHandler: {
30
+ type: Function,
31
+ default: null,
32
+ required: false,
33
+ },
34
+ },
35
+ setup(props) {
36
+ const formEl = toRef('formEl');
37
+ const formData = reactive(props.modelValue);
38
+ const form = useForm(formData);
39
+
40
+ form.getID = function (name) {
41
+ return (formEl.value && formEl.value.id || 'form') + name;
42
+ };
43
+
44
+ return {form, formData, formEl};
45
+ },
46
+ provide() {
47
+ return {
48
+ form: computed(() => this.form)
49
+ }
50
+ },
51
+ watch: {
52
+ form: {
53
+ handler: function (newVal) {
54
+ const {
55
+ isDirty,
56
+ errors,
57
+ hasErrors,
58
+ processing,
59
+ progress,
60
+ wasSuccessful,
61
+ recentlySuccessful,
62
+ __rememberable,
63
+ ...cleanedData
64
+ } = newVal;
65
+
66
+ this.$emit('change', cleanedData);
67
+ this.$emit('update:modelValue', cleanedData)
68
+ },
69
+ deep: true,
70
+ }
71
+ },
72
+ methods: {
73
+ reset(){
74
+ this.form.reset();
75
+ },
76
+ async submit(event) {
77
+ const formValues = this.modelValue;
78
+
79
+ if (this.submitHandler) {
80
+ await this.submitHandler(event);
81
+ } else {
82
+ this.$emit('submit', event);
83
+ await this.form.transform(function (formDataValues) {
84
+ return JSON.parse(Numbers.toEnglish(JSON.stringify({
85
+ ...formValues,
86
+ ...formDataValues,
87
+ })));
88
+ }).submit(this.method.toString(), this.url, {
89
+ only: this.only,
90
+ onStart: () => {
91
+ this.$emit('onStart');
92
+ this.form.clearErrors();
93
+ },
94
+ onFinish: (data) => {
95
+ this.$emit('onFinish', data);
96
+ },
97
+ onError: (errors) => {
98
+ },
99
+ onSuccess: (data) => {
100
+ this.reset();
101
+ this.$emit('onSuccess', data);
102
+ }
103
+ });
104
+ }
105
+ }
106
+ },
107
+ expose: ['submit', 'reset'],
108
+ }
109
+
110
+ </script>
111
+
112
+ <template>
113
+ <form ref="formEl"
114
+ :action="url"
115
+ :method="method"
116
+ @submit.prevent="submit"
117
+ @reset="$emit('reset')"
118
+ :class="{'form-processing loading': form.processing,}"
119
+ :novalidate="!!Object.values(form.errors).length">
120
+ <slot name="errors" v-if="form.hasErrors" :form="form">
121
+ <Alert type="danger">
122
+ <ul class="list-unstyled p-0 m-0">
123
+ <li v-for="error in form.errors">{{error}}</li>
124
+ </ul>
125
+ </Alert>
126
+ </slot>
127
+ <slot :form="form" :submit="submit"/>
128
+ </form>
129
+ </template>
@@ -0,0 +1,21 @@
1
+ <script>
2
+ export default {
3
+ props: {
4
+ default: false,
5
+ required: false,
6
+ },
7
+ emits: ['update:modelValue'],
8
+ }
9
+ </script>
10
+
11
+ <template>
12
+ <label class="mb-2 d-block">
13
+ <slot/>
14
+ <span class="text-danger" v-if="required">*</span>
15
+ </label>
16
+ </template>
17
+ <style scoped>
18
+ label {
19
+ cursor: pointer;
20
+ }
21
+ </style>
@@ -0,0 +1,22 @@
1
+ <script>
2
+ import TextInput from "./TextInput.vue";
3
+
4
+ export default {
5
+ components: {
6
+ TextInput
7
+ },
8
+ props: {
9
+ name: {
10
+ type: String,
11
+ default: 'mobile',
12
+ required: true,
13
+ },
14
+ },
15
+ }
16
+ </script>
17
+ <template>
18
+ <TextInput
19
+ :name="name"
20
+ placeholder="موبایل خود را وارد کنید"
21
+ type="tel"/>
22
+ </template>
@@ -0,0 +1,22 @@
1
+ <script>
2
+ import TextInput from "./TextInput.vue";
3
+
4
+ export default {
5
+ components: {
6
+ TextInput
7
+ },
8
+ props: {
9
+ name: {
10
+ type: String,
11
+ default: 'password',
12
+ required: true,
13
+ },
14
+ },
15
+ }
16
+ </script>
17
+ <template>
18
+ <TextInput
19
+ :name="name"
20
+ placeholder="گذرواژه خود را وارد کنید"
21
+ type="password"/>
22
+ </template>
@@ -0,0 +1,193 @@
1
+ <script>
2
+ import '../vue-select.css';
3
+ import vSelect from 'vue-select';
4
+ import axios from "axios";
5
+ import Icon from "@/Components/Icon.vue";
6
+ import {inject} from "vue";
7
+
8
+ export default {
9
+ components: {Icon, vSelect},
10
+ emits: ['update:modelValue', 'search', 'change', 'selected'],
11
+ props: {
12
+ name: {
13
+ type: String,
14
+ required: true,
15
+ },
16
+ modelValue: '',
17
+ label: {
18
+ type: String,
19
+ default: 'name',
20
+ },
21
+ placeholder: {
22
+ type: String,
23
+ default: '-- انتخاب کنید',
24
+ },
25
+ multiple: {
26
+ type: Boolean,
27
+ default: false,
28
+ },
29
+ options: Array,
30
+ search: {
31
+ type: Object,
32
+ default: {
33
+ url: null,
34
+ }
35
+ },
36
+ reduce: {
37
+ type: Function,
38
+ default: (item) => item.id
39
+ },
40
+ },
41
+ computed: {
42
+ allOptions() {
43
+ return [
44
+ ...(this.options !== undefined ? this.options : []),
45
+ // ...this.selectedSearchOptions,
46
+ ...this.searchOptions,
47
+ ];
48
+ },
49
+ },
50
+ watch: {
51
+ 'options': function (newVal) {
52
+ if (this.form && this.form[this.name] && !newVal.filter(item => item.id === this.form[this.name] || item === this.form[this.name]).length) {
53
+ this.form[this.name] = null;
54
+ }
55
+ },
56
+ },
57
+ setup(props) {
58
+ let form = inject('form');
59
+
60
+ if (form === undefined) {
61
+ form = {
62
+ errors: {},
63
+ getID(name) {
64
+ }
65
+ };
66
+ }
67
+
68
+ return {form};
69
+ },
70
+ methods: {
71
+ async searchHandler(search, loading) {
72
+ if (!search || search.length < 3) return;
73
+
74
+ if (this.searchTimeout) {
75
+ clearTimeout(this.searchTimeout);
76
+ }
77
+
78
+ let _this = this;
79
+ this.searchTimeout = setTimeout(function () {
80
+ _this.doSearch(search, loading);
81
+ }, 500);
82
+ },
83
+ async doSearch(search, loading) {
84
+ if (!this.search.url) return false;
85
+
86
+ if (this.searchController) {
87
+ this.searchController.abort();
88
+ }
89
+ this.searchController = new AbortController();
90
+
91
+ loading(true);
92
+ this.searchOptions = [];
93
+ await axios.post(this.search.url, {
94
+ query: search,
95
+ }).then(res => {
96
+ this.searchOptions = res.data;
97
+ }).finally(() => {
98
+ loading(false);
99
+ });
100
+ },
101
+ itemsSelected(items) {
102
+ this.$emit('change', items);
103
+ this.$emit('selected', items);
104
+ if (this.search && this.search.url && this.searchOptions && this.searchOptions.length) {
105
+ this.selectedSearchOptions = items;
106
+ }
107
+ },
108
+ input(event) {
109
+ this.$emit('change', event);
110
+ this.form?.handlers?.handleDomInput(event);
111
+ },
112
+ change(event) {
113
+ this.$emit('change', event);
114
+ this.form?.handlers?.handleChange(event);
115
+ }
116
+ },
117
+ data() {
118
+ return {
119
+ searchTimeout: null,
120
+ searchController: null,
121
+ selectedSearchOptions: [],
122
+ searchOptions: [],
123
+ }
124
+ }
125
+ }
126
+ </script>
127
+ <template>
128
+ <v-select
129
+ ref="input"
130
+ v-model="form[name]"
131
+ @change="change"
132
+ @input="input"
133
+ @search="searchHandler"
134
+ @option:selected="itemsSelected"
135
+ @option:deselected="change"
136
+ :label="label"
137
+ :multiple="multiple"
138
+ :options="allOptions"
139
+ :disabled="form?.processing"
140
+ :filterable="!search?.url"
141
+ :placeholder="placeholder"
142
+ class="fanum"
143
+ :class="{'is-invalid': form?.errors[name]}"
144
+ :reduce="reduce"
145
+ :autocomplete="false"
146
+ :deselectFromDropdown="true"
147
+ :clearable="false">
148
+ <template #no-options="{ search, searching, loading }">
149
+ <span v-if="this.search?.url" class="p-2 d-block bg-body-tertiary">
150
+ <Icon icon="search" class="align-middle"/>
151
+ برای جستجو تایپ کنید
152
+ </span>
153
+ <span v-else>هیچ آیتمی پیدا نشد</span>
154
+ </template>
155
+ <slot/>
156
+ </v-select>
157
+ </template>
158
+ <style>
159
+ .v-select {
160
+ min-width: 100px;
161
+ max-width: 100%;
162
+ }
163
+
164
+ .v-select > ul {
165
+ margin: 0 !important;
166
+ padding: 0 !important;
167
+ }
168
+
169
+ .input-group > .v-select, .input-group > .form-floating, .input-group > .v-select {
170
+ position: relative;
171
+ flex: 1 1 auto;
172
+ width: 1%;
173
+ min-width: 0;
174
+ }
175
+
176
+ .input-group > .v-select:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) .vs__dropdown-toggle {
177
+ margin-right: calc(var(--bs-border-width) * -1);
178
+ border-top-right-radius: 0;
179
+ border-bottom-right-radius: 0;
180
+ }
181
+
182
+ .input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3),
183
+ .input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-control,
184
+ .input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-select,
185
+ .input-group:not(.has-validation) > .v-select:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating) .vs__dropdown-toggle {
186
+ border-top-left-radius: 0;
187
+ border-bottom-left-radius: 0;
188
+ }
189
+
190
+ .is-invalid .vs__dropdown-toggle {
191
+ border-color: var(--bs-form-invalid-border-color);
192
+ }
193
+ </style>
@@ -0,0 +1,20 @@
1
+ <script>
2
+ import Spinner from "@/Bootstrap/Spinner.vue";
3
+
4
+ export default {
5
+ components: {
6
+ Spinner
7
+ },
8
+ inject: ['form'],
9
+ props: {},
10
+ }
11
+ </script>
12
+ <template>
13
+ <button
14
+ type="submit"
15
+ :disabled="form?.processing"
16
+ class="btn btn-primary px-3 px-sm-4">
17
+ <Spinner v-if="form?.processing"/>
18
+ تایید و ثبت اطلاعات
19
+ </button>
20
+ </template>