Initial import
This commit is contained in:
122
src/layer/search/context/SearchLayerContext.tsx
Executable file
122
src/layer/search/context/SearchLayerContext.tsx
Executable file
@@ -0,0 +1,122 @@
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type PropsWithChildren,
|
||||
} from 'react';
|
||||
import { SearchCommandModal, type SearchKeywordOption } from '../../../components/search';
|
||||
import type { SearchLayerSnapshot, SearchOpenMode, SearchWindowSelection, SearchWindowSelectionDraft } from '../types';
|
||||
|
||||
type SearchLayerContextValue = SearchLayerSnapshot & {
|
||||
setOptions: (options: SearchKeywordOption[]) => void;
|
||||
openSearch: (mode?: SearchOpenMode) => void;
|
||||
closeSearch: () => void;
|
||||
clearWindowSelection: (instanceId: string) => void;
|
||||
addWindowSelections: (selections: SearchWindowSelectionDraft[]) => void;
|
||||
};
|
||||
|
||||
const SearchLayerContext = createContext<SearchLayerContextValue | null>(null);
|
||||
const WINDOW_SELECTION_DEDUP_MS = 500;
|
||||
|
||||
export function SearchLayerProvider({ children }: PropsWithChildren) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [options, setOptions] = useState<SearchKeywordOption[]>([]);
|
||||
const [mode, setMode] = useState<SearchOpenMode>('navigate');
|
||||
const [windowSelections, setWindowSelections] = useState<SearchWindowSelection[]>([]);
|
||||
const lastWindowSelectionRef = useRef<{ id: string; at: number } | null>(null);
|
||||
|
||||
const value = useMemo<SearchLayerContextValue>(
|
||||
() => ({
|
||||
open,
|
||||
options,
|
||||
mode,
|
||||
windowSelections,
|
||||
setOptions,
|
||||
openSearch: (nextMode = 'navigate') => {
|
||||
lastWindowSelectionRef.current = null;
|
||||
setMode(nextMode);
|
||||
setOpen(true);
|
||||
},
|
||||
closeSearch: () => {
|
||||
setOpen(false);
|
||||
},
|
||||
clearWindowSelection: (instanceId: string) => {
|
||||
setWindowSelections((previous) => previous.filter((selection) => selection.instanceId !== instanceId));
|
||||
},
|
||||
addWindowSelections: (selections) => {
|
||||
if (selections.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setWindowSelections((previous) => [
|
||||
...previous,
|
||||
...selections.map((selection) => ({
|
||||
...selection,
|
||||
instanceId: `window-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
})),
|
||||
]);
|
||||
},
|
||||
}),
|
||||
[mode, open, options, windowSelections],
|
||||
);
|
||||
|
||||
return (
|
||||
<SearchLayerContext.Provider value={value}>
|
||||
{children}
|
||||
<SearchCommandModal
|
||||
open={open}
|
||||
options={options}
|
||||
mode={mode}
|
||||
onClose={value.closeSearch}
|
||||
onSelectOption={(option) => {
|
||||
if (mode === 'window') {
|
||||
const now = Date.now();
|
||||
const previousSelection = lastWindowSelectionRef.current;
|
||||
|
||||
if (
|
||||
previousSelection &&
|
||||
previousSelection.id === option.id &&
|
||||
now - previousSelection.at <= WINDOW_SELECTION_DEDUP_MS
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastWindowSelectionRef.current = {
|
||||
id: option.id,
|
||||
at: now,
|
||||
};
|
||||
|
||||
setOpen(false);
|
||||
setWindowSelections((previous) => [
|
||||
...previous,
|
||||
{
|
||||
instanceId: `window-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
id: option.id,
|
||||
label: option.label,
|
||||
group: option.group,
|
||||
description: option.description,
|
||||
keywords: option.keywords ?? [],
|
||||
},
|
||||
]);
|
||||
(option.onSelectWindow ?? option.onSelect)();
|
||||
return;
|
||||
}
|
||||
|
||||
option.onSelect();
|
||||
}}
|
||||
/>
|
||||
</SearchLayerContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSearchLayerContext() {
|
||||
const context = useContext(SearchLayerContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useSearchLayerContext must be used within a SearchLayerProvider.');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
5
src/layer/search/hooks/useSearchLayer.ts
Executable file
5
src/layer/search/hooks/useSearchLayer.ts
Executable file
@@ -0,0 +1,5 @@
|
||||
import { useSearchLayerContext } from '../context/SearchLayerContext';
|
||||
|
||||
export function useSearchLayer() {
|
||||
return useSearchLayerContext();
|
||||
}
|
||||
3
src/layer/search/index.ts
Executable file
3
src/layer/search/index.ts
Executable file
@@ -0,0 +1,3 @@
|
||||
export * from './context/SearchLayerContext';
|
||||
export * from './hooks/useSearchLayer';
|
||||
export * from './types';
|
||||
21
src/layer/search/types/index.ts
Executable file
21
src/layer/search/types/index.ts
Executable file
@@ -0,0 +1,21 @@
|
||||
import type { SearchKeywordOption } from '../../../components/search';
|
||||
|
||||
export type SearchOpenMode = 'navigate' | 'window';
|
||||
|
||||
export type SearchWindowSelection = {
|
||||
instanceId: string;
|
||||
id: string;
|
||||
label: string;
|
||||
group: string;
|
||||
description?: string;
|
||||
keywords: string[];
|
||||
};
|
||||
|
||||
export type SearchWindowSelectionDraft = Omit<SearchWindowSelection, 'instanceId'>;
|
||||
|
||||
export type SearchLayerSnapshot = {
|
||||
open: boolean;
|
||||
options: SearchKeywordOption[];
|
||||
mode: SearchOpenMode;
|
||||
windowSelections: SearchWindowSelection[];
|
||||
};
|
||||
Reference in New Issue
Block a user