import jQ from 'jquery';
import 'jquery-ui/ui/widgets/autocomplete';
import Settings from "../../../helpers/settings";
import Utils from "../../../helpers/utils";
import Globals from "../../../helpers/globals";
import Class from "../../../helpers/class";
import Labels from "../../../helpers/labels";
import BaseComponent from "../../base-component";
import InstantSearchApi from "../../../api/instant-search-api";
import InstantSearchStyle from "../instant-search-style/instant-search-style";
import InstantSearchResultRedirect from "../others/instant-search-result-redirect";
import AutocompleteMenuCustom from "../others/autocomplete-menu-custom";
/**
* Search input compoment
* @extends BaseComponent
*/
class SearchInput extends BaseComponent {
/**
* @constructs
* @param {String} id The element ID if search input: boost-pfs-search-box-0, boost-pfs-search-box-1, ..., boost-pfs-search-box-n
* @param {Object} $inputElement jQuery object of search input element
*/
constructor(id, $inputElement) {
super();
this.id = id;
this.autocomplete = null;
this.instantSearchResult = null;
this.isRendered = false;
this.isBoundEvents = false;
this.$element = $inputElement ? $inputElement : jQ('#' + this.id);
this.$searchForm = this.$element.closest('form');
this.$uiMenuElement = null;
};
/**
* Initialize the search input component
*/
init() {
this.instantSearchResult = InstantSearchStyle.instantSearchResult(this.id, this.$element);
this.addComponent(this.instantSearchResult);
};
/**
* Returns whether or not the input component is rendered
* @returns {boolean} TRUE or FALSE
*/
isRender() {
return !this.isRendered;
};
/**
* Render the search input
* @returns {Object} jQuery object
*/
render() {
var queryValue = Utils.getParam(Globals.searchTermKey);
this.$element.val(queryValue)
.addClass(Class.searchBox)
.attr('id', this.id)
.attr('data-search-box', this.id)
.attr('aria-live', 'assertive')
.attr('aria-label', Labels.suggestion.searchBoxPlaceholder)
.attr('placeholder', Labels.suggestion.searchBoxPlaceholder);
this.isRendered = true;
};
/**
* Returns whether or not the events are bind on search input
*/
isBindEvents() {
return !this.isBoundEvents;
};
/**
* Bind the events on search input
*/
bindEvents() {
// Apply autocomplete
this.$element.autocomplete({
appendTo: this.instantSearchResult.selector.wrapper,
minLength: Settings.getSettingValue('search.suggestionMinLength'),
delay: Settings.getSettingValue('search.suggestionDelay'),
classes: { 'ui-autocomplete': Class.searchSuggestion},
source: this._bindAutoCompleteSource.bind(this),
response: this._bindAutoCompleteResponse.bind(this),
// Disable default position of autocomplete
position: {
using: () => {
return false;
}
},
focus: this.onFocusAutocomplete.bind(this),
select: this.onSelectAutocomplete.bind(this),
open: this.onOpenAutocomplete.bind(this),
close: this.onCloseAutocomplete.bind(this)
});
this.autocomplete = this.$element.autocomplete('instance');
this.$uiMenuElement = this.autocomplete.menu.element;
// Custom the template of instant search result
this.autocomplete._renderMenu = this._bindAutoCompleteRenderMenu.bind(this);
// Resize instant search result dropdown
this.autocomplete._resizeMenu = this._bindAutoCompleteResizeMenu.bind(this);
// Custom some functions of Search menu widget
this.autocomplete = new AutocompleteMenuCustom(this.autocomplete);
// Bind events on search input
this.$element
.on('click', this._onClickSearchBox.bind(this))
.on('focus', this._onFocusSearchBox.bind(this))
.on('keyup', this._onTypeSearchBoxEvent.bind(this));
// Build search submit event
if (this.$searchForm.length) {
this.$searchForm.on('submit', this._onSubmit.bind(this));
}
this.isBoundEvents = true;
};
/**
* Prepare source for Autocomplete event
* @param {Object} request A request object, with a single term property
* @param {requestCallback} response The callback that handles the response
*/
_bindAutoCompleteSource(request, response) {
window.suggestionCallback = response;
Globals.currentTerm = request.term;
var term = (request.term).trim().replace(/\s+/g, ' ');
term = encodeURIComponent(term);
if (term != '') {
var $instantSearchResult = this.autocomplete.menu.element;
this.instantSearchResult.setData($instantSearchResult, null, true);
this.instantSearchResult.refresh();
if (term in Globals.suggestionCache) {
window.suggestionCallback(Globals.suggestionCache[term]);
return;
}
InstantSearchApi.getSuggestionData(term, 0, 'suggest');
}
};
/**
* Bind autocomplete response
* @param {Event} event The ui-autocomplete event
* @param {Object} autocompleteUi
* @param {Array} autocompleteUi.content List result data
*/
_bindAutoCompleteResponse(event, autocompleteUi) {
// Prepare data
var result = autocompleteUi.content;
var searchTerm = Utils.getValueInObjectArray('query', result);
var eventType = Utils.getValueInObjectArray('event_type', result);
var suggestQuery = Utils.getValueInObjectArray('suggest_query', result);
var localCache = Utils.getValueInObjectArray('local_cache', result);
var redirect = Utils.getValueInObjectArray('redirect', result);
// Cache
if (Object.keys(Globals.suggestionCache).length == 25) Globals.suggestionCache = {};
if (!(searchTerm in Globals.suggestionCache) && eventType != 'suggest_dym') {
Globals.suggestionCache[searchTerm] = result;
}
// Send another request to get the product list of suggest_query
if (suggestQuery != '' && eventType == 'suggest' && !localCache) {
InstantSearchApi.getSuggestionData(suggestQuery, 0, 'suggest_dym', searchTerm);
}
// Check for search redirect after receiving the API response
InstantSearchResultRedirect.checkForSearchRedirect(this.$element);
};
/**
* Render the result menu for autocomplete
* @param {Object} ulElement The result menu element object
* @param {Array} data The search result data
*/
_bindAutoCompleteRenderMenu(ulElement, data) {
this.instantSearchResult.setData(jQ(ulElement), data, false);
this.instantSearchResult.refresh();
};
/**
* Bind the event when resize the result menu
*/
_bindAutoCompleteResizeMenu() {
this.customizeInstantSearch();
}
/**
* Customize the result menu after resize; Use for customization
*/
customizeInstantSearch() {
// Override this method for customization
}
/**
* Bind the focus event on instant search result item
* @param {Event} event the ui-autocomplete event
* @param {Object} autocompleteUi the ui-autocomplete object
* @param {Object} autocompleteUi.widget The result menu element
* @param {Object} autocompleteUi.item The result item data
*/
onFocusAutocomplete(event, autocompleteUi) {
var widget = this.autocomplete.widget();
// TODO: return TRUE to replace search input value by the focus value
if (autocompleteUi.item && autocompleteUi.item['label'] !== undefined) {
return true;
} else {
return false;
}
}
/**
* Bind the open event on instant search result
* @param {Event} event The ui-autocomplete event
* @param {Object} autocompleteUi The ui-autocomplete object
*/
onOpenAutocomplete(event, autocompleteUi) {
// Prevent double tap on iOS
if (Utils.isiOS()) {
jQ('.' + Class.searchSuggestionItem + ' a')
.on('touchstart', () => {
this.isScrolling = false;
})
.on('touchmove', () => {
this.isScrolling = true;
})
.on('touchend', (touchEvent) => {
if (!this.isScrolling) {
window.location = jQ(touchEvent.currentTarget).attr('href');
}
});
}
// On mobile, prevent body from scrolling if it is full Instant search result style
if (Utils.InstantSearch.isFullWidthMobile() && !jQ('body').hasClass(Class.searchSuggestionMobileOpen)) {
jQ('body').addClass(Class.searchSuggestionMobileOpen);
}
this.instantSearchResult.$wrapper.addClass(Class.searchSuggestionOpen);
}
/**
* Bind the close event on instant search result
* @param {*} event The ui-autocomplete event
* @param {*} autocompleteUi The ui-autocomplete object
*/
onCloseAutocomplete(event, autocompleteUi) {
/**
* Test Mode - Turn on when need check elements of Auto Complete
* Prevent closing Auto complete when opening Inspect Element
*/
if (Settings.getSettingValue('search.suggestionMode') == 'test' || Utils.InstantSearch.isFullWidthMobile()) {
this.instantSearchResult.$instantSearchResult.show();
} else {
this.instantSearchResult.$instantSearchResult.siblings().hide();
}
this.instantSearchResult.$wrapper.removeClass(Class.searchSuggestionOpen);
}
/**
* Bind the select event on instant search result
* @param {Event} event The ui-autocomplete event
* @param {Object} autocompleteUi The ui-autocomplete object
*/
onSelectAutocomplete(event, autocompleteUi) {
var widget = this.autocomplete.widget();
var selectElement = widget.find('.' + Class.searchSuggestionItem + '.selected');
if (selectElement.length) {
var link = selectElement.find('> a');
if (link.length) {
Utils.setWindowLocation(link.eq(0).attr('href'));
}
}
return false;
}
/**
* Bind the click event on the search input
* @param {Event} event DOM event
*/
_onClickSearchBox(event) {
if (this.$element.val() != '') {
if (!Utils.InstantSearch.isFullWidthMobile()) {
if (this.$element.data('ui-autocomplete')) {
this.$element.autocomplete('search', this.$element.val());
}
}
}
}
/**
* Bind the focus event on the search input
* @param {Event} event DOM event
*/
_onFocusSearchBox(event) {}
/**
* Bind the typeahead event on the search input
* @param {Event} event DOM event
*/
_onTypeSearchBoxEvent(event) {
Globals.currentTerm = event.target.value;
}
/**
* Bind the submit event on the search input
* @param {Event} event DOM event
* @param {Boolean} redirect Set TRUE if want to redirect to search page / redirect page
*/
_onSubmit(event, redirect) {
if (!redirect) {
// Stop all default submit events
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
// Fix case can not get current value from input
Globals.currentTerm = this.$element.val();
if (!Globals.currentTerm && event && event.target) {
Globals.searchTerm = event.target.value;
}
var redirectUrl = InstantSearchResultRedirect.getSearchRedirectUrl();
/**
* if API returned results or the suggestionCache has the search term, submit search
* else: add the data-search-submit attribute to the search input box, wait for the api returns the result, then remove this attribute in the checkForSearchRedirect function
* @type {Boolean}
*/
var isApiReturnedResult = Globals.suggestionCache.hasOwnProperty(Globals.currentTerm.toString().trim());
// If the API returned results, either redirect or perform submit
if (isApiReturnedResult) {
if (redirectUrl) {
Utils.setWindowLocation(redirectUrl);
} else {
this.$searchForm.trigger('submit', [true]);
}
} else {
// If the API hasn't returned results, set data-search-submit on the search input and wait
this.$element.data('search-submit', true);
}
}
}
};
export default SearchInput;