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