PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.5
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.5
4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 All 203 releases
betterdocs / react-src / public / shortcodes / search-modal / SearchField.js

SearchField.js in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.5, at react-src/public/shortcodes/search-modal/SearchField.js

215 lines 7.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useState, useEffect, useRef } from "react";
2 import apiFetch from "@wordpress/api-fetch";
3 import LayoutOne from "./LayoutOne";
4 import LayoutTwo from "./LayoutTwo";
5 import LayoutSidebar from "./LayoutSidebar";
6 import SearchModal from "./SearchModal";
7
8 const SearchField = (props) => {
9 const [searchQuery, setSearchQuery] = useState("");
10 const [isModalOpen, setIsModalOpen] = useState(false);
11 const [docCategories, setDocCategories] = useState([]);
12 const [popularDocs, setPopularDocs] = useState([]);
13 const [recentFaqs, setRecentFaqs] = useState([]);
14
15 // Extracting attributes from props
16 const {
17 layout,
18 placeholder,
19 heading,
20 subheading,
21 headingTag,
22 subheadingTag,
23 buttonText,
24 numberofdocs,
25 numberoffaqs,
26 doc_ids,
27 doc_categories_ids,
28 faq_categories_ids,
29 searchButton,
30 enable_docs_search,
31 enable_faq_search,
32 enable_ai_powered_search,
33 kbBasedSearch,
34 topWrapperRef = null
35 } = props;
36
37 const modalRef = useRef(null); // Ref to keep track of the modal element
38
39 useEffect(() => {
40 const handleKeyDown = (e) => {
41 if (e.key === "Escape") {
42 setIsModalOpen(false);
43 }
44 if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
45 setIsModalOpen(true);
46 }
47 };
48
49 document.addEventListener("keydown", handleKeyDown);
50 return () => {
51 document.removeEventListener("keydown", handleKeyDown);
52 };
53 }, []);
54
55 useEffect(() => {
56 const fetchDocCategories = async () => {
57 try {
58 const namespace = 'betterdocs/v1/';
59 let relativePath = `get-terms?taxonomy=doc_category`;
60
61 // Add KB filter if provided
62 if (kbBasedSearch) {
63 relativePath += `&knowledge_base=${encodeURIComponent(kbBasedSearch)}`;
64 }
65
66 const categories = await apiFetch({
67 path: `${namespace}${relativePath}`,
68 });
69 setDocCategories(categories);
70 } catch (error) {
71 console.error("Error fetching doc categories:", error);
72 }
73 };
74
75 fetchDocCategories();
76 }, [kbBasedSearch]);
77
78 useEffect(() => {
79 const fetchInitialResults = async () => {
80 try {
81 const popularDocs = await apiFetch({
82 path: `betterdocs/v1/search?initial=true&per_page=${numberofdocs}${
83 doc_ids ? `&doc_ids=${doc_ids}` : ""
84 }${
85 doc_categories_ids
86 ? `&doc_categories_ids=${doc_categories_ids}`
87 : ""
88 }${
89 kbBasedSearch
90 ? `&knowledge_base=${encodeURIComponent(kbBasedSearch)}`
91 : ""
92 }`,
93 });
94 setPopularDocs(popularDocs);
95
96 const recentFaqs = await apiFetch({
97 path: `betterdocs/v1/search?initial=true&post_type=betterdocs_faq&per_page=${numberoffaqs}${
98 faq_categories_ids
99 ? `&faq_categories_ids=${faq_categories_ids}`
100 : ""
101 }`,
102 });
103 setRecentFaqs(recentFaqs);
104 } catch (error) {
105 console.error("Error fetching initial results:", error);
106 }
107 };
108
109 fetchInitialResults();
110 }, [numberofdocs, numberoffaqs, doc_ids, doc_categories_ids, faq_categories_ids, kbBasedSearch]);
111
112 // Use stopPropagation on the trigger so that the click that opens the modal
113 // does NOT continue bubbling to document / React's event delegation.
114 // Background: React 18 with createRoot still uses event delegation at the
115 // root container. setState flushes cause the modal overlay to enter the DOM
116 // before the click finishes bubbling, so React's delegation fires the
117 // overlay's onMouseDown on the SAME click that opened the modal.
118 const handleSearchFieldClick = (e) => {
119 if (e && e.stopPropagation) {
120 e.stopPropagation();
121 }
122 setIsModalOpen(true);
123 };
124
125 const closeModal = () => {
126 setIsModalOpen(false);
127 };
128
129 // Use mousedown (not click) on the document so that it fires BEFORE the
130 // click event and is not affected by the trigger's stopPropagation on click.
131 // Store the handler in a ref so the same function reference is always used
132 // with addEventListener / removeEventListener — prevents stale-handler leak.
133 const outsideMouseDownRef = useRef(null);
134
135 useEffect(() => {
136 outsideMouseDownRef.current = (e) => {
137 if (modalRef.current && !modalRef.current.contains(e.target)) {
138 setIsModalOpen(false);
139 }
140 };
141 }); // runs after every render — keeps ref current
142
143 useEffect(() => {
144 const handler = (e) => {
145 if (outsideMouseDownRef.current) {
146 outsideMouseDownRef.current(e);
147 }
148 };
149
150 if (isModalOpen) {
151 // Use mousedown instead of click:
152 // - fires before click, so it doesn't race with the trigger's click
153 // - stopPropagation on the trigger's click doesn't affect mousedown
154 document.addEventListener("mousedown", handler);
155 }
156
157 return () => {
158 document.removeEventListener("mousedown", handler);
159 };
160 }, [isModalOpen]);
161
162 return (
163 <>
164 {layout === "betterdocs-search-modal-sidebar" && (
165 <div onClick={handleSearchFieldClick}>
166 <LayoutSidebar placeholder={placeholder} />
167 </div>
168 )}
169
170 {layout === "betterdocs-search-modal-archive" && (
171 <div onClick={handleSearchFieldClick}>
172 <LayoutTwo
173 placeholder={placeholder}
174 buttonText={buttonText}
175 searchButton={searchButton}
176 />
177 </div>
178 )}
179
180 {layout === "betterdocs-search-modal-layout-1" && (
181 <LayoutOne
182 handleSearchFieldClick={handleSearchFieldClick}
183 placeholder={placeholder}
184 heading={heading}
185 subheading={subheading}
186 headingTag={headingTag}
187 subheadingTag={subheadingTag}
188 buttonText={buttonText}
189 searchButton={searchButton}
190 />
191 )}
192
193 {isModalOpen && (
194 <div ref={modalRef}>
195 <SearchModal
196 topWrapperRef={topWrapperRef}
197 initialSearchQuery={searchQuery}
198 closeModal={closeModal}
199 docCategories={docCategories}
200 popularDocs={popularDocs}
201 recentFaqs={recentFaqs}
202 placeholder={placeholder}
203 enableFaqSearch={enable_faq_search}
204 enableDocsSearch={enable_docs_search}
205 enableAiSearchSuggestions={enable_ai_powered_search}
206 kbBasedSearch={kbBasedSearch}
207 />
208 </div>
209 )}
210 </>
211 );
212 };
213
214 export default SearchField;
215