@westartcom/bread-quasar-fields 0.0.2

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,115 @@
1
+ <template>
2
+ <div class="field-wrap" :id="id">
3
+ <div class="field-wrap-inner">
4
+ <q-input
5
+ :label="field.label + ((field.required) ? ' *' : '')"
6
+ :name="field.name"
7
+ :id="id + '-input'"
8
+ :placeholder="field.placeholder"
9
+ type="email"
10
+ class="field"
11
+ :class="{'invalid': (invalid && internalErrors && internalErrors.length)}"
12
+ :maxlength="(field.extra_data && field.extra_data.max) ? field.extra_data.max : null"
13
+ :minlength="(field.extra_data && field.extra_data.min) ? field.extra_data.min : null"
14
+ v-model="internalValue"
15
+ :rules="(field.required) ? [val => !!val || 'Field is required'] : []"
16
+ :hint="field.description"
17
+ outlined>
18
+ </q-input>
19
+ </div>
20
+
21
+ <div class="validation-errors" v-if="invalid && internalErrors && internalErrors.length">
22
+ <span class="error danger-txt-color" v-for="(error, index) in internalErrors" :key="index + 'error'">{{ error }}</span>
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <script lang="ts">
28
+ import { defineComponent, PropType } from 'vue';
29
+ import { ModelFieldDefinition } from '@westartcom/bread-quasar-fields/interfaces/ModelDefinition';
30
+ import { FieldType } from '@westartcom/bread-quasar-fields/enums/FieldType';
31
+ import { InputType } from '@westartcom/bread-quasar-fields/enums/InputType';
32
+
33
+ interface EmailFieldDefinition extends ModelFieldDefinition<FieldType.TEXT, InputType.EMAIL> {
34
+ default: string | null;
35
+ }
36
+
37
+ export default defineComponent({
38
+ props: {
39
+ id: {
40
+ type: String,
41
+ required: true
42
+ },
43
+ field: {
44
+ type: Object as PropType<EmailFieldDefinition>,
45
+ required: true
46
+ },
47
+ modelValue: String,
48
+ errors: Array as PropType<string[]>
49
+ },
50
+
51
+ emits: [
52
+ 'update:modelValue',
53
+ 'update:invalid'
54
+ ],
55
+
56
+ data() {
57
+ return {
58
+ invalid: null as boolean | null,
59
+ internalValue: '',
60
+ internalErrors: [] as string[]
61
+ };
62
+ },
63
+
64
+ mounted() {
65
+ if (this.modelValue) {
66
+ this.internalValue = this.modelValue;
67
+ } else if (this.field.default) {
68
+ this.internalValue = this.field.default;
69
+ }
70
+
71
+ this.validate(this.internalValue);
72
+ },
73
+
74
+ watch: {
75
+ modelValue: function(val) {
76
+ if (val !== this.internalValue) {
77
+ this.internalValue = val;
78
+ }
79
+ },
80
+
81
+ internalValue: function(val) {
82
+ this.validate(val);
83
+ this.$emit('update:modelValue', val);
84
+ },
85
+
86
+ errors: {
87
+ handler: function(val) {
88
+ this.internalErrors = val;
89
+ },
90
+ deep: true
91
+ },
92
+
93
+ internalErrors: {
94
+ handler: function(val) {
95
+ this.invalid = !!(val && val.length);
96
+ },
97
+ deep: true
98
+ },
99
+
100
+ invalid: function(val) {
101
+ this.$emit('update:invalid', val);
102
+ }
103
+ },
104
+
105
+ methods: {
106
+ validate: function(val) {
107
+ this.invalid = this.field.required && !val;
108
+ }
109
+ }
110
+ });
111
+ </script>
112
+
113
+ <style scoped lang="scss">
114
+
115
+ </style>
@@ -0,0 +1,394 @@
1
+ <template>
2
+ <div class="field-wrap" :id="id">
3
+ <div class="field-wrap-inner">
4
+ <div class="field-label-wrap">
5
+ <label v-if="field.label" :for="id + '-input'">{{ field.label }}<span class="required-asterix" v-if="field.required"> *</span></label>
6
+ </div>
7
+
8
+ <div class="files-upload">
9
+ <div v-show="canAdd">
10
+ <button type="button" class="btn hollow" @click="$refs.fileUpload.click()">
11
+ <template v-if="allowMultiple">
12
+ {{ $i18n.get('actions.add_files') }}
13
+ </template>
14
+ <template v-else>
15
+ {{ $i18n.get('actions.add_file') }}
16
+ </template>
17
+ </button>
18
+ <input
19
+ ref="fileUpload"
20
+ type="file"
21
+ style="display: none;"
22
+ :id="id + '-input'"
23
+ :accept="allowedMimeTypes"
24
+ :multiple="allowMultiple"
25
+ @change="onFileSelect($event.target.files)">
26
+ </div>
27
+
28
+ <div class="added-files">
29
+ <div class="file" v-for="(file, index) in addedFiles" :key="index">
30
+ <div class="column download">
31
+ <template v-if="file.url">
32
+ <a :href="file.url" :download="file.name" class="view-file">
33
+ <download-icon class="icon-2x"></download-icon>
34
+ </a>
35
+ </template>
36
+ <template v-else-if="file.error">
37
+ <upload-error-icon class="icon-2x"></upload-error-icon>
38
+ </template>
39
+ <template v-else>
40
+ <file-upload-icon class="icon-2x"></file-upload-icon>
41
+ </template>
42
+ </div>
43
+ <div class="column progress" v-if="file.hasOwnProperty('progress') && file.progress < 100">{{ file.progress }}%</div>
44
+ <div class="column name"><span>{{ file.name }}</span></div>
45
+ <div class="column remove">
46
+ <delete-icon class="icon-2x" @click="onRemoveFile(index, file)"></delete-icon>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <div class="field-description" v-if="field.description">{{ field.description }}</div>
54
+
55
+ <div class="validation-errors" v-if="invalid && internalErrors && internalErrors.length">
56
+ <span class="error danger-txt-color" v-for="(error, index) in internalErrors" :key="index + 'error'">{{ error }}</span>
57
+ </div>
58
+ </div>
59
+ </template>
60
+
61
+ <script lang="ts">
62
+ import { defineAsyncComponent, defineComponent, PropType } from 'vue';
63
+ import { ModelFieldDefinition } from '@westartcom/bread-quasar-fields/interfaces/ModelDefinition';
64
+ import { FieldType } from '@westartcom/bread-quasar-fields/enums/FieldType';
65
+ import { InputType } from '@westartcom/bread-quasar-fields/enums/InputType';
66
+ import { UploadFile } from '@westartcom/bread-quasar-fields/interfaces/UploadFile';
67
+ // import * as tus from 'tus-js-client';
68
+
69
+ interface FileUploadFieldDefinition extends ModelFieldDefinition<FieldType.MEDIA, InputType.FILE_UPLOAD> {
70
+ default: string | null;
71
+ extra_data: {
72
+ media_type: 'files';
73
+ tus: boolean;
74
+ tus_endpoint: string;
75
+ mime_types: string[];
76
+ multiple: boolean;
77
+ collection: string;
78
+ }
79
+ }
80
+
81
+ export default defineComponent({
82
+ components: {
83
+ DeleteIcon: defineAsyncComponent(() => import('@westartcom/bread-quasar-fields/icons/DeleteIcon.vue')),
84
+ DownloadIcon: defineAsyncComponent(() => import('@westartcom/bread-quasar-fields/icons/DownloadIcon.vue')),
85
+ FileUploadIcon: defineAsyncComponent(() => import('@westartcom/bread-quasar-fields/icons/FileUploadIcon.vue')),
86
+ UploadErrorIcon: defineAsyncComponent(() => import('@westartcom/bread-quasar-fields/icons/FileAlertOutlineIcon.vue'))
87
+ },
88
+
89
+ props: {
90
+ id: {
91
+ type: String,
92
+ required: true
93
+ },
94
+ field: {
95
+ type: Object as PropType<FileUploadFieldDefinition>,
96
+ required: true
97
+ },
98
+ modelValue: Array,
99
+ errors: Array as PropType<string[]>
100
+ },
101
+
102
+ emits: [
103
+ 'update:modelValue',
104
+ 'update:uploading',
105
+ 'update:invalid'
106
+ ],
107
+
108
+ data() {
109
+ return {
110
+ invalid: null as boolean | null,
111
+ files: [] as UploadFile[],
112
+ internalErrors: [],
113
+ tusUploads: {} as {
114
+ [key: string]: any;
115
+ }
116
+ };
117
+ },
118
+
119
+ computed : {
120
+ allowMultiple() {
121
+ return (this.field.extra_data && this.field.extra_data.multiple);
122
+ },
123
+
124
+ allowedMimeTypes() {
125
+ return ((this.field.extra_data?.mime_types) ? this.field.extra_data.mime_types : ['*']).join('|');
126
+ },
127
+
128
+ canAdd() {
129
+ return this.allowMultiple || !this.files.filter(file => !file.remove).length;
130
+ },
131
+
132
+ useTus() {
133
+ return (this.field.extra_data?.tus === true);
134
+ },
135
+
136
+ addedFiles() {
137
+ return this.files.filter(file => !file.remove);
138
+ }
139
+ },
140
+
141
+ mounted() {
142
+ this.tusUploads = {};
143
+
144
+ if (this.modelValue) {
145
+ this.files = (Array.isArray(this.modelValue)) ? this.modelValue : [this.modelValue];
146
+ } else if (this.field.default && Array.isArray(this.field.default)) {
147
+ this.files = this.field.default;
148
+ }
149
+
150
+ this.validate(this.files);
151
+ },
152
+
153
+ watch: {
154
+ modelValue: {
155
+ handler: function(val) {
156
+ if (
157
+ val &&
158
+ Array.isArray(val)
159
+ ) {
160
+ this.files = val;
161
+ } else {
162
+ this.files = [];
163
+ (this.$refs.fileUpload as any).value = '';
164
+ }
165
+ },
166
+ deep: true
167
+ },
168
+
169
+ files: {
170
+ handler: function(val) {
171
+ this.validate(val);
172
+ this.$emit('update:modelValue', val);
173
+
174
+ if (this.useTus) {
175
+ if (val.some(file => file.progress < 100)) {
176
+ this.$emit('update:uploading', true);
177
+ } else {
178
+ this.$emit('update:uploading', false);
179
+ }
180
+ }
181
+ },
182
+ deep: true
183
+ },
184
+
185
+ errors: {
186
+ handler: function(val) {
187
+ this.internalErrors = val;
188
+ },
189
+ deep: true
190
+ },
191
+
192
+ internalErrors: {
193
+ handler: function(val) {
194
+ this.invalid = !!(val && val.length);
195
+ },
196
+ deep: true
197
+ },
198
+
199
+ invalid: function(val) {
200
+ this.$emit('update:invalid', val);
201
+ }
202
+ },
203
+
204
+ methods: {
205
+ async onFileSelect(files: FileList) {
206
+ if (files.length) {
207
+ for (let i = 0; i < files.length; i++) {
208
+ const file = files.item(i);
209
+
210
+ if (!file) {
211
+ continue;
212
+ }
213
+
214
+ if (this.useTus) {
215
+ await this.startTus(file);
216
+ } else {
217
+ this.addFile(await this.getBase64(file), file.name);
218
+ }
219
+ }
220
+ }
221
+ },
222
+
223
+ async startTus(file: File) {
224
+ throw 'TUS not implemented';
225
+ // const vueInstance = this;
226
+ // const tempKey = Date.now().toString() + '/' + file.name;
227
+ //
228
+ // this.addFile(tempKey, file.name, true);
229
+ //
230
+ // const upload = new tus.Upload(file, {
231
+ // endpoint: window.axios.defaults.baseURL + 'v1/' + this.field.extra_data.tus_endpoint,
232
+ // chunkSize: (2 * (1024 * 1024)), // 2 MB
233
+ // metadata: {
234
+ // filename: file.name,
235
+ // filetype: file.type,
236
+ // },
237
+ // onBeforeRequest(req) {
238
+ // let xhr = req.getUnderlyingObject()
239
+ // xhr.withCredentials = true;
240
+ // },
241
+ // onProgress(bytesUploaded, bytesTotal) {
242
+ // let percentage = (bytesUploaded / bytesTotal * 100).toFixed(0)
243
+ // vueInstance.files = vueInstance.files.map((uploadFile) => {
244
+ // if (uploadFile.tempKey && uploadFile.tempKey === tempKey) {
245
+ // uploadFile.progress = percentage;
246
+ // }
247
+ // return uploadFile;
248
+ // });
249
+ // },
250
+ // onSuccess() {
251
+ // const urlParts = upload.url.split('/');
252
+ // const tusKey = urlParts[urlParts.length - 1];
253
+ // vueInstance.files = vueInstance.files.map((uploadFile) => {
254
+ // if (uploadFile.tempKey && uploadFile.tempKey === tempKey) {
255
+ // uploadFile.tusKey = tusKey;
256
+ // }
257
+ // return uploadFile;
258
+ // });
259
+ // },
260
+ // onError() {
261
+ // vueInstance.$modal.show('error', this.$i18n.get('generic.an_error_occurred'), this.$i18n.get('validation.file_upload_failed'));
262
+ // vueInstance.files = vueInstance.files.map((uploadFile) => {
263
+ // if (uploadFile.tempKey && uploadFile.tempKey === tempKey) {
264
+ // uploadFile.error = true;
265
+ // }
266
+ // return uploadFile;
267
+ // });
268
+ // }
269
+ // });
270
+ //
271
+ // upload.start();
272
+ // this.tusUploads[tempKey] = upload;
273
+ },
274
+
275
+ getBase64(file: File): Promise<string> {
276
+ return new Promise((resolve, reject) => {
277
+ const reader = new FileReader();
278
+ reader.readAsDataURL(file);
279
+ reader.onload = () => resolve(reader.result as string);
280
+ reader.onerror = error => reject(error);
281
+ });
282
+ },
283
+
284
+ addFile(file: string, name: string, isTus = false) {
285
+ if (isTus) {
286
+ this.files.push({
287
+ tempKey: file,
288
+ tusKey: null,
289
+ error: false,
290
+ progress: 0,
291
+ name: name,
292
+ add: true
293
+ });
294
+ } else {
295
+ this.files.push({
296
+ base64: file,
297
+ name: name,
298
+ add: true
299
+ });
300
+ }
301
+ },
302
+
303
+ onRemoveFile(index, file) {
304
+ this.$modal.confirm(
305
+ undefined,
306
+ undefined,
307
+ undefined,
308
+ undefined,
309
+ () => {
310
+ this.removeFile(index, file);
311
+ }
312
+ );
313
+ },
314
+
315
+ removeFile(index: number, file: UploadFile) {
316
+ if (file.add === true) {
317
+ // Safe to just pop it from array since it's not been saved yet
318
+ this.files = this.files.filter((addedFile, idx) => idx !== index);
319
+
320
+ if (
321
+ file.tempKey &&
322
+ this.tusUploads[file.tempKey]
323
+ ) {
324
+ this.tusUploads[file.tempKey].abort(true);
325
+ }
326
+
327
+ (this.$refs.fileUpload as any).value = ''; // Need to clear the input field so the file can be selected again
328
+ } else {
329
+ // We need to flag the file for removal in API
330
+ file.remove = true;
331
+ this.files = this.files.map((existing, existingIndex) => {
332
+ return (existingIndex === index) ? file : existing;
333
+ });
334
+ }
335
+ },
336
+
337
+ validate(val) {
338
+ this.invalid = (this.field.required && (!val.length || !val.filter(file => !file.remove).length));
339
+ },
340
+
341
+ safeUrl(file) {
342
+ return (file.url) ? file.url : file.base64;
343
+ }
344
+ }
345
+ });
346
+ </script>
347
+
348
+ <style scoped lang="scss">
349
+ .files-upload {
350
+ max-width: var(--max-input-width);
351
+ width: 100%;
352
+ }
353
+
354
+ .added-files {
355
+ .file {
356
+ display: flex;
357
+ align-items: center;
358
+
359
+ &:not(:last-of-type) {
360
+ border-bottom: 1px solid var(--color-medium-tint);
361
+ }
362
+ }
363
+
364
+ .name {
365
+ overflow: hidden;
366
+ white-space: nowrap;
367
+ text-overflow: ellipsis;
368
+ flex-grow: 2;
369
+ padding: var(--global-spacing);
370
+ }
371
+
372
+ .download,
373
+ .remove {
374
+ flex-shrink: 1;
375
+ padding: var(--global-spacing) 0;
376
+ }
377
+
378
+ .download {
379
+ color: var(--color-medium);
380
+
381
+ a {
382
+ color: var(--color-primary);
383
+ }
384
+ }
385
+
386
+ .remove {
387
+ color: var(--color-danger);
388
+
389
+ span {
390
+ cursor: pointer;
391
+ }
392
+ }
393
+ }
394
+ </style>