Skip to content

Commit d001c2f

Browse files
committed
feat(note): 列表多选纯逻辑——Ctrl/Shift 区间/批量模式(REQ-287 地基)
1 parent 305d8cc commit d001c2f

2 files changed

Lines changed: 99 additions & 0 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// @vitest-environment jsdom
2+
/**
3+
* noteSelection.test.ts — 笔记列表多选纯逻辑(REQ-287,v0.19.7)。
4+
*
5+
* @ai-context: 覆盖交互矩阵关键路径——Ctrl 单点加/减、Shift 区间并集
6+
* (乱序也按列表位置)、单选替换、全选/清空;纯函数零依赖。
7+
*/
8+
import { describe, expect, it } from "vitest";
9+
import {
10+
emptySelection, rangeSelection, selectAll, singleSelection, toggleSelection,
11+
} from "./noteSelection";
12+
13+
const ordered = [3, 1, 5, 2, 4]; // 注意:非自然序——验证"按列表位置"语义
14+
15+
describe("noteSelection 多选逻辑(REQ-287)", () => {
16+
it("Ctrl 单击:加/减单个成员", () => {
17+
const s0 = emptySelection();
18+
const s1 = toggleSelection(s0, 1);
19+
expect([...s1]).toEqual([1]);
20+
const s2 = toggleSelection(s1, 5);
21+
expect([...s2].sort()).toEqual([1, 5]);
22+
expect(toggleSelection(s2, 1).has(1)).toBe(false);
23+
});
24+
25+
it("Shift 单击:按列表位置区间并入(非 id 大小)", () => {
26+
const sel = rangeSelection(emptySelection(), ordered, 1, 5);
27+
// ordered 中 1 在位置 1、5 在位置 2 → 区间 {1,5}
28+
expect([...sel].sort()).toEqual([1, 5]);
29+
const wider = rangeSelection(sel, ordered, 5, 4);
30+
// 5 在位置 2、4 在位置 4 → 并集后 {1,2,4,5}
31+
expect([...wider].sort()).toEqual([1, 2, 4, 5]);
32+
});
33+
34+
it("anchor 缺失时 Shift 只选目标;反向区间同样成立", () => {
35+
expect([...rangeSelection(emptySelection(), ordered, null, 2)]).toEqual([2]);
36+
const back = rangeSelection(emptySelection(), ordered, 4, 3); // 位置 4→0 全段
37+
expect([...back].sort()).toEqual([1, 2, 3, 4, 5]);
38+
});
39+
40+
it("批量模式行单击=单选替换;全选/清空", () => {
41+
const a = toggleSelection(emptySelection(), 1);
42+
expect([...singleSelection(a, 7)]).toEqual([7]);
43+
expect([...selectAll(ordered)].sort()).toEqual([1, 2, 3, 4, 5]);
44+
expect(emptySelection().size).toBe(0);
45+
});
46+
});

‎app/src/utils/noteSelection.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* noteSelection — 笔记列表多选纯逻辑(REQ-287,v0.19.7)。
3+
*
4+
* @ai-context: 交互矩阵——去行内 checkbox:Ctrl/Shift+单击多选(Ctrl=加/减单
5+
* id;Shift=首尾区间并集)、「批量选择」模式下行单击=勾选;行
6+
* 列顺序(树视图含组序)由调用方以 orderedIds 传入,区间语义
7+
* 锚定**列表位置**而非 id 大小。全部纯函数可单测。
8+
*/
9+
10+
/** Ctrl+单击:切换单个 id 成员资格 */
11+
export function toggleSelection(prev: ReadonlySet<number>, id: number): Set<number> {
12+
const next = new Set(prev);
13+
if (next.has(id)) next.delete(id);
14+
else next.add(id);
15+
return next;
16+
}
17+
18+
/** Shift+单击:anchor→target 连续区间并入(含两端;目标不在序内=仅目标) */
19+
export function rangeSelection(
20+
prev: ReadonlySet<number>,
21+
orderedIds: readonly number[],
22+
anchor: number | null,
23+
target: number,
24+
): Set<number> {
25+
const next = new Set(prev);
26+
const pos = new Map<number, number>();
27+
orderedIds.forEach((id, i) => pos.set(id, i));
28+
const b = pos.get(target);
29+
if (b == null) {
30+
next.add(target);
31+
return next;
32+
}
33+
const a = anchor != null ? pos.get(anchor) : null;
34+
const [lo, hi] = a == null || a === b
35+
? [b, b]
36+
: a < b ? [a, b] : [b, a];
37+
for (let i = lo; i <= hi; i += 1) next.add(orderedIds[i]);
38+
return next;
39+
}
40+
41+
/** 批量选择模式 / 修饰键外的行单击:单选(不并集) */
42+
export function singleSelection(_prev: ReadonlySet<number>, id: number): Set<number> {
43+
return new Set([id]);
44+
}
45+
46+
/** 全选当前可见 / 清空 */
47+
export function selectAll(ids: readonly number[]): Set<number> {
48+
return new Set(ids);
49+
}
50+
51+
export function emptySelection(): Set<number> {
52+
return new Set();
53+
}

0 commit comments

Comments
 (0)