@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,485 @@
1
+ <template>
2
+ <div class="field-wrap" :id="id" :class="{'disabled': (isDisabled)}">
3
+ <div class="field-wrap-inner">
4
+ <q-select
5
+ :rules="(field.required) ? [val => !!val || 'Field is required'] : []"
6
+ :options="qFilteredOptions"
7
+ @filter="qFilter"
8
+ map-options
9
+ dropdown-icon="fa-solid fa-angle-down"
10
+ use-input
11
+ outlined
12
+ :disable="isDisabled"
13
+ input-debounce="100"
14
+ :label="field.label + ((field.required) ? ' *' : '')"
15
+ emit-value
16
+ v-model="internalValue">
17
+ <template #selected-item="scope">
18
+ <q-chip
19
+ removable
20
+ icon-remove="fa-solid fa-remove"
21
+ @remove="scope.removeAtIndex(scope.index)">
22
+ {{ scope.opt.label }}
23
+ </q-chip>
24
+ </template>
25
+ </q-select>
26
+
27
+ <!-- <div class="select">-->
28
+ <!-- <div class="selected" @click="toggleOptions">-->
29
+ <!-- <span class="label" :class="{'placeholder': (!allOptions[internalValue] && field.placeholder || field.extra_data.multiple)}">-->
30
+ <!-- <template v-if="(!allOptions[internalValue] || field.extra_data.multiple) && field.placeholder">-->
31
+ <!-- {{ field.placeholder }}-->
32
+ <!-- </template>-->
33
+ <!-- <template v-else-if="field.extra_data.multiple">-->
34
+ <!-- {{ $i18n.get('actions.add') }}-->
35
+ <!-- </template>-->
36
+ <!-- <template v-else>-->
37
+ <!-- {{ allOptions[internalValue] }}-->
38
+ <!-- </template>-->
39
+ <!-- </span>-->
40
+ <!-- <span class="toggle-button">-->
41
+ <!-- <chevron-down-icon v-if="!select.visible" />-->
42
+ <!-- <chevron-up-icon v-else />-->
43
+ <!-- </span>-->
44
+ <!-- </div>-->
45
+
46
+ <!-- <div class="options" v-if="select.visible">-->
47
+ <!-- <div class="search" v-if="Object.keys(allOptions).length > 20">-->
48
+ <!-- <input type="text" :placeholder="$i18n.get('actions.search')" v-model="select.searchQuery">-->
49
+ <!-- </div>-->
50
+
51
+ <!-- <div v-if="!field.required && !field.extra_data.multiple" class="option" @click="selectOption(null)">-->
52
+ <!-- <slot name="no-option-label">-->
53
+ <!-- {{ $i18n.get('generic.no_option') }}-->
54
+ <!-- </slot>-->
55
+ <!-- </div>-->
56
+
57
+ <!-- <div-->
58
+ <!-- v-for="(label, optionValue) in filteredOptions"-->
59
+ <!-- :key="optionValue + '-option'"-->
60
+ <!-- class="option"-->
61
+ <!-- :class="{'selected-option': (internalValue && internalValue.includes(optionValue))}"-->
62
+ <!-- @click="selectOption(optionValue)">-->
63
+ <!-- <slot name="option" :option-value="optionValue" :label="label">-->
64
+ <!-- {{ label }}-->
65
+ <!-- </slot>-->
66
+ <!-- </div>-->
67
+
68
+ <!-- <div class="no-result" v-if="!Object.keys(filteredOptions).length">-->
69
+ <!-- {{ $i18n.get('generic.no_result') }}-->
70
+ <!-- </div>-->
71
+ <!-- </div>-->
72
+ <!-- </div>-->
73
+ </div>
74
+
75
+ <div class="field-description" v-if="field.description">{{ field.description }}</div>
76
+
77
+ <div class="validation-errors" v-if="invalid && internalErrors && internalErrors.length">
78
+ <span class="error danger-txt-color" v-for="(error, index) in internalErrors" :key="index + 'error'">{{ error }}</span>
79
+ </div>
80
+
81
+ <!-- <div class="selected-options-multiple table-list" v-if="multiValueStyle === 'table' && internalValue.length && field.extra_data.multiple">-->
82
+ <!-- <div class="selected-option-row table-row" v-for="(option, index) in internalValue" :key="index">-->
83
+ <!-- <div class="selected-option-label column">-->
84
+ <!-- <slot name="selected-option-label" :option="option" :all-options="allOptions">-->
85
+ <!-- {{ allOptions[option] }}-->
86
+ <!-- </slot>-->
87
+ <!-- </div>-->
88
+ <!-- <div class="column actions">-->
89
+ <!-- <slot name="selected-option-actions" :option="option" :all-options="allOptions" />-->
90
+ <!-- <button class="btn action delete" type="button" @click="removeOption(option)" :title="$i18n.get('actions.remove')">-->
91
+ <!-- <link-variant-off-icon :title="$i18n.get('actions.remove')" />-->
92
+ <!-- </button>-->
93
+ <!-- </div>-->
94
+ <!-- </div>-->
95
+ <!-- </div>-->
96
+ <!-- <div class="selected-options-multiple tags" v-else-if="multiValueStyle === 'tags' && internalValue.length && field.extra_data.multiple">-->
97
+ <!-- <div class="selected-option-tag" v-for="(option, index) in internalValue" :key="index">-->
98
+ <!-- <slot name="selected-option-tag" :option="option">-->
99
+ <!-- <div class="selected-option-label">-->
100
+ <!-- <slot name="selected-option-tag-label" :option="option" :all-options="allOptions">-->
101
+ <!-- {{ allOptions[option] }}-->
102
+ <!-- </slot>-->
103
+ <!-- </div>-->
104
+ <!-- <slot name="selected-option-actions" :option="option" :all-options="allOptions" />-->
105
+ <!-- <button class="btn action delete" type="button" @click="removeOption(option)" :title="$i18n.get('actions.remove')">-->
106
+ <!-- <close-icon :title="$i18n.get('actions.remove')" />-->
107
+ <!-- </button>-->
108
+ <!-- </slot>-->
109
+ <!-- </div>-->
110
+ <!-- </div>-->
111
+ </div>
112
+ </template>
113
+
114
+ <script lang="ts">
115
+ import { defineComponent, PropType } from 'vue';
116
+ import escapeRegExp from 'lodash-es/escapeRegExp';
117
+ import pickBy from 'lodash-es/pickBy';
118
+ import { ModelFieldDefinition } from '@westartcom/bread-quasar-fields/interfaces/ModelDefinition';
119
+ import { FieldType } from '@westartcom/bread-quasar-fields/enums/FieldType';
120
+ import { InputType } from '@westartcom/bread-quasar-fields/enums/InputType';
121
+
122
+ interface SelectFieldDefinition extends ModelFieldDefinition<FieldType.ENUM, InputType.SELECT> {
123
+ default: string;
124
+ disabled?: boolean;
125
+ extra_data: {
126
+ multiple?: boolean;
127
+ styling_format: 'select' | 'tags' | 'table';
128
+ options: {
129
+ [key: string]: string;
130
+ };
131
+ };
132
+ }
133
+
134
+ export default defineComponent({
135
+ props: {
136
+ id: {
137
+ type: String,
138
+ required: true
139
+ },
140
+ field: {
141
+ type: Object as PropType<SelectFieldDefinition>,
142
+ required: true
143
+ },
144
+ disabled: Boolean,
145
+ modelValue: [String, Number, Array],
146
+ errors: Array as PropType<string[]>,
147
+ hiddenOptionValues: {
148
+ type: Array as PropType<string[]>,
149
+ default: () => []
150
+ }
151
+ },
152
+
153
+ emits: [
154
+ 'update:modelValue',
155
+ 'update:invalid'
156
+ ],
157
+
158
+ components: {
159
+
160
+ },
161
+
162
+ data() {
163
+ return {
164
+ invalid: null as boolean | null,
165
+ internalValue: [] as string[],
166
+ internalErrors: [] as string[],
167
+ select: {
168
+ visible: false,
169
+ showSearch: false,
170
+ searchQuery: ''
171
+ }
172
+ };
173
+ },
174
+
175
+ mounted() {
176
+ if (this.modelValue) {
177
+ this.internalValue = (Array.isArray(this.modelValue) ? this.modelValue : [this.modelValue]) as string[];
178
+ } else if (this.field.default) {
179
+ this.internalValue = Array.isArray(this.field.default) ? this.field.default : [this.field.default];
180
+ }
181
+
182
+ // document.addEventListener('click', this.onOutsideClick);
183
+
184
+ this.validate(this.internalValue);
185
+ },
186
+
187
+ // beforeUnmount() {
188
+ // document.removeEventListener('click', this.onOutsideClick);
189
+ // },
190
+
191
+ computed: {
192
+ // allOptions: function() {
193
+ // return (this.field?.extra_data?.options) ? this.field.extra_data.options : {};
194
+ // },
195
+ qFilteredOptions: function() {
196
+ const filteredOptions = this.filteredOptions;
197
+ let qOptions = [];
198
+ Object.keys(filteredOptions).forEach((key) => {
199
+ qOptions.push({
200
+ label: filteredOptions[key],
201
+ value: key
202
+ });
203
+ });
204
+
205
+ return qOptions;
206
+ },
207
+ /**
208
+ * Filters available options
209
+ * (eg. when searching or passing hidden options)
210
+ *
211
+ * @returns {*}
212
+ */
213
+ filteredOptions: function(): {[key: string]: string} {
214
+ const options = (this.field?.extra_data?.options) ? this.field.extra_data.options : {};
215
+ const pattern = new RegExp(escapeRegExp(this.select.searchQuery), 'i');
216
+
217
+ return (this.select.searchQuery || this.hiddenOptionValues.length) ? pickBy(options, (label, value) => {
218
+ return !this.hiddenOptionValues.includes(value) && (this.select.searchQuery && pattern.test(label) || !this.select.searchQuery);
219
+ }) : options;
220
+ },
221
+
222
+ isDisabled: function(): boolean {
223
+ return !!(this.disabled || this.field.disabled);
224
+ },
225
+
226
+ // multiValueStyle: function(): 'select' | 'tags' | 'table' {
227
+ // return this.field?.extra_data?.styling_format || 'table';
228
+ // }
229
+ },
230
+
231
+ watch: {
232
+ modelValue: {
233
+ handler: function(val) {
234
+ if (val !== this.internalValue) {
235
+ if (Array.isArray(val)) {
236
+ if (
237
+ (!this.internalValue || !Array.isArray(this.internalValue)) ||
238
+ (val.toString() !== this.internalValue.toString())
239
+ ) {
240
+ this.internalValue = val;
241
+ }
242
+ } else {
243
+ if (
244
+ !this.internalValue ||
245
+ this.internalValue[0] !== val
246
+ ) {
247
+ this.internalValue = [val];
248
+ }
249
+ }
250
+ }
251
+ },
252
+ deep: true
253
+ },
254
+
255
+ // 'select.visible': function(val) {
256
+ // // Reset searchQuery on close
257
+ // if (!val) {
258
+ // this.select.searchQuery = '';
259
+ // }
260
+ // },
261
+
262
+ internalValue: {
263
+ handler: function(val) {
264
+ this.validate(val);
265
+
266
+ if(!Array.isArray(val)) {
267
+ val = [val];
268
+ }
269
+
270
+ if (this.field.extra_data.multiple) {
271
+ this.$emit('update:modelValue', val);
272
+ } else {
273
+ this.$emit('update:modelValue', (Array.isArray(val) && val.length) ? val[0] : null);
274
+ }
275
+ },
276
+ deep: true
277
+ },
278
+
279
+ errors: {
280
+ handler: function(val) {
281
+ this.internalErrors = val;
282
+ },
283
+ deep: true
284
+ },
285
+
286
+ internalErrors: {
287
+ handler: function(val) {
288
+ this.invalid = !!(val && val.length);
289
+ },
290
+ deep: true
291
+ },
292
+
293
+ invalid: function(val) {
294
+ this.$emit('update:invalid', val);
295
+ }
296
+ },
297
+
298
+ methods: {
299
+ qFilter(val, update) {
300
+ this.select.searchQuery = val;
301
+ update(() => {
302
+ return this.qFilteredOptions;
303
+ });
304
+ },
305
+
306
+ // onOutsideClick: function(event) {
307
+ // if (this.select.visible && !this.$el.querySelector('.select').contains(event.target)) {
308
+ // this.toggleOptions();
309
+ // }
310
+ // },
311
+ //
312
+ // toggleOptions: function(event?: any) {
313
+ // if (this.isDisabled) {
314
+ // this.select.visible = false;
315
+ // return;
316
+ // }
317
+ //
318
+ // this.select.visible = !(this.select.visible);
319
+ //
320
+ // if (this.select.visible && this.select.showSearch) {
321
+ // nextTick(() => {
322
+ // const searchInput = this.$el.querySelector('.select .search > input');
323
+ // if (searchInput) {
324
+ // searchInput.focus();
325
+ // }
326
+ // });
327
+ // }
328
+ // },
329
+ //
330
+ // selectOption: function(option) {
331
+ // if (option === null && !this.field.extra_data.multiple) {
332
+ // this.internalValue = [];
333
+ // }
334
+ //
335
+ // // Only add if it's not added already
336
+ // if (option !== null && !this.internalValue.includes(option)) {
337
+ // if (this.field.extra_data.multiple) {
338
+ // this.internalValue.push(option);
339
+ // } else {
340
+ // this.internalValue = [option];
341
+ // }
342
+ // }
343
+ //
344
+ // this.select.visible = false;
345
+ // },
346
+ //
347
+ // removeOption(option) {
348
+ // this.internalValue = this.internalValue.filter((selectedOption) => selectedOption !== option);
349
+ // },
350
+
351
+ validate(val) {
352
+ this.invalid = (this.field.required && (!val || !val.length));
353
+ }
354
+ }
355
+ });
356
+ </script>
357
+
358
+ <style scoped lang="scss">
359
+ //.select {
360
+ // display: block;
361
+ // position: relative;
362
+ // user-select: none;
363
+ // width: 100%;
364
+ // max-width: var(--max-input-width);
365
+ //
366
+ // > .selected {
367
+ // display: flex;
368
+ // align-items: center;
369
+ // cursor: pointer;
370
+ // border: 2px solid var(--color-medium);
371
+ // border-radius: var(--input-radius);
372
+ // width: 100%;
373
+ // line-height: 1.3;
374
+ //
375
+ // .label {
376
+ // flex-grow: 1;
377
+ // padding: var(--global-spacing);
378
+ // overflow: hidden;
379
+ // white-space: nowrap;
380
+ // text-overflow: ellipsis;
381
+ //
382
+ // &.placeholder {
383
+ // color: var(--color-medium-txt);
384
+ // }
385
+ // }
386
+ //
387
+ // .toggle-button {
388
+ // flex-shrink: 1;
389
+ // padding: var(--global-spacing);
390
+ // border-left: 2px solid var(--color-medium);
391
+ // }
392
+ // }
393
+ //
394
+ // .options {
395
+ // position: absolute;
396
+ // display: block;
397
+ // width: 100%;
398
+ // background-color: #fff;
399
+ // border: 2px solid var(--color-dark-txt);
400
+ // border-radius: var(--input-radius);
401
+ // box-shadow: var(--global-shadow);
402
+ // z-index: 9999;
403
+ // max-width: var(--max-input-width);
404
+ // max-height: 300px;
405
+ // overflow-y: auto;
406
+ // margin-top: -5px;
407
+ //
408
+ // .search {
409
+ // input {
410
+ // display: block;
411
+ // width: 100%;
412
+ // padding: var(--global-spacing);
413
+ // border: none;
414
+ // border-bottom: 2px solid var(--color-medium);
415
+ // outline: none;
416
+ // font-size: var(--font-size-base);
417
+ //
418
+ // &:focus {
419
+ // border-color: var(--color-primary);
420
+ // }
421
+ // }
422
+ // }
423
+ //
424
+ // .no-result {
425
+ // color: var(--color-medium);
426
+ // padding: var(--global-spacing);
427
+ // }
428
+ //
429
+ // .option {
430
+ // display: block;
431
+ // padding: var(--global-spacing);
432
+ // overflow: hidden;
433
+ // white-space: nowrap;
434
+ // text-overflow: ellipsis;
435
+ // cursor: pointer;
436
+ //
437
+ // &.selected-option {
438
+ // font-weight: bold;
439
+ // cursor: default;
440
+ // text-decoration: underline;
441
+ // text-decoration-color: var(--color-primary);
442
+ // }
443
+ //
444
+ // &:hover:not(.selected-option) {
445
+ // background-color: var(--color-dark-txt);
446
+ // color: var(--color-light-txt);
447
+ // }
448
+ // }
449
+ // }
450
+ //}
451
+ //
452
+ //.selected-options-multiple {
453
+ // max-width: var(--max-content-width);
454
+ // margin-top: var(--global-spacing);
455
+ // margin-bottom: var(--global-spacing);
456
+ //
457
+ // &.tags {
458
+ // display: flex;
459
+ // flex-wrap: wrap;
460
+ //
461
+ // .selected-option-tag {
462
+ // display: flex;
463
+ // align-items: center;
464
+ // background-color: var(--color-dark-txt);
465
+ // border-radius: var(--global-radius);
466
+ // color: var(--color-light-txt);
467
+ // margin-right: var(--global-spacing);
468
+ // margin-bottom: var(--global-spacing-half);
469
+ //
470
+ // .selected-option-label {
471
+ // padding: var(--global-spacing-half);
472
+ // line-height: 1;
473
+ // margin-top: 2px;
474
+ // }
475
+ //
476
+ // button.delete {
477
+ // border: 0;
478
+ // padding: var(--global-spacing-half);
479
+ // margin: 0;
480
+ // color: var(--color-light-txt);
481
+ // }
482
+ // }
483
+ // }
484
+ //}
485
+ </style>
@@ -0,0 +1,130 @@
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
+ <input
9
+ :name="field.name"
10
+ :id="id + '-input'"
11
+ maxlength="255"
12
+ :placeholder="field.placeholder"
13
+ type="tel"
14
+ class="field"
15
+ :class="{'invalid': (invalid && internalErrors && internalErrors.length)}"
16
+ v-model="internalValue">
17
+ </div>
18
+
19
+ <div class="field-description" v-if="field.description">{{ field.description }}</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 { E164Number, parsePhoneNumberFromString } from 'libphonenumber-js';
29
+ import { defineComponent, PropType } from 'vue';
30
+ import { ModelFieldDefinition } from '@westartcom/bread-quasar-fields/interfaces/ModelDefinition';
31
+ import { FieldType } from '@westartcom/bread-quasar-fields/enums/FieldType';
32
+ import { InputType } from '@westartcom/bread-quasar-fields/enums/InputType';
33
+
34
+ interface TelFieldDefinition extends ModelFieldDefinition<FieldType.TEXT, InputType.TEL> {
35
+ default: string | null;
36
+ }
37
+
38
+ export default defineComponent({
39
+ props: {
40
+ id: {
41
+ type: String,
42
+ required: true
43
+ },
44
+ field: {
45
+ type: Object as PropType<TelFieldDefinition>,
46
+ required: true
47
+ },
48
+ modelValue: String,
49
+ errors: Array as PropType<string[]>
50
+ },
51
+
52
+ emits: [
53
+ 'update:modelValue',
54
+ 'update:invalid'
55
+ ],
56
+
57
+ data() {
58
+ return {
59
+ invalid: null as boolean | null,
60
+ internalValue: null as E164Number | null,
61
+ internalErrors: [] as string[]
62
+ };
63
+ },
64
+
65
+ mounted() {
66
+ if (this.modelValue) {
67
+ let phoneNumber = parsePhoneNumberFromString(this.modelValue);
68
+ this.internalValue = (phoneNumber) ? phoneNumber.number : null;
69
+ } else if (this.field.default) {
70
+ let phoneNumber = parsePhoneNumberFromString(this.field.default);
71
+ this.internalValue = (phoneNumber) ? phoneNumber.number : null;
72
+ }
73
+
74
+ this.validate(this.internalValue);
75
+ },
76
+
77
+ watch: {
78
+ modelValue: function(val) {
79
+ if (val !== this.internalValue) {
80
+ this.internalValue = val;
81
+ }
82
+ },
83
+
84
+ internalValue: function(val) {
85
+ this.validate(val);
86
+
87
+ let phoneNumber = (val && typeof val === 'string') ? parsePhoneNumberFromString(val) : null;
88
+ this.$emit('update:modelValue', (phoneNumber) ? phoneNumber.number : val);
89
+ },
90
+
91
+ errors: {
92
+ handler: function(val) {
93
+ this.internalErrors = val;
94
+ },
95
+ deep: true
96
+ },
97
+
98
+ internalErrors: {
99
+ handler: function(val) {
100
+ this.invalid = !!(val && val.length);
101
+ },
102
+ deep: true
103
+ },
104
+
105
+ invalid: function(val) {
106
+ this.$emit('update:invalid', val);
107
+ }
108
+ },
109
+
110
+ methods: {
111
+ validate: function(val) {
112
+ if (this.field.required && !val) {
113
+ this.invalid = true;
114
+ } else {
115
+ const phoneNumber = (val && typeof val === 'string') ? parsePhoneNumberFromString(val) : null;
116
+ this.internalErrors = [];
117
+ this.invalid = (val && !(phoneNumber && phoneNumber.isValid()));
118
+
119
+ if (this.invalid) {
120
+ this.internalErrors.push(this.$i18n.get('validation.phone', {attribute: this.field.label}));
121
+ }
122
+ }
123
+ }
124
+ }
125
+ });
126
+ </script>
127
+
128
+ <style scoped lang="scss">
129
+
130
+ </style>