-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathlistSelection.js
More file actions
122 lines (112 loc) · 4.81 KB
/
Copy pathlistSelection.js
File metadata and controls
122 lines (112 loc) · 4.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// ========== FILE: listSelection.js ==========
// DOM-free multi-selection model for the flow list, plus the ONE shared label
// formatter every background-run launch surface uses (sidebar action bar,
// context menu, modal picker) so the count string can never disagree between
// them. Kept free of the DOM so it is unit-testable under jsdom and reusable.
//
// The model tracks a Set of file paths and an `anchor` (the last row toggled on
// its own), which shift-range selection extends from. Callers own all DOM
// wiring and re-render; this module only answers "what is selected".
/**
* Create a selection model. Instance-based (not a singleton) so tests are
* isolated and a second list could reuse it later.
* @returns {object} selection API
*/
export function createListSelection() {
const selected = new Set();
let anchor = null;
return {
/** @returns {number} count of selected paths */
size() { return selected.size; },
/** @returns {boolean} true when nothing is selected */
isEmpty() { return selected.size === 0; },
/** @param {string} path @returns {boolean} */
has(path) { return selected.has(path); },
/** @returns {string[]} selected paths, insertion order */
list() { return [...selected]; },
/** @returns {string|null} the current range anchor */
anchor() { return anchor; },
/**
* Toggle one path. Becomes the new anchor when it is turned ON.
* @param {string} path
* @returns {boolean} the new membership state
*/
toggle(path) {
if (selected.has(path)) {
selected.delete(path);
if (anchor === path) anchor = null;
return false;
}
selected.add(path);
anchor = path;
return true;
},
/**
* Add one path (idempotent); sets the anchor.
* @param {string} path
*/
add(path) { selected.add(path); anchor = path; },
/**
* Replace the whole selection with exactly these paths. The last one
* becomes the anchor. Used by right-click-on-unselected-row.
* @param {string[]} paths
*/
set(paths) {
selected.clear();
paths.forEach((p) => selected.add(p));
anchor = paths.length ? paths[paths.length - 1] : null;
},
/**
* Select every path in `orderedPaths` (e.g. the currently-visible rows).
* Anchor is unchanged if still present, else the last path.
* @param {string[]} orderedPaths
*/
all(orderedPaths) {
orderedPaths.forEach((p) => selected.add(p));
if (!anchor || !selected.has(anchor)) {
anchor = orderedPaths.length ? orderedPaths[orderedPaths.length - 1] : anchor;
}
},
/**
* Contiguous range select from the anchor (or `to` if no anchor) to
* `to`, over the given ordered path list. Everything between is added;
* existing selections outside the range are preserved (Finder/VS Code).
* @param {string} to the clicked path
* @param {string[]} orderedPaths flattened visible order
*/
range(to, orderedPaths) {
const toIdx = orderedPaths.indexOf(to);
if (toIdx < 0) { this.toggle(to); return; }
const fromIdx = anchor != null ? orderedPaths.indexOf(anchor) : toIdx;
const start = fromIdx < 0 ? toIdx : fromIdx;
const [lo, hi] = start <= toIdx ? [start, toIdx] : [toIdx, start];
for (let i = lo; i <= hi; i++) selected.add(orderedPaths[i]);
// Anchor stays where it was so a subsequent shift-click re-extends.
if (anchor == null) anchor = to;
},
/** Empty the selection. */
clear() { selected.clear(); anchor = null; },
/**
* Drop any selected paths not in `validPaths` (e.g. a file was removed).
* @param {string[]} validPaths
*/
retain(validPaths) {
const keep = new Set(validPaths);
[...selected].forEach((p) => { if (!keep.has(p)) selected.delete(p); });
if (anchor && !keep.has(anchor)) anchor = null;
},
};
}
/**
* The single background-run count label, used by EVERY launch surface so they
* never disagree. Sentence case, verb-first, no em dashes (DESIGN copy rules).
* @param {number} count how many flows will launch
* @param {object} [opts]
* @param {boolean} [opts.compact=false] "Run 3 in background" vs "Run 3 flows in background"
* @returns {string}
*/
export function formatRunLabel(count, opts = {}) {
const n = Math.max(0, count | 0);
if (n <= 1) return 'Run in background';
return opts.compact ? `Run ${n} in background` : `Run ${n} flows in background`;
}