/** * External dependencies. */ import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; /** * WordPress dependencies. */ import apiFetch from '@wordpress/api-fetch'; import { Component, Fragment } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies. */ import { pluck, debounce } from '../utils/helpers'; apiFetch.use(apiFetch.createRootURLMiddleware(window.epOrdering.restApiRoot)); export class Pointers extends Component { titleInput = null; debouncedDefaultResults = debounce(() => { this.getDefaultResults(); }, 200); doSearch = debounce(() => { const { searchText, searchResults } = this.state; const searchTerm = searchText; // Set loading state searchResults[searchTerm] = false; this.setState({ searchResults }); apiFetch({ path: `/elasticpress/v1/pointer_search?s=${searchTerm}`, }).then((result) => { searchResults[searchTerm] = result; this.setState({ searchResults }); }); }, 200); debouncedHandleTitleChange = debounce(() => { this.handleTitleChange(); }, 200); /** * Initializes the component with initial state set by WP * * @param {object} props Component props */ constructor(props) { super(props); // We need to know the title of the page and react to changes since this is the query we search for this.titleInput = document.getElementById('title'); this.state = { pointers: window.epOrdering.pointers, posts: window.epOrdering.posts, title: this.titleInput.value, defaultResults: {}, searchText: '', searchResults: {}, }; } componentDidMount() { this.titleInput.addEventListener('keyup', this.debouncedHandleTitleChange); const { title } = this.state; if (title?.length > 0) { this.getDefaultResults(); } } componentWillUnmount() { this.titleInput.removeEventListener('keyup', this.debouncedHandleTitleChange); } handleTitleChange = () => { this.setState({ title: this.titleInput.value }); this.debouncedDefaultResults(); }; getDefaultResults = () => { const { title: searchTerm } = this.state; apiFetch({ path: `/elasticpress/v1/pointer_preview?s=${searchTerm}`, }).then((result) => { const { defaultResults } = this.state; defaultResults[searchTerm] = result; this.setState({ defaultResults }); }); }; removePointer = (pointer) => { let { pointers } = this.state; delete pointers[pointers.indexOf(pointer)]; pointers = pointers.filter((item) => item !== null); this.setState({ pointers }); }; getMergedPosts = () => { let { pointers } = this.state; const { title, defaultResults } = this.state; let merged = defaultResults[title].slice(); const setIds = {}; merged.forEach((item) => { setIds[item.ID] = item; }); pointers = pointers.sort((a, b) => { return a.order > b.order ? 1 : -1; }); pointers.forEach((pointer) => { // Remove the original if a duplicate if (setIds[pointer.ID]) { delete merged[merged.indexOf(setIds[pointer.ID])]; merged = merged.filter((item) => item); } // Insert into proper location merged.splice(parseInt(pointer.order, 10) - 1, 0, pointer); }); return merged; }; /** * Gets the next available position for a pointer * * @returns {number|false} The available position */ getNextAvailablePosition = () => { const { pointers } = this.state; const availablePositions = {}; for (let i = 1; i <= window.epOrdering.postsPerPage; i++) { availablePositions[i] = true; } pointers.forEach((item) => { delete availablePositions[item.order]; }); const keys = Object.keys(availablePositions); if (keys.length === 0) { return false; } return parseInt(keys[0], 10); }; /** * Adds a new pointer. We place the new pointer at the highest available position * * @param {object} post Post object */ addPointer = (post) => { const id = post.ID; const { posts, pointers } = this.state; if (!posts[id]) { posts[id] = post; this.setState({ posts }); } const position = this.getNextAvailablePosition(); if (!position) { /* eslint-disable no-alert */ window.alert( __('You have added the maximum number of custom results.', 'elasticpress'), ); /* eslint-enable no-alert */ return; } pointers.push({ ID: id, order: position, }); this.setState({ pointers }); }; /** * Callback when drag/drop is complete. * * Only the pointers are able to be dragged around, so all we need to do is increase any pointer by one that is * either at the current position or greater * * @param {object} result Dragged object */ onDragComplete = (result) => { // dropped outside the list if (!result.destination) { return; } const items = this.getMergedPosts(); // Offsetting indexes when over posts per page to account for the non-sortable notice const ppp = parseInt(window.epOrdering.postsPerPage, 10); const startIndex = result.source.index >= ppp ? result.source.index - 1 : result.source.index; const endIndex = result.destination.index > ppp ? result.destination.index - 1 : result.destination.index; const [removed] = items.splice(startIndex, 1); items.splice(endIndex, 0, removed); // Now _all_ the items are in order - grab the pointers and set the new positions to state const pointers = []; items.forEach((item, index) => { if (item.order) { // Reordering an existing pointer pointers.push({ ID: item.ID, order: index + 1, }); } else if (item.ID === result.draggableId) { // Adding a default post to the pointers array pointers.push({ ID: item.ID, order: index + 1, }); } }); this.setState({ pointers }); }; searchResults = (searchResults) => { const { searchText } = this.state; if (searchText === '') { return null; } if (searchResults === false) { return (
Loading...
); } if (searchResults.length === 0) { return
{__('No results found.', 'elasticpress')}
; } return searchResults.map((result) => { return (
{result.post_title} { event.preventDefault(); this.addPointer(result); }} onKeyDown={(event) => { event.preventDefault(); this.addPointer(result); }} > {__('Add Post', 'elasticpress')}
); }); }; /** * Renders the component * * @returns {*} The component */ render() { const { posts, defaultResults, title, pointers, searchText, searchResults: searchResultsFromState, } = this.state; if (title.length === 0) { return (

{__( 'Enter your search query above to preview the results.', 'elasticpress', )}

); } if (!defaultResults[title]) { return (
{__('Loading Result Preview…', 'elasticpress')}
); } // We need to reference these by ID later const defaultResultsById = {}; defaultResults[title].forEach((item) => { defaultResultsById[item.ID] = item; }); const mergedPosts = this.getMergedPosts(); const renderedIds = pluck(pointers, 'ID'); const searchResults = searchResultsFromState[searchText] ? searchResultsFromState[searchText].filter( (item) => renderedIds.indexOf(item.ID) === -1, ) : false; return (
{(provided) => (
{mergedPosts.map((item, index) => { const draggableIndex = parseInt(window.epOrdering.postsPerPage, 10) <= index ? index + 1 : index; let { title } = item; if (undefined === title) { title = undefined !== posts[item.ID] ? posts[item.ID].post_title : defaultResultsById[item.ID].post_title; } // Determine if this result is part of default search results or not const isDefaultResult = undefined !== defaultResultsById[item.ID]; const tooltipText = isDefaultResult === true ? __('Return to original position', 'elasticpress') : __( 'Remove custom result from results list', 'elasticpress', ); return ( {parseInt(window.epOrdering.postsPerPage, 10) === index && ( {(component) => (
{__( 'The following posts have been displaced to the next page of search results.', 'elasticpress', )}
)}
)} {(provided2) => (
{item.order && isDefaultResult === true && ( RD )} {item.order && isDefaultResult === false && ( CR )} {title}
{item.order && ( { event.preventDefault(); this.removePointer(item); }} onKeyDown={(event) => { event.preventDefault(); this.removePointer(item); }} > Remove Post )}
)}
); })} {provided.placeholder}
)}
CR {__('Custom Result (manually added to list)', 'elasticpress')}
RD {__( 'Reordered Default (originally in results, but repositioned)', 'elasticpress', )}

{__('Add to results', 'elasticpress')}

{ this.setState({ searchText: e.target.value }); this.doSearch(); }} />
{this.searchResults(searchResults)}
); } }