@farm-investimentos/front-mfe-components 15.3.6 → 15.4.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,532 @@
1
+ <template>
2
+ <div ref="outsideClickHandler" @click="handleOutsideClick">
3
+ <div
4
+ :class="{
5
+ 'farm-textfield': true,
6
+ 'farm-textfield--validatable': rules.length > 0,
7
+ 'farm-textfield--touched': isTouched,
8
+ 'farm-textfield--blured': isBlured,
9
+ 'farm-textfield--error': hasError,
10
+ 'farm-textfield--disabled': disabled,
11
+ 'farm-textfield--focused': isFocus || isVisible,
12
+ 'farm-textfield--hiddendetails': hideDetails,
13
+ }"
14
+ v-if="!readonly && !disabled"
15
+ :id="customId"
16
+ >
17
+ <farm-contextmenu bottom v-model="isVisible" :stay-open="multiple" ref="contextmenu">
18
+ <farm-list v-if="!readonly" ref="listRef" @keyup="onKeyUp">
19
+ <farm-listitem
20
+ tabindex="0"
21
+ v-for="(item, index) in showFilteredItems ? filteredItems : items"
22
+ clickable
23
+ hoverColorVariation="lighten"
24
+ hover-color="primary"
25
+ :key="'contextmenu_item_' + index"
26
+ :class="{ 'farm-listitem--selected': item[itemValue] === innerValue }"
27
+ @click='selectItem(item)'
28
+ >
29
+ <farm-checkbox
30
+ class="farm-select__checkbox"
31
+ v-model="checked"
32
+ value="1"
33
+ size="sm"
34
+ v-if="isChecked(item)"
35
+ />
36
+ <farm-checkbox
37
+ class="farm-select__checkbox"
38
+ v-model="checked"
39
+ value="2"
40
+ size="sm"
41
+ v-else-if="multiple"
42
+ />
43
+ <farm-caption bold tag="span">{{ item[itemText] }}</farm-caption>
44
+ </farm-listitem>
45
+ <farm-listitem v-if=" (!items || items.length === 0) || (showFilteredItems && filteredItems.length === 0)">
46
+ {{ noDataText }}
47
+ </farm-listitem>
48
+ </farm-list>
49
+ <template v-slot:activator="{}">
50
+ <div class="farm-textfield--input farm-textfield--input--iconed">
51
+ <input
52
+ v-bind="$attrs"
53
+ v-model="selectedText"
54
+ ref="inputField"
55
+ :id="$props.id"
56
+ @focusin="onFocus(true)"
57
+ @focusout="onFocus(false)"
58
+ @input="onInput"
59
+ @blur="onBlur"
60
+ @keyup="onKeyUp"
61
+ autocomplete="off"
62
+ />
63
+ <farm-icon color="gray" :class="{ 'farm-icon--rotate': isVisible }" @click="addFocusToInput">
64
+ menu-down
65
+ </farm-icon>
66
+ </div>
67
+ </template>
68
+ </farm-contextmenu>
69
+ <farm-caption v-if="showErrorText" color="error" variation="regular">
70
+ {{ errorBucket[0] }}
71
+ </farm-caption>
72
+ <farm-caption
73
+ v-if="hint && !showErrorText"
74
+ class="farm-select__hint-text"
75
+ :class="{ 'farm-select__hint-text--show': persistentHint || isFocus }"
76
+ color="gray"
77
+ variation="regular"
78
+ >
79
+ {{ hint }}
80
+ </farm-caption>
81
+ </div>
82
+ <farm-textfield-v2 v-else v-model="selectedText" :disabled="disabled" :readonly="readonly" />
83
+ </div>
84
+ </template>
85
+
86
+ <script lang="ts">
87
+ import { computed, onBeforeMount, onMounted, PropType, ref, toRefs, watch, defineComponent } from 'vue';
88
+ import validateFormStateBuilder from '../../composition/validateFormStateBuilder';
89
+ import validateFormFieldBuilder from '../../composition/validateFormFieldBuilder';
90
+ import validateFormMethodBuilder from '../../composition/validateFormMethodBuilder';
91
+ import deepEqual from '../../composition/deepEqual';
92
+ import randomId from '../../helpers/randomId';
93
+ import { useSelectAutoComplete } from './composables';
94
+
95
+ export default defineComponent({
96
+ name: 'farm-select-auto-complete',
97
+ inheritAttrs: true,
98
+ props: {
99
+ /**
100
+ * v-model binding
101
+ */
102
+ value: { type: [String, Number, Array], default: '' },
103
+ hint: {
104
+ type: String,
105
+ default: null,
106
+ },
107
+ /**
108
+ * Always show hint text
109
+ */
110
+ persistentHint: {
111
+ type: Boolean,
112
+ default: true,
113
+ },
114
+ /**
115
+ * Disabled the input
116
+ */
117
+ disabled: {
118
+ type: Boolean,
119
+ default: false,
120
+ },
121
+ /**
122
+ * Puts input in readonly state
123
+ */
124
+ readonly: {
125
+ type: Boolean,
126
+ default: false,
127
+ },
128
+ /**
129
+ * Array of rules used for validation
130
+ */
131
+ rules: {
132
+ type: Array as PropType<Array<Function>>,
133
+ default: () => [],
134
+ },
135
+ /**
136
+ * An array of objects. Will look for a text, value and disabled keys.
137
+ * This can be changed using the item-text ad item-value
138
+ */
139
+ items: {
140
+ type: Array,
141
+ default: () => [],
142
+ },
143
+ /**
144
+ * Set property of items's text value
145
+ */
146
+ itemText: {
147
+ type: String,
148
+ default: 'text',
149
+ },
150
+ /**
151
+ * Set property of items's value
152
+ */
153
+ itemValue: {
154
+ type: String,
155
+ default: 'value',
156
+ },
157
+ /**
158
+ * No data text
159
+ */
160
+ noDataText: {
161
+ type: String,
162
+ default: 'Não há dados',
163
+ },
164
+ /**
165
+ * Set a multiple select
166
+ */
167
+ multiple: {
168
+ type: Boolean,
169
+ default: false,
170
+ },
171
+ /**
172
+ * Hides hint and validation errors
173
+ */
174
+ hideDetails: {
175
+ type: Boolean,
176
+ default: false,
177
+ },
178
+ /**
179
+ * Select id
180
+ */
181
+ id: {
182
+ type: String,
183
+ default: '',
184
+ },
185
+ /**
186
+ * The updated bound model<br />
187
+ * _event_
188
+ */
189
+ input: {
190
+ type: Function,
191
+ // eslint-disable-next-line
192
+ default: (value: [String, Number, Array<any>]) => {},
193
+ },
194
+ /**
195
+ * Emitted when the select is changed by user interaction<br />
196
+ * _event_
197
+ */
198
+ change: {
199
+ type: Function,
200
+ // eslint-disable-next-line
201
+ default: (value: [String, Number, Array<any>]) => {},
202
+ },
203
+ /**
204
+ * Emitted when any key is pressed<br />
205
+ * _event_
206
+ */
207
+ keyup: {
208
+ type: Function,
209
+ // eslint-disable-next-line
210
+ default: (event: Event) => {},
211
+ },
212
+ /**
213
+ * Emitted when the select is blurred<br />
214
+ * _event_
215
+ */
216
+ blur: {
217
+ type: Function,
218
+ // eslint-disable-next-line
219
+ default: (event: Event) => {},
220
+ },
221
+ },
222
+ setup(props, { emit }) {
223
+ const { rules, items, itemText, itemValue, disabled, multiple } = toRefs(props);
224
+
225
+ const {
226
+ multipleValues,
227
+ innerValue,
228
+ isTouched,
229
+ isFocus,
230
+ isBlured,
231
+ isVisible,
232
+ selectedText,
233
+ checked,
234
+ notChecked,
235
+ filteredItems,
236
+ inputField,
237
+ } = useSelectAutoComplete(props);
238
+
239
+ const listRef = ref();
240
+
241
+ const contextmenu = ref(null);
242
+
243
+ const { errorBucket, valid, validatable } = validateFormStateBuilder();
244
+
245
+ let fieldValidator = validateFormFieldBuilder(rules.value);
246
+ let validate = validateFormMethodBuilder(errorBucket, valid, fieldValidator);
247
+
248
+ const hasError = computed(() => {
249
+ return errorBucket.value.length > 0;
250
+ });
251
+
252
+ const customId = 'farm-select-' + (props.id || randomId(2));
253
+
254
+ const showErrorText = computed(() => hasError.value && isTouched.value);
255
+
256
+ const searchText = ref('');
257
+
258
+ const filterOptions = () => {
259
+ searchText.value = selectedText.value.toLowerCase();
260
+ if (!searchText || searchText.value.includes('+')) {
261
+ filteredItems.value = items.value;
262
+ return;
263
+ }
264
+
265
+ filteredItems.value = items.value.filter(
266
+ (item) => item[itemText.value].toLowerCase().includes(searchText.value)
267
+ );
268
+
269
+ if (filteredItems.value.length === 0 && searchText.value.trim() !== '') {
270
+ filteredItems.value = [];
271
+ }
272
+ };
273
+
274
+ const showFilteredItems = computed(() => {
275
+ return isVisible.value && searchText.value.trim() !== '';
276
+ });
277
+
278
+ watch(
279
+ () => props.value,
280
+ newValue => {
281
+ innerValue.value = newValue;
282
+ errorBucket.value = [];
283
+
284
+ if (
285
+ (multiple.value && newValue === null) ||
286
+ (Array.isArray(newValue) && newValue.length === 0)
287
+ ) {
288
+ multipleValues.value = [];
289
+ }
290
+ if (Array.isArray(newValue) && newValue.length > 0) {
291
+ multipleValues.value = [...newValue];
292
+ }
293
+ validate(newValue);
294
+ updateSelectedTextValue();
295
+ emit('input', newValue);
296
+ }
297
+ );
298
+
299
+ watch(
300
+ () => props.items,
301
+ () => {
302
+ errorBucket.value = [];
303
+ validate(innerValue.value);
304
+ updateSelectedTextValue();
305
+ }
306
+ );
307
+
308
+ watch(
309
+ () => innerValue.value,
310
+ () => {
311
+ isTouched.value = true;
312
+ isBlured.value = true;
313
+ validate(innerValue.value);
314
+ emit('input', innerValue.value);
315
+ }
316
+ );
317
+
318
+ watch(
319
+ () => props.rules,
320
+ (newVal, oldVal) => {
321
+ if (deepEqual(newVal, oldVal)) return;
322
+ fieldValidator = validateFormFieldBuilder(rules.value);
323
+ validate(innerValue.value);
324
+ }
325
+ );
326
+
327
+
328
+ const handleOutsideClick = (event) => {
329
+ clearSearchAndReturnSelection(event);
330
+
331
+ };
332
+
333
+ onBeforeMount(() => {
334
+ validate(innerValue.value);
335
+ updateSelectedTextValue();
336
+ document.removeEventListener('click', handleOutsideClick);
337
+ });
338
+
339
+ onMounted(() => {
340
+ document.addEventListener('click', handleOutsideClick);
341
+ });
342
+
343
+ const reset = () => {
344
+ if (disabled.value) {
345
+ return;
346
+ }
347
+ innerValue.value = null;
348
+ multipleValues.value = [];
349
+ selectedText.value = '';
350
+ isTouched.value = true;
351
+ if (multiple.value) {
352
+ innerValue.value = [];
353
+ return;
354
+ }
355
+ emit('input', innerValue.value);
356
+ };
357
+
358
+ const onBlur = (event: Event) => {
359
+ isBlured.value = true;
360
+ validate(innerValue.value);
361
+ emit('blur', event);
362
+
363
+ setTimeout(() => {
364
+ if (multiple.value){
365
+ searchText.value = '';
366
+ addLabelToMultiple();
367
+ return;
368
+ }
369
+ }, 100);
370
+
371
+ };
372
+
373
+ const clearSearchAndReturnSelection = (event) => {
374
+ if (!event.srcElement.className.includes('farm-listitem')) {
375
+ if (innerValue.value !== null) {
376
+ if (!selectedText.value) {
377
+ innerValue.value = null;
378
+ }
379
+ searchText.value = '';
380
+ }
381
+ updateSelectedTextValue();
382
+ }
383
+ };
384
+
385
+ const onFocus = (focus: boolean) => {
386
+
387
+ isFocus.value = focus;
388
+ };
389
+
390
+ const selectItem = item => {
391
+
392
+ if (multiple.value) {
393
+ const alreadyAdded = multipleValues.value.findIndex(
394
+ val => val === item[itemValue.value]
395
+ );
396
+ checked.value = '1';
397
+ if (alreadyAdded !== -1) {
398
+ multipleValues.value.splice(alreadyAdded, 1);
399
+ } else {
400
+ multipleValues.value.push(item[itemValue.value]);
401
+ }
402
+ innerValue.value = [...multipleValues.value];
403
+
404
+ return;
405
+ }
406
+
407
+ innerValue.value = item[itemValue.value];
408
+ isVisible.value = false;
409
+
410
+ setTimeout(() => {
411
+ emit('change', innerValue.value);
412
+ searchText.value = '';
413
+ }, 100);
414
+ };
415
+
416
+ const clickInput = () => {
417
+
418
+ isTouched.value = true;
419
+ emit('click');
420
+ };
421
+
422
+ const makePristine = () => {
423
+ isTouched.value = false;
424
+ };
425
+
426
+ const updateSelectedTextValue = () => {
427
+ if (
428
+ !items.value ||
429
+ items.value.length === 0 ||
430
+ innerValue.value === null ||
431
+ (multiple.value && multipleValues.value.length === 0)
432
+ ) {
433
+ selectedText.value = '';
434
+ return;
435
+ }
436
+ const selectedItem = items.value.find(
437
+ item => item[itemValue.value] == innerValue.value
438
+ );
439
+
440
+ if (selectedItem) {
441
+ selectedText.value = selectedItem[itemText.value];
442
+ }
443
+
444
+ addLabelToMultiple();
445
+ };
446
+
447
+ const addLabelToMultiple = () => {
448
+ if (multiple.value && Array.isArray(innerValue.value) && innerValue.value.length > 0) {
449
+ const labelItem = items.value.find(
450
+ item => item[itemValue.value] === innerValue.value[0]
451
+ );
452
+
453
+ if (innerValue.value.length === 0) {
454
+ selectedText.value = '';
455
+ return;
456
+ } else if (innerValue.value.length === 1) {
457
+ selectedText.value = labelItem[itemText.value];
458
+ return;
459
+ }
460
+
461
+ selectedText.value = `${labelItem[itemText.value]} (+${
462
+ innerValue.value.length - 1
463
+ } ${innerValue.value.length - 1 === 1 ? 'outro' : 'outros'})`;
464
+ }
465
+ };
466
+
467
+ const isChecked = item => {
468
+ return (
469
+ multiple.value &&
470
+ multipleValues.value.findIndex(val => val === item[itemValue.value]) !== -1
471
+ );
472
+ };
473
+
474
+ const onInput = () => {
475
+ isVisible.value = true;
476
+ };
477
+
478
+ function onKeyUp(event) {
479
+ if (props.readonly) return;
480
+ filterOptions();
481
+ event.preventDefault();
482
+ }
483
+
484
+ function addFocusToInput() {
485
+ inputField.value.focus();
486
+ }
487
+
488
+ return {
489
+ items,
490
+ innerValue,
491
+ selectedText,
492
+ errorBucket,
493
+ valid,
494
+ validatable,
495
+ hasError,
496
+ isTouched,
497
+ isBlured,
498
+ isFocus,
499
+ isVisible,
500
+ customId,
501
+ showErrorText,
502
+ contextmenu,
503
+ validate,
504
+ reset,
505
+ selectItem,
506
+ onBlur,
507
+ onFocus,
508
+ clickInput,
509
+ updateSelectedTextValue,
510
+ makePristine,
511
+ checked,
512
+ notChecked,
513
+ isChecked,
514
+ multipleValues,
515
+ addLabelToMultiple,
516
+ inputField,
517
+ onKeyUp,
518
+ addFocusToInput,
519
+ filterOptions,
520
+ onInput,
521
+ listRef,
522
+ filteredItems,
523
+ showFilteredItems,
524
+ searchText,
525
+ handleOutsideClick
526
+ };
527
+ },
528
+ });
529
+ </script>
530
+ <style lang="scss" scoped>
531
+ @import 'SelectAutoComplete';
532
+ </style>
@@ -0,0 +1,130 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import SelectAutoComplete from '../SelectAutoComplete.vue';
3
+
4
+ describe('SelectAutoComplet component', () => {
5
+ let wrapper;
6
+ let component;
7
+
8
+ beforeEach(() => {
9
+ wrapper = shallowMount(SelectAutoComplete);
10
+ component = wrapper.vm;
11
+ });
12
+
13
+ test('Created hook', () => {
14
+ expect(wrapper).toBeDefined();
15
+ });
16
+
17
+
18
+ describe('mount component', () => {
19
+ it('renders correctly', () => {
20
+ expect(wrapper.element).toMatchSnapshot();
21
+ });
22
+ });
23
+
24
+ describe('methods', () => {
25
+ it('reset', () => {
26
+ component.reset();
27
+ expect(component.isTouched).toBeTruthy();
28
+ expect(component.innerValue).toEqual(null);
29
+ });
30
+
31
+ it('onBlur', () => {
32
+ component.onBlur();
33
+ expect(component.isBlured).toBeTruthy();
34
+ });
35
+
36
+ it('clickInput', () => {
37
+ component.clickInput();
38
+ expect(component.isTouched).toBeTruthy();
39
+ });
40
+
41
+ it('updateSelectedTextValue', () => {
42
+ component.updateSelectedTextValue();
43
+ expect(component.selectedText).toBeDefined();
44
+ });
45
+
46
+ it('makePristine', () => {
47
+ component.isTouched = true;
48
+ component.makePristine();
49
+ expect(component.isTouched).toBeFalsy();
50
+ });
51
+
52
+ describe('isChecked', () => {
53
+ it('should return false when multiple is false', async () => {
54
+ component.multipleValues = [0, 1, 2];
55
+ const result = component.isChecked({ value: 1 });
56
+ expect(result).toBe(false);
57
+ });
58
+ it('should return true when multiple is true and item is checked', async () => {
59
+ await wrapper.setProps({
60
+ multiple: true,
61
+ });
62
+ component.multipleValues = [0, 1, 2];
63
+ const result = component.isChecked({ value: 1 });
64
+ expect(result).toBe(true);
65
+ });
66
+ it('should return false when item is not checked', async () => {
67
+ await wrapper.setProps({
68
+ multiple: true,
69
+ });
70
+ component.multipleValues = [0, 1, 2];
71
+ const result = component.isChecked({ value: 3 });
72
+ expect(result).toBe(false);
73
+ });
74
+ });
75
+
76
+ describe('addLabelToMultiple', () => {
77
+ it('should not do anything when multiple is false', async () => {
78
+ component.addLabelToMultiple();
79
+
80
+ expect(component.selectedText).toBe('');
81
+ });
82
+ it('should return a selectedText to a selected item', async () => {
83
+ await wrapper.setProps({
84
+ multiple: true,
85
+ items: [
86
+ { value: 0, text: 'value 0' },
87
+ { value: 1, text: 'value 1' },
88
+ { value: 2, text: 'value 2' },
89
+ { value: 3, text: 'value 3' },
90
+ ],
91
+ value: [0],
92
+ });
93
+
94
+ component.addLabelToMultiple();
95
+ expect(component.selectedText).toBe('value 0');
96
+ });
97
+ it('should return a selectedText to two selected item', async () => {
98
+ await wrapper.setProps({
99
+ multiple: true,
100
+ items: [
101
+ { value: 0, text: 'value 0' },
102
+ { value: 1, text: 'value 1' },
103
+ { value: 2, text: 'value 2' },
104
+ { value: 3, text: 'value 3' },
105
+ ],
106
+ value: [0, 1],
107
+ });
108
+
109
+ component.addLabelToMultiple();
110
+ expect(component.selectedText).toBe('value 0 (+1 outro)');
111
+ });
112
+
113
+ it('should return a selectedText to three or more selected item', async () => {
114
+ await wrapper.setProps({
115
+ multiple: true,
116
+ items: [
117
+ { value: 0, text: 'value 0' },
118
+ { value: 1, text: 'value 1' },
119
+ { value: 2, text: 'value 2' },
120
+ { value: 3, text: 'value 3' },
121
+ ],
122
+ value: [0, 1, 2],
123
+ });
124
+
125
+ component.addLabelToMultiple();
126
+ expect(component.selectedText).toBe('value 0 (+2 outros)');
127
+ });
128
+ });
129
+ });
130
+ });
@@ -0,0 +1,28 @@
1
+ import useSelectAutoComplete from '../composables/useSelectAutoComplete';
2
+
3
+ describe('useSelectAutoComplete', () => {
4
+ it('should initialize with empty arrays and false values', () => {
5
+ const props = { value: [] };
6
+ const result = useSelectAutoComplete(props);
7
+
8
+ expect(result.multipleValues.value).toEqual([]);
9
+ expect(result.innerValue.value).toEqual([]);
10
+ expect(result.isTouched.value).toBe(false);
11
+ expect(result.isFocus.value).toBe(false);
12
+ expect(result.isBlured.value).toBe(false);
13
+ expect(result.isVisible.value).toBe(false);
14
+ expect(result.selectedText.value).toBe('');
15
+ expect(result.checked.value).toBe('1');
16
+ expect(result.notChecked.value).toBe(false);
17
+ expect(result.inputField.value).toBe(undefined);
18
+ expect(result.filteredItems.value).toBe(undefined);
19
+ });
20
+
21
+ it('should initialize with provided values', () => {
22
+ const props = { value: 'test' };
23
+ const result = useSelectAutoComplete(props);
24
+
25
+ expect(result.multipleValues.value).toEqual([]);
26
+ expect(result.innerValue.value).toBe('test');
27
+ });
28
+ });
@@ -0,0 +1,3 @@
1
+ import useSelectAutoComplete from './useSelectAutoComplete';
2
+
3
+ export { useSelectAutoComplete };