PluginProbe
ElasticPress / 4.7.0
ElasticPress v4.7.0
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / assets / js / api-search / index.js

index.js in ElasticPress 4.7.0, at assets/js/api-search/index.js

376 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * WordPress dependencies.
3 */
4 import {
5 createContext,
6 useCallback,
7 useContext,
8 useEffect,
9 useMemo,
10 useReducer,
11 useRef,
12 WPElement,
13 } from '@wordpress/element';
14
15 /**
16 * Internal dependencies.
17 */
18 import { useFetchResults } from './src/hooks';
19 import reducer from './src/reducer';
20 import {
21 getArgsFromUrlParams,
22 getDefaultArgsFromSchema,
23 getUrlParamsFromArgs,
24 getUrlWithParams,
25 } from './src/utilities';
26
27 /**
28 * Instant Results context.
29 */
30 const Context = createContext();
31
32 /**
33 * Instant Results provider component.
34 *
35 * @param {object} props Component props.
36 * @param {string} props.apiEndpoint API endpoint.
37 * @param {string} props.apiHost API Host.
38 * @param {object} props.argsSchema Schema describing supported args.
39 * @param {string} props.authorization Authorization header.
40 * @param {string} props.requestIdBase Base of Requests IDs.
41 * @param {WPElement} props.children Component children.
42 * @param {string} props.paramPrefix Prefix used to set and parse URL parameters.
43 * @param {Function} props.onAuthError Function to run when request authentication fails.
44 * @returns {WPElement} Component.
45 */
46 export const ApiSearchProvider = ({
47 apiEndpoint,
48 apiHost,
49 authorization,
50 requestIdBase,
51 argsSchema,
52 children,
53 paramPrefix,
54 onAuthError,
55 }) => {
56 /**
57 * Any default args from the URL.
58 */
59 const defaultArgsFromUrl = useMemo(() => {
60 if (!paramPrefix) {
61 return {};
62 }
63
64 return getArgsFromUrlParams(argsSchema, paramPrefix);
65 }, [argsSchema, paramPrefix]);
66
67 /**
68 * All default args including defaults from the schema.
69 */
70 const defaultArgs = useMemo(() => {
71 const defaultArgsFromSchema = getDefaultArgsFromSchema(argsSchema);
72
73 return {
74 ...defaultArgsFromSchema,
75 ...defaultArgsFromUrl,
76 };
77 }, [argsSchema, defaultArgsFromUrl]);
78
79 /**
80 * Whether the provider is "on" by default.
81 */
82 const defaultIsOn = useMemo(() => {
83 return Object.keys(defaultArgsFromUrl).length > 0;
84 }, [defaultArgsFromUrl]);
85
86 /**
87 * Set up fetch method.
88 */
89 const fetchResults = useFetchResults(
90 apiHost,
91 apiEndpoint,
92 authorization,
93 onAuthError,
94 requestIdBase,
95 );
96
97 /**
98 * Set up the reducer.
99 */
100 const [state, dispatch] = useReducer(reducer, {
101 aggregations: {},
102 args: defaultArgs,
103 argsSchema,
104 isLoading: false,
105 isOn: defaultIsOn,
106 isPoppingState: false,
107 searchResults: [],
108 totalResults: 0,
109 suggestedTerms: [],
110 isFirstSearch: true,
111 searchTerm: '',
112 });
113
114 /**
115 * Create state ref.
116 *
117 * Helps to avoid dependency hell.
118 */
119 const stateRef = useRef(state);
120
121 stateRef.current = state;
122
123 /**
124 * Clear facet constraints.
125 *
126 * @returns {void}
127 */
128 const clearConstraints = useCallback(() => {
129 dispatch({ type: 'CLEAR_CONSTRAINTS' });
130 }, []);
131
132 /**
133 * Clear search results.
134 *
135 * @returns {void}
136 */
137 const clearResults = useCallback(() => {
138 dispatch({ type: 'CLEAR_RESULTS' });
139 }, []);
140
141 /**
142 * Update the search query args, triggering a search.
143 *
144 * @param {object} args Search args.
145 * @returns {void}
146 */
147 const search = useCallback((args) => {
148 dispatch({ type: 'SEARCH', args });
149 }, []);
150
151 /**
152 * Update the search term, triggering a search and resetting facet
153 * constraints.
154 *
155 * @param {string} searchTerm Search term.
156 * @returns {void}
157 */
158 const searchFor = (searchTerm) => {
159 dispatch({ type: 'SEARCH_FOR', searchTerm });
160 };
161
162 /**
163 * Set loading state.
164 *
165 * @param {boolean} isLoading Is loading?
166 * @returns {void}
167 */
168 const setIsLoading = (isLoading) => {
169 dispatch({ type: 'SET_IS_LOADING', isLoading });
170 };
171
172 /**
173 * Set search results based on an Elasticsearch response.
174 *
175 * @param {object} response Elasticsearch response.
176 * @returns {void}
177 */
178 const setResults = (response) => {
179 dispatch({ type: 'SET_RESULTS', response });
180 };
181
182 /**
183 * Go to the next page of search results.
184 *
185 * @returns {void}
186 */
187 const nextPage = () => {
188 dispatch({ type: 'NEXT_PAGE' });
189 };
190
191 /**
192 * Go to the previous page of search results.
193 *
194 * @returns {void}
195 */
196 const previousPage = () => {
197 dispatch({ type: 'PREVIOUS_PAGE' });
198 };
199
200 /**
201 * Set search args from popped history state.
202 *
203 * @param {object} args Search args.
204 */
205 const popState = (args) => {
206 dispatch({ type: 'POP_STATE', args });
207 };
208
209 /**
210 * Turn off the provider.
211 *
212 * @returns {void}
213 */
214 const turnOff = () => {
215 dispatch({ type: 'TURN_OFF' });
216 };
217
218 /**
219 * Push search args to browser history.
220 *
221 * @returns {void}
222 */
223 const pushState = useCallback(() => {
224 if (typeof paramPrefix === 'undefined') {
225 return;
226 }
227
228 const { args, isOn } = stateRef.current;
229 const state = { args, isOn };
230
231 if (window.history.state) {
232 if (isOn) {
233 const params = getUrlParamsFromArgs(args, argsSchema, paramPrefix);
234 const url = getUrlWithParams(paramPrefix, params);
235
236 window.history.pushState(state, document.title, url);
237 } else {
238 const url = getUrlWithParams(paramPrefix);
239
240 window.history.pushState(state, document.title, url);
241 }
242 } else {
243 window.history.replaceState(state, document.title, window.location.href);
244 }
245 }, [argsSchema, paramPrefix]);
246
247 /**
248 * Handle popstate event.
249 *
250 * @param {Event} event popstate event.
251 */
252 const onPopState = useCallback(
253 (event) => {
254 if (typeof paramPrefix === 'undefined') {
255 return;
256 }
257
258 const hasState = event.state && Object.keys(event.state).length > 0;
259
260 if (hasState) {
261 popState(event.state);
262 }
263 },
264 [paramPrefix],
265 );
266
267 /**
268 * Handle initialization.
269 *
270 * @returns {Function} A cleanup function.
271 */
272 const handleInit = useCallback(() => {
273 window.addEventListener('popstate', onPopState);
274
275 return () => {
276 window.removeEventListener('popstate', onPopState);
277 };
278 }, [onPopState]);
279
280 /**
281 * Handle a change to search args.
282 *
283 * @returns {void}
284 */
285 const handleSearch = useCallback(() => {
286 const handle = async () => {
287 const { args, isOn, isPoppingState } = stateRef.current;
288
289 if (!isPoppingState) {
290 pushState();
291 }
292
293 if (!isOn) {
294 return;
295 }
296
297 const urlParams = getUrlParamsFromArgs(args, argsSchema);
298
299 setIsLoading(true);
300
301 const response = await fetchResults(urlParams);
302
303 if (!response) {
304 return;
305 }
306
307 setResults(response);
308 setIsLoading(false);
309 };
310
311 handle();
312 }, [argsSchema, fetchResults, pushState]);
313
314 /**
315 * Effects.
316 */
317 useEffect(handleInit, [handleInit]);
318 useEffect(handleSearch, [
319 handleSearch,
320 state.args,
321 state.args.orderby,
322 state.args.order,
323 state.args.offset,
324 state.args.search,
325 ]);
326
327 /**
328 * Provide state to context.
329 */
330 const {
331 aggregations,
332 args,
333 isLoading,
334 isOn,
335 searchResults,
336 searchTerm,
337 totalResults,
338 suggestedTerms,
339 isFirstSearch,
340 } = stateRef.current;
341
342 // eslint-disable-next-line react/jsx-no-constructed-context-values
343 const contextValue = {
344 aggregations,
345 args,
346 clearConstraints,
347 clearResults,
348 getUrlParamsFromArgs,
349 getUrlWithParams,
350 isLoading,
351 isOn,
352 searchResults,
353 searchTerm,
354 search,
355 searchFor,
356 setResults,
357 nextPage,
358 previousPage,
359 totalResults,
360 turnOff,
361 suggestedTerms,
362 isFirstSearch,
363 };
364
365 return <Context.Provider value={contextValue}>{children}</Context.Provider>;
366 };
367
368 /**
369 * Use the API Search context.
370 *
371 * @returns {object} API Search Context.
372 */
373 export const useApiSearch = () => {
374 return useContext(Context);
375 };
376