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

388 lines 7.5 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 search args from popped history state.
203 *
204 * @param {object} args Search args.
205 */
206 const popState = (args) => {
207 dispatch({ type: 'POP_STATE', args });
208 };
209
210 /**
211 * Turn off the provider.
212 *
213 * @returns {void}
214 */
215 const turnOff = () => {
216 dispatch({ type: 'TURN_OFF' });
217 };
218
219 /**
220 * Push search args to browser history.
221 *
222 * @returns {void}
223 */
224 const pushState = useCallback(() => {
225 if (typeof paramPrefix === 'undefined') {
226 return;
227 }
228
229 const { args, isOn } = stateRef.current;
230 const state = { args, isOn };
231
232 if (window.history.state) {
233 if (isOn) {
234 const params = getUrlParamsFromArgs(args, argsSchema, paramPrefix);
235 const url = getUrlWithParams(paramPrefix, params);
236
237 window.history.pushState(state, document.title, url);
238 } else {
239 const url = getUrlWithParams(paramPrefix);
240
241 window.history.pushState(state, document.title, url);
242 }
243 } else {
244 window.history.replaceState(state, document.title, window.location.href);
245 }
246 }, [argsSchema, paramPrefix]);
247
248 /**
249 * Handle popstate event.
250 *
251 * @param {Event} event popstate event.
252 */
253 const onPopState = useCallback(
254 (event) => {
255 if (typeof paramPrefix === 'undefined') {
256 return;
257 }
258
259 const hasState = event.state && Object.keys(event.state).length > 0;
260
261 if (hasState) {
262 popState(event.state);
263 }
264 },
265 [paramPrefix],
266 );
267
268 /**
269 * Handle initialization.
270 *
271 * @returns {Function} A cleanup function.
272 */
273 const handleInit = useCallback(() => {
274 window.addEventListener('popstate', onPopState);
275
276 return () => {
277 window.removeEventListener('popstate', onPopState);
278 };
279 }, [onPopState]);
280
281 /**
282 * Handle a change to search args.
283 *
284 * @returns {void}
285 */
286 const handleSearch = useCallback(() => {
287 const handle = async () => {
288 const { args, isOn, isPoppingState } = stateRef.current;
289
290 if (!isPoppingState) {
291 pushState();
292 }
293
294 if (!isOn) {
295 return;
296 }
297
298 const urlParams = getUrlParamsFromArgs(args, argsSchema);
299
300 setIsLoading(true);
301
302 try {
303 const response = await fetchResults(urlParams);
304
305 if (!response) {
306 return;
307 }
308
309 setResults(response);
310 } catch (e) {
311 const errorMessage = sprintf(
312 /* translators: Error message */
313 __('ElasticPress: Unable to fetch results. %s', 'elasticpress'),
314 e.message,
315 );
316
317 console.error(errorMessage); // eslint-disable-line no-console
318 }
319
320 setIsLoading(false);
321 };
322
323 handle();
324 }, [argsSchema, fetchResults, pushState]);
325
326 /**
327 * Effects.
328 */
329 useEffect(handleInit, [handleInit]);
330 useEffect(handleSearch, [
331 handleSearch,
332 state.args,
333 state.args.orderby,
334 state.args.order,
335 state.args.offset,
336 state.args.search,
337 ]);
338
339 /**
340 * Provide state to context.
341 */
342 const {
343 aggregations,
344 args,
345 isLoading,
346 isOn,
347 searchResults,
348 searchTerm,
349 totalResults,
350 suggestedTerms,
351 isFirstSearch,
352 } = stateRef.current;
353
354 // eslint-disable-next-line react/jsx-no-constructed-context-values
355 const contextValue = {
356 aggregations,
357 args,
358 clearConstraints,
359 clearResults,
360 getUrlParamsFromArgs,
361 getUrlWithParams,
362 isLoading,
363 isOn,
364 searchResults,
365 searchTerm,
366 search,
367 searchFor,
368 setResults,
369 nextPage,
370 previousPage,
371 totalResults,
372 turnOff,
373 suggestedTerms,
374 isFirstSearch,
375 };
376
377 return <Context.Provider value={contextValue}>{children}</Context.Provider>;
378 };
379
380 /**
381 * Use the API Search context.
382 *
383 * @returns {object} API Search Context.
384 */
385 export const useApiSearch = () => {
386 return useContext(Context);
387 };
388