Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .wp-env.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
"wp-content/plugins/sync-error.php": "./tests/cypress/wordpress-files/test-plugins/sync-error.php",
"wp-content/plugins/unsupported-server-software.php": "./tests/cypress/wordpress-files/test-plugins/unsupported-server-software.php",
"wp-content/plugins/unsupported-elasticsearch-version.php": "./tests/cypress/wordpress-files/test-plugins/unsupported-elasticsearch-version.php",
"wp-content/uploads/content-example.xml": "./tests/cypress/wordpress-files/test-docs/content-example.xml"
"wp-content/uploads/content-example.xml": "./tests/cypress/wordpress-files/test-docs/content-example.xml",
"wp-content/plugins/autosuggestv2-proxy.php": "./tests/cypress/wordpress-files/test-plugins/autosuggestv2-proxy.php",
"wp-content/plugins/autosuggestv2-proxy-plugin.php": "./tests/cypress/wordpress-files/test-plugins/autosuggestv2-proxy-plugin.php"
}
}
}
Expand Down
49 changes: 49 additions & 0 deletions assets/css/autosuggest-v2.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
@import "./global/colors.css";

.ep-autosuggest-wrapper {
position: relative;

& .ep-autosuggest {
background: var(--ep-c-white);
border: 1px solid var(--ep-c-white-gray);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
width: 100%;
z-index: 200;

& ul {
list-style: none;
margin: 0 !important;

& li {
font-family: sans-serif;

& a {
color: var(--ep-c-black);
cursor: pointer;
display: block;
font-size: 14px;
font-weight: 700;
padding: 2px 10px;
text-decoration: none;
transition: background-color 0.15s, color 0.15s;

&:hover,
&:active {
background-color: var(--ep-c-medium-white);
text-decoration: none;
}
}
}
}
}

& .selected {
background-color: var(--ep-c-medium-white);
text-decoration: none;
}
}

.ep-autosuggest-dropdown-container {
position: absolute;
width: 100%;
}
46 changes: 29 additions & 17 deletions assets/js/api-search/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
getUrlWithParams,
} from './src/utilities';

import { applyResultsFilter } from '../autosuggest-v2/hooks';

/**
* Instant Results context.
*/
Expand All @@ -42,6 +44,7 @@ const Context = createContext();
* @param {WPElement} props.children Component children.
* @param {string} props.paramPrefix Prefix used to set and parse URL parameters.
* @param {Function} props.onAuthError Function to run when request authentication fails.
* @param {boolean} props.useUrlParams Whether to use URL parameters for state (default true).
* @returns {WPElement} Component.
*/
export const ApiSearchProvider = ({
Expand All @@ -53,17 +56,18 @@ export const ApiSearchProvider = ({
children,
paramPrefix,
onAuthError,
useUrlParams = true,
}) => {
/**
* Any default args from the URL.
*/
const defaultArgsFromUrl = useMemo(() => {
if (!paramPrefix) {
if (!paramPrefix || !useUrlParams) {
return {};
}

return getArgsFromUrlParams(argsSchema, paramPrefix);
}, [argsSchema, paramPrefix]);
}, [argsSchema, paramPrefix, useUrlParams]);

/**
* All default args including defaults from the schema.
Expand All @@ -81,8 +85,8 @@ export const ApiSearchProvider = ({
* Whether the provider is "on" by default.
*/
const defaultIsOn = useMemo(() => {
return Object.keys(defaultArgsFromUrl).length > 0;
}, [defaultArgsFromUrl]);
return useUrlParams ? Object.keys(defaultArgsFromUrl).length > 0 : false;
}, [defaultArgsFromUrl, useUrlParams]);

/**
* Set up fetch method.
Expand Down Expand Up @@ -222,7 +226,7 @@ export const ApiSearchProvider = ({
* @returns {void}
*/
const pushState = useCallback(() => {
if (typeof paramPrefix === 'undefined') {
if (typeof paramPrefix === 'undefined' || !useUrlParams) {
return;
}

Expand All @@ -243,7 +247,7 @@ export const ApiSearchProvider = ({
} else {
window.history.replaceState(state, document.title, window.location.href);
}
}, [argsSchema, paramPrefix]);
}, [argsSchema, paramPrefix, useUrlParams]);

/**
* Handle popstate event.
Expand All @@ -252,7 +256,7 @@ export const ApiSearchProvider = ({
*/
const onPopState = useCallback(
(event) => {
if (typeof paramPrefix === 'undefined') {
if (typeof paramPrefix === 'undefined' || !useUrlParams) {
return;
}

Expand All @@ -262,7 +266,7 @@ export const ApiSearchProvider = ({
popState(event.state);
}
},
[paramPrefix],
[paramPrefix, useUrlParams],
);

/**
Expand All @@ -271,12 +275,14 @@ export const ApiSearchProvider = ({
* @returns {Function} A cleanup function.
*/
const handleInit = useCallback(() => {
window.addEventListener('popstate', onPopState);

return () => {
window.removeEventListener('popstate', onPopState);
};
}, [onPopState]);
if (useUrlParams) {
window.addEventListener('popstate', onPopState);
return () => {
window.removeEventListener('popstate', onPopState);
};
}
return () => {};
}, [onPopState, useUrlParams]);

/**
* Handle a change to search args.
Expand All @@ -285,9 +291,9 @@ export const ApiSearchProvider = ({
*/
const handleSearch = useCallback(() => {
const handle = async () => {
const { args, isOn, isPoppingState } = stateRef.current;
const { args, isOn, isPoppingState, searchTerm } = stateRef.current;

if (!isPoppingState) {
if (!isPoppingState && useUrlParams) {
pushState();
}

Expand All @@ -306,6 +312,11 @@ export const ApiSearchProvider = ({
return;
}

// Apply filters to search results if hooks are available
if (response.hits && response.hits.hits && !useUrlParams) {
response.hits.hits = applyResultsFilter(response.hits.hits, searchTerm);
}

setResults(response);
} catch (e) {
const errorMessage = sprintf(
Expand All @@ -321,7 +332,7 @@ export const ApiSearchProvider = ({
};

handle();
}, [argsSchema, fetchResults, pushState]);
}, [argsSchema, fetchResults, pushState, useUrlParams]);

/**
* Effects.
Expand Down Expand Up @@ -372,6 +383,7 @@ export const ApiSearchProvider = ({
turnOff,
suggestedTerms,
isFirstSearch,
useUrlParams,
};

return <Context.Provider value={contextValue}>{children}</Context.Provider>;
Expand Down
59 changes: 59 additions & 0 deletions assets/js/autosuggest-v2/components/AutosuggestUI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useState } from 'react';
import { useApiSearch } from '../../api-search';
import SuggestionItem from './SuggestionItem'; // Original SuggestionItem component
import SuggestionList from './SuggestionList';
import { useKeyboardInput } from '../hooks';

const AutosuggestUI = ({ inputEl, minLength = 2 }) => {
const { searchResults, searchFor } = useApiSearch();

const [inputValue, setInputValue] = useState('');
const [activeIndex, setActiveIndex] = useState(-1);
const [show, setShow] = useState(false);

const suggestions = (searchResults || []).map((hit) => ({
id: hit._source.ID,
title: hit._source.post_title,
url: hit._source.permalink,
type: hit._source.post_type,
thumbnail: typeof hit._source.thumbnail === 'string' ? hit._source.thumbnail : null,
category: hit._source.category || null,
_source: hit._source,
}));

useKeyboardInput({
inputEl,
minLength,
show,
setShow,
inputValue,
setInputValue,
suggestions,
activeIndex,
setActiveIndex,
searchFor,
});

const handleItemClick = (idx) => {
if (typeof wp !== 'undefined' && wp.hooks) {
wp.hooks.doAction('ep.Autosuggest.onItemClick', suggestions[idx], idx, inputValue);
}
window.location.href = suggestions[idx].url;
setShow(false);
};

const suggestionListProps = {
suggestions,
activeIndex,
onItemClick: handleItemClick,
SuggestionItemTemplate: SuggestionItem,
};

return (
<div className="ep-autosuggest">
{show && suggestions.length > 0 && <SuggestionList {...suggestionListProps} />}
</div>
);
};

export default AutosuggestUI;
38 changes: 38 additions & 0 deletions assets/js/autosuggest-v2/components/SuggestionItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { applySuggestionItemFilter } from '../hooks';

const SuggestionItem = ({ suggestion, isActive, onClick }) => {
const filteredProps = applySuggestionItemFilter(suggestion, isActive, onClick);

if (filteredProps.renderSuggestion) {
return filteredProps.renderSuggestion();
}

const {
suggestion: filteredSuggestion,
isActive: filteredIsActive,
onClick: filteredOnClick,
} = filteredProps;

return (
<li
className={`autosuggest-item${filteredIsActive ? ' selected' : ''}`}
role="option"
aria-selected={filteredIsActive}
id={`autosuggest-option-${filteredSuggestion.id}`}
onMouseDown={filteredOnClick}
tabIndex={-1}
>
<a href={filteredSuggestion.url}>
{filteredSuggestion.thumbnail && (
<img src={filteredSuggestion.thumbnail} alt="" className="autosuggest-thumb" />
)}
<span className="autosuggest-title">{filteredSuggestion.title}</span>
{filteredSuggestion.category && (
<span className="autosuggest-category">{filteredSuggestion.category}</span>
)}
</a>
</li>
);
};

export default SuggestionItem;
30 changes: 30 additions & 0 deletions assets/js/autosuggest-v2/components/SuggestionList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { applySuggestionListFilter } from '../hooks';

const SuggestionList = (props) => {
const filteredProps = applySuggestionListFilter(props);
if (filteredProps.renderSuggestionList) {
return filteredProps.renderSuggestionList();
}

const { suggestions, activeIndex, onItemClick, SuggestionItemTemplate } = filteredProps;

return (
<div className="ep-autosuggest-list-wrapper">
<div className="ep-autosuggest-header">
{window.epasI18n?.searchIn && <span>{window.epasI18n?.searchIn}</span>}
</div>
<ul className="autosuggest-list" role="listbox">
{suggestions.map((suggestion, idx) => (
<SuggestionItemTemplate
key={suggestion.id}
suggestion={suggestion}
isActive={idx === activeIndex}
onClick={() => onItemClick(idx)}
/>
))}
</ul>
</div>
);
};

export default SuggestionList;
Loading