@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,860 @@
1
+ <template>
2
+ <div class="field-wrap" :id="id" :class="{'disabled': (isDisabled)}">
3
+ <div class="field-wrap-inner">
4
+ <div class="field-label-wrap">
5
+ <label v-if="field.label">{{ field.label }}<span class="required-asterix" v-if="field.required"> *</span></label>
6
+ </div>
7
+
8
+ <div class="select" :class="{'multiple': (field.extra_data.multiple)}">
9
+ <div class="selected">
10
+ <template v-if="isLoadingInitial">
11
+ <div class="label">
12
+ {{ $i18n.get('generic.loading') }}
13
+ </div>
14
+ </template>
15
+ <template v-else>
16
+ <div class="label" v-if="selectedOptions.length && !field.extra_data.multiple" @click="toggleOptions">
17
+ <template v-for="displayField in displayFields">
18
+ {{ selectedOptions[0][displayField] }}
19
+ </template>
20
+ </div>
21
+
22
+ <div class="label placeholder q-mb-sm" v-else-if="!selectedOptions.length || field.extra_data.multiple" @click="toggleOptions">
23
+ <template v-if="field.placeholder">
24
+ <q-btn icon="fa-solid fa-add" push :label="field.placeholder" />
25
+ </template>
26
+ <template v-else-if="field.extra_data.multiple">
27
+ {{ $i18n.get('actions.add') }}
28
+ </template>
29
+ <template v-else>
30
+ {{ $i18n.get('actions.select') }}
31
+ </template>
32
+ </div>
33
+ </template>
34
+
35
+ <div class="toggle-button" v-show="!isLoadingInitial" @click="toggleOptions">
36
+ <chevron-down-icon v-if="!select.visible"></chevron-down-icon>
37
+ <chevron-up-icon v-else></chevron-up-icon>
38
+ </div>
39
+ </div>
40
+
41
+ <div class="options" v-if="select.visible">
42
+ <div class="search" v-if="select.showSearch">
43
+ <q-input
44
+ :label="field.placeholder"
45
+ class="q-px-md"
46
+ :placeholder="$i18n.get('actions.search')"
47
+ v-model="select.searchQuery"
48
+ borderless
49
+ clearable
50
+ clear-icon="fa-regular fa-circle-xmark"></q-input>
51
+ <span class="query-too-short" v-if="select.searchQuery && select.searchQuery.length < 3">{{ $i18n.get('generic.search_query_too_short') }}</span>
52
+ </div>
53
+
54
+ <template v-if="!isLoadingResults">
55
+ <div v-if="!field.required && !field.extra_data.multiple" class="option" @click="selectOption(null)">
56
+ <slot name="no-option-label">
57
+ {{ $i18n.get('generic.no_option') }}
58
+ </slot>
59
+ </div>
60
+
61
+ <div
62
+ v-for="(option, index) in options"
63
+ :key="index"
64
+ class="option"
65
+ :class="{'selected-option': (internalValue && internalValue.includes(option.id))}"
66
+ @click="selectOption(option)">
67
+ <slot
68
+ name="option"
69
+ :option="option"
70
+ :field="field"
71
+ :display-fields="displayFields"
72
+ :extra-display-field="field.extra_data.extra_display_field">
73
+ <slot name="option-display-fields" :option="option" :field="field" :display-fields="displayFields">
74
+ <template v-for="displayField in displayFields">
75
+ {{ option[displayField] }}
76
+ </template>
77
+ </slot>
78
+ <span v-if="field.extra_data.extra_display_field" class="extra-label">
79
+ <slot name="option-extra-field" :option="option" :field="field" :extra-display-field="field.extra_data.extra_display_field">
80
+ {{ option[field.extra_data.extra_display_field] }}
81
+ </slot>
82
+ </span>
83
+ </slot>
84
+ </div>
85
+
86
+ <div class="result-totals" v-if="select.showSearch && options.length && select.total > options.length">
87
+ {{ $i18n.get('pagination.showing_count_of_total', {count: options.length, total: select.total}) }}
88
+ </div>
89
+ </template>
90
+ <template v-else>
91
+ <div class="loading">{{ $i18n.get('generic.loading') }}</div>
92
+ </template>
93
+
94
+ <div class="no-result" v-if="!isLoadingResults && !Object.keys(options).length && (!select.showSearch || select.searchQuery)">
95
+ {{ $i18n.get('generic.no_result') }}
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ <div class="field-description" v-if="field.description">{{ field.description }}</div>
102
+
103
+ <div class="validation-errors" v-if="invalid && internalErrors && internalErrors.length">
104
+ <span class="error danger-txt-color" v-for="(error, index) in internalErrors" :key="index + 'error'">{{ error }}</span>
105
+ </div>
106
+
107
+ <div class="selected-options-multiple table-list" v-if="multiValueStyle === 'table' && selectedOptions.length && field.extra_data.multiple">
108
+ <div class="labels table-header" v-if="showLabels">
109
+ <div class="label column" v-for="(label, displayField) of field.extra_data.display_field_labels" :key="displayField">
110
+ {{ label }}
111
+ </div>
112
+ </div>
113
+ <div class="selected-option-row table-row" v-for="option in selectedOptions" :key="option.id">
114
+ <template v-if="showLabels">
115
+ <slot name="selected-option-label" :option="option" :display-fields="displayFields">
116
+ <div class="selected-option-label column" v-for="(label, displayField) of field.extra_data.display_field_labels" :key="displayField">{{ option[displayField] }}</div>
117
+ </slot>
118
+ </template>
119
+ <template v-else>
120
+ <div class="selected-option-label text-weight-bold column">
121
+ <slot name="selected-option-label" :option="option" :display-fields="displayFields">
122
+ <template v-for="displayField in displayFields">
123
+ {{ option[displayField] }}
124
+ </template>
125
+ </slot>
126
+ </div>
127
+ </template>
128
+ <div class="column actions">
129
+ <slot name="selected-option-actions" :option="option" />
130
+ <q-btn
131
+ flat
132
+ round
133
+ icon="fa-solid fa-trash"
134
+ color="negative"
135
+ :title="$i18n.get('actions.remove')"
136
+ @click="removeOption(option)"></q-btn>
137
+ </div>
138
+ </div>
139
+ </div>
140
+
141
+ <div class="selected-options-multiple tags" v-else-if="multiValueStyle === 'tags' && selectedOptions.length && field.extra_data.multiple">
142
+ <div class="selected-option-tag" v-for="option in selectedOptions" :key="option.id">
143
+ <slot name="selected-option-tag" :option="option">
144
+ <div class="selected-option-label">
145
+ <slot name="selected-option-tag-label" :option="option" :display-fields="displayFields">
146
+ <template v-for="displayField in displayFields">
147
+ {{ option[displayField] }}
148
+ </template>
149
+ </slot>
150
+ </div>
151
+ <slot name="selected-option-actions" :option="option" />
152
+ <button class="btn action delete" type="button" @click="removeOption(option)" :title="$i18n.get('actions.remove')">
153
+ <close-icon :title="$i18n.get('actions.remove')" />
154
+ </button>
155
+ </slot>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ </template>
160
+
161
+ <script lang="ts">
162
+ import { defineAsyncComponent, defineComponent, nextTick, PropType } from 'vue';
163
+ import debounce from 'lodash-es/debounce';
164
+ import concat from 'lodash-es/concat';
165
+ import find from 'lodash-es/find';
166
+ import merge from 'lodash-es/merge';
167
+ import snakeCase from 'lodash-es/snakeCase';
168
+ import { ApiQueryParams } from '@westartcom/bread-quasar-fields/interfaces/ApiQueryParams';
169
+ import { ModelFieldDefinition } from '@westartcom/bread-quasar-fields/interfaces/ModelDefinition';
170
+ import { FieldType } from '@westartcom/bread-quasar-fields/enums/FieldType';
171
+ import { InputType } from '@westartcom/bread-quasar-fields/enums/InputType';
172
+ import { ApiErrorResponse, ApiResponse } from '@westartcom/bread-quasar-fields/interfaces/ApiResponse';
173
+ import { PaginatedApiResponse } from '@westartcom/bread-quasar-fields/interfaces/PaginatedApiResponse';
174
+
175
+ interface ModelSearchFieldDefinition extends ModelFieldDefinition<FieldType.HASONE | FieldType.HASMANY, InputType.MODEL_SEARCH> {
176
+ default: number | Array<number | string> | null;
177
+ disabled?: boolean;
178
+ extra_data: {
179
+ multiple?: boolean;
180
+ prefetch?: boolean;
181
+ search_field: string;
182
+ display_field: string;
183
+ extra_display_field?: false | string;
184
+ display_field_labels?: false | {
185
+ [key: string]: string;
186
+ };
187
+ relation: string;
188
+ related_to: string;
189
+ endpoint: string;
190
+ split_query: boolean;
191
+ parent_model: string;
192
+ styling_format: 'select' | 'tags' | 'table';
193
+ }
194
+ }
195
+
196
+ interface SearchOption {
197
+ id: number | string;
198
+ [key: string]: any;
199
+ }
200
+
201
+ export default defineComponent({
202
+ props: {
203
+ id: {
204
+ type: String,
205
+ required: true
206
+ },
207
+ field: {
208
+ type: Object as PropType<ModelSearchFieldDefinition>,
209
+ required: true
210
+ },
211
+ disabled: Boolean,
212
+ modelValue: [String, Number, Array],
213
+ errors: Array as PropType<string[]>,
214
+ modelId: [String, Number],
215
+ fullModel: Object,
216
+ customParams: Object as PropType<ApiQueryParams>
217
+ },
218
+
219
+ emits: [
220
+ 'update:modelValue',
221
+ 'update:invalid'
222
+ ],
223
+
224
+ components: {
225
+ ChevronDownIcon: defineAsyncComponent(() => import('@westartcom/bread-quasar-fields/icons/ChevronDownIcon.vue')),
226
+ ChevronUpIcon: defineAsyncComponent(() => import('@westartcom/bread-quasar-fields/icons/ChevronUpIcon.vue')),
227
+ CloseIcon: defineAsyncComponent(() => import('@westartcom/bread-quasar-fields/icons/CloseIcon.vue'))
228
+ },
229
+
230
+ data() {
231
+ return {
232
+ invalid: null as boolean | null,
233
+ internalValue: [] as Array<number | string>,
234
+ internalErrors: [] as string[],
235
+ select: {
236
+ visible: false,
237
+ showSearch: false,
238
+ searchQuery: '',
239
+ total: 0
240
+ },
241
+ isLoadingInitial: true,
242
+ isLoadingResults: true,
243
+ hasInitialized: false,
244
+ options: [] as SearchOption[],
245
+ selectedOptions: [] as SearchOption[],
246
+ loadedCustomParams: null as string | null,
247
+ };
248
+ },
249
+
250
+ mounted() {
251
+ if (this.modelValue) {
252
+ this.internalValue = (Array.isArray(this.modelValue) ? this.modelValue : [this.modelValue]) as Array<number | string>;
253
+ } else if (this.field.default) {
254
+ this.internalValue = Array.isArray(this.field.default) ? this.field.default : [this.field.default];
255
+ }
256
+
257
+ document.addEventListener('click', this.onOutsideClick);
258
+
259
+ this.validate(this.internalValue);
260
+ setTimeout(() => {
261
+ this.loadInitial();
262
+ }, 50);
263
+ },
264
+
265
+ beforeUnmount() {
266
+ document.removeEventListener('click', this.onOutsideClick);
267
+ },
268
+
269
+ computed: {
270
+ isDisabled: function(): boolean {
271
+ return !!(this.disabled || this.field.disabled);
272
+ },
273
+
274
+ queryFields: function(): string[] {
275
+ let queryFields: string[] = (Array.isArray(this.field.extra_data.display_field)) ?
276
+ this.field.extra_data.display_field :
277
+ this.field.extra_data.display_field.split(',');
278
+
279
+ if (this.field.extra_data.extra_display_field) {
280
+ queryFields = concat(
281
+ queryFields,
282
+ (Array.isArray(this.field.extra_data.extra_display_field)) ?
283
+ this.field.extra_data.extra_display_field :
284
+ this.field.extra_data.extra_display_field.split(',')
285
+ );
286
+ }
287
+
288
+ if (!queryFields.includes('id')) {
289
+ queryFields.push('id');
290
+ }
291
+
292
+ return queryFields;
293
+ },
294
+
295
+ displayFields: function(): string[] {
296
+ return (Array.isArray(this.field.extra_data.display_field)) ?
297
+ this.field.extra_data.display_field :
298
+ this.field.extra_data.display_field.split(',');
299
+ },
300
+
301
+ concatenatedDisplayFields: function(): string[] {
302
+ let displayFields: string[] = [];
303
+
304
+ if (this.field.extra_data.extra_display_field) {
305
+ displayFields = concat(
306
+ this.displayFields,
307
+ (Array.isArray(this.field.extra_data.extra_display_field)) ?
308
+ this.field.extra_data.extra_display_field :
309
+ this.field.extra_data.extra_display_field.split(',')
310
+ );
311
+ }
312
+
313
+ return displayFields;
314
+ },
315
+
316
+ searchFields: function(): string[] {
317
+ return (Array.isArray(this.field.extra_data.search_field)) ?
318
+ this.field.extra_data.search_field :
319
+ this.field.extra_data.search_field.split(',');
320
+ },
321
+
322
+ splitSearchQuery: function(): boolean {
323
+ return this.field.extra_data?.split_query || false;
324
+ },
325
+
326
+ showLabels: function(): boolean {
327
+ return !!(Object.keys(this.concatenatedDisplayFields).length > 1 && this.field.extra_data.display_field_labels);
328
+ },
329
+
330
+ multiValueStyle: function(): 'select' | 'tags' | 'table' {
331
+ return this.field?.extra_data?.styling_format || 'table';
332
+ }
333
+ },
334
+
335
+ watch: {
336
+ modelValue: {
337
+ handler: function(val) {
338
+ if (val !== this.internalValue) {
339
+ if (Array.isArray(val)) {
340
+ if (
341
+ (!this.internalValue || !Array.isArray(this.internalValue)) ||
342
+ (val.toString() !== this.internalValue.toString())
343
+ ) {
344
+ this.internalValue = val;
345
+ this.loadInitial();
346
+ }
347
+ } else {
348
+ if (
349
+ !this.internalValue ||
350
+ this.internalValue[0] !== val
351
+ ) {
352
+ this.internalValue = (val) ? [val] : [];
353
+ this.loadInitial();
354
+ }
355
+ }
356
+
357
+ nextTick(() => {
358
+ this.selectedOptions = this.selectedOptions.filter(
359
+ option => option && this.internalValue.includes(option.id));
360
+ });
361
+ }
362
+ },
363
+ deep: true
364
+ },
365
+
366
+ customParams: {
367
+ handler: function(val: ApiQueryParams | null) {
368
+ const stringifiedVal = (val) ? val.toString() : null;
369
+ if (stringifiedVal !== this.loadedCustomParams) {
370
+ this.loadedCustomParams = stringifiedVal;
371
+ this.loadInitial(true);
372
+ }
373
+ },
374
+ deep: true
375
+ },
376
+
377
+ 'select.visible': function(val) {
378
+ // Reset searchQuery on close
379
+ if (!val) {
380
+ this.select.searchQuery = '';
381
+ }
382
+ },
383
+
384
+ 'select.searchQuery': debounce(function(this: any, query) {
385
+ if (query && query.length >= 3) {
386
+ this.searchAPI(query);
387
+ }
388
+ }, 300),
389
+
390
+ selectedOptions: {
391
+ handler: function(val: SearchOption[]) {
392
+ if (val.length && val[0]) {
393
+ if (this.field.extra_data.multiple) {
394
+ const mappedOptions = val.map((option) => {
395
+ return option.id;
396
+ });
397
+
398
+ if (this.internalValue.toString() !== mappedOptions.toString()) {
399
+ this.internalValue = mappedOptions;
400
+ }
401
+ } else {
402
+ this.internalValue = [val[0].id];
403
+ }
404
+ } else {
405
+ this.internalValue = [];
406
+ }
407
+ },
408
+ deep: true
409
+ },
410
+
411
+ internalValue: {
412
+ handler: function(val: Array<number | string>) {
413
+ this.validate(val);
414
+
415
+ if (this.field.extra_data.multiple) {
416
+ this.$emit('update:modelValue', val);
417
+ } else {
418
+ this.$emit('update:modelValue', (Array.isArray(val) && val.length) ? val[0] : null);
419
+ }
420
+ },
421
+ deep: true
422
+ },
423
+
424
+ errors: {
425
+ handler: function(val: string[]) {
426
+ this.internalErrors = val;
427
+ },
428
+ deep: true
429
+ },
430
+
431
+ internalErrors: {
432
+ handler: function(val: string[]) {
433
+ this.invalid = !!(val && val.length);
434
+ },
435
+ deep: true
436
+ },
437
+
438
+ invalid: function(val: boolean) {
439
+ this.$emit('update:invalid', val);
440
+ }
441
+ },
442
+
443
+ methods: {
444
+ async loadInitial(force = false) {
445
+ if (this.hasInitialized && (!this.modelValue || (Array.isArray(this.modelValue) && !this.modelValue.length)) && !force) {
446
+ return;
447
+ }
448
+
449
+ this.isLoadingInitial = true;
450
+ this.isLoadingResults = true;
451
+ this.select.showSearch = false;
452
+
453
+ // Fetch the currently selected option(s)
454
+ if (this.internalValue[0] || (this.field.extra_data.multiple && this.modelId)) {
455
+ if (this.field.extra_data.multiple && this.modelId) {
456
+ this.loadInitialMultiOptions();
457
+ } else {
458
+ this.loadInitialSingleOption();
459
+ }
460
+ } else {
461
+ this.isLoadingInitial = false;
462
+ }
463
+
464
+ // Fetch first few options and determine if search is necessary
465
+ if (!this.hasInitialized || force) {
466
+ if (this.field.extra_data.prefetch) {
467
+ await this.loadPrefetchedOptions();
468
+ } else {
469
+ this.select.showSearch = true;
470
+ this.options = [];
471
+ this.select.total = 0;
472
+ this.isLoadingResults = false;
473
+ }
474
+ } else {
475
+ this.isLoadingResults = false;
476
+ }
477
+
478
+ this.hasInitialized = true;
479
+ },
480
+
481
+ async loadPrefetchedOptions() {
482
+ let params: ApiQueryParams = {
483
+ select: this.queryFields,
484
+ per_page: 20
485
+ };
486
+
487
+ if (this.customParams) {
488
+ params = merge(params, this.customParams);
489
+ }
490
+
491
+ this.options = await this.$http.get(this.field.extra_data.endpoint, {
492
+ params: params
493
+ }).then((response: PaginatedApiResponse) => {
494
+ this.isLoadingResults = false;
495
+ let options: SearchOption[] = [];
496
+
497
+ if (response.data.data && response.data.data.data.length) {
498
+ this.select.showSearch = (response.data.data.last_page > 1);
499
+ this.select.total = response.data.data.total;
500
+ options = response.data.data.data;
501
+ }
502
+
503
+ return options;
504
+ }).catch(err => {
505
+ console.error(err);
506
+ this.select.total = 0;
507
+ this.isLoadingResults = false;
508
+ this.$modal.show('error');
509
+ return [];
510
+ });
511
+ },
512
+
513
+ loadInitialMultiOptions() {
514
+ const relationProp = (this.field.extra_data.relation) ? snakeCase(this.field.extra_data.relation) : null;
515
+ // If the provided model already has eager-loaded the relation, lets reuse that instead of calling the endpoint again
516
+ if (
517
+ relationProp &&
518
+ this.fullModel &&
519
+ this.fullModel.hasOwnProperty(relationProp) &&
520
+ Array.isArray(this.fullModel[relationProp])
521
+ ) {
522
+ this.selectedOptions = this.fullModel[relationProp];
523
+ this.isLoadingInitial = false;
524
+ } else {
525
+ let whereHasQuery: {
526
+ [column: string]: any
527
+ } = {};
528
+ whereHasQuery[this.field.extra_data.parent_model] = {
529
+ id: this.modelId
530
+ };
531
+
532
+ let params: ApiQueryParams = {
533
+ select: this.queryFields,
534
+ whereHas: [whereHasQuery],
535
+ paginate: false
536
+ };
537
+
538
+ if (this.customParams) {
539
+ params = merge(params, this.customParams);
540
+ }
541
+
542
+ this.$http.get(this.field.extra_data.endpoint, {
543
+ params: params
544
+ }).then((response: ApiResponse) => {
545
+ this.selectedOptions = response.data.data;
546
+ this.isLoadingInitial = false;
547
+ }).catch((err: ApiErrorResponse) => {
548
+ console.error(err);
549
+ this.isLoadingInitial = false;
550
+ this.$modal.show('error');
551
+ });
552
+ }
553
+ },
554
+
555
+ loadInitialSingleOption() {
556
+ const relationProp = (this.field.extra_data.relation) ? snakeCase(this.field.extra_data.relation) : null;
557
+ // If the provided model already has eager-loaded the relation, lets reuse that instead of calling the endpoint again
558
+ if (
559
+ relationProp &&
560
+ this.fullModel &&
561
+ this.fullModel.hasOwnProperty(relationProp)
562
+ ) {
563
+ this.selectedOptions = (this.fullModel[relationProp]) ? [this.fullModel[relationProp]] : [];
564
+ this.isLoadingInitial = false;
565
+ } else {
566
+ // If options has already been loaded once before and the selected option exists there, pluck value from there, otherwise fetch new
567
+ let selectedOption = find(this.options, (option) => this.internalValue[0] === option.id);
568
+ if (selectedOption) {
569
+ this.selectedOptions = [selectedOption];
570
+ this.isLoadingInitial = false;
571
+ } else {
572
+ let params: ApiQueryParams = {
573
+ select: this.queryFields,
574
+ whereIn: {
575
+ id: this.internalValue
576
+ },
577
+ paginate: false
578
+ };
579
+
580
+ if (this.customParams) {
581
+ params = merge(params, this.customParams);
582
+ }
583
+
584
+ this.$http.get(this.field.extra_data.endpoint, {
585
+ params: params
586
+ }).then((response: ApiResponse) => {
587
+ this.selectedOptions = response.data.data;
588
+ this.isLoadingInitial = false;
589
+ }).catch((err: ApiErrorResponse) => {
590
+ console.error(err);
591
+ this.isLoadingInitial = false;
592
+ this.$modal.show('error');
593
+ });
594
+ }
595
+ }
596
+ },
597
+
598
+ async searchAPI(query: string) {
599
+ this.isLoadingResults = true;
600
+
601
+ let params: ApiQueryParams = {
602
+ select: this.queryFields,
603
+ search: {
604
+ columns: this.searchFields.join(','),
605
+ value: query,
606
+ split: this.splitSearchQuery
607
+ },
608
+ paginate: true,
609
+ per_page: 20
610
+ };
611
+
612
+ if (this.customParams) {
613
+ params = merge(params, this.customParams);
614
+ }
615
+
616
+ this.options = await this.$http.get(this.field.extra_data.endpoint, {
617
+ params: params
618
+ }).then((response: PaginatedApiResponse) => {
619
+ this.select.total = response.data.data.total;
620
+ return response.data.data.data;
621
+ }).catch((err: ApiErrorResponse) => {
622
+ console.error(err);
623
+ this.select.total = 0;
624
+ this.$modal.show('error');
625
+ return [];
626
+ });
627
+ this.isLoadingResults = false;
628
+ },
629
+
630
+ onOutsideClick(event) {
631
+ if (this.select.visible && !this.$el.querySelector('.select').contains(event.target)) {
632
+ this.toggleOptions();
633
+ }
634
+ },
635
+
636
+ toggleOptions(event?: any) {
637
+ if (this.isDisabled) {
638
+ this.select.visible = false;
639
+ return;
640
+ }
641
+
642
+ this.select.visible = !(this.select.visible);
643
+
644
+ if (this.select.visible && this.select.showSearch) {
645
+ nextTick(() => {
646
+ const searchInput = this.$el.querySelector('.select .search > input');
647
+ if (searchInput) {
648
+ searchInput.focus();
649
+ }
650
+ });
651
+ }
652
+ },
653
+
654
+ selectOption(option: SearchOption | null) {
655
+ if (option === null && !this.field.extra_data.multiple) {
656
+ this.selectedOptions = [];
657
+ }
658
+
659
+ // Only add if it's not added already
660
+ if (option !== null && !this.selectedOptions.some((selectedOption) => selectedOption.id === option.id)) {
661
+ if (this.field.extra_data.multiple) {
662
+ this.selectedOptions.push(option);
663
+ } else {
664
+ this.selectedOptions = [option];
665
+ }
666
+ }
667
+
668
+ this.select.visible = false;
669
+ },
670
+
671
+ removeOption(option: SearchOption) {
672
+ this.selectedOptions = this.selectedOptions.filter((selectedOption) => selectedOption.id !== option.id);
673
+ },
674
+
675
+ validate(val) {
676
+ this.invalid = (this.field.required && (!val || !val.length));
677
+ }
678
+ }
679
+ });
680
+ </script>
681
+
682
+ <style scoped lang="scss">
683
+ .field-wrap.disabled {
684
+ .select {
685
+ .selected {
686
+ cursor: not-allowed;
687
+ border-color: var(--color-medium-tint);
688
+ color: var(--color-medium);
689
+
690
+ .label {
691
+ color: var(--color-medium);
692
+ }
693
+
694
+ .toggle-button {
695
+ border-color: var(--color-medium-tint);
696
+ }
697
+ }
698
+ }
699
+ }
700
+
701
+ .select {
702
+ display: block;
703
+ position: relative;
704
+ user-select: none;
705
+ width: 100%;
706
+ max-width: var(--max-input-width);
707
+
708
+ > .selected {
709
+ display: flex;
710
+ align-items: center;
711
+ cursor: pointer;
712
+ border: 2px solid var(--color-medium);
713
+ border-radius: var(--input-radius);
714
+ width: 100%;
715
+ line-height: 1.3;
716
+
717
+ .label {
718
+ flex-grow: 1;
719
+ padding: var(--global-spacing);
720
+ white-space: nowrap;
721
+ text-overflow: ellipsis;
722
+
723
+ &.placeholder {
724
+ color: var(--color-medium-txt);
725
+ }
726
+ }
727
+
728
+ .toggle-button {
729
+ flex-shrink: 1;
730
+ padding: var(--global-spacing);
731
+ border-left: 2px solid var(--color-medium);
732
+ }
733
+ }
734
+
735
+ .options {
736
+ position: absolute;
737
+ display: block;
738
+ width: 100%;
739
+ background-color: white;
740
+ border: 1px solid $grey-3;
741
+ border-radius: 6px;
742
+ box-shadow: 0px 2px 15px rgba(0, 0, 0, 0.2);
743
+ z-index: 9999;
744
+ max-height: 300px;
745
+ overflow-y: auto;
746
+ margin-top: -10px;
747
+
748
+ .search {
749
+ .query-too-short {
750
+ display: block;
751
+ padding: 10px 15px;
752
+ font-size: 0.75em;
753
+ color: $negative;
754
+ border-top: 1px solid $negative;
755
+ }
756
+ border-bottom: 1px solid $grey-3;
757
+ }
758
+
759
+ .loading {
760
+ color: $grey-5;
761
+ padding: 10px 15px;
762
+ }
763
+
764
+ .result-totals {
765
+ display: block;
766
+ color: var(--color-medium);
767
+ padding: var(--global-spacing-half);
768
+ font-size: var(--font-size-small);
769
+ }
770
+
771
+ .no-result {
772
+ color: $grey-5;
773
+ padding: 10px 15px;
774
+ }
775
+
776
+ .option {
777
+ display: block;
778
+ padding: 10px 15px;
779
+ white-space: nowrap;
780
+ text-overflow: ellipsis;
781
+ cursor: pointer;
782
+ background: white;
783
+
784
+ .extra-label {
785
+ display: block;
786
+ font-size: var(--font-size-small);
787
+ }
788
+
789
+ &.selected-option {
790
+ font-weight: bold;
791
+ cursor: default;
792
+ background-color: $accent;
793
+ text-decoration-color: var(--color-primary);
794
+ }
795
+
796
+ &:hover:not(.selected-option) {
797
+ background-color: $grey-2;
798
+ color: var(--color-light-txt);
799
+ }
800
+ }
801
+ }
802
+ }
803
+
804
+ .selected-options-multiple {
805
+ max-width: var(--max-content-width);
806
+ margin-top: var(--global-spacing);
807
+ margin-bottom: var(--global-spacing);
808
+
809
+ &.tags {
810
+ display: flex;
811
+ flex-wrap: wrap;
812
+
813
+ .selected-option-tag {
814
+ display: flex;
815
+ align-items: center;
816
+ background-color: var(--color-dark-txt);
817
+ border-radius: var(--global-radius);
818
+ color: var(--color-light-txt);
819
+ margin-right: var(--global-spacing);
820
+ margin-bottom: var(--global-spacing-half);
821
+
822
+ .selected-option-label {
823
+ padding: var(--global-spacing-half);
824
+ line-height: 1;
825
+ margin-top: 2px;
826
+ }
827
+
828
+ button.delete {
829
+ border: 0;
830
+ padding: var(--global-spacing-half);
831
+ margin: 0;
832
+ color: var(--color-light-txt);
833
+ }
834
+ }
835
+ }
836
+
837
+ &.table-list {
838
+ display: flex;
839
+ flex-direction: column;
840
+
841
+ .table-header, .table-row {
842
+ display: flex;
843
+ flex-direction: row;
844
+ align-items: center;
845
+ justify-content: space-between;
846
+ }
847
+
848
+ .table-row {
849
+ border-bottom: 1px solid $grey-3;
850
+ padding: 10px 0;
851
+ &:first-of-type {
852
+ border-top: 1px solid $grey-3;
853
+ }
854
+ &:last-of-type {
855
+ border-bottom: 0;
856
+ }
857
+ }
858
+ }
859
+ }
860
+ </style>