addsearch-search-ui
Version:
JavaScript library to develop Search UIs for the web
291 lines (245 loc) • 9.03 kB
JavaScript
/* global window */
import './searchfield.scss';
import { SEARCHFIELD_TEMPLATE } from './templates';
import handlebars from 'handlebars';
import {
autocompleteHide,
autocompleteShow,
keyboardEvent,
setActiveSuggestion,
ARROW_DOWN,
ARROW_UP,
autocompleteHideAndDropRendering
} from '../../actions/autocomplete';
import { setPage } from '../../actions/pagination';
import { setKeyword } from '../../actions/keyword';
import { observeStoreByKey } from '../../store';
import { MATCH_ALL_QUERY, WARMUP_QUERY_PREFIX } from '../../index';
import { redirectToSearchResultsPage } from '../../util/history';
import { validateContainer } from '../../util/dom';
import {clearSelected, clearSelectedRangeFacets} from "../../actions/filters";
const KEYCODES = {
ARROW_DOWN: 40,
ARROW_UP: 38,
ENTER: 13,
BACKSPACE: 8,
DELETE: 46
};
export default class SearchField {
constructor(client, reduxStore, conf, matchAllQueryWhenSearchFieldEmpty, onSearch) {
this.client = client;
this.reduxStore = reduxStore;
this.conf = conf;
this.matchAllQuery = matchAllQueryWhenSearchFieldEmpty;
this.firstRenderDone = false;
this.firstSelectorBindDone = false;
this.onSearch = onSearch;
if (conf.selectorToBind) {
this.bindContainer();
observeStoreByKey(this.reduxStore, 'keyword', (kw) => {
if (kw.setSearchFieldValue) {
this.boundField.value = kw.value;
}
});
observeStoreByKey(this.reduxStore, 'autocomplete', (ac) => this.onAutocompleteUpdateBoundField(ac));
} else if (validateContainer(conf.containerId)) {
observeStoreByKey(this.reduxStore, 'keyword', (kw) => {
if (kw.searchFieldContainerId === this.conf.containerId || kw.searchFieldContainerId === null) {
this.render(kw.value);
}
});
observeStoreByKey(this.reduxStore, 'autocomplete', (ac) => this.onAutocompleteUpdate(ac));
}
}
onAutocompleteUpdate(state) {
if ((state.suggestions.length > 0 || state.customFields.length > 0) && state.setSuggestionToSearchField) {
// Set field value
if (state.activeSuggestionIndex !== null && state.setSuggestionToSearchField) {
const suggestionObj = state.suggestions[state.activeSuggestionIndex] || state.customFields[state.activeSuggestionIndex];
const suggestion = suggestionObj.value;
this.render(suggestion);
}
// Revert to original typed keyword
else if (state.activeSuggestionIndex === null) {
this.render(this.reduxStore.getState().keyword.value);
}
}
}
onAutocompleteUpdateBoundField(state) {
if (!state.setSuggestionToSearchField) {
return;
}
if (state.activeSuggestionIndex !== null) {
const suggestionObj = state.suggestions[state.activeSuggestionIndex] || state.customFields[state.activeSuggestionIndex];
if (!suggestionObj) return;
const suggestion = suggestionObj.value
this.boundField.value = suggestion;
} else {
this.boundField.value = this.reduxStore.getState().keyword.value;
}
}
executeSearch(client, keyword, searchAsYouType) {
if (!searchAsYouType) {
this.reduxStore.dispatch(autocompleteHideAndDropRendering());
}
let kw = keyword;
if (kw === '' && this.matchAllQuery) {
kw = MATCH_ALL_QUERY;
}
if (kw.indexOf(WARMUP_QUERY_PREFIX) !== 0) {
this.reduxStore.dispatch(setPage(client, 1, null, this.reduxStore));
}
this.reduxStore.dispatch(clearSelectedRangeFacets(false, true));
this.onSearch(kw, false, searchAsYouType, this.conf.fieldForInstantRedirect,
this.reduxStore.getState().configuration.fieldForInstantRedirect);
}
redirectOrSearch(keyword) {
const searchResultsPageUrl = this.reduxStore.getState().search.searchResultsPageUrl;
// Redirect to results page
if (searchResultsPageUrl &&
this.conf.ignoreSearchResultsPageUrl !== true &&
keyword && keyword.length > 0) {
redirectToSearchResultsPage(searchResultsPageUrl, keyword);
}
// Search
else {
this.executeSearch(this.client, keyword, false);
}
}
addEventListenersToField(field) {
field.oninput = (e) => this.oninput(e);
field.onkeypress = (e) => this.onkeypress(e);
field.onkeyup = (e) => this.onkeyup(e);
field.onfocus = (e) => this.onfocus(e);
field.onblur = (e) => setTimeout(() => this.onblur(), 200); // Possible search button onclick event first
}
handleAutoFocus(field) {
if (this.conf.autofocus !== false &&
this.firstRenderDone === false) {
field.focus();
this.firstRenderDone = true;
}
}
handleSubmitKeyword(keyword) {
const store = this.reduxStore;
if (keyword === '' && this.matchAllQuery) {
keyword = MATCH_ALL_QUERY;
}
store.dispatch(setKeyword(keyword, true, null, false));
store.dispatch(autocompleteHide());
this.redirectOrSearch(keyword);
}
render(preDefinedKeyword) {
const container = document.getElementById(this.conf.containerId);
// Field already exists. Don't re-render
if (container.querySelector('input')) {
if (preDefinedKeyword !== null &&
preDefinedKeyword !== MATCH_ALL_QUERY &&
container.querySelector('input').value !== preDefinedKeyword) {
container.querySelector('input').value = preDefinedKeyword;
}
return;
}
// New field. Render
container.innerHTML = handlebars.compile(this.conf.template || SEARCHFIELD_TEMPLATE)(this.conf);
this.field = container.querySelector('input');
// Set value. Don't pass with data to handlebars to get the keyboard caret position right on all browsers
if (preDefinedKeyword !== MATCH_ALL_QUERY) {
this.field.value = preDefinedKeyword;
}
// Event listeners to the field
this.addEventListenersToField(this.field);
// Event listeners to the possible search button
if (container.querySelector('button')) {
container.querySelector('button').onclick = () => {
let keyword = this.field.value;
this.handleSubmitKeyword(keyword);
}
}
// Event listeners to the form
if (container.querySelector('form')) {
container.querySelector('form').onsubmit = (e) => e.preventDefault();
}
// Autofocus when loaded first time
this.handleAutoFocus(this.field);
}
bindContainer() {
this.boundField = document.querySelector(this.conf.selectorToBind);
this.addEventListenersToField(this.boundField);
// Event listeners to the possible search button
if (this.conf.buttonSelector && document.querySelector(this.conf.buttonSelector)) {
const boundButton = document.querySelector(this.conf.buttonSelector);
if (boundButton.type === 'submit') {
boundButton.type = 'button';
}
boundButton.onclick = () => {
let keyword = this.boundField.value;
this.handleSubmitKeyword(keyword);
}
}
// Event listeners to the form
if (this.boundField.form) {
this.boundField.form.onsubmit = (e) => {
e.preventDefault();
}
}
// Autofocus when loaded first time
this.handleAutoFocus(this.boundField);
}
/**
* Input field events
*/
// Handle characters and backspace
oninput(e) {
const store = this.reduxStore;
let keyword = e.target.value;
if (keyword === '' && this.matchAllQuery) {
keyword = MATCH_ALL_QUERY;
}
// Keyword being erased
if (e.keyCode === KEYCODES.BACKSPACE || e.keyCode === KEYCODES.DELETE) {
store.dispatch(setActiveSuggestion(null, false));
}
const skipAutocomplete = this.conf.ignoreAutocomplete === true;
store.dispatch(setKeyword(keyword, skipAutocomplete, this.conf.containerId));
if (this.conf.searchAsYouType === true) {
this.executeSearch(this.client, keyword, true);
}
}
// Handle keyboard navi
onkeyup(e) {
const store = this.reduxStore;
if (e.keyCode === KEYCODES.ARROW_DOWN) {
store.dispatch(keyboardEvent(ARROW_DOWN));
}
else if (e.keyCode === KEYCODES.ARROW_UP) {
store.dispatch(keyboardEvent(ARROW_UP));
}
}
// Handle enter
onkeypress(e) {
if (e.keyCode === KEYCODES.ENTER) {
const keyword = e.target.value;
this.handleSubmitKeyword(keyword);
}
}
onfocus(e) {
if (e.target.value === '') {
// Execute match all query for autocomplete box
if (this.conf.onfocusAutocompleteMatchAllQuery) {
this.reduxStore.dispatch(setKeyword(MATCH_ALL_QUERY, false));
}
// Warmup query unless match all query is sent
else if (!this.warmupQueryCompleted && !this.matchAllQuery) {
this.executeSearch(this.client, WARMUP_QUERY_PREFIX + Math.random(), false);
this.warmupQueryCompleted = true;
}
}
this.reduxStore.dispatch(autocompleteShow());
}
onblur() {
if (this.reduxStore.getState().autocomplete.hideAutomatically) {
this.reduxStore.dispatch(autocompleteHide());
}
}
}