File size: 2,661 Bytes
7ed5764
 
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
 
 
 
 
 
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 hits: { item: OpsOp }[] = searchText
    ? fuse.search<OpsOp>(searchText)
    : Object.values(props.boxes).map((box) => ({ item: box }));
  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>
  );
}