Skip to content

Commit 7d7f21f

Browse files
committed
feat: update move Up/Down.
1 parent 475dfdd commit 7d7f21f

File tree

6 files changed

+217
-58
lines changed

6 files changed

+217
-58
lines changed

examples/input-select/App.vue

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,26 @@
66
</div>
77

88
<div class="wrap-app ">
9+
<select name="select">
10+
<!--Supplement an id here instead of using 'name'-->
11+
<option value="value1">Значение 1</option>
12+
<option value="value2" selected>Значение 2</option>
13+
<option value="value3">Значение 3</option>
14+
<option value="value3">Значение 4</option>
15+
<option value="value3">Значение 5</option>
16+
</select>
17+
<select name="select" multiple size =10>
18+
<!--Supplement an id here instead of using 'name'-->
19+
<option value="value1">Значение 1</option>
20+
<option value="value2">Значение 2</option>
21+
<option value="value3">Значение 3</option>
22+
<option value="value3">Значение 4</option>
23+
<option value="value3">Значение 5</option>
24+
<option value="value3">Значение 6</option>
25+
<option value="value3">Значение 7</option>
26+
<option value="value3">Значение 8</option>
27+
<option value="value3">Значение 9</option>
28+
</select>
929
<input-field type="select" name = "sex" :options = "sexOptions"/>
1030
<input-field type = "select" name = "language" :options = "languageOptions"/>
1131
<input-field type = "select" name = "languages" :options = "languageOptions" multiple label = "Multi languages"/>

src/classes/Form.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,13 @@ export default class Form extends EventEmitter implements FormDependence {
407407
unsubscribe(element: any) {
408408
this.dependencies.remove(element);
409409
}
410-
410+
411+
/**
412+
*
413+
* @param name - tracked field
414+
* @param callback
415+
* @description The method fires every time the given field has been changed.
416+
*/
411417
oninput(name: string, callback: (newValue: any) => void) {
412418
return this.on(Form.getEventValueByName(name), callback)
413419
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {OptionRow} from "../types";
2+
3+
/**
4+
* @description Метод возвращает OptionRow элемента, который будет являться проецирование duration на текущий элемент.
5+
* @param options Массив всех опций
6+
* @param currentValue Текущее значение
7+
* @param duration Величина смещения
8+
*/
9+
export default function getOptionRowByDuration(options: OptionRow[], currentValue: unknown, duration: number) {
10+
let index = options.findIndex(item => item.value === currentValue) ;
11+
index = index === -1 ? duration > 0 ? -1 : 0 : index;
12+
index = index + duration;
13+
14+
if (index < 0) index = options.length + index;
15+
16+
return options[index % options.length];
17+
}

src/utils/update-input-position.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
11
import {OptionRow} from "../types";
2+
import getOptionRowByDuration from "./get-option-row-by-duration";
23

34
/**
45
* @description Method using for to move bottom/up options
56
* @param params.value Current value (modelValue)
67
* @param {Number} params.duration Duration to step (1, -1 or other)
78
*
89
* */
9-
export default function updateInputPosition(params: {options: OptionRow[], value: any, onInput: any, duration: number}) {
10+
export default function updateInputPosition(params: {options: OptionRow[], value: any, onInput: ((data: any) => unknown), duration: number}) {
11+
params.onInput(getOptionRowByDuration(params.options, params.value, params.duration));
12+
}
1013

11-
const values = params.options.map(v => v.value);
12-
13-
let currentIndex = values.indexOf(params.value) + params.duration;
14-
15-
// Limits
16-
if (currentIndex >= values.length) currentIndex = 0;
17-
if (currentIndex < 0) currentIndex = values.length - 1;
18-
19-
params.onInput(values[currentIndex]);
20-
}

src/widgets/inputs/input-select/input-select.vue

Lines changed: 119 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@
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
@@ -34,7 +35,7 @@
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

@@ -53,13 +54,13 @@
5354
import {OptionRow} from "../../../types";
5455
import {computed, nextTick, ref} from "vue";
5556
import WidgetInputSelectCurrent from "./widget-input-select-current.vue";
56-
import updateInputPosition from "../../../utils/update-input-position";
5757
import WidgetInputSelectSearch from "./widget-input-select-search.vue";
5858
import getLabelFromOptionRow from "../../../utils/get-label-from-option-row";
5959
import FieldWrap from "../field-wrap.vue";
6060
import debounce from "../../../utils/debounce";
6161
import store from "../../../config/store";
6262
import toggleValueFromArray from "../../../utils/toggle-value-from-array";
63+
import getOptionRowByDuration from "../../../utils/get-option-row-by-duration";
6364
6465
const 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
}>()
7579
const emit = defineEmits<{
@@ -78,39 +82,65 @@ const emit = defineEmits<{
7882
7983
const 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
* */
8493
const 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
* */
99104
const 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+
114144
function 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
* */
134159
const 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
*/
161222
function 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

Comments
 (0)