Spaces:
Running
Running
File size: 2,825 Bytes
7ed5764 499182c 7ed5764 499182c 7ed5764 499182c 7ed5764 65a0c5b 7ed5764 65a0c5b 499182c 7ed5764 499182c 7ed5764 499182c 7ed5764 499182c 7ed5764 499182c 7ed5764 499182c 7ed5764 499182c 7ed5764 499182c 7ed5764 499182c 7ed5764 499182c 7ed5764 499182c 7ed5764 499182c 7ed5764 499182c |
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 |
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 (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>
);
}
|