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>
  );
}