docbrown/app/javascript/Search/Search.jsx
Nick Taylor df31a94f2e
Prep work to sync mobile search in #10424 (#11525)
* Prep work to sync mobile search in #10424

* Added a comment about the custom search event.

* Refactor to fix already defined error caused by const and InstantClick.

* Moved <SearchFormSync /> out of the pack file.

* Fixed broken tests.

* Renamed getInitialSearchTerm utility function to getSearchTermFromUrl

* Added some API docs to getSearchTermFromUrl

* Fixed a typo.

* Added some more API docs.

* Switched to a custom event so a custom payload can be passed.

* Added some API docs.

* Fixed bad destructuring statement.

* wip for some more tests.

* Refactored getSearchTermFromUrl to use URLSearchParams

* lazy loaded state for search term initial value.

* test broken still. Need to think on it.

* filterXSS is now set up in testSetup.js

* Fixed tests.

* Added the @utilities alias to Storybooks webpack config.

* Almost 100% coverage with two useful tests.

* Added some comments explaining document.body usage in the tests.

* Fixed issue where search attemps to get results on first load.

* Small refactor for setting the window.location in tests.

* Clarified an explanation in a comment.
2020-11-30 06:13:06 -05:00

149 lines
3.9 KiB
JavaScript

import { h, Component, Fragment, createRef } from 'preact';
import PropTypes from 'prop-types';
import {
displaySearchResults,
getSearchTermFromUrl,
hasInstantClick,
preloadSearchResults,
} from '../utilities/search';
import { KeyboardShortcuts } from '../shared/components/useKeyboardShortcuts';
import { SearchForm } from './SearchForm';
const GLOBAL_MINIMIZE_KEY = 'Digit0';
const GLOBAL_SEARCH_KEY = 'Slash';
const ENTER_KEY = 'Enter';
export class Search extends Component {
constructor(props) {
super(props);
this.enableSearchPageChecker = true;
this.syncSearchUrlWithInput = this.syncSearchUrlWithInput.bind(this);
this.searchInputRef = createRef(null);
}
componentWillMount() {
const { searchTerm, setSearchTerm } = this.props;
const searchPageChecker = () => {
if (
this.enableSearchPageChecker &&
searchTerm !== '' &&
/^http(s)?:\/\/[^/]+\/search/.exec(window.location.href) === null
) {
setSearchTerm('');
}
setTimeout(searchPageChecker, 500);
};
searchPageChecker();
}
/**
* Synchronizes the search input value with the search term defined in the URL.
*/
syncSearchUrlWithInput() {
// TODO: Consolidate search functionality.
// Note that push states for search occur in _search.html.erb
// in initializeSortingTabs(query)
const { setSearchTerm } = this.props;
const searchTerm = getSearchTermFromUrl(window.location.search);
// We set the value outside of React state so that there is no flickering of placeholder
// to search term.
const searchBox = this.searchInputRef.current;
searchBox.value = searchTerm;
// Even though we set the search term directly via the DOM, it still needs to reside
// in component state.
setSearchTerm(searchTerm);
}
componentDidMount() {
InstantClick.on('change', this.enableSearchPageListener);
window.addEventListener('popstate', this.syncSearchUrlWithInput);
}
enableSearchPageListener = () => {
this.enableSearchPageChecker = true;
};
hasKeyModifiers = (event) => {
return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
};
submit = (event) => {
if (hasInstantClick) {
event.preventDefault();
}
};
search(key, searchTerm) {
const { searchTerm: currentSearchTerm } = this.props;
this.enableSearchPageChecker = false;
if (
hasInstantClick() &&
key === ENTER_KEY &&
currentSearchTerm !== searchTerm
) {
const { setSearchTerm } = this.props;
setSearchTerm(searchTerm);
preloadSearchResults({ searchTerm });
displaySearchResults({ searchTerm });
}
}
componentWillUnmount() {
document.removeEventListener('keydown', this.globalKeysListener);
window.removeEventListener('popstate', this.syncSearchUrlWithInput);
InstantClick.off &&
InstantClick.off('change', this.enableSearchPageListener);
}
minimizeHeader = (event) => {
event.preventDefault();
document.body.classList.toggle('zen-mode');
};
focusOnSearchBox = (event) => {
event.preventDefault();
document.body.classList.remove('zen-mode');
const searchBox = this.searchInputRef.current;
searchBox.focus();
searchBox.select();
};
render({ searchTerm }) {
return (
<Fragment>
<KeyboardShortcuts
shortcuts={{
[GLOBAL_SEARCH_KEY]: this.focusOnSearchBox,
[GLOBAL_MINIMIZE_KEY]: this.minimizeHeader,
}}
/>
<SearchForm
searchTerm={searchTerm}
onSearch={(event) => {
const {
key,
target: { value },
} = event;
this.search(key, value);
}}
onSubmitSearch={this.submit}
ref={this.searchInputRef}
/>
</Fragment>
);
}
}
Search.propTypes = {
searchTerm: PropTypes.string.isRequired,
setSearchTerm: PropTypes.func.isRequired,
};