Spaces:
Running
Running
File size: 2,810 Bytes
d460634 ab53490 d460634 ab53490 da1f2d0 d460634 ab53490 d460634 42d6b38 a112474 ab53490 d460634 ab53490 d460634 ab53490 d460634 ab53490 d460634 ab53490 d460634 ab53490 a112474 ab53490 d460634 ab53490 d460634 ab53490 d460634 ab53490 d460634 ab53490 d460634 ab53490 d460634 ab53490 |
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 |
import Fuse from "fuse.js";
import { useEffect, useMemo, useRef, useState } from "react";
export type OpsOp = {
name: string;
type: string;
position: { x: number; y: number };
params: { name: string; default: any }[];
};
export type Catalog = { [op: string]: OpsOp };
export type Catalogs = { [env: string]: Catalog };
export default function NodeSearch(props: {
boxes: Catalog;
onCancel: any;
onAdd: any;
pos: { x: number; y: number };
}) {
const searchBox = useRef(null as unknown as HTMLInputElement);
const [searchText, setSearchText] = useState("");
const fuse = useMemo(
() =>
new Fuse(Object.values(props.boxes), {
keys: ["name"],
}),
[props.boxes],
);
const allOps = useMemo(() => {
const boxes = Object.values(props.boxes).map((box) => ({ item: box }));
boxes.sort((a, b) => a.item.name.localeCompare(b.item.name));
return boxes;
}, [props.boxes]);
const hits: { item: OpsOp }[] = searchText ? fuse.search<OpsOp>(searchText) : allOps;
const [selectedIndex, setSelectedIndex] = useState(0);
useEffect(() => searchBox.current.focus());
function typed(text: string) {
setSearchText(text);
setSelectedIndex(Math.max(0, Math.min(selectedIndex, hits.length - 1)));
}
function onKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === "ArrowDown") {
e.preventDefault();
setSelectedIndex(Math.min(selectedIndex + 1, hits.length - 1));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setSelectedIndex(Math.max(selectedIndex - 1, 0));
} else if (e.key === "Enter") {
addSelected();
} else if (e.key === "Escape") {
props.onCancel();
}
}
function addSelected() {
const node = { ...hits[selectedIndex].item };
node.position = props.pos;
props.onAdd(node);
}
async function lostFocus(e: any) {
// If it's a click on a result, let the click handler handle it.
if (e.relatedTarget?.closest(".node-search")) return;
props.onCancel();
}
return (
<div className="node-search" style={{ top: props.pos.y, left: props.pos.x }}>
<input
ref={searchBox}
value={searchText}
onChange={(event) => typed(event.target.value)}
onKeyDown={onKeyDown}
onBlur={lostFocus}
placeholder="Search for box"
/>
<div className="matches">
{hits.map((box, index) => (
<div
key={box.item.name}
tabIndex={0}
onFocus={() => setSelectedIndex(index)}
onMouseEnter={() => setSelectedIndex(index)}
onClick={addSelected}
className={`search-result ${index === selectedIndex ? "selected" : ""}`}
>
{box.item.name}
</div>
))}
</div>
</div>
);
}
|