@formixru/b2b-ui 4.0.0 → 4.1.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.
package/package.json
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
@use '../../variables' as *;
|
|
2
|
+
|
|
3
|
+
$b: '.search-input';
|
|
4
|
+
|
|
5
|
+
#{$b} {
|
|
6
|
+
position: relative;
|
|
7
|
+
|
|
8
|
+
&__submit {
|
|
9
|
+
align-items: center;
|
|
10
|
+
display: flex;
|
|
11
|
+
height: 24px;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
left: 12px;
|
|
14
|
+
position: absolute;
|
|
15
|
+
top: calc(50% - 12px);
|
|
16
|
+
width: 24px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
&__submit-icon {
|
|
20
|
+
fill: $search-input-submit-icon-color;
|
|
21
|
+
height: 16px;
|
|
22
|
+
width: 16px;
|
|
23
|
+
|
|
24
|
+
#{$b}__input:not(:placeholder-shown) ~ #{$b}__submit & {
|
|
25
|
+
fill: $search-input-submit-icon-active-color;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&__spinner {
|
|
30
|
+
container-type: size;
|
|
31
|
+
height: 16px;
|
|
32
|
+
width: 16px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&__input {
|
|
36
|
+
padding-left: 44px;
|
|
37
|
+
width: 100%;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&__clear {
|
|
41
|
+
align-items: center;
|
|
42
|
+
background-color: $search-input-clear-bg;
|
|
43
|
+
border-bottom-right-radius: $search-input-clear-radius;
|
|
44
|
+
border-top-right-radius: $search-input-clear-radius;
|
|
45
|
+
color: $search-input-clear-color;
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
display: flex;
|
|
48
|
+
height: $input-height - $input-border-width * 2;
|
|
49
|
+
justify-content: center;
|
|
50
|
+
position: absolute;
|
|
51
|
+
right: 1px;
|
|
52
|
+
top: 1px;
|
|
53
|
+
transition-duration: var(--duration-micro-s);
|
|
54
|
+
transition-property: color;
|
|
55
|
+
transition-timing-function: var(--easing-standard);
|
|
56
|
+
width: $input-height - $input-border-width * 2;
|
|
57
|
+
|
|
58
|
+
.input_small ~ & {
|
|
59
|
+
height: $input-small-height - $input-border-width * 2;
|
|
60
|
+
width: $input-small-height - $input-border-width * 2;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
&:hover,
|
|
64
|
+
&:focus-visible {
|
|
65
|
+
color: $search-input-clear-active-color;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
&__clear-icon {
|
|
70
|
+
fill: currentcolor;
|
|
71
|
+
height: 16px;
|
|
72
|
+
width: 16px;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="$root" class="search-input">
|
|
3
|
+
<input
|
|
4
|
+
ref="$input"
|
|
5
|
+
v-model="query"
|
|
6
|
+
class="search-input__input input"
|
|
7
|
+
:class="props.inputCssClasses"
|
|
8
|
+
:name="props.name"
|
|
9
|
+
type="text"
|
|
10
|
+
:placeholder="props.placeholder"
|
|
11
|
+
autocomplete="off"
|
|
12
|
+
@input="emit('input', $event)"
|
|
13
|
+
@focusin="emit('focusin', $event)"
|
|
14
|
+
@focusout="emit('focusout', $event)"
|
|
15
|
+
@keydown="emit('keydown', $event)"
|
|
16
|
+
/>
|
|
17
|
+
<button
|
|
18
|
+
class="search-input__submit"
|
|
19
|
+
type="button"
|
|
20
|
+
@click="
|
|
21
|
+
updateModel();
|
|
22
|
+
emit('submit');
|
|
23
|
+
"
|
|
24
|
+
>
|
|
25
|
+
<span v-show="props.isLoading" class="search-input__spinner"><span class="spinner"></span></span>
|
|
26
|
+
<svg v-show="!props.isLoading" class="search-input__submit-icon">
|
|
27
|
+
<use :href="`${ICONS_PATH}#search`" />
|
|
28
|
+
</svg>
|
|
29
|
+
</button>
|
|
30
|
+
<button v-show="query.length > 0" class="search-input__clear" type="button" @click="query = ''">
|
|
31
|
+
<svg class="search-input__clear-icon"><use :href="`${ICONS_PATH}#close`" /></svg>
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup>
|
|
37
|
+
import { cssVar } from '@advdominion/css-var';
|
|
38
|
+
import { debounce } from 'es-toolkit';
|
|
39
|
+
import { ref, useTemplateRef, watch, watchEffect } from 'vue';
|
|
40
|
+
|
|
41
|
+
const ICONS_PATH = window.ICONS_PATH;
|
|
42
|
+
|
|
43
|
+
const emit = defineEmits(['input', 'focusin', 'focusout', 'keydown', 'submit']);
|
|
44
|
+
const model = defineModel({ type: String, default: '' });
|
|
45
|
+
|
|
46
|
+
const props = defineProps({
|
|
47
|
+
name: {
|
|
48
|
+
type: String,
|
|
49
|
+
default: '',
|
|
50
|
+
},
|
|
51
|
+
placeholder: {
|
|
52
|
+
type: String,
|
|
53
|
+
default: 'Поиск',
|
|
54
|
+
},
|
|
55
|
+
minlength: {
|
|
56
|
+
type: Number,
|
|
57
|
+
default: 3,
|
|
58
|
+
},
|
|
59
|
+
inputDelay: {
|
|
60
|
+
type: Number,
|
|
61
|
+
default() {
|
|
62
|
+
return cssVar('--input-delay');
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
inputCssClasses: {
|
|
66
|
+
type: Array,
|
|
67
|
+
default() {
|
|
68
|
+
return [];
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
isLoading: {
|
|
72
|
+
type: Boolean,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const $root = useTemplateRef('$root');
|
|
77
|
+
const $input = useTemplateRef('$input');
|
|
78
|
+
|
|
79
|
+
const query = ref(model.value);
|
|
80
|
+
watchEffect(() => {
|
|
81
|
+
query.value = model.value;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const debouncedUpdateModel = debounce(() => {
|
|
85
|
+
model.value = query.value;
|
|
86
|
+
}, props.inputDelay);
|
|
87
|
+
|
|
88
|
+
const updateModel = () => {
|
|
89
|
+
debouncedUpdateModel.cancel();
|
|
90
|
+
|
|
91
|
+
if (query.value.length === 0) {
|
|
92
|
+
model.value = query.value;
|
|
93
|
+
} else if (query.value.length >= props.minlength) {
|
|
94
|
+
debouncedUpdateModel();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
watch(query, (newQueryValue) => {
|
|
99
|
+
if (newQueryValue !== model.value) {
|
|
100
|
+
updateModel();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const focus = () => {
|
|
105
|
+
$input.value.focus();
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
defineExpose({
|
|
109
|
+
$root,
|
|
110
|
+
focus,
|
|
111
|
+
});
|
|
112
|
+
</script>
|
|
@@ -109,9 +109,6 @@ $b: '.suggest-input';
|
|
|
109
109
|
display: flex;
|
|
110
110
|
min-height: $suggest-input-item-height;
|
|
111
111
|
padding: 4px $input-padding-x;
|
|
112
|
-
transition-duration: var(--duration-micro-s);
|
|
113
|
-
transition-property: background-color, color;
|
|
114
|
-
transition-timing-function: var(--easing-standard);
|
|
115
112
|
|
|
116
113
|
@include font($suggest-input-item-font);
|
|
117
114
|
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div ref="$root" class="suggest-input" @keydown.esc="esc()">
|
|
3
|
+
<SearchInput
|
|
4
|
+
v-if="props.searchInput"
|
|
5
|
+
v-model="model"
|
|
6
|
+
class="suggest-input__input"
|
|
7
|
+
:class="[showSuggests ? 'suggest-input__input_active' : '']"
|
|
8
|
+
:placeholder="props.placeholder"
|
|
9
|
+
:minlength="0"
|
|
10
|
+
:input-delay="0"
|
|
11
|
+
:input-css-classes="props.inputCssClasses"
|
|
12
|
+
:is-loading="isLoading"
|
|
13
|
+
@input="handleInput()"
|
|
14
|
+
@focusin="focusin()"
|
|
15
|
+
@focusout="focusout($event)"
|
|
16
|
+
@keydown="arrowNavigation($event)"
|
|
17
|
+
/>
|
|
3
18
|
<input
|
|
19
|
+
v-else
|
|
4
20
|
:id="props.id"
|
|
5
|
-
ref="$input"
|
|
6
21
|
v-model="model"
|
|
7
22
|
class="suggest-input__input input"
|
|
8
|
-
:class="[
|
|
9
|
-
showSuggests ? 'suggest-input__input_active' : '',
|
|
10
|
-
props.small ? 'input_small' : '',
|
|
11
|
-
props.error ? 'input_error' : '',
|
|
12
|
-
...props.inputCssClasses,
|
|
13
|
-
]"
|
|
23
|
+
:class="[showSuggests ? 'suggest-input__input_active' : '', ...props.inputCssClasses]"
|
|
14
24
|
type="text"
|
|
15
25
|
:name="props.name"
|
|
16
26
|
:required="props.required"
|
|
@@ -22,8 +32,8 @@
|
|
|
22
32
|
@focusout="focusout($event)"
|
|
23
33
|
@keydown="arrowNavigation($event)"
|
|
24
34
|
/>
|
|
25
|
-
<div v-show="isLoading" class="suggest-input__loader"><div class="spinner"></div></div>
|
|
26
|
-
<button v-show="isClearVisible" ref="$clear" class="suggest-input__clear" type="button" @click="clear()">
|
|
35
|
+
<div v-show="isLoading && !props.searchInput" class="suggest-input__loader"><div class="spinner"></div></div>
|
|
36
|
+
<button v-show="isClearVisible && !props.searchInput" ref="$clear" class="suggest-input__clear" type="button" @click="clear()">
|
|
27
37
|
<svg class="suggest-input__clear-icon"><use :href="`${ICONS_PATH}#close`" /></svg>
|
|
28
38
|
</button>
|
|
29
39
|
<div v-show="showSuggests" ref="$list" class="suggest-input__list" @mouseover="focusSuggestIndex = -1">
|
|
@@ -52,6 +62,7 @@ import { debounce } from 'es-toolkit';
|
|
|
52
62
|
import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue';
|
|
53
63
|
|
|
54
64
|
import { request } from '../request/index.js';
|
|
65
|
+
import SearchInput from '../search-input/index.vue';
|
|
55
66
|
|
|
56
67
|
const ICONS_PATH = window.ICONS_PATH;
|
|
57
68
|
|
|
@@ -109,12 +120,6 @@ const props = defineProps({
|
|
|
109
120
|
type: String,
|
|
110
121
|
default: 'По вашему запросу ничего не найдено',
|
|
111
122
|
},
|
|
112
|
-
small: {
|
|
113
|
-
type: Boolean,
|
|
114
|
-
},
|
|
115
|
-
error: {
|
|
116
|
-
type: Boolean,
|
|
117
|
-
},
|
|
118
123
|
loaderDelay: {
|
|
119
124
|
type: Number,
|
|
120
125
|
default() {
|
|
@@ -133,10 +138,12 @@ const props = defineProps({
|
|
|
133
138
|
return cssVar('--fallback-timeout');
|
|
134
139
|
},
|
|
135
140
|
},
|
|
141
|
+
searchInput: {
|
|
142
|
+
type: Boolean,
|
|
143
|
+
},
|
|
136
144
|
});
|
|
137
145
|
|
|
138
146
|
const $root = useTemplateRef('$root');
|
|
139
|
-
const $input = useTemplateRef('$input');
|
|
140
147
|
const $list = useTemplateRef('$list');
|
|
141
148
|
const $clear = useTemplateRef('$clear');
|
|
142
149
|
|
|
@@ -157,41 +164,11 @@ watch(suggests, () => {
|
|
|
157
164
|
});
|
|
158
165
|
|
|
159
166
|
const showSuggests = ref(false);
|
|
160
|
-
const itemHeight = ref(0);
|
|
161
167
|
|
|
162
168
|
watch(showSuggests, async (value) => {
|
|
163
|
-
if (value
|
|
164
|
-
await nextTick();
|
|
165
|
-
itemHeight.value = $list.value.querySelector('.suggest-input__list-item').offsetHeight;
|
|
166
|
-
} else {
|
|
169
|
+
if (!value) {
|
|
167
170
|
suggests.value = [];
|
|
168
|
-
await nextTick();
|
|
169
|
-
itemHeight.value = 0;
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const maxItemsWithoutScroll = computed(() => {
|
|
174
|
-
if (itemHeight.value) {
|
|
175
|
-
return Math.floor($list.value.offsetHeight / itemHeight.value);
|
|
176
|
-
}
|
|
177
|
-
return 0;
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
const indexesToScroll = computed(() => {
|
|
181
|
-
if (maxItemsWithoutScroll.value) {
|
|
182
|
-
const indexes = [0];
|
|
183
|
-
for (const index of suggests.value.keys()) {
|
|
184
|
-
const nextIndex = indexes.at(-1) + maxItemsWithoutScroll.value;
|
|
185
|
-
if (nextIndex > suggests.value.length) {
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
if (index === nextIndex) {
|
|
189
|
-
indexes.push(index);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return indexes;
|
|
193
171
|
}
|
|
194
|
-
return [];
|
|
195
172
|
});
|
|
196
173
|
|
|
197
174
|
const isLoading = ref(false);
|
|
@@ -316,44 +293,46 @@ const select = ({ label, value }) => {
|
|
|
316
293
|
});
|
|
317
294
|
};
|
|
318
295
|
|
|
319
|
-
const arrowNavigation = (event) => {
|
|
296
|
+
const arrowNavigation = async (event) => {
|
|
320
297
|
if (showSuggests.value) {
|
|
321
298
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
setTimeout(() => {
|
|
325
|
-
$input.value.setSelectionRange(selectionStart, selectionEnd);
|
|
326
|
-
});
|
|
299
|
+
event.preventDefault();
|
|
300
|
+
|
|
327
301
|
switch (event.key) {
|
|
328
302
|
case 'ArrowDown': {
|
|
329
303
|
focusSuggestIndex.value =
|
|
330
304
|
suggests.value.length === focusSuggestIndex.value + 1 ? 0 : focusSuggestIndex.value + 1;
|
|
331
|
-
if (
|
|
332
|
-
focusSuggestIndex.value !== 0 &&
|
|
333
|
-
focusSuggestIndex.value !== suggests.value.length - 1 &&
|
|
334
|
-
indexesToScroll.value.includes(focusSuggestIndex.value)
|
|
335
|
-
) {
|
|
336
|
-
$list.value.scrollTop += maxItemsWithoutScroll.value * itemHeight.value;
|
|
337
|
-
} else if (focusSuggestIndex.value === 0) {
|
|
338
|
-
$list.value.scrollTop = 0;
|
|
339
|
-
}
|
|
340
305
|
break;
|
|
341
306
|
}
|
|
342
307
|
case 'ArrowUp': {
|
|
343
308
|
focusSuggestIndex.value =
|
|
344
309
|
focusSuggestIndex.value <= 0 ? suggests.value.length - 1 : focusSuggestIndex.value - 1;
|
|
345
|
-
if (
|
|
346
|
-
focusSuggestIndex.value !== 0 &&
|
|
347
|
-
focusSuggestIndex.value !== suggests.value.length - 1 &&
|
|
348
|
-
indexesToScroll.value.includes(focusSuggestIndex.value + 1)
|
|
349
|
-
) {
|
|
350
|
-
$list.value.scrollTop -= maxItemsWithoutScroll.value * itemHeight.value;
|
|
351
|
-
} else if (focusSuggestIndex.value === suggests.value.length - 1) {
|
|
352
|
-
$list.value.scrollTop = suggests.value.length * itemHeight.value;
|
|
353
|
-
}
|
|
354
310
|
break;
|
|
355
311
|
}
|
|
356
312
|
}
|
|
313
|
+
|
|
314
|
+
await nextTick();
|
|
315
|
+
|
|
316
|
+
const activeElement = $list.value?.querySelectorAll('.suggest-input__list-item')[focusSuggestIndex.value];
|
|
317
|
+
if (activeElement && $list.value) {
|
|
318
|
+
const containerRect = $list.value.getBoundingClientRect();
|
|
319
|
+
const elemRect = activeElement.getBoundingClientRect();
|
|
320
|
+
|
|
321
|
+
// У $list задан paddint-top, получаем его значение
|
|
322
|
+
const paddingTop = Number.parseInt(getComputedStyle($list.value).paddingTop) || 0;
|
|
323
|
+
// Верхняя видимая граница = реальные координаты + отступ
|
|
324
|
+
const visibleTop = containerRect.top + paddingTop;
|
|
325
|
+
|
|
326
|
+
if (focusSuggestIndex.value === 0) {
|
|
327
|
+
$list.value.scrollTop = 0;
|
|
328
|
+
} else if (focusSuggestIndex.value === suggests.value.length - 1) {
|
|
329
|
+
$list.value.scrollTop = $list.value.scrollHeight;
|
|
330
|
+
} else if (elemRect.top < visibleTop) {
|
|
331
|
+
$list.value.scrollTop -= visibleTop - elemRect.top;
|
|
332
|
+
} else if (elemRect.bottom > containerRect.bottom) {
|
|
333
|
+
$list.value.scrollTop += elemRect.bottom - containerRect.bottom;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
357
336
|
}
|
|
358
337
|
if (event.key === 'Enter' && focusSuggestIndex.value >= 0) {
|
|
359
338
|
// Ждём, чтобы событие нажатия Enter сработало у других обработчиков (например, в формах)
|
package/src/variables.scss
CHANGED
|
@@ -581,8 +581,18 @@ $input-textarea-small-min-height: 72px !default;
|
|
|
581
581
|
|
|
582
582
|
/********** Поле ввода пароля (.password) **********/
|
|
583
583
|
|
|
584
|
-
$password-color: $color-grey-40;
|
|
585
|
-
$password-active-color: $color-primary-40;
|
|
584
|
+
$password-color: $color-grey-40 !default;
|
|
585
|
+
$password-active-color: $color-primary-40 !default;
|
|
586
|
+
|
|
587
|
+
/********** Поле поиска (.search-input) **********/
|
|
588
|
+
|
|
589
|
+
$search-input-submit-icon-color: $color-grey-70 !default;
|
|
590
|
+
$search-input-submit-icon-active-color: $color-error !default;
|
|
591
|
+
|
|
592
|
+
$search-input-clear-bg: $color-white !default;
|
|
593
|
+
$search-input-clear-radius: $input-radius !default;
|
|
594
|
+
$search-input-clear-color: $color-grey-70 !default;
|
|
595
|
+
$search-input-clear-active-color: $color-grey-40 !default;
|
|
586
596
|
|
|
587
597
|
/********** Поле ввода с подсказкамми (.suggest-input) **********/
|
|
588
598
|
|