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