55 :class =" {
66 'input-select_disabled': disabled,
77 'input-select_error': errors.length,
8- 'input-select_active': isActive
8+ 'input-select_active': isActive,
9+ 'vf-input-select_multi': !!multiple
910 }"
1011 :tabindex =" !disabled ? 0 : 'none' "
1112
1213 @focusout = " deactivate()"
1314 @keyup.enter =" setActive()"
14- @keyup.space = " setActive() "
15- @keydown.down .prevent = " handleMove(1) "
16- @keydown.up .prevent = " handleMove(-1) "
15+ @keydown.down.prevent = " handleArrowKeyMove "
16+ @keydown.up .prevent = " handleArrowKeyMove "
17+ @keydown.space .prevent = " handleSpace "
1718 ref =" refInputSelect"
1819 >
1920 <widget-input-select-current
3435 <p
3536 v-for = " option in filteredOptions"
3637 :key = " option.value"
37- :class =" {'input-select-option-list-item_active': isActiveItem(option.value)}"
38+ :class =" {'input-select-option-list-item_active': isActiveItem(option.value), 'vf-input-select-row_current': valueOfActiveItem === option.value }"
3839 class =" input-select-option-list-item"
3940 :title = " option.value"
4041
5354import {OptionRow } from " ../../../types" ;
5455import {computed , nextTick , ref } from " vue" ;
5556import WidgetInputSelectCurrent from " ./widget-input-select-current.vue" ;
56- import updateInputPosition from " ../../../utils/update-input-position" ;
5757import WidgetInputSelectSearch from " ./widget-input-select-search.vue" ;
5858import getLabelFromOptionRow from " ../../../utils/get-label-from-option-row" ;
5959import FieldWrap from " ../field-wrap.vue" ;
6060import debounce from " ../../../utils/debounce" ;
6161import store from " ../../../config/store" ;
6262import toggleValueFromArray from " ../../../utils/toggle-value-from-array" ;
63+ import getOptionRowByDuration from " ../../../utils/get-option-row-by-duration" ;
6364
6465const props = defineProps <{
6566 label? : string ,
@@ -70,6 +71,9 @@ const props = defineProps<{
7071 errors: string [],
7172 hiddenValues? : OptionRow [' value' ][],
7273 multiple? : boolean ,
74+ /**
75+ * @description Максимальное возможное число выборки элементов в случае, когда установлен multiple атрибут.
76+ */
7377 limit? : number | string
7478}>()
7579const emit = defineEmits <{
@@ -78,39 +82,65 @@ const emit = defineEmits<{
7882
7983const refInputSelect = ref <HTMLElement >()
8084
85+ // Приведённый в числовой вид предел.
86+ const parsedLimit = computed (() => {
87+ return typeof props .limit === ' number' ? props .limit : (typeof props .limit === ' string' ? Number .parseInt (props .limit , 10 ) : undefined );
88+ });
89+
8190/**
8291 * @description true when user open the list of options.
8392 * */
8493const isActive = ref (false );
85- function setActive(v = ! isActive .value ) {
86- if (props .disabled ) return isActive .value = false ;
87-
88- isActive .value = v ;
89-
90- if (! v ) filter .value = ' ' ;
91- if (v ) nextTick (scrollToActiveItem .bind (null ,' auto' ))
92- }
93-
94+ /**
95+ * @description Значение активного элемента(не выбранного) на котором находится пользователь при проходе по списку с Ctrl.
96+ * элемента не установлено.
97+ */
98+ const valueOfActiveItem = ref <unknown >(undefined );
9499/**
95100 * @description Метка отображаемая в поле. В случае с одиночной выборкой отображается либо текущий элемент, либо placeholder.
96101 * В случае множественной выборки (multiple) - отображается первый выбранный элемент. Если элементов больше одного,
97102 * то отображается ещё + N, где N - количество выбранных элементов - 1
98103 * */
99104const inputTitle = computed (() => {
100-
101105 const value = props .multiple ? props .modelValue ?.[0 ] : props .modelValue ;
102-
103106 const selected = props .options .find (x => x .value === value );
104107 if (selected ) {
105108 const resultLabel = getLabelFromOptionRow (selected );
106109 if (! props .multiple ) return resultLabel ;
107110
108111 return resultLabel + (props .modelValue .length > 1 ? ` + ${props .modelValue .length - 1 } ` : ' ' )
109- }
110-
112+ }
111113 return props .disabled ? ' ' : props .placeholder || ' ' ;
112114})
113115
116+ /**
117+ * @description Текущий фильтр введённый пользователем.
118+ */
119+ const filter = ref (' ' );
120+ const filteredOptions = computed (() => {
121+ const _search = filter .value .toLowerCase ();
122+ return props .options .filter (option =>
123+ // Если объекта нет в скрытых значениях
124+ ! props .hiddenValues ?.includes (option .value ) &&
125+ // Если поиск пуст или если label содержит search
126+ // *String* used to convert number or other types(not string) to string
127+ // Resolve https://github.com/Jenesius/vue-form/issues/107
128+ (_search .length === 0 || String (getLabelFromOptionRow (option ))?.toLowerCase ?.().includes (_search ))
129+ )
130+ })
131+
132+ function setActive(v = ! isActive .value ) {
133+ if (props .disabled ) return isActive .value = false ;
134+
135+ isActive .value = v ;
136+
137+ if (! v ) filter .value = ' ' ;
138+ if (v ) {
139+ nextTick (scrollToActiveItem .bind (null ,' auto' ))
140+ valueOfActiveItem .value = getCurrentLastValue ()
141+ }
142+ }
143+
114144function deactivate() {
115145 const elem = refInputSelect .value ;
116146 if (! elem ) return ;
@@ -120,56 +150,96 @@ function deactivate() {
120150 setActive (false );
121151}
122152
123- /**
124- * @description Функция для обработки перехода по списку, если пользователь нажимает клавиши вниз/вверх
125- * */
126- function handleMove(duration : number ) {
127- updateInputPosition ({options: filteredOptions .value , value: props .modelValue , onInput , duration });
128- scrollToActiveItem (' smooth' )
153+ function getCurrentLastValue() {
154+ return props .multiple ? Array .isArray (props .modelValue ) ? props .modelValue [props .modelValue .length - 1 ] : undefined : props .modelValue
129155}
130-
131156/**
132157 * @description Для того, чтобы предотвратить повторный scroll - используем debounce.
133158 * */
134159const scrollToActiveItem = debounce (function (behavior : ' auto' | ' smooth' = ' auto' ) {
135160 if (! isActive .value ) return ;
136161 nextTick (() => {
137- refInputSelect .value ?.querySelector (' .input-select-option-list-item_active ' )?.scrollIntoView ({
162+ refInputSelect .value ?.querySelector (' .vf- input-select-row_current ' )?.scrollIntoView ({
138163 block: ' nearest' ,
139164 behavior
140165 })
141166 })
142167})
143168
169+ function handleSpace() {
170+ if (! props .multiple ) return ;
171+ input (toggleMultipleValue (props .modelValue ,valueOfActiveItem .value ))
172+ }
144173
145- const filter = ref (' ' );
146- const filteredOptions = computed (() => {
147- const _search = filter .value .toLowerCase ();
148- return props .options .filter (option =>
149- // Если объекта нет в скрытых значениях
150- ! props .hiddenValues ?.includes (option .value ) &&
151- // Если поиск пуст или если label содержит search
152- // *String* used to convert number or other types(not string) to string
153- // Resolve https://github.com/Jenesius/vue-form/issues/107
154- (_search .length === 0 || String (getLabelFromOptionRow (option ))?.toLowerCase ?.().includes (_search ))
155- )
156- })
174+ /**
175+ * @description Карта переходов.
176+ * undefined - предыдущее значение не определено
177+ * 0 - не выбрано, 1- выбрано
178+ * 1, 0, 1, 1 - означает, что при переходе с активного на неактивный (1 -> 0), у нас должно получится активный и активный(1, 1)
179+
180+ const MAP_SHIFT_TRANSITION = [
181+ undefined, 0, undefined, 1, // 0
182+ 1, 0, 1, 1, // 1
183+ 1, 0, 0, 1, // 2
184+ 0, 0, 1, 1, // 3
185+ 0, 1, 0, 0, // 4
186+ ]
187+ * @description Функция для обработки перехода по списку, если пользователь нажимает клавиши вниз/вверх.
188+ * В режиме multi с зажатой Shift зависит от текущего положения пользователя.
189+ * */
190+ function handleArrowKeyMove(event : KeyboardEvent ) {
191+ const duration = event .key === ' ArrowDown' ? 1 : event .key === ' ArrowUp' ? - 1 : 0 ;
192+ // Запоминаем элемент с которого мы начали при инициировании прохода. Это требуется в случае с multiple & shiftKey
193+ const savedPrevActiveItem = valueOfActiveItem .value ;
194+ valueOfActiveItem .value = getOptionRowByDuration (filteredOptions .value , valueOfActiveItem .value , duration ).value ;
195+ if (! props .multiple ) input (valueOfActiveItem .value );
196+ else {
197+ /**
198+ * В данном случае работа идёт с multiple select.
199+ * Если есть shift - выборка является срезом(добавляем элемент)
200+ * Иначе если зажат ctrl - ничего не делаем(перемещаемся, изменение position уже сделано выше) Данная проверка
201+ * уже сделана на первом if.
202+ * Иначе(нет ни Shift, ни Ctrl) устанавливаем элемент
203+ */
204+ let result: unknown [] = Array .isArray (props .modelValue ) ? props .modelValue : [];
205+ if (event .shiftKey ) {
206+ const movement = [result .length ? isActiveItem (savedPrevActiveItem ) : undefined , isActiveItem (valueOfActiveItem .value )]
207+ // 1-> 1. Для более краткой записи используется
208+ const isPositiveMovement = (movement [0 ] && movement [1 ])
209+
210+ if (! isPositiveMovement ) result = toggleMultipleValue (result , valueOfActiveItem .value )
211+ if ((movement [0 ] === false && movement [1 ] === false ) || isPositiveMovement ) result = toggleMultipleValue (result , savedPrevActiveItem )
212+ input (result );
213+ }
214+ else if (! event .ctrlKey ) input ([valueOfActiveItem .value ]);
215+ }
216+ scrollToActiveItem (' smooth' )
217+ }
157218
158219/**
159220 * @description Wrapper over data input.
160221 */
161222function handleSelect(value : any ) {
162- onInput ( value );
223+ if ( ! props . disabled ) input ( props . multiple ? toggleMultipleValue ( props . modelValue , value ) : value )
163224 if (! props .multiple ) setActive (false )
164225}
165- function onInput(value : any ) {
166-
167- if (props .disabled ) return ;
168226
169- const limit = typeof props .limit === ' number' ? props .limit : (typeof props .limit === ' string' ? Number .parseInt (props .limit , 10 ) : undefined );
227+ /**
228+ * @description Абстракция нам метод переключения элемента из выборки. Используется как обёртка на этой функцией для того,
229+ * чтобы каждый раз не проверять массив и подключать предел.
230+ */
231+ function toggleMultipleValue(array : unknown [], value : unknown ) {
232+ return toggleValueFromArray (Array .isArray (array ) ? array : [], value , parsedLimit .value )
233+ }
170234
171- const resultValue = props .multiple ? toggleValueFromArray (Array .isArray (props .modelValue ) ? props .modelValue : [], value , limit ) : value
172- emit (' update:modelValue' , resultValue )
235+ /**
236+ * @description Конечная точка ввода данных. Используется для проверки типа.
237+ * @param value
238+ */
239+ function input(value : unknown ) {
240+ if (props .multiple && ! (Array .isArray (value ) || value === null || value === undefined ))
241+ return console .warn (' An attempt to set a value for input-select(multiple: true) failed. The data is not an array, null or undefined.' , value );
242+ emit (' update:modelValue' , value )
173243}
174244
175245/**
@@ -263,4 +333,7 @@ function isActiveItem(value: any) {
263333 z-index : 2 ;
264334}
265335
336+ .vf-input-select_multi .vf-input-select-row_current {
337+ border : 1px solid var (--vf-input-gray-light ) !important ;
338+ }
266339 </style >
0 commit comments