154 lines
4.5 KiB
TypeScript
154 lines
4.5 KiB
TypeScript
|
|
import { useCallback, useSyncExternalStore } from "react";
|
||
|
|
|
||
|
|
export interface SftpTreeSelectionItem {
|
||
|
|
path: string;
|
||
|
|
name: string;
|
||
|
|
isDirectory: boolean;
|
||
|
|
sourcePath: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface SftpTreeSelectionState {
|
||
|
|
visibleItems: SftpTreeSelectionItem[];
|
||
|
|
visibleItemsByPath: Map<string, SftpTreeSelectionItem>;
|
||
|
|
visibleIndexByPath: Map<string, number>;
|
||
|
|
visiblePathsSet: Set<string>;
|
||
|
|
selectedPaths: Set<string>;
|
||
|
|
}
|
||
|
|
|
||
|
|
const EMPTY_PATHS = new Set<string>();
|
||
|
|
|
||
|
|
const EMPTY_STATE: SftpTreeSelectionState = {
|
||
|
|
visibleItems: [],
|
||
|
|
visibleItemsByPath: new Map(),
|
||
|
|
visibleIndexByPath: new Map(),
|
||
|
|
visiblePathsSet: new Set(),
|
||
|
|
selectedPaths: EMPTY_PATHS,
|
||
|
|
};
|
||
|
|
|
||
|
|
type Listener = () => void;
|
||
|
|
|
||
|
|
const paneStates = new Map<string, SftpTreeSelectionState>();
|
||
|
|
const paneListeners = new Map<string, Set<Listener>>();
|
||
|
|
|
||
|
|
const notifyPaneListeners = (paneId: string) => {
|
||
|
|
paneListeners.get(paneId)?.forEach((listener) => listener());
|
||
|
|
};
|
||
|
|
|
||
|
|
const getPaneState = (paneId: string): SftpTreeSelectionState =>
|
||
|
|
paneStates.get(paneId) ?? EMPTY_STATE;
|
||
|
|
|
||
|
|
const setPaneState = (
|
||
|
|
paneId: string,
|
||
|
|
updater: (state: SftpTreeSelectionState) => SftpTreeSelectionState,
|
||
|
|
) => {
|
||
|
|
const prev = getPaneState(paneId);
|
||
|
|
const next = updater(prev);
|
||
|
|
if (next === prev) return;
|
||
|
|
if (next.visibleItems.length === 0 && next.selectedPaths.size === 0) {
|
||
|
|
paneStates.delete(paneId);
|
||
|
|
} else {
|
||
|
|
paneStates.set(paneId, next);
|
||
|
|
}
|
||
|
|
notifyPaneListeners(paneId);
|
||
|
|
};
|
||
|
|
|
||
|
|
export const sftpTreeSelectionStore = {
|
||
|
|
getPaneState,
|
||
|
|
|
||
|
|
getSelectedItems: (paneId: string): SftpTreeSelectionItem[] => {
|
||
|
|
const state = getPaneState(paneId);
|
||
|
|
const result: SftpTreeSelectionItem[] = [];
|
||
|
|
for (const path of state.selectedPaths) {
|
||
|
|
const item = state.visibleItemsByPath.get(path);
|
||
|
|
if (item) result.push(item);
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
},
|
||
|
|
|
||
|
|
setVisibleItems: (paneId: string, visibleItems: SftpTreeSelectionItem[]) => {
|
||
|
|
const visibleItemsByPath = new Map<string, SftpTreeSelectionItem>();
|
||
|
|
const visibleIndexByPath = new Map<string, number>();
|
||
|
|
const visiblePathsSet = new Set(visibleItems.map((item) => item.path));
|
||
|
|
visibleItems.forEach((item, index) => {
|
||
|
|
visibleItemsByPath.set(item.path, item);
|
||
|
|
visibleIndexByPath.set(item.path, index);
|
||
|
|
});
|
||
|
|
setPaneState(paneId, (state) => {
|
||
|
|
const newSelected = new Set([...state.selectedPaths].filter((p) => visiblePathsSet.has(p)));
|
||
|
|
const changed =
|
||
|
|
newSelected.size !== state.selectedPaths.size ||
|
||
|
|
[...newSelected].some((p) => !state.selectedPaths.has(p));
|
||
|
|
return {
|
||
|
|
visibleItems,
|
||
|
|
visibleItemsByPath,
|
||
|
|
visibleIndexByPath,
|
||
|
|
visiblePathsSet,
|
||
|
|
selectedPaths: changed ? newSelected : state.selectedPaths,
|
||
|
|
};
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
setSelection: (paneId: string, selectedPaths: Iterable<string>) => {
|
||
|
|
setPaneState(paneId, (state) => ({
|
||
|
|
...state,
|
||
|
|
selectedPaths: new Set(Array.from(selectedPaths).filter((path) => state.visiblePathsSet.has(path))),
|
||
|
|
}));
|
||
|
|
},
|
||
|
|
|
||
|
|
clearSelection: (paneId: string) => {
|
||
|
|
setPaneState(paneId, (state) => ({ ...state, selectedPaths: EMPTY_PATHS }));
|
||
|
|
},
|
||
|
|
|
||
|
|
clearAllExcept: (paneIdsToKeep?: Iterable<string>) => {
|
||
|
|
const keep = new Set(paneIdsToKeep ?? []);
|
||
|
|
Array.from(paneStates.keys()).forEach((paneId) => {
|
||
|
|
if (keep.has(paneId)) return;
|
||
|
|
setPaneState(paneId, (state) => {
|
||
|
|
if (state.selectedPaths.size === 0) return state;
|
||
|
|
return { ...state, selectedPaths: EMPTY_PATHS };
|
||
|
|
});
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
selectAllVisible: (paneId: string) => {
|
||
|
|
setPaneState(paneId, (state) => ({
|
||
|
|
...state,
|
||
|
|
selectedPaths: new Set(
|
||
|
|
state.visibleItems.map((item) => item.path),
|
||
|
|
),
|
||
|
|
}));
|
||
|
|
},
|
||
|
|
|
||
|
|
clearPane: (paneId: string) => {
|
||
|
|
if (!paneStates.has(paneId)) return;
|
||
|
|
paneStates.delete(paneId);
|
||
|
|
notifyPaneListeners(paneId);
|
||
|
|
},
|
||
|
|
|
||
|
|
subscribe: (paneId: string, listener: Listener) => {
|
||
|
|
const listeners = paneListeners.get(paneId) ?? new Set<Listener>();
|
||
|
|
listeners.add(listener);
|
||
|
|
paneListeners.set(paneId, listeners);
|
||
|
|
return () => {
|
||
|
|
const current = paneListeners.get(paneId);
|
||
|
|
if (!current) return;
|
||
|
|
current.delete(listener);
|
||
|
|
if (current.size === 0) {
|
||
|
|
paneListeners.delete(paneId);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
export const useSftpTreeSelectionState = (paneId: string): SftpTreeSelectionState => {
|
||
|
|
const subscribe = useCallback(
|
||
|
|
(listener: () => void) => sftpTreeSelectionStore.subscribe(paneId, listener),
|
||
|
|
[paneId],
|
||
|
|
);
|
||
|
|
return useSyncExternalStore(
|
||
|
|
subscribe,
|
||
|
|
() => sftpTreeSelectionStore.getPaneState(paneId),
|
||
|
|
() => sftpTreeSelectionStore.getPaneState(paneId),
|
||
|
|
);
|
||
|
|
};
|