File size: 2,589 Bytes
a66000a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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 && 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 >
  );
}