@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formixru/b2b-ui",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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: 'По&nbsp;вашему запросу ничего не&nbsp;найдено',
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 && suggests.value.length > 0) {
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
- const selectionStart = $input.value.selectionStart;
323
- const selectionEnd = $input.value.selectionEnd;
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 сработало у других обработчиков (например, в формах)
@@ -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