1 | import React, { useState, useEffect, useRef } from "react";
|
---|
2 | import ReactDOM from "react-dom";
|
---|
3 | import searchIcon from "../../assets/search_icon.png";
|
---|
4 | import routeIcon from "../../assets/route_icon.png";
|
---|
5 | import closeIcon from "../../assets/close_icon.png";
|
---|
6 | import styles from "./SearchBar.module.css";
|
---|
7 |
|
---|
8 | function SearchBar({
|
---|
9 | map,
|
---|
10 | handleDirectionsSubmit,
|
---|
11 | setIsPanelOpen,
|
---|
12 | setSelectedRoom,
|
---|
13 | availableShapes,
|
---|
14 | handleFloorChange,
|
---|
15 | }) {
|
---|
16 | const [isExpanded, setIsExpanded] = useState(false);
|
---|
17 | const [from, setFrom] = useState("");
|
---|
18 | const [to, setTo] = useState("");
|
---|
19 | const [availableOptions, setAvailableOptions] = useState([]);
|
---|
20 | const [filteredOptions, setFilteredOptions] = useState([]);
|
---|
21 | const [dropdownVisible, setDropdownVisible] = useState(false);
|
---|
22 | const [inputFieldType, setInputFieldType] = useState("");
|
---|
23 |
|
---|
24 | const wrapperRef = useRef(null);
|
---|
25 | const dropdownContainerRef = useRef(null);
|
---|
26 | const activeInputRef = useRef(null); // Track the currently focused input field
|
---|
27 |
|
---|
28 | const toggleExpanded = () => setIsExpanded(!isExpanded);
|
---|
29 |
|
---|
30 | const handleClickOutside = (event) => {
|
---|
31 | if (
|
---|
32 | wrapperRef.current &&
|
---|
33 | !wrapperRef.current.contains(event.target) &&
|
---|
34 | dropdownContainerRef.current &&
|
---|
35 | !dropdownContainerRef.current.contains(event.target)
|
---|
36 | ) {
|
---|
37 | setDropdownVisible(false);
|
---|
38 | }
|
---|
39 | };
|
---|
40 |
|
---|
41 | useEffect(() => {
|
---|
42 | document.addEventListener("mousedown", handleClickOutside);
|
---|
43 | return () => {
|
---|
44 | document.removeEventListener("mousedown", handleClickOutside);
|
---|
45 | };
|
---|
46 | }, []);
|
---|
47 |
|
---|
48 | const searchRoom = () => {
|
---|
49 | const foundRoom = availableShapes.find((sh) => sh.info.name === from);
|
---|
50 | if (foundRoom && foundRoom.floorNum !== map.floorNum) {
|
---|
51 | handleFloorChange(foundRoom.floorNum);
|
---|
52 | }
|
---|
53 | map.highlightShape(from);
|
---|
54 | setSelectedRoom(foundRoom);
|
---|
55 | setIsPanelOpen(true);
|
---|
56 | };
|
---|
57 |
|
---|
58 | const handleInputFocus = (field, inputRef) => {
|
---|
59 | if (availableOptions.length === 0 && map) {
|
---|
60 | setAvailableOptions(
|
---|
61 | availableShapes
|
---|
62 | .filter((sh) => sh.className === "RenderedRoom")
|
---|
63 | .map((shape) => shape.info.name)
|
---|
64 | );
|
---|
65 | }
|
---|
66 | setInputFieldType(field);
|
---|
67 | setDropdownVisible(true);
|
---|
68 | activeInputRef.current = inputRef; // Set the active input ref
|
---|
69 | };
|
---|
70 |
|
---|
71 | const handleInputChange = (setter) => (event) => {
|
---|
72 | const value = event.target.value;
|
---|
73 | setter(value);
|
---|
74 | setDropdownVisible(true);
|
---|
75 |
|
---|
76 | const filtered = availableOptions.filter((option) =>
|
---|
77 | option.toLowerCase().includes(value.toLowerCase())
|
---|
78 | );
|
---|
79 | setFilteredOptions(filtered);
|
---|
80 | };
|
---|
81 |
|
---|
82 | const handleOptionClick = (option) => {
|
---|
83 | if (inputFieldType === "from") {
|
---|
84 | setFrom(option);
|
---|
85 | } else if (inputFieldType === "to") {
|
---|
86 | setTo(option);
|
---|
87 | }
|
---|
88 | setDropdownVisible(false);
|
---|
89 | };
|
---|
90 |
|
---|
91 | const renderDropdown = () => {
|
---|
92 | if (!dropdownVisible || filteredOptions.length === 0) return null;
|
---|
93 |
|
---|
94 | const position = activeInputRef.current?.getBoundingClientRect() || {
|
---|
95 | top: 0,
|
---|
96 | left: 0,
|
---|
97 | width: 0,
|
---|
98 | };
|
---|
99 |
|
---|
100 | return ReactDOM.createPortal(
|
---|
101 | <ul
|
---|
102 | ref={dropdownContainerRef}
|
---|
103 | className={styles.dropdown}
|
---|
104 | style={{
|
---|
105 | position: "absolute",
|
---|
106 | top: position.top + position.height + window.scrollY,
|
---|
107 | left: position.left + window.scrollX,
|
---|
108 | width: position.width,
|
---|
109 | }}
|
---|
110 | >
|
---|
111 | {filteredOptions.map((option, index) => (
|
---|
112 | <li
|
---|
113 | key={index}
|
---|
114 | className={styles.dropdownItem}
|
---|
115 | onClick={() => handleOptionClick(option)}
|
---|
116 | >
|
---|
117 | {option}
|
---|
118 | </li>
|
---|
119 | ))}
|
---|
120 | </ul>,
|
---|
121 | document.body
|
---|
122 | );
|
---|
123 | };
|
---|
124 |
|
---|
125 | return (
|
---|
126 | <div className={styles.wrapper} ref={wrapperRef}>
|
---|
127 | {!isExpanded ? (
|
---|
128 | <div className={styles.searchBar}>
|
---|
129 | <input
|
---|
130 | type="search"
|
---|
131 | className={styles.inputField}
|
---|
132 | placeholder="Search location"
|
---|
133 | aria-label="Search"
|
---|
134 | onFocus={(e) => handleInputFocus("from", e.target)}
|
---|
135 | onChange={handleInputChange(setFrom)}
|
---|
136 | value={from}
|
---|
137 | />
|
---|
138 | {renderDropdown()}
|
---|
139 | <div className={styles.buttons}>
|
---|
140 | <button
|
---|
141 | type="button"
|
---|
142 | className={styles.iconButton}
|
---|
143 | onClick={searchRoom}
|
---|
144 | >
|
---|
145 | <img src={searchIcon} alt="Search Icon" />
|
---|
146 | </button>
|
---|
147 | <button
|
---|
148 | type="button"
|
---|
149 | className={styles.iconButton}
|
---|
150 | onClick={toggleExpanded}
|
---|
151 | >
|
---|
152 | <img src={routeIcon} alt="Route Icon" />
|
---|
153 | </button>
|
---|
154 | </div>
|
---|
155 | </div>
|
---|
156 | ) : (
|
---|
157 | <div className={styles.directionsContainer}>
|
---|
158 | <div className={styles.directionsInputs}>
|
---|
159 | <input
|
---|
160 | type="text"
|
---|
161 | placeholder="From"
|
---|
162 | aria-label="From"
|
---|
163 | value={from}
|
---|
164 | onFocus={(e) => handleInputFocus("from", e.target)}
|
---|
165 | onChange={handleInputChange(setFrom)}
|
---|
166 | className={styles.inputField}
|
---|
167 | />
|
---|
168 | <input
|
---|
169 | type="text"
|
---|
170 | placeholder="To"
|
---|
171 | aria-label="To"
|
---|
172 | value={to}
|
---|
173 | onFocus={(e) => handleInputFocus("to", e.target)}
|
---|
174 | onChange={handleInputChange(setTo)}
|
---|
175 | className={styles.inputField}
|
---|
176 | />
|
---|
177 | {renderDropdown()}
|
---|
178 | </div>
|
---|
179 | <div className={styles.buttons}>
|
---|
180 | <button
|
---|
181 | type="button"
|
---|
182 | className={styles.iconButton}
|
---|
183 | onClick={() => handleDirectionsSubmit(from, to)}
|
---|
184 | >
|
---|
185 | <img src={searchIcon} alt="Submit Directions" />
|
---|
186 | </button>
|
---|
187 | <button
|
---|
188 | type="button"
|
---|
189 | className={styles.iconButton}
|
---|
190 | onClick={toggleExpanded}
|
---|
191 | >
|
---|
192 | <img src={closeIcon} alt="Close Icon" />
|
---|
193 | </button>
|
---|
194 | </div>
|
---|
195 | </div>
|
---|
196 | )}
|
---|
197 | </div>
|
---|
198 | );
|
---|
199 | }
|
---|
200 |
|
---|
201 | export default SearchBar;
|
---|