UNPKG

addsearch-search-ui

Version:

JavaScript library to develop Search UIs for the web

291 lines (245 loc) 9.03 kB
/* 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()); } } }