Source: components/instant-search/instant-search-style/instant-search-result.js

import jQ from "jquery";

import Settings from "../../../helpers/settings";
import Utils from "../../../helpers/utils";
import Class from "../../../helpers/class";
import InstantSearchEnum from "../../../enum/instant-search-enum";
import BaseComponent from "../../base-component";
import InstantSearchResultBlock from "../instant-search-block/instant-search-result-block";
import InstantSearchResultBlockViewAll from "../instant-search-block/instant-search-result-block-view-all";
import InstantSearchResultBlockLoading from "../instant-search-block/instant-search-result-block-loading";
import InstantSearchResultBlockEmpty from "../instant-search-block/instant-search-result-block-empty";

/**
 * Instant search result
 * @extends BaseComponent
 */
class InstantSearchResult extends BaseComponent {
	/**
	 * @constructs
	 * @param {String} searchInputId The element ID of search input
	 * @param {Object} $searchInputElement The jQuery object of search input element
	 */
	constructor(searchInputId, $searchInputElement) {
		super();
		this.searchInputId = searchInputId;
		this.isLoading = false;
		this.isFirstLoad = true;
		this.blocks = [];
		this.loadingBlock = null;

		// Set selector
		var wrapperSelector = '.' + Class.searchSuggestionWrapper + '[data-search-box-id="' + this.searchInputId + '"]';
		this.selector = {
			wrapper: wrapperSelector,
			popover: wrapperSelector + ' .' + Class.searchSuggestion + '-popover',
			loading: wrapperSelector + ' .' + Class.searchSuggestion + '-loading',
		}
		
		this.$scriptInstantSearchNotFoundJson = jQ('#boost-pfs-instant-search-products-not-found-json');
		this.$searchInputElement = $searchInputElement;	
		this.$instantSearchResult = null;
		this.$wrapper = null;
		this.$popoverElement = null;
		this.$loadingElement = null;

		this.suggestionDirection = this._getSuggestionDirection();
		this.position = '';
		this.settings = {
			suggestionNoResult: Settings.getSettingValue('search.suggestionNoResult')
		};
	};

	/**
	 * Returns whether or not this instant search result style is used
	 */
	static isActive() {
		return true
	}

	/**
	 * Initialize the instant search result component
	 */
	init() {
		// Loading block
		this.loadingBlock = new InstantSearchResultBlockLoading();
		this.addComponent(this.loadingBlock);

		// Result block
		this.blockSettings = Settings.getSettingValue('search.suggestionBlocks');
		// Put the Product block to top for Style2
		this._applyFilterBlockSettings();
		
		this.blockSettings.forEach((blockSetting) => {
			var block = new InstantSearchResultBlock(blockSetting);
			this.addComponent(block);
			this.blocks.push(block);
		});

		// View all block
		var blockViewAll = new InstantSearchResultBlockViewAll();
		this.addComponent(blockViewAll);
		this.blockViewAll = blockViewAll;
		
		// Empty block
		var blockEmpty = new InstantSearchResultBlockEmpty();
		this.addComponent(blockEmpty);
		this.blockEmpty = blockEmpty;
	};

	/**
	 * Get the raw HTML template of instant search result
	 * @returns {String} Raw HTML string
	 */
	getTemplate() {
		return `
			<div class="{{class.searchSuggestionWrapper}}" data-search-box-id="{{searchInputId}}">
				<div class="{{class.searchSuggestion}}-popover" data-direction="{{suggestionDirection}}"></div>
			</div>
		`;
	};

	/**
	 * Replace the brackets in raw html template with proper values
	 * @returns {String} HTML string
	 */
	compileTemplate() {
		return this.getTemplate()
			.replace(/{{searchInputId}}/g, this.searchInputId)
			.replace(/{{suggestionDirection}}/g, this.suggestionDirection)
			.replace(/{{class.searchSuggestionWrapper}}/g, Class.searchSuggestionWrapper)
			.replace(/{{class.searchSuggestion}}/g, Class.searchSuggestion);
	};

	/**
	 * Sorting the blocks in result data
	 */
	_applyFilterBlockSettings() {}

	/**
	 * Render the result template to DOM
	 */
	render() {
		if (this.isFirstLoad) {
			// Render initial suggestion container and append it to the dom
			var suggestionHtml = this.compileTemplate();
			this.appendToSelector = 'body';
			this._applyFilterAutocompleteAppendElement();

			jQ(this.appendToSelector).append(suggestionHtml);
			this.$wrapper = jQ(this.selector.wrapper);
			this.$popoverElement = jQ(this.selector.popover);
			this.isFirstLoad = false;
		} else {
			// Show the suggestion popover
			this.$instantSearchResult.siblings().show();
			// Render suggestion blocks and items
			if (this.isLoading) {
				this._renderSuggestionLoading();
				this._renderWrapper();
			} else {
				this._renderWrapper();
				this._renderSuggestion();
			}
		}
	}

	/**
	 * Change the element selector where the instant search result append to
	 */
	_applyFilterAutocompleteAppendElement() {}

	/**
	 * Setup Classes, Position for Instant search wrapper
	 */
	_renderWrapper() {
		var mobileClass = Utils.InstantSearch.isFullWidthMobile() ? Class.searchSuggestionMobile : '';
		if (mobileClass !== '') {
			this.$wrapper.addClass(mobileClass);
		}

		// Set suggestion wrapper, popover, and ul position
		var suggestionPosition = this._setSuggestionPosition();
		suggestionPosition.setSuggetionPosition();
		suggestionPosition.setSuggetionPopoverPosition();
		
		// Set suggestion width
		var width = '';
		if (Settings.getSettingValue('search.suggestionWidth') == 'auto' || Utils.isMobile()) {
			width = this.$searchInputElement.outerWidth();
		} else {
			width = Settings.getSettingValue('search.suggestionWidth');
		}
		this.$wrapper.outerWidth(width);
	}

	/**
	 * Setup position for Instant search dropdown
	 */
	_setSuggestionPosition() {
		var topPosition = Utils.InstantSearch.isFullWidthMobile() ? 'top' : 'top+12';
		if (this.suggestionDirection == 'left') {
			this.$wrapper.position({
				my: 'left ' + topPosition,
				at: 'left bottom',
				of: this.$searchInputElement[0]
			});
			var setSuggetionPosition = () => {
				this.$instantSearchResult.position({
					my: 'left ' + topPosition,
					at: 'left bottom',
					of: this.$searchInputElement[0]
				})
			};
			var setSuggetionPopoverPosition = () => {
					this.$popoverElement.position({
					my: 'left+20 top-8' + topPosition,
					at: 'left bottom',
					of: this.$searchInputElement[0]
				})
			};
		} else {
			this.$wrapper.position({
				my: 'right ' + topPosition,
				at: 'right bottom',
				of: this.$searchInputElement[0]
			});
			var setSuggetionPosition = () => {
					this.$instantSearchResult.position({
					my: 'right ' + topPosition,
					at: 'right bottom',
					of: this.$searchInputElement[0]
				})
			};
			var setSuggetionPopoverPosition = () => {
					this.$popoverElement.position({
					my: 'right-20 top-8',
					at: 'right bottom',
					of: this.$searchInputElement[0]
				})
			};
		}

		return {
			setSuggetionPosition: setSuggetionPosition,
			setSuggetionPopoverPosition: setSuggetionPopoverPosition
		}
	}

	/**
	 * Render the result blocks into Instant search dropdown
	 */
	_renderSuggestion() {
		if (this.$instantSearchResult) {
			this.$instantSearchResult.attr('data-search-box', this.id);
		}
		if (this.data) {
			var suggesionChildElements = [];
			var isAllEmpty = Utils.getValueInObjectArray(InstantSearchEnum.ResultType.ALL_EMPTY, this.data);
			if (isAllEmpty) {
				// If have search redirect and no result, hide the search suggestion
				if (this.blockEmpty.$element) {
					suggesionChildElements.push(this.blockEmpty.$element);
					this.blocks.forEach((block) => {
						suggesionChildElements.push(block.$element);
					});
				} else {
					this.$wrapper.hide();
				}
			} else {
				this.blocks.forEach((block) => {
					suggesionChildElements.push(block.$element);
				});
				// Append view all block
				suggesionChildElements.push(this.blockViewAll.$element);
			}
			// Append to DOM
			this.$instantSearchResult.append(suggesionChildElements);
		}
	};

	/**
	 * Render Instant search - Loading block
	 */
	_renderSuggestionLoading() {
		// TODO: render loading block if it is enabled in settings and have not rendered yet
		if (this.loadingBlock.$element && !jQ(this.selector.loading).length) {
			// Hide all suggestion content
			this.$instantSearchResult.children().hide();
			// Add Loading to the first children of the suggestion content if it isn't existed
			this.$instantSearchResult.prepend(this.loadingBlock.$element);
			this.$loadingElement = jQ(this.selector.loading);
			// Show Loading
			this.$wrapper.addClass(Class.searchSuggestionOpen);
			this.$instantSearchResult.show();
			this.$loadingElement.show();
		}
	};

	/**
	 * Get direction of instant search result box
	 * @returns {String} Return left OR right 
	 */
	_getSuggestionDirection() {
		var suggestionPosition = Settings.getSettingValue('search.suggestionPosition');
		if (suggestionPosition != '') return suggestionPosition;
		var middleScreenPosition = jQ(window).width() / 2;
		var leftSearchBoxPosition = this.$searchInputElement.offset().left;
		return leftSearchBoxPosition < middleScreenPosition ? 'left' : 'right';
	};

	/**
	 * Setup data for Instant search box
	 * @param {Object} $instantSearchResult jQuery selector object of Intant search rerult element 
	 * @param {Object} data The Suggestion result data
	 * @param {Boolean} isLoading Render loading block OR result blocks
	 */
	setData($instantSearchResult, data, isLoading) {
		this.$instantSearchResult = $instantSearchResult;
		this.data = data;
		this.isLoading = isLoading;

		if (this.data) {
			// TODO: Set data for Suggestion blocks
			var isAllEmpty = Utils.getValueInObjectArray(InstantSearchEnum.ResultType.ALL_EMPTY, this.data);
			var instantSearchNotFoundJson = [];
			if (this.$scriptInstantSearchNotFoundJson.length) {
				instantSearchNotFoundJson = JSON.parse(this.$scriptInstantSearchNotFoundJson.html());
			}
			this.blocks.forEach((block) => {
				var blockData = Utils.getValueInObjectArray(block.type, this.data);
				// Set the popular data for products block and sugestion block if return not found result
				if (isAllEmpty) {
					if (block.type == InstantSearchEnum.ResultType.PRODUCTS 
						&& instantSearchNotFoundJson.hasOwnProperty('products')
						&& this.settings.suggestionNoResult.products.status
					) {
						this._preparePopularProducts(instantSearchNotFoundJson.products);
						blockData = instantSearchNotFoundJson.products;
						block.notFoundLabel = this.settings.suggestionNoResult.products.label;
					} else if (block.type == InstantSearchEnum.ResultType.SUGGESTIONS 
						&& instantSearchNotFoundJson.hasOwnProperty('search_terms')
						&& this.settings.suggestionNoResult.search_terms.status
					) {
						blockData = instantSearchNotFoundJson.search_terms;
						block.notFoundLabel = this.settings.suggestionNoResult.search_terms.label;
					}
				}
				
				block.setData(blockData, isAllEmpty);
			});

			// TODO: Set data for View all block
			this.blockEmpty.setData(this.data);

			// TODO: Set data for View all block
			this.blockViewAll.setData(this.data);
		}
	}

	/**
	 * Prepare the popular products for Not found block
	 * @param {Object} products - The popular product data
	 */
	_preparePopularProducts(products) {
		products.forEach((product) => {
			// Generate the images_info of product
			var images_info = [];
			product.media.forEach((media) => {
				if (media.media_type == 'image') {
					images_info.push({
						id: media.id,
						position: media.position,
						src: media.src,
						width: media.width,
						height: media.height
					});
				}
			});
			product.images_info = images_info;
			product.price /= 100;
			product.price_min /= 100;
			product.price_max /= 100;
			product.compare_at_price /= 100;
			product.compare_at_price_min /= 100;
			product.compare_at_price_max /= 100;
		});
		return products;
	}
}

export default InstantSearchResult;