354730fb6c
- конфликтные слоты нельзя добавить в выбор под бронь (клик по строке, Shift-диапазон, выбор полосы на карте, «выбрать все») — жёсткая блокировка; снятие выбора не блокируется (конфликт считается только для невыбранных) - слоты одной полосы между собой не конфликтуют (groupOf) — иначе нельзя было бы набрать полосу поштучным кликом - чекбокс «Конфликты» теперь и гасит, и блокирует выбор; выкл. — выбирать любые - переход на прохождение (◀/▶) больше не подсвечивает никакой слот: активная подсветка строки — только по явному клику; добавлена прокрутка к началу прохода - повторный Shift+клик по тому же концу снимает выделение (таблица и карта)
112 lines
5.4 KiB
TypeScript
112 lines
5.4 KiB
TypeScript
// Чистая логика выбора слотов и разбиения на чанки — выделена из TguSlotsTab,
|
|
// чтобы покрыть юнит-тестами без рендера компонента.
|
|
|
|
/**
|
|
* Новое множество выбора после клика по строке.
|
|
* Обычный клик — переключает один ключ. Shift+клик при заданном якоре — выбирает (или
|
|
* снимает) непрерывный диапазон от якоря до текущего индекса; направление задаётся новым
|
|
* состоянием текущей строки (если она была не выбрана — диапазон выбираем, иначе снимаем).
|
|
*
|
|
* @param current Текущее множество выбранных ключей.
|
|
* @param keyAt Доступ к ключу слота по индексу в видимом (отфильтрованном) списке.
|
|
* @param index Индекс строки, по которой кликнули.
|
|
* @param shiftKey Был ли зажат Shift.
|
|
* @param anchorIndex Индекс предыдущего клика (якорь) либо null.
|
|
*/
|
|
export function selectionAfterCheck(
|
|
current: ReadonlySet<string>,
|
|
keyAt: (index: number) => string,
|
|
index: number,
|
|
shiftKey: boolean,
|
|
anchorIndex: number | null,
|
|
): Set<string> {
|
|
const next = new Set(current)
|
|
const targetKey = keyAt(index)
|
|
if (shiftKey && anchorIndex != null) {
|
|
const lo = Math.min(anchorIndex, index)
|
|
const hi = Math.max(anchorIndex, index)
|
|
const select = !current.has(targetKey)
|
|
for (let i = lo; i <= hi; i++) {
|
|
const k = keyAt(i)
|
|
if (select) next.add(k)
|
|
else next.delete(k)
|
|
}
|
|
} else if (next.has(targetKey)) {
|
|
next.delete(targetKey)
|
|
} else {
|
|
next.add(targetKey)
|
|
}
|
|
return next
|
|
}
|
|
|
|
/**
|
|
* Новое множество выбора после жеста «полоса от первого к последнему»: выбор полосы становится
|
|
* РОВНО диапазоном [fromKey..toKey] (в порядке времени). Члены полосы вне диапазона снимаются —
|
|
* так Shift+клик по слоту ближе к якорю «сжимает» выделение. Выбор за пределами полосы (другие
|
|
* полосы/КА) сохраняется: снимаем/добавляем только ключи из members. Жест ограничен одной полосой:
|
|
* вызывающий передаёт members именно её (один КА, один крен, подряд по времени); граница полосы —
|
|
* гарантия «нельзя выделить не полосу». Если fromKey или toKey не из members — выбор не меняется
|
|
* (страховка). Вход не мутируется.
|
|
*
|
|
* @param current Текущее множество выбранных ключей.
|
|
* @param members Ключи членов полосы в порядке времени.
|
|
* @param fromKey Ключ якоря (первый клик).
|
|
* @param toKey Ключ второго клика.
|
|
*/
|
|
export function selectStripRange(
|
|
current: ReadonlySet<string>,
|
|
members: readonly string[],
|
|
fromKey: string,
|
|
toKey: string,
|
|
): Set<string> {
|
|
const next = new Set(current)
|
|
const from = members.indexOf(fromKey)
|
|
const to = members.indexOf(toKey)
|
|
if (from < 0 || to < 0) return next
|
|
const lo = Math.min(from, to)
|
|
const hi = Math.max(from, to)
|
|
members.forEach((key, i) => {
|
|
if (i >= lo && i <= hi) next.add(key)
|
|
else next.delete(key)
|
|
})
|
|
return next
|
|
}
|
|
|
|
/**
|
|
* Снимает выбор с диапазона полосы [fromKey..toKey] (в порядке времени). Зеркало selectStripRange:
|
|
* убирает ровно члены диапазона, не трогая остальной выбор. Нужно для повторного Shift+клика по тому
|
|
* же концу — он снимает выделенный диапазон. Если fromKey/toKey не из members — выбор не меняется.
|
|
*
|
|
* @param current Текущее множество выбранных ключей.
|
|
* @param members Ключи членов полосы в порядке времени.
|
|
* @param fromKey Ключ якоря (первый клик).
|
|
* @param toKey Ключ второго клика.
|
|
*/
|
|
export function deselectStripRange(
|
|
current: ReadonlySet<string>,
|
|
members: readonly string[],
|
|
fromKey: string,
|
|
toKey: string,
|
|
): Set<string> {
|
|
const next = new Set(current)
|
|
const from = members.indexOf(fromKey)
|
|
const to = members.indexOf(toKey)
|
|
if (from < 0 || to < 0) return next
|
|
const lo = Math.min(from, to)
|
|
const hi = Math.max(from, to)
|
|
for (let i = lo; i <= hi; i++) next.delete(members[i])
|
|
return next
|
|
}
|
|
|
|
/**
|
|
* Разбивает массив на чанки заданного размера (для чанкованной брони крупных наборов).
|
|
* @param items Исходный массив.
|
|
* @param size Размер чанка; <= 0 — один чанк со всем массивом.
|
|
*/
|
|
export function chunk<T>(items: readonly T[], size: number): T[][] {
|
|
if (size <= 0) return items.length ? [items.slice()] : []
|
|
const out: T[][] = []
|
|
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size))
|
|
return out
|
|
}
|