PluginProbe
ElasticPress / 4.6.1
ElasticPress v4.6.1
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.6.1, at assets/js/api-search/index.js

363 lines 7.0 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 searchedTerm: '',
109 totalResults: 0,
110 });
111
112 /**
113 * Create state ref.
114 *
115 * Helps to avoid dependency hell.
116 */
117 const stateRef = useRef(state);
118
119 stateRef.current = state;
120
121 /**
122 * Clear facet contraints.
123 *
124 * @returns {void}
125 */
126 const clearConstraints = useCallback(() => {
127 dispatch({ type: 'CLEAR_CONSTRAINTS' });
128 }, []);
129
130 /**
131 * Clear search resu;ts.
132 *
133 * @returns {void}
134 */
135 const clearResults = useCallback(() => {
136 dispatch({ type: 'CLEAR_RESULTS' });
137 }, []);
138
139 /**
140 * Update the search query args, triggering a search.
141 *
142 * @param {object} args Search args.
143 * @returns {void}
144 */
145 const search = useCallback((args) => {
146 dispatch({ type: 'SEARCH', args });
147 }, []);
148
149 /**
150 * Update the search term, triggering a search and resetting facet
151 * constraints.
152 *
153 * @param {string} searchTerm Search term.
154 * @returns {void}
155 */
156 const searchFor = (searchTerm) => {
157 dispatch({ type: 'SEARCH_FOR', searchTerm });
158 };
159
160 /**
161 * Set loading state.
162 *
163 * @param {boolean} isLoading Is loading?
164 * @returns {void}
165 */
166 const setIsLoading = (isLoading) => {
167 dispatch({ type: 'SET_IS_LOADING', isLoading });
168 };
169
170 /**
171 * Set search results based on an Elasticsearch response.
172 *
173 * @param {object} response Elasticsearch response.
174 * @returns {void}
175 */
176 const setResults = (response) => {
177 dispatch({ type: 'SET_RESULTS', response });
178 };
179
180 /**
181 * Go to the next page of search results.
182 *
183 * @returns {void}
184 */
185 const nextPage = () => {
186 dispatch({ type: 'NEXT_PAGE' });
187 };
188
189 /**
190 * Go to the previous page of search results.
191 *
192 * @returns {void}
193 */
194 const previousPage = () => {
195 dispatch({ type: 'PREVIOUS_PAGE' });
196 };
197
198 /**
199 * Set search args from popped history state.
200 *
201 * @param {object} args Search args.
202 */
203 const popState = (args) => {
204 dispatch({ type: 'POP_STATE', args });
205 };
206
207 /**
208 * Turn off the provider.
209 *
210 * @returns {void}
211 */
212 const turnOff = () => {
213 dispatch({ type: 'TURN_OFF' });
214 };
215
216 /**
217 * Push search args to browser history.
218 *
219 * @returns {void}
220 */
221 const pushState = useCallback(() => {
222 if (typeof paramPrefix === 'undefined') {
223 return;
224 }
225
226 const { args, isOn } = stateRef.current;
227 const state = { args, isOn };
228
229 if (window.history.state) {
230 if (isOn) {
231 const params = getUrlParamsFromArgs(args, argsSchema, paramPrefix);
232 const url = getUrlWithParams(paramPrefix, params);
233
234 window.history.pushState(state, document.title, url);
235 } else {
236 const url = getUrlWithParams(paramPrefix);
237
238 window.history.pushState(state, document.title, url);
239 }
240 } else {
241 window.history.replaceState(state, document.title, window.location.href);
242 }
243 }, [argsSchema, paramPrefix]);
244
245 /**
246 * Handle popstate event.
247 *
248 * @param {Event} event popstate event.
249 */
250 const onPopState = useCallback(
251 (event) => {
252 if (typeof paramPrefix === 'undefined') {
253 return;
254 }
255
256 const hasState = event.state && Object.keys(event.state).length > 0;
257
258 if (hasState) {
259 popState(event.state);
260 }
261 },
262 [paramPrefix],
263 );
264
265 /**
266 * Handle initialization.
267 *
268 * @returns {Function} A cleanup function.
269 */
270 const handleInit = useCallback(() => {
271 window.addEventListener('popstate', onPopState);
272
273 return () => {
274 window.removeEventListener('popstate', onPopState);
275 };
276 }, [onPopState]);
277
278 /**
279 * Handle a change to search args.
280 *
281 * @returns {void}
282 */
283 const handleSearch = useCallback(() => {
284 const handle = async () => {
285 const { args, isOn, isPoppingState } = stateRef.current;
286
287 if (!isPoppingState) {
288 pushState();
289 }
290
291 if (!isOn) {
292 return;
293 }
294
295 const urlParams = getUrlParamsFromArgs(args, argsSchema);
296
297 setIsLoading(true);
298
299 const response = await fetchResults(urlParams);
300
301 if (!response) {
302 return;
303 }
304
305 setResults(response);
306 setIsLoading(false);
307 };
308
309 handle();
310 }, [argsSchema, fetchResults, pushState]);
311
312 /**
313 * Effects.
314 */
315 useEffect(handleInit, [handleInit]);
316 useEffect(handleSearch, [
317 handleSearch,
318 state.args,
319 state.args.orderby,
320 state.args.order,
321 state.args.offset,
322 state.args.search,
323 ]);
324
325 /**
326 * Provide state to context.
327 */
328 const { aggregations, args, isLoading, isOn, searchResults, searchTerm, totalResults } =
329 stateRef.current;
330
331 // eslint-disable-next-line react/jsx-no-constructed-context-values
332 const contextValue = {
333 aggregations,
334 args,
335 clearConstraints,
336 clearResults,
337 getUrlParamsFromArgs,
338 getUrlWithParams,
339 isLoading,
340 isOn,
341 searchResults,
342 searchTerm,
343 search,
344 searchFor,
345 setResults,
346 nextPage,
347 previousPage,
348 totalResults,
349 turnOff,
350 };
351
352 return <Context.Provider value={contextValue}>{children}</Context.Provider>;
353 };
354
355 /**
356 * Use the API Search context.
357 *
358 * @returns {object} API Search Context.
359 */
360 export const useApiSearch = () => {
361 return useContext(Context);
362 };
363