@adecore/ui 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AccentSwatches.js +2 -3
- package/dist/Banner.js +1 -6
- package/dist/ChoiceCards.js +0 -5
- package/dist/ColorSwatch.js +1 -4
- package/dist/ColorWheelPicker.js +3 -6
- package/dist/DockShell.js +3 -5
- package/dist/EmptyState.js +0 -1
- package/dist/ErrorBoundary.js +1 -5
- package/dist/Field.js +2 -6
- package/dist/FileIcon.js +2 -4
- package/dist/Icon.js +1 -2
- package/dist/IconButton.js +1 -1
- package/dist/IconPicker.js +3 -7
- package/dist/Input.js +1 -2
- package/dist/Kbd.js +1 -3
- package/dist/LazyBoundary.js +2 -3
- package/dist/Meter.js +0 -1
- package/dist/MiddleTruncate.js +2 -6
- package/dist/PanelEmpty.js +0 -1
- package/dist/PanelHeader.js +1 -4
- package/dist/Pill.js +1 -6
- package/dist/ProjectSwitcher.js +1 -1
- package/dist/PromptDialog.js +5 -8
- package/dist/RelativeTime.js +1 -1
- package/dist/SegmentBar.js +1 -2
- package/dist/Segmented.js +2 -5
- package/dist/Select.js +1 -4
- package/dist/ShortcutHints.js +2 -3
- package/dist/Slider.js +0 -1
- package/dist/SlidingColumn.js +2 -4
- package/dist/SplitButton.js +1 -5
- package/dist/TextMenu.js +1 -5
- package/dist/Tile.js +0 -4
- package/dist/Toasts.js +1 -9
- package/dist/Tooltip.js +1 -1
- package/dist/UIProvider.js +0 -5
- package/dist/Waveform.js +4 -2
- package/dist/Wipe.js +1 -6
- package/dist/ZoomControls.js +0 -1
- package/dist/clipboard.js +2 -5
- package/dist/combobox/parts.js +3 -9
- package/dist/command-palette/parts.js +5 -14
- package/dist/dialog/parts.js +5 -10
- package/dist/dialog-layer.js +2 -3
- package/dist/error-boundary.js +4 -20
- package/dist/file-icon.js +4 -9
- package/dist/file-tree/empty-lines.js +2 -8
- package/dist/file-tree/use-empty-lines.js +1 -5
- package/dist/file-tree/use-slots.js +6 -10
- package/dist/format/datetime.js +32 -29
- package/dist/format/duration.js +2 -10
- package/dist/format/locale.js +19 -39
- package/dist/format/number.js +9 -22
- package/dist/format/regions.js +4 -7
- package/dist/icon-picker.js +5 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/key-value-list/parts.js +1 -5
- package/dist/lazy.js +4 -19
- package/dist/locales.js +2 -6
- package/dist/menu/parts.js +61 -133
- package/dist/middle-truncate.js +2 -3
- package/dist/placement.js +0 -1
- package/dist/popover/parts.js +1 -5
- package/dist/popup-layer.js +2 -4
- package/dist/prefetch.js +2 -5
- package/dist/selection.js +1 -5
- package/dist/settings/MasterDetail.js +1 -7
- package/dist/settings/SettingsDialog.js +1 -2
- package/dist/settings/SettingsRow.js +2 -5
- package/dist/settings/SettingsSection.js +1 -1
- package/dist/shortcut-hints.js +1 -5
- package/dist/shortcut.js +1 -2
- package/dist/tabs/overflow.js +1 -6
- package/dist/tabs/parts.js +69 -95
- package/dist/testing/dedupe.js +5 -7
- package/dist/tint.d.ts +2 -0
- package/dist/tint.js +11 -0
- package/dist/toast-store.js +2 -10
- package/dist/tree/use-tree-shift.js +2 -6
- package/dist/useColumnResize.js +9 -10
- package/dist/useNow.js +2 -9
- package/dist/workspace/DropPreview.js +1 -5
- package/dist/workspace/preview.js +4 -9
- package/package.json +9 -9
- package/src/AccentSwatches.tsx +9 -3
- package/src/Banner.tsx +5 -11
- package/src/Button.tsx +4 -0
- package/src/Checkbox.tsx +3 -0
- package/src/ChoiceCards.tsx +12 -11
- package/src/CloseButton.tsx +1 -0
- package/src/ColorSwatch.tsx +7 -8
- package/src/ColorWheelPicker.tsx +28 -12
- package/src/ColumnResizeHandle.tsx +1 -0
- package/src/CopyButton.tsx +6 -0
- package/src/DisabledReason.tsx +1 -0
- package/src/DockShell.tsx +11 -10
- package/src/EmptyState.tsx +10 -6
- package/src/ErrorBoundary.tsx +13 -10
- package/src/Field.tsx +11 -13
- package/src/FileIcon.tsx +4 -4
- package/src/Icon.tsx +2 -4
- package/src/IconButton.tsx +14 -5
- package/src/IconPicker.tsx +30 -15
- package/src/Input.tsx +11 -9
- package/src/Kbd.tsx +8 -7
- package/src/LazyBoundary.tsx +3 -3
- package/src/Meter.tsx +7 -2
- package/src/MiddleTruncate.tsx +15 -6
- package/src/PanelEmpty.tsx +9 -7
- package/src/PanelHeader.tsx +2 -5
- package/src/Pill.tsx +7 -8
- package/src/ProjectSwitcher.tsx +16 -6
- package/src/PromptDialog.tsx +31 -26
- package/src/RelativeTime.tsx +6 -1
- package/src/SegmentBar.tsx +22 -11
- package/src/Segmented.tsx +16 -11
- package/src/Select.tsx +7 -4
- package/src/SelectSearch.tsx +3 -0
- package/src/ShortcutHints.tsx +24 -3
- package/src/Skeleton.tsx +1 -0
- package/src/Slider.tsx +4 -1
- package/src/SlidingColumn.tsx +18 -14
- package/src/Spinner.tsx +1 -0
- package/src/SplitButton.tsx +8 -8
- package/src/Stepper.tsx +6 -0
- package/src/Switch.tsx +2 -0
- package/src/TextMenu.tsx +2 -5
- package/src/Tile.tsx +2 -6
- package/src/Toasts.tsx +8 -12
- package/src/Tooltip.tsx +10 -5
- package/src/UIProvider.tsx +7 -7
- package/src/Waveform.tsx +41 -12
- package/src/Wipe.tsx +5 -6
- package/src/ZoomControls.tsx +5 -2
- package/src/clipboard.ts +4 -5
- package/src/combobox/parts.tsx +18 -14
- package/src/command-palette/context.ts +2 -0
- package/src/command-palette/index.parts.ts +1 -0
- package/src/command-palette/parts.tsx +20 -14
- package/src/command-palette/virtual.tsx +21 -0
- package/src/dialog/parts.tsx +10 -13
- package/src/dialog-layer.ts +5 -3
- package/src/error-boundary.ts +7 -20
- package/src/file-icon.ts +8 -9
- package/src/file-tree/empty-lines.ts +24 -8
- package/src/file-tree/helpers.ts +34 -0
- package/src/file-tree/highlight.ts +19 -0
- package/src/file-tree/index.parts.ts +1 -0
- package/src/file-tree/parts.tsx +41 -5
- package/src/file-tree/use-empty-lines.ts +22 -5
- package/src/file-tree/use-file-tree-shift.tsx +10 -0
- package/src/file-tree/use-file-tree.ts +2 -0
- package/src/file-tree/use-slots.ts +77 -12
- package/src/format/datetime.ts +42 -17
- package/src/format/duration.ts +28 -10
- package/src/format/index.ts +3 -0
- package/src/format/locale.ts +33 -42
- package/src/format/number.ts +21 -27
- package/src/format/regions.ts +4 -7
- package/src/icon-picker.ts +23 -16
- package/src/index.ts +9 -0
- package/src/key-value-list/parts.tsx +1 -5
- package/src/lazy-landings.ts +2 -0
- package/src/lazy.tsx +23 -19
- package/src/locales.ts +2 -6
- package/src/match.ts +14 -0
- package/src/menu/parts.tsx +30 -55
- package/src/middle-truncate.ts +12 -3
- package/src/modality.ts +4 -0
- package/src/placement.ts +2 -1
- package/src/platform.ts +2 -0
- package/src/popover/parts.tsx +3 -5
- package/src/popup-layer.ts +6 -4
- package/src/prefetch.ts +18 -5
- package/src/project-switcher-row.tsx +2 -0
- package/src/segmented-keys.ts +6 -0
- package/src/select-shared.ts +2 -0
- package/src/selection.ts +5 -5
- package/src/settings/ConfirmDialog.tsx +3 -0
- package/src/settings/MasterDetail.tsx +3 -9
- package/src/settings/SettingsDialog.tsx +27 -9
- package/src/settings/SettingsRow.tsx +16 -10
- package/src/settings/SettingsSection.tsx +10 -5
- package/src/settings/index.ts +3 -0
- package/src/settings/target.ts +1 -0
- package/src/shortcut-hints.ts +10 -5
- package/src/shortcut.ts +26 -6
- package/src/tabs/overflow.ts +8 -6
- package/src/tabs/parts.tsx +50 -26
- package/src/testing/dedupe.ts +27 -7
- package/src/testing/fake-source.ts +1 -0
- package/src/tint.ts +13 -0
- package/src/toast-store.ts +20 -14
- package/src/tree/parts.tsx +8 -0
- package/src/tree/shift.ts +6 -0
- package/src/tree/use-tree-shift.tsx +23 -7
- package/src/useAsyncAction.ts +8 -0
- package/src/useColumnResize.ts +24 -15
- package/src/useContentSize.ts +5 -0
- package/src/useMeasuredWidth.ts +1 -0
- package/src/useNow.ts +6 -11
- package/src/workspace/DropPreview.tsx +3 -5
- package/src/workspace/SplitView.tsx +109 -0
- package/src/workspace/TabStrip.tsx +42 -0
- package/src/workspace/Workspace.tsx +5 -0
- package/src/workspace/context.ts +1 -0
- package/src/workspace/geometry.ts +49 -0
- package/src/workspace/model.ts +72 -0
- package/src/workspace/preview.ts +15 -15
|
@@ -11,12 +11,15 @@ export function matchHighlight(): Highlight | null {
|
|
|
11
11
|
if (typeof CSS === 'undefined' || !('highlights' in CSS) || typeof Highlight === 'undefined') {
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
|
+
|
|
14
15
|
if (shared === null) {
|
|
15
16
|
shared = new Highlight();
|
|
16
17
|
}
|
|
18
|
+
|
|
17
19
|
if (CSS.highlights.get(MATCH_HIGHLIGHT) !== shared) {
|
|
18
20
|
CSS.highlights.set(MATCH_HIGHLIGHT, shared);
|
|
19
21
|
}
|
|
22
|
+
|
|
20
23
|
return shared;
|
|
21
24
|
}
|
|
22
25
|
|
|
@@ -24,51 +27,66 @@ export function matchHighlight(): Highlight | null {
|
|
|
24
27
|
export function matchRanges(text: string, query: string): [number, number][] {
|
|
25
28
|
const wanted = query.toLowerCase();
|
|
26
29
|
const haystack = text.toLowerCase();
|
|
30
|
+
|
|
27
31
|
// A letter that lowercases to more than one would shift every offset after it.
|
|
28
32
|
if (wanted === '' || haystack.length !== text.length) {
|
|
29
33
|
return [];
|
|
30
34
|
}
|
|
35
|
+
|
|
31
36
|
const found: [number, number][] = [];
|
|
32
37
|
let at = haystack.indexOf(wanted);
|
|
38
|
+
|
|
33
39
|
while (at !== -1) {
|
|
34
40
|
found.push([at, at + wanted.length]);
|
|
35
41
|
at = haystack.indexOf(wanted, at + wanted.length);
|
|
36
42
|
}
|
|
43
|
+
|
|
37
44
|
return found;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
/* The ranges over the visible text of each row's name where `query` occurs. */
|
|
41
48
|
export function rangesOfMatches(rows: readonly HTMLElement[], query: string): Range[] {
|
|
42
49
|
const ranges: Range[] = [];
|
|
50
|
+
|
|
43
51
|
if (query === '') {
|
|
44
52
|
return ranges;
|
|
45
53
|
}
|
|
54
|
+
|
|
46
55
|
for (const row of rows) {
|
|
47
56
|
const content = row.querySelector('[data-item-section="content"]');
|
|
57
|
+
|
|
48
58
|
if (content === null) {
|
|
49
59
|
continue;
|
|
50
60
|
}
|
|
61
|
+
|
|
51
62
|
const nodes: Text[] = [];
|
|
52
63
|
const walker = row.ownerDocument.createTreeWalker(content, NodeFilter.SHOW_TEXT);
|
|
64
|
+
|
|
53
65
|
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
54
66
|
if (node.parentElement?.closest(HIDDEN_PARTS) === null) {
|
|
55
67
|
nodes.push(node as Text);
|
|
56
68
|
}
|
|
57
69
|
}
|
|
70
|
+
|
|
58
71
|
const starts: number[] = [];
|
|
59
72
|
let text = '';
|
|
73
|
+
|
|
60
74
|
for (const node of nodes) {
|
|
61
75
|
starts.push(text.length);
|
|
62
76
|
text += node.data;
|
|
63
77
|
}
|
|
78
|
+
|
|
64
79
|
// The node an offset falls in, and the offset inside it; an end may sit at the end of a node.
|
|
65
80
|
const locate = (offset: number, end: boolean): [Text, number] => {
|
|
66
81
|
let index = nodes.length - 1;
|
|
82
|
+
|
|
67
83
|
while (index > 0 && (end ? starts[index]! >= offset : starts[index]! > offset)) {
|
|
68
84
|
index--;
|
|
69
85
|
}
|
|
86
|
+
|
|
70
87
|
return [nodes[index]!, offset - starts[index]!];
|
|
71
88
|
};
|
|
89
|
+
|
|
72
90
|
for (const [start, end] of matchRanges(text, query)) {
|
|
73
91
|
const range = row.ownerDocument.createRange();
|
|
74
92
|
range.setStart(...locate(start, false));
|
|
@@ -76,5 +94,6 @@ export function rangesOfMatches(rows: readonly HTMLElement[], query: string): Ra
|
|
|
76
94
|
ranges.push(range);
|
|
77
95
|
}
|
|
78
96
|
}
|
|
97
|
+
|
|
79
98
|
return ranges;
|
|
80
99
|
}
|
package/src/file-tree/parts.tsx
CHANGED
|
@@ -27,16 +27,20 @@ export type FileTreeRootProps = useRender.ComponentProps<'div'> & {
|
|
|
27
27
|
onExpandedPathsChange?(paths: readonly string[]): void;
|
|
28
28
|
renderControl?(row: FileTreeVisibleRow): ReactNode;
|
|
29
29
|
renderDecoration?(row: FileTreeVisibleRow): ReactNode;
|
|
30
|
-
/* `after-status` draws the decoration after the engine's git status column instead of between the name and that column. */
|
|
31
30
|
decorationOrder?: FileTreeDecorationOrder;
|
|
32
|
-
|
|
31
|
+
|
|
32
|
+
/* In place of the row's own glyph (the file icon, or a directory's chevron); undefined keeps that glyph. */
|
|
33
33
|
rowIcon?(row: FileTreeVisibleRow): LucideIcon | undefined;
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
/* As tall as its rows, up to the height its container leaves it. */
|
|
35
36
|
fitContent?: boolean;
|
|
36
|
-
|
|
37
|
+
|
|
38
|
+
/* Marks where this occurs in every name, ignoring case. */
|
|
37
39
|
highlight?: string;
|
|
38
|
-
|
|
40
|
+
|
|
41
|
+
/* The line under an open, empty directory; undefined draws none, such as while its children still load. */
|
|
39
42
|
emptyLabel?: FileTreeEmptyLabel;
|
|
43
|
+
|
|
40
44
|
treeProps?: Omit<PierreProps, 'model'>;
|
|
41
45
|
};
|
|
42
46
|
|
|
@@ -75,20 +79,26 @@ export function FileTreeRoot({
|
|
|
75
79
|
const { t } = useTranslation('ui');
|
|
76
80
|
const [frame, setFrame] = useState<HTMLDivElement | null>(null);
|
|
77
81
|
const { attach, bar } = useFileTreeShift(model, resetKey);
|
|
82
|
+
|
|
78
83
|
const frameRef = (node: Element | null) => {
|
|
79
84
|
setFrame(node as HTMLDivElement | null);
|
|
80
85
|
attach(node as HTMLElement | null);
|
|
81
86
|
mergeRefs<Element>(ref)(node);
|
|
82
87
|
};
|
|
88
|
+
|
|
83
89
|
const subscribeToRows = (listener: () => void): (() => void) => (fitContent ? model.subscribe(listener) : () => {});
|
|
90
|
+
|
|
84
91
|
const countRows = (): number => model.getVisibleCount();
|
|
92
|
+
|
|
85
93
|
const rowCount = useSyncExternalStore(subscribeToRows, countRows, countRows);
|
|
86
94
|
const slots = useFileTreeSlots(model, frame, label ?? t('tree.label'), { order: decorationOrder, rowIcon, highlight, emptyLabel });
|
|
87
95
|
const passEmptyLine = useEmptyLines(model, frame, emptyLabel);
|
|
88
96
|
const latestLoads = useRef({ onLoadChildren, onExpandedPathsChange });
|
|
97
|
+
|
|
89
98
|
useLayoutEffect(() => {
|
|
90
99
|
latestLoads.current = { onLoadChildren, onExpandedPathsChange };
|
|
91
100
|
}, [onLoadChildren, onExpandedPathsChange]);
|
|
101
|
+
|
|
92
102
|
const cancelFocus = useRef<(() => void) | undefined>(undefined);
|
|
93
103
|
|
|
94
104
|
useEffect(() => () => cancelFocus.current?.(), [model]);
|
|
@@ -97,37 +107,47 @@ export function FileTreeRoot({
|
|
|
97
107
|
let previous = new Set<string>();
|
|
98
108
|
let disposed = false;
|
|
99
109
|
let pending = false;
|
|
110
|
+
|
|
100
111
|
const report = (): void => {
|
|
101
112
|
pending = false;
|
|
113
|
+
|
|
102
114
|
if (disposed) {
|
|
103
115
|
return;
|
|
104
116
|
}
|
|
117
|
+
|
|
105
118
|
const rows = visibleRows(model).filter((row) => row.kind === 'directory');
|
|
106
119
|
const known = new Set(rows.map((row) => row.path));
|
|
107
120
|
const expanded = new Set([...previous].filter((path) => !known.has(path)));
|
|
121
|
+
|
|
108
122
|
for (const row of rows) {
|
|
109
123
|
if (row.isExpanded) {
|
|
110
124
|
expanded.add(row.path);
|
|
111
125
|
}
|
|
112
126
|
}
|
|
127
|
+
|
|
113
128
|
const opened = [...expanded].filter((path) => !previous.has(path));
|
|
114
129
|
const changed = opened.length > 0 || [...previous].some((path) => !expanded.has(path));
|
|
115
130
|
previous = expanded;
|
|
131
|
+
|
|
116
132
|
if (changed) {
|
|
117
133
|
latestLoads.current.onExpandedPathsChange?.([...expanded]);
|
|
118
134
|
}
|
|
135
|
+
|
|
119
136
|
for (const path of opened) {
|
|
120
137
|
latestLoads.current.onLoadChildren?.(path);
|
|
121
138
|
}
|
|
122
139
|
};
|
|
140
|
+
|
|
123
141
|
const schedule = (): void => {
|
|
124
142
|
if (!pending) {
|
|
125
143
|
pending = true;
|
|
126
144
|
queueMicrotask(report);
|
|
127
145
|
}
|
|
128
146
|
};
|
|
147
|
+
|
|
129
148
|
const unsubscribe = model.subscribe(schedule);
|
|
130
149
|
report();
|
|
150
|
+
|
|
131
151
|
return () => {
|
|
132
152
|
disposed = true;
|
|
133
153
|
unsubscribe();
|
|
@@ -138,33 +158,43 @@ export function FileTreeRoot({
|
|
|
138
158
|
if (frame === null) {
|
|
139
159
|
return;
|
|
140
160
|
}
|
|
161
|
+
|
|
141
162
|
const onClickRow = (event: MouseEvent): void => {
|
|
142
163
|
if (event.defaultPrevented || inSlot(event) || extendsSelection(event)) {
|
|
143
164
|
return;
|
|
144
165
|
}
|
|
166
|
+
|
|
145
167
|
const path = rowPathOf({ nativeEvent: event });
|
|
168
|
+
|
|
146
169
|
if (path !== null && directoryHandle(model, path) === null) {
|
|
147
170
|
onActivate?.(path);
|
|
148
171
|
}
|
|
149
172
|
};
|
|
173
|
+
|
|
150
174
|
const onKey = (event: KeyboardEvent): void => {
|
|
151
175
|
if (event.defaultPrevented || inSlot(event)) {
|
|
152
176
|
return;
|
|
153
177
|
}
|
|
178
|
+
|
|
154
179
|
if (movesFocus(event) && selectionFollowsFocus) {
|
|
155
180
|
cancelFocus.current?.();
|
|
156
181
|
// beta.6 stops the key in its own handler; selection must follow after that handler moves focus.
|
|
157
182
|
cancelFocus.current = followFocus(model, onFocusMove);
|
|
158
183
|
}
|
|
184
|
+
|
|
159
185
|
passEmptyLine(event);
|
|
186
|
+
|
|
160
187
|
if (event.key === 'Enter' && !event.altKey && !event.metaKey && !event.ctrlKey) {
|
|
161
188
|
const path = model.getFocusedPath();
|
|
189
|
+
|
|
162
190
|
if (path === null) {
|
|
163
191
|
return;
|
|
164
192
|
}
|
|
193
|
+
|
|
165
194
|
event.preventDefault();
|
|
166
195
|
event.stopPropagation();
|
|
167
196
|
const directory = directoryHandle(model, path);
|
|
197
|
+
|
|
168
198
|
if (directory) {
|
|
169
199
|
directory.toggle();
|
|
170
200
|
} else {
|
|
@@ -172,23 +202,28 @@ export function FileTreeRoot({
|
|
|
172
202
|
}
|
|
173
203
|
}
|
|
174
204
|
};
|
|
205
|
+
|
|
175
206
|
const withTargets =
|
|
176
207
|
<RowEvent extends Event>(handle?: (path: string, targets: readonly string[], event: RowEvent) => void) =>
|
|
177
208
|
(event: RowEvent): void => {
|
|
178
209
|
if (inSlot(event)) {
|
|
179
210
|
return;
|
|
180
211
|
}
|
|
212
|
+
|
|
181
213
|
const path = rowPathOf({ nativeEvent: event });
|
|
214
|
+
|
|
182
215
|
if (path !== null) {
|
|
183
216
|
handle?.(path, menuTargetsOf(path, model.getSelectedPaths()), event);
|
|
184
217
|
}
|
|
185
218
|
};
|
|
219
|
+
|
|
186
220
|
const onContext = withTargets(onRowContextMenu);
|
|
187
221
|
const onDrag = withTargets(onRowDragStart);
|
|
188
222
|
frame.addEventListener('click', onClickRow);
|
|
189
223
|
frame.addEventListener('keydown', onKey, true);
|
|
190
224
|
frame.addEventListener('contextmenu', onContext);
|
|
191
225
|
frame.addEventListener('dragstart', onDrag);
|
|
226
|
+
|
|
192
227
|
return () => {
|
|
193
228
|
frame.removeEventListener('click', onClickRow);
|
|
194
229
|
frame.removeEventListener('keydown', onKey, true);
|
|
@@ -212,6 +247,7 @@ export function FileTreeRoot({
|
|
|
212
247
|
<PierreFileTree {...treeProps} model={model} className={clsx('adecore-file-tree-host', treeProps?.className)} />
|
|
213
248
|
{slots.map(({ row, control, decoration, icon }) => {
|
|
214
249
|
const ownIcon = icon === null ? undefined : rowIcon?.(row);
|
|
250
|
+
|
|
215
251
|
return (
|
|
216
252
|
<span key={control.slot} style={{ display: 'contents' }}>
|
|
217
253
|
{createPortal(renderControl?.(row), control)}
|
|
@@ -9,11 +9,7 @@ function onEmptyLine(event: Event): boolean {
|
|
|
9
9
|
return event.composedPath().some((node) => node instanceof HTMLElement && node.dataset.emptyLine === 'true');
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/*
|
|
13
|
-
* Keeps a line under every open directory that is known to be empty, and keeps the line out of
|
|
14
|
-
* reach: a pointer on it does nothing and a key passes it. Returns what to call with every key the
|
|
15
|
-
* tree hears, after anything that notes where focus was.
|
|
16
|
-
*/
|
|
12
|
+
/* A pointer on a line does nothing and a key passes it. Call what it returns with every key the tree hears, after anything that notes where focus was. */
|
|
17
13
|
export function useEmptyLines(model: FileTree, frame: HTMLElement | null, emptyLabel: FileTreeEmptyLabel | undefined): (event: KeyboardEvent) => void {
|
|
18
14
|
const latest = useRef(emptyLabel);
|
|
19
15
|
const reconcile = useRef<(() => void) | undefined>(undefined);
|
|
@@ -30,49 +26,64 @@ export function useEmptyLines(model: FileTree, frame: HTMLElement | null, emptyL
|
|
|
30
26
|
if (!enabled) {
|
|
31
27
|
return;
|
|
32
28
|
}
|
|
29
|
+
|
|
33
30
|
const added = new Set<string>();
|
|
34
31
|
let pending = false;
|
|
35
32
|
let disposed = false;
|
|
33
|
+
|
|
36
34
|
const run = (): void => {
|
|
37
35
|
pending = false;
|
|
38
36
|
const labelOf = latest.current;
|
|
37
|
+
|
|
39
38
|
if (disposed || labelOf === undefined) {
|
|
40
39
|
return;
|
|
41
40
|
}
|
|
41
|
+
|
|
42
42
|
const selectedLines = model.getSelectedPaths().filter(isEmptyLine);
|
|
43
|
+
|
|
43
44
|
for (const path of selectedLines) {
|
|
44
45
|
model.getItem(path)?.deselect();
|
|
45
46
|
}
|
|
47
|
+
|
|
46
48
|
// A selection that followed focus onto the line follows it on to where it went.
|
|
47
49
|
const focused = skipEmptyLine(model, lastKey.current) ? model.getFocusedPath() : null;
|
|
50
|
+
|
|
48
51
|
if (focused !== null && selectedLines.length > 0) {
|
|
49
52
|
model.getItem(focused)?.select();
|
|
50
53
|
}
|
|
54
|
+
|
|
51
55
|
const { add, remove } = emptyLineChanges(visibleRows(model), labelOf, (path) => model.getItem(path) !== null, added);
|
|
56
|
+
|
|
52
57
|
for (const path of remove) {
|
|
53
58
|
added.delete(path);
|
|
54
59
|
}
|
|
60
|
+
|
|
55
61
|
for (const path of add) {
|
|
56
62
|
added.add(path);
|
|
57
63
|
}
|
|
64
|
+
|
|
58
65
|
if (add.length > 0 || remove.length > 0) {
|
|
59
66
|
model.batch([...remove.map((path) => ({ type: 'remove' as const, path })), ...add.map((path) => ({ type: 'add' as const, path }))]);
|
|
60
67
|
}
|
|
61
68
|
};
|
|
69
|
+
|
|
62
70
|
const schedule = (): void => {
|
|
63
71
|
if (!pending && !disposed) {
|
|
64
72
|
pending = true;
|
|
65
73
|
queueMicrotask(run);
|
|
66
74
|
}
|
|
67
75
|
};
|
|
76
|
+
|
|
68
77
|
reconcile.current = schedule;
|
|
69
78
|
const unsubscribe = model.subscribe(schedule);
|
|
70
79
|
run();
|
|
80
|
+
|
|
71
81
|
return () => {
|
|
72
82
|
disposed = true;
|
|
73
83
|
reconcile.current = undefined;
|
|
74
84
|
unsubscribe();
|
|
75
85
|
const left = [...added].filter((path) => model.getItem(path) !== null);
|
|
86
|
+
|
|
76
87
|
if (left.length > 0) {
|
|
77
88
|
model.batch(left.map((path) => ({ type: 'remove' as const, path })));
|
|
78
89
|
}
|
|
@@ -83,15 +94,18 @@ export function useEmptyLines(model: FileTree, frame: HTMLElement | null, emptyL
|
|
|
83
94
|
if (frame === null) {
|
|
84
95
|
return;
|
|
85
96
|
}
|
|
97
|
+
|
|
86
98
|
const block = (event: Event): void => {
|
|
87
99
|
if (onEmptyLine(event)) {
|
|
88
100
|
event.preventDefault();
|
|
89
101
|
event.stopImmediatePropagation();
|
|
90
102
|
}
|
|
91
103
|
};
|
|
104
|
+
|
|
92
105
|
for (const type of POINTER_EVENTS) {
|
|
93
106
|
frame.addEventListener(type, block, true);
|
|
94
107
|
}
|
|
108
|
+
|
|
95
109
|
return () => {
|
|
96
110
|
for (const type of POINTER_EVENTS) {
|
|
97
111
|
frame.removeEventListener(type, block, true);
|
|
@@ -102,12 +116,15 @@ export function useEmptyLines(model: FileTree, frame: HTMLElement | null, emptyL
|
|
|
102
116
|
return (event: KeyboardEvent): void => {
|
|
103
117
|
lastKey.current = event.key;
|
|
104
118
|
const past = latest.current !== undefined && movesFocus(event) ? pastEmptyLine(model, event.key) : null;
|
|
119
|
+
|
|
105
120
|
if (past === null) {
|
|
106
121
|
return;
|
|
107
122
|
}
|
|
123
|
+
|
|
108
124
|
// The engine never sees this key, so focus never lands on the line for a moment.
|
|
109
125
|
event.preventDefault();
|
|
110
126
|
event.stopPropagation();
|
|
127
|
+
|
|
111
128
|
if (past !== model.getFocusedPath()) {
|
|
112
129
|
model.scrollToPath(past, { focus: true, offset: 'nearest' });
|
|
113
130
|
}
|
|
@@ -13,28 +13,37 @@ function limitOf(row: HTMLElement, content: Element, gap: number): number {
|
|
|
13
13
|
const start = content.getBoundingClientRect().left;
|
|
14
14
|
const contentMargin = Number.parseFloat(getComputedStyle(content).marginInlineEnd) || 0;
|
|
15
15
|
let limit = Number.POSITIVE_INFINITY;
|
|
16
|
+
|
|
16
17
|
for (const part of [...row.children].filter((child) => child !== content).flatMap((child) => boxesOf(child))) {
|
|
17
18
|
const rect = part.getBoundingClientRect();
|
|
19
|
+
|
|
18
20
|
if (rect.width === 0 || rect.left < start) {
|
|
19
21
|
continue;
|
|
20
22
|
}
|
|
23
|
+
|
|
21
24
|
if (part.childNodes.length === 0 && Number.parseFloat(getComputedStyle(part).flexGrow) > 0) {
|
|
22
25
|
continue;
|
|
23
26
|
}
|
|
27
|
+
|
|
24
28
|
limit = Math.min(limit, rect.left - gap);
|
|
25
29
|
}
|
|
30
|
+
|
|
26
31
|
if (limit !== Number.POSITIVE_INFINITY) {
|
|
27
32
|
return limit - contentMargin;
|
|
28
33
|
}
|
|
34
|
+
|
|
29
35
|
return row.getBoundingClientRect().right - Number.parseFloat(getComputedStyle(row).paddingInlineEnd) - contentMargin;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
function needOf(row: HTMLElement, shift: number, gap: number, range: Range): number | null {
|
|
33
39
|
const content = row.querySelector(':scope > [data-item-section="content"]');
|
|
40
|
+
|
|
34
41
|
if (content === null || row.getBoundingClientRect().width === 0) {
|
|
35
42
|
return null;
|
|
36
43
|
}
|
|
44
|
+
|
|
37
45
|
range.selectNodeContents(content);
|
|
46
|
+
|
|
38
47
|
return shiftNeed(range.getBoundingClientRect().right, limitOf(row, content, gap), shift);
|
|
39
48
|
}
|
|
40
49
|
|
|
@@ -45,5 +54,6 @@ export function useFileTreeShift(model: FileTree, resetKey?: string): { attach(n
|
|
|
45
54
|
rows: (root) => [...root.querySelectorAll<HTMLElement>(ROWS)],
|
|
46
55
|
need: needOf
|
|
47
56
|
};
|
|
57
|
+
|
|
48
58
|
return useTreeShift(source, resetKey);
|
|
49
59
|
}
|
|
@@ -13,9 +13,11 @@ export type FileTreeOptions = WithoutSizing<PierreOptions>;
|
|
|
13
13
|
// Model options are initial; paths and status can subsequently change through the model's mutation methods.
|
|
14
14
|
export function useFileTree(options: FileTreeOptions) {
|
|
15
15
|
const latest = useRef(options.onSelectionChange);
|
|
16
|
+
|
|
16
17
|
useLayoutEffect(() => {
|
|
17
18
|
latest.current = options.onSelectionChange;
|
|
18
19
|
}, [options.onSelectionChange]);
|
|
20
|
+
|
|
19
21
|
return usePierreFileTree({
|
|
20
22
|
composition: { contextMenu: { enabled: false } },
|
|
21
23
|
icons: FILE_TREE_ICONS,
|