Source: components/instant-search/instant-search-element/search-input.js

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;