/** * Copyright since 2007 PrestaShop SA and Contributors * PrestaShop is an International Registered Trademark & Property of PrestaShop SA * * NOTICE OF LICENSE * * This source file is subject to the Open Software License (OSL 3.0) * that is bundled with this package in the file LICENSE.md. * It is also available through the world-wide-web at this URL: * https://opensource.org/licenses/OSL-3.0 * If you did not receive a copy of the license and are unable to * obtain it through the world-wide-web, please send an email * to license@prestashop.com so we can send you a copy immediately. * * DISCLAIMER * * Do not edit or add to this file if you wish to upgrade PrestaShop to newer * versions in the future. If you wish to customize PrestaShop for your * needs please refer to https://devdocs.prestashop.com/ for more information. * * @author PrestaShop SA and Contributors * @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) */ import AutoCompleteSearch from '@components/auto-complete-search'; import Bloodhound from 'typeahead.js'; /** * This component is used to search and select an entity, it is uses the AutoSearchComplete * component which displays a list of suggestion based on an API returned response. Then when * an element is selected it is added to the selection container and hidden inputs are created to * send an array of entity IDs in the form request. * * This component is used with TypeaheadType forms, and is tightly linked to the content of this * twig file src/PrestaShopBundle/Resources/views/Admin/TwigTemplateForm/typeahead.html.twig * * @todo: the component relies on this TypeaheadType because it was the historical type but it would be worth * creating a new clean form type with better templating (the tplcollection brings nearly no value as is) */ export default class EntitySearchInput { constructor($entitySearchInput, options) { this.$entitySearchInput = $entitySearchInput; this.entitySearchInputId = this.$entitySearchInput.prop('id'); this.$autoCompleteSearchContainer = this.$entitySearchInput.closest('.autocomplete-search'); this.$selectionContainer = $(`#${this.entitySearchInputId}-data`); this.searchInputFullName = this.$autoCompleteSearchContainer.data('fullname'); const inputOptions = options || {}; this.options = { value: 'id', dataLimit: 1, ...inputOptions, }; this.buildRemoteSource(); this.buildAutoCompleteSearch(); } /** * Change the remote url of the endpoint that returns suggestions. * * @param remoteUrl {string} */ setRemoteUrl(remoteUrl) { this.entityRemoteSource.remote.url = remoteUrl; } /** * Force selected values, the input is an array of object that must match the format from * the API if you want the selected entities to be correctly displayed. * * @param values {array} */ setValue(values) { this.clearSelectedItems(); if (!values || values.length <= 0) { return; } values.each((value) => { this.appendSelectedItem(value); }); } /** * Build the AutoCompleteSearch component */ buildAutoCompleteSearch() { const autoSearchConfig = { source: this.entityRemoteSource, dataLimit: this.options.dataLimit, templates: { suggestion: (entity) => { let entityImage; if (Object.prototype.hasOwnProperty.call(entity, 'image')) { entityImage = ` `; } return `
${entityImage}${entity.name}
`; }, }, onClose: (event) => { this.onSelectionClose(event); }, /* eslint-disable-next-line no-unused-vars */ onSelect: (selectedItem, event) => { // When limit is one we cannot select additional elements so we replace them instead if (this.options.dataLimit === 1) { return this.replaceSelectedItem(selectedItem); } return this.appendSelectedItem(selectedItem); }, }; // Can be used to format value depending on selected item if (this.options.value !== undefined) { autoSearchConfig.value = this.options.value; } this.autoSearch = new AutoCompleteSearch(this.$entitySearchInput, autoSearchConfig); } /** * Build the Bloodhound remote source which will call the API. The placeholder to * inject the query search parameter is __QUERY__ (@todo: could be configurable) * * @returns {Bloodhound} */ buildRemoteSource() { const sourceConfig = { mappingValue: this.$autoCompleteSearchContainer.data('mappingvalue'), remoteUrl: this.$autoCompleteSearchContainer.data('remoteurl'), }; this.entityRemoteSource = new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.whitespace, queryTokenizer: Bloodhound.tokenizers.whitespace, identify(obj) { return obj[sourceConfig.mappingValue]; }, remote: { url: sourceConfig.remoteUrl, cache: false, wildcard: '__QUERY__', transform(response) { if (!response) { return []; } return response; }, }, }); } /** * When an item is selected we empty the input search, since the selected data is stored in hidden inputs anyway * * @param event */ onSelectionClose(event) { $(event.target).val(''); } /** * Removes selected items. */ clearSelectedItems() { const formIdItem = $('li', this.$selectionContainer); formIdItem.remove(); } /** * When the component is configured to have only one selected element on each selection * the previous selection is removed and then replaced. * * @param selectedItem {Object} * @returns {boolean} */ replaceSelectedItem(selectedItem) { this.clearSelectedItems(); this.addSelectedContentToContainer(selectedItem); return true; } /** * When the component is configured to have more than one selected item on each selection * the item is added to the list. * * @param selectedItem {Object} * @returns {boolean} */ appendSelectedItem(selectedItem) { // If collection length is up to limit, return const formIdItem = $('li', this.$selectionContainer); if (this.options.dataLimit !== 0 && formIdItem.length >= this.options.dataLimit) { return false; } this.addSelectedContentToContainer(selectedItem); return true; } /** * Add the selected content to the selection container, the HTML is generated based on the render function * then a hidden input is automatically added inside it, and finally the rendered selection is added to the list. * * @param selectedItem {Object} */ addSelectedContentToContainer(selectedItem) { let value; if (typeof this.options.value === 'function') { value = this.options.value(selectedItem); } else { value = selectedItem[this.options.value]; } const selectedHtml = this.renderSelected(selectedItem); // Hidden input is added into the selected li const $selectedNode = $(selectedHtml); const $hiddenInput = $(``); $selectedNode.append($hiddenInput); // Then the li is added to the list this.$selectionContainer.append($selectedNode); // Trigger the change so that listeners detect the form data has been modified $hiddenInput.trigger('change'); } /** * Render the selected element, this will be appended in the selection list (ul), * no need to include the hidden input as it is automatically handled in addSelectedContentToContainer * * @param entity {Object} * * @returns {string} */ renderSelected(entity) { // @todo: the tplcollection idea is not bad but it only contains a span for now, to fo to the end of this idea // it should contain the whole div (with media-left media-body and all) const $templateContainer = $(`#tplcollection-${this.entitySearchInputId}`); const innerTemplateHtml = $templateContainer .html() .replace('%s', entity.name); return `
  • ${innerTemplateHtml}
  • `; } }