PluginProbe
ElasticPress / 5.3.5
ElasticPress v5.3.5
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 5.3.5, at assets/js/api-search/index.js

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