PluginProbe
ElasticPress / 4.2.2
ElasticPress v4.2.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 / ordering / pointers.js

pointers.js in ElasticPress 4.2.2, at assets/js/ordering/pointers.js

514 lines 12.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * External dependencies.
3 */
4 import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
5
6 /**
7 * WordPress dependencies.
8 */
9 import apiFetch from '@wordpress/api-fetch';
10 import { Component, Fragment } from '@wordpress/element';
11 import { __ } from '@wordpress/i18n';
12
13 /**
14 * Internal dependencies.
15 */
16 import { pluck, debounce } from '../utils/helpers';
17
18 apiFetch.use(apiFetch.createRootURLMiddleware(window.epOrdering.restApiRoot));
19
20 export class Pointers extends Component {
21 titleInput = null;
22
23 debouncedDefaultResults = debounce(() => {
24 this.getDefaultResults();
25 }, 200);
26
27 doSearch = debounce(() => {
28 const { searchText, searchResults } = this.state;
29 const searchTerm = searchText;
30
31 // Set loading state
32 searchResults[searchTerm] = false;
33 this.setState({ searchResults });
34
35 apiFetch({
36 path: `/elasticpress/v1/pointer_search?s=${searchTerm}`,
37 }).then((result) => {
38 searchResults[searchTerm] = result;
39
40 this.setState({ searchResults });
41 });
42 }, 200);
43
44 debouncedHandleTitleChange = debounce(() => {
45 this.handleTitleChange();
46 }, 200);
47
48 /**
49 * Initializes the component with initial state set by WP
50 *
51 * @param {object} props Component props
52 */
53 constructor(props) {
54 super(props);
55
56 // We need to know the title of the page and react to changes since this is the query we search for
57 this.titleInput = document.getElementById('title');
58
59 this.state = {
60 pointers: window.epOrdering.pointers,
61 posts: window.epOrdering.posts,
62 title: this.titleInput.value,
63 defaultResults: {},
64 searchText: '',
65 searchResults: {},
66 };
67 }
68
69 componentDidMount() {
70 this.titleInput.addEventListener('keyup', this.debouncedHandleTitleChange);
71
72 const { title } = this.state;
73
74 if (title?.length > 0) {
75 this.getDefaultResults();
76 }
77 }
78
79 componentWillUnmount() {
80 this.titleInput.removeEventListener('keyup', this.debouncedHandleTitleChange);
81 }
82
83 handleTitleChange = () => {
84 this.setState({ title: this.titleInput.value });
85 this.debouncedDefaultResults();
86 };
87
88 getDefaultResults = () => {
89 const { title: searchTerm } = this.state;
90
91 apiFetch({
92 path: `/elasticpress/v1/pointer_preview?s=${searchTerm}`,
93 }).then((result) => {
94 const { defaultResults } = this.state;
95
96 defaultResults[searchTerm] = result;
97
98 this.setState({ defaultResults });
99 });
100 };
101
102 removePointer = (pointer) => {
103 let { pointers } = this.state;
104
105 delete pointers[pointers.indexOf(pointer)];
106 pointers = pointers.filter((item) => item !== null);
107
108 this.setState({ pointers });
109 };
110
111 getMergedPosts = () => {
112 let { pointers } = this.state;
113 const { title, defaultResults } = this.state;
114 let merged = defaultResults[title].slice();
115
116 const setIds = {};
117 merged.forEach((item) => {
118 setIds[item.ID] = item;
119 });
120
121 pointers = pointers.sort((a, b) => {
122 return a.order > b.order ? 1 : -1;
123 });
124
125 pointers.forEach((pointer) => {
126 // Remove the original if a duplicate
127 if (setIds[pointer.ID]) {
128 delete merged[merged.indexOf(setIds[pointer.ID])];
129 merged = merged.filter((item) => item);
130 }
131
132 // Insert into proper location
133 merged.splice(parseInt(pointer.order, 10) - 1, 0, pointer);
134 });
135
136 return merged;
137 };
138
139 /**
140 * Gets the next available position for a pointer
141 *
142 * @returns {number|false} The available position
143 */
144 getNextAvailablePosition = () => {
145 const { pointers } = this.state;
146 const availablePositions = {};
147
148 for (let i = 1; i <= window.epOrdering.postsPerPage; i++) {
149 availablePositions[i] = true;
150 }
151
152 pointers.forEach((item) => {
153 delete availablePositions[item.order];
154 });
155
156 const keys = Object.keys(availablePositions);
157
158 if (keys.length === 0) {
159 return false;
160 }
161
162 return parseInt(keys[0], 10);
163 };
164
165 /**
166 * Adds a new pointer. We place the new pointer at the highest available position
167 *
168 * @param {object} post Post object
169 */
170 addPointer = (post) => {
171 const id = post.ID;
172 const { posts, pointers } = this.state;
173
174 if (!posts[id]) {
175 posts[id] = post;
176 this.setState({ posts });
177 }
178
179 const position = this.getNextAvailablePosition();
180
181 if (!position) {
182 /* eslint-disable no-alert */
183 window.alert(
184 __('You have added the maximum number of custom results.', 'elasticpress'),
185 );
186 /* eslint-enable no-alert */
187 return;
188 }
189
190 pointers.push({
191 ID: id,
192 order: position,
193 });
194
195 this.setState({ pointers });
196 };
197
198 /**
199 * Callback when drag/drop is complete.
200 *
201 * Only the pointers are able to be dragged around, so all we need to do is increase any pointer by one that is
202 * either at the current position or greater
203 *
204 * @param {object} result Dragged object
205 */
206 onDragComplete = (result) => {
207 // dropped outside the list
208 if (!result.destination) {
209 return;
210 }
211
212 const items = this.getMergedPosts();
213
214 // Offsetting indexes when over posts per page to account for the non-sortable notice
215 const ppp = parseInt(window.epOrdering.postsPerPage, 10);
216 const startIndex =
217 result.source.index >= ppp ? result.source.index - 1 : result.source.index;
218 const endIndex =
219 result.destination.index > ppp
220 ? result.destination.index - 1
221 : result.destination.index;
222
223 const [removed] = items.splice(startIndex, 1);
224 items.splice(endIndex, 0, removed);
225
226 // Now _all_ the items are in order - grab the pointers and set the new positions to state
227 const pointers = [];
228
229 items.forEach((item, index) => {
230 if (item.order) {
231 // Reordering an existing pointer
232 pointers.push({
233 ID: item.ID,
234 order: index + 1,
235 });
236 } else if (item.ID === result.draggableId) {
237 // Adding a default post to the pointers array
238 pointers.push({
239 ID: item.ID,
240 order: index + 1,
241 });
242 }
243 });
244
245 this.setState({ pointers });
246 };
247
248 searchResults = (searchResults) => {
249 const { searchText } = this.state;
250
251 if (searchText === '') {
252 return null;
253 }
254
255 if (searchResults === false) {
256 return (
257 <div className="loading">
258 <div className="spinner is-active" />
259 Loading...
260 </div>
261 );
262 }
263
264 if (searchResults.length === 0) {
265 return <div className="no-results">{__('No results found.', 'elasticpress')}</div>;
266 }
267
268 return searchResults.map((result) => {
269 return (
270 <div className="pointer-result" key={result.ID}>
271 <span className="title">{result.post_title}</span>
272 <span
273 role="button"
274 tabIndex="0"
275 className="dashicons dashicons-plus add-pointer"
276 onClick={(event) => {
277 event.preventDefault();
278 this.addPointer(result);
279 }}
280 onKeyDown={(event) => {
281 event.preventDefault();
282 this.addPointer(result);
283 }}
284 >
285 <span className="screen-reader-text">{__('Add Post', 'elasticpress')}</span>
286 </span>
287 </div>
288 );
289 });
290 };
291
292 /**
293 * Renders the component
294 *
295 * @returns {*} The component
296 */
297 render() {
298 const {
299 posts,
300 defaultResults,
301 title,
302 pointers,
303 searchText,
304 searchResults: searchResultsFromState,
305 } = this.state;
306
307 if (title.length === 0) {
308 return (
309 <div className="new-post">
310 <p>
311 {__(
312 'Enter your search query above to preview the results.',
313 'elasticpress',
314 )}
315 </p>
316 </div>
317 );
318 }
319
320 if (!defaultResults[title]) {
321 return (
322 <div className="loading">
323 <div className="spinner is-active" />
324 <span>{__('Loading Result Preview…', 'elasticpress')}</span>
325 </div>
326 );
327 }
328
329 // We need to reference these by ID later
330 const defaultResultsById = {};
331 defaultResults[title].forEach((item) => {
332 defaultResultsById[item.ID] = item;
333 });
334
335 const mergedPosts = this.getMergedPosts();
336 const renderedIds = pluck(pointers, 'ID');
337
338 const searchResults = searchResultsFromState[searchText]
339 ? searchResultsFromState[searchText].filter(
340 (item) => renderedIds.indexOf(item.ID) === -1,
341 )
342 : false;
343
344 return (
345 <div>
346 <input type="hidden" name="search-ordering-nonce" value={window.epOrdering.nonce} />
347 <input type="hidden" name="ordered_posts" value={JSON.stringify(pointers)} />
348 <DragDropContext onDragEnd={this.onDragComplete}>
349 <Droppable droppableId="droppable">
350 {(provided) => (
351 <div
352 className="pointers"
353 {...provided.droppableProps}
354 ref={provided.innerRef}
355 >
356 {mergedPosts.map((item, index) => {
357 const draggableIndex =
358 parseInt(window.epOrdering.postsPerPage, 10) <= index
359 ? index + 1
360 : index;
361
362 let { title } = item;
363 if (undefined === title) {
364 title =
365 undefined !== posts[item.ID]
366 ? posts[item.ID].post_title
367 : defaultResultsById[item.ID].post_title;
368 }
369
370 // Determine if this result is part of default search results or not
371 const isDefaultResult =
372 undefined !== defaultResultsById[item.ID];
373 const tooltipText =
374 isDefaultResult === true
375 ? __('Return to original position', 'elasticpress')
376 : __(
377 'Remove custom result from results list',
378 'elasticpress',
379 );
380
381 return (
382 <Fragment key={item.ID}>
383 {parseInt(window.epOrdering.postsPerPage, 10) ===
384 index && (
385 <Draggable
386 key="divider"
387 draggableId="divider"
388 index={index}
389 isDragDisabled={false}
390 >
391 {(component) => (
392 <div
393 className={`next-page-notice ${index}`}
394 ref={component.innerRef}
395 {...component.draggableProps}
396 {...component.dragHandleProps}
397 >
398 <span>
399 {__(
400 'The following posts have been displaced to the next page of search results.',
401 'elasticpress',
402 )}
403 </span>
404 </div>
405 )}
406 </Draggable>
407 )}
408
409 <Draggable
410 key={item.ID}
411 draggableId={item.ID}
412 index={draggableIndex}
413 >
414 {(provided2) => (
415 <div
416 className={`pointer ${draggableIndex}`}
417 ref={provided2.innerRef}
418 {...provided2.draggableProps}
419 >
420 {item.order && isDefaultResult === true && (
421 <span className="pointer-type">RD</span>
422 )}
423 {item.order &&
424 isDefaultResult === false && (
425 <span className="pointer-type">
426 CR
427 </span>
428 )}
429 <strong className="title">{title}</strong>
430 <div className="pointer-actions">
431 <span
432 className="dashicons dashicons-menu handle"
433 {...provided2.dragHandleProps}
434 title={__(
435 'Drag post up or down to reposition',
436 'elasticpress',
437 )}
438 />
439 {item.order && (
440 <span
441 role="button"
442 tabIndex="0"
443 title={tooltipText}
444 className="dashicons dashicons-undo delete-pointer"
445 onClick={(event) => {
446 event.preventDefault();
447 this.removePointer(item);
448 }}
449 onKeyDown={(event) => {
450 event.preventDefault();
451 this.removePointer(item);
452 }}
453 >
454 <span className="screen-reader-text">
455 Remove Post
456 </span>
457 </span>
458 )}
459 </div>
460 </div>
461 )}
462 </Draggable>
463 </Fragment>
464 );
465 })}
466 {provided.placeholder}
467 </div>
468 )}
469 </Droppable>
470 </DragDropContext>
471
472 <div className="legend">
473 <div className="legend-item">
474 <span className="pointer-type">CR</span>
475 <span className="type-description">
476 {__('Custom Result (manually added to list)', 'elasticpress')}
477 </span>
478 </div>
479 <div className="legend-item">
480 <span className="pointer-type">RD</span>
481 <span className="type-description">
482 {__(
483 'Reordered Default (originally in results, but repositioned)',
484 'elasticpress',
485 )}
486 </span>
487 </div>
488 </div>
489
490 <div className="pointer-search">
491 <h2 className="section-title">{__('Add to results', 'elasticpress')}</h2>
492
493 <div className="search-wrapper">
494 <div className="input-wrap">
495 <input
496 type="text"
497 className="widefat search-pointers"
498 placeholder="Search for Post"
499 value={searchText}
500 onChange={(e) => {
501 this.setState({ searchText: e.target.value });
502 this.doSearch();
503 }}
504 />
505 </div>
506
507 <div className="pointer-results">{this.searchResults(searchResults)}</div>
508 </div>
509 </div>
510 </div>
511 );
512 }
513 }
514