feat(tgu-ui): Shift+клик ближе к якорю сжимает выделение полосы

This commit is contained in:
Дмитрий Соловьев
2026-06-17 10:20:19 +03:00
parent b092e50dd8
commit 61a2c92d28
3 changed files with 28 additions and 8 deletions
@@ -781,7 +781,8 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
return !(slot && slotIsBusy(slot)); return !(slot && slotIsBusy(slot));
}); });
setSelectedKeys((current) => selectStripRange(current, members, anchorKey, targetKey)); setSelectedKeys((current) => selectStripRange(current, members, anchorKey, targetKey));
setAnchorKey(targetKey); // Якорь НЕ двигаем: остаётся первым кликом, чтобы повторный Shift+клик задавал диапазон
// от того же якоря — ближе к нему «сжимал» выделение, дальше — расширял.
return; return;
} }
setSelectedKeys((current) => setSelectedKeys((current) =>
@@ -60,6 +60,20 @@ describe("selectStripRange", () => {
expect([...next]).toEqual(["c"]); expect([...next]).toEqual(["c"]);
}); });
it("сжимает выделение: повторный диапазон ближе к якорю снимает хвост полосы", () => {
// якорь "a", сначала растянули до "d" (a..d), потом Shift+клик по "b" → остаётся a..b, c и d сняты.
const expanded = selectStripRange(new Set(), members, "a", "d");
expect([...expanded].sort()).toEqual(["a", "b", "c", "d"]);
const shrunk = selectStripRange(expanded, members, "a", "b");
expect([...shrunk].sort()).toEqual(["a", "b"]);
});
it("снимает хвост полосы, но не трогает выбор в других полосах", () => {
const current = new Set(["a", "b", "c", "d", "x"]);
const next = selectStripRange(current, members, "a", "b");
expect([...next].sort()).toEqual(["a", "b", "x"]);
});
it("сохраняет уже выбранное за пределами полосы", () => { it("сохраняет уже выбранное за пределами полосы", () => {
const next = selectStripRange(new Set(["x"]), members, "a", "b"); const next = selectStripRange(new Set(["x"]), members, "a", "b");
expect([...next].sort()).toEqual(["a", "b", "x"]); expect([...next].sort()).toEqual(["a", "b", "x"]);
@@ -40,15 +40,17 @@ export function selectionAfterCheck(
} }
/** /**
* Новое множество выбора после жеста «полоса от первого к последнему»: добавляет всех членов * Новое множество выбора после жеста «полоса от первого к последнему»: выбор полосы становится
* полосы между fromKey и toKey включительно (в порядке времени). Это ВЫДЕЛЕНИЕ, не toggle * РОВНО диапазоном [fromKey..toKey] (в порядке времени). Члены полосы вне диапазона снимаются
* существующий выбор сохраняется. Жест ограничен одной полосой: вызывающий передаёт members * так Shift+клик по слоту ближе к якорю «сжимает» выделение. Выбор за пределами полосы (другие
* именно её (один КА, один крен, подряд по времени); граница полосы — гарантия «нельзя выделить * полосы/КА) сохраняется: снимаем/добавляем только ключи из members. Жест ограничен одной полосой:
* не полосу». Если fromKey или toKey не из members — выбор не меняется (страховка). Вход не мутируется. * вызывающий передаёт members именно её (один КА, один крен, подряд по времени); граница полосы —
* гарантия «нельзя выделить не полосу». Если fromKey или toKey не из members — выбор не меняется
* (страховка). Вход не мутируется.
* *
* @param current Текущее множество выбранных ключей. * @param current Текущее множество выбранных ключей.
* @param members Ключи членов полосы в порядке времени. * @param members Ключи членов полосы в порядке времени.
* @param fromKey Ключ первого клика (якорь). * @param fromKey Ключ якоря (первый клик).
* @param toKey Ключ второго клика. * @param toKey Ключ второго клика.
*/ */
export function selectStripRange( export function selectStripRange(
@@ -63,7 +65,10 @@ export function selectStripRange(
if (from < 0 || to < 0) return next if (from < 0 || to < 0) return next
const lo = Math.min(from, to) const lo = Math.min(from, to)
const hi = Math.max(from, to) const hi = Math.max(from, to)
for (let i = lo; i <= hi; i++) next.add(members[i]!) members.forEach((key, i) => {
if (i >= lo && i <= hi) next.add(key)
else next.delete(key)
})
return next return next
} }