front_fields_dropdown.js

const findstr = window.findstr || {};
import TomSelect from 'tom-select';
import 'tom-select/src/scss/tom-select.default.scss';
import { _x, sprintf } from '@wordpress/i18n';

document.addEventListener('findstrLoaded', function (e) {
	const { buildSearchQuery, parseFilters, parseSort, getFacetDistribution } =
		window.findstr.helpers;

	function isMultiselect(field) {
		return field.options && field.options.multiselect === true;
	}

	function hideEmptyItems(field) {
		return (
			isMultiselect(field) &&
			field.options?.multiselectLogic !== 'OR' &&
			field.options?.hideEmpty
		);
	}

	function showCount(field) {
		return field.options?.showCount;
	}

	function optionCount(field, option, group, facets) {
		if (facets) {
			return facets[field.source_name][option] || 0;
		}

		return showCount(field)
			? findstr.groups[group]?.facets[field.source_name]?.[option] || 0
			: findstr.facets[field.source_name][option] || 0;
	}

	function manageItems(dropdown) {
		const items = dropdown.control.children;
		const countElement = dropdown.control.querySelector('.items-count');
		if (2 < dropdown.items.length) {
			for (let i = 0; i < items.length; i++) {
				if (items[i].classList.contains('item')) {
					items[i].classList.add('item--hidden');
				}
			}
			if (countElement) {
				countElement.textContent = sprintf(
					/* Translators: %s is the number of selected items */
					_x('%s selected', 'items selected count', 'findstr'),
					dropdown.items.length
				);
			} else {
				const selectionCount = document.createElement('span');
				selectionCount.classList.add('items-count');
				selectionCount.textContent = sprintf(
					/* Translators: %s is the number of selected items */
					_x('%s selected', 'items selected count', 'findstr'),
					dropdown.items.length
				);
				dropdown.control.appendChild(selectionCount);
			}
		} else {
			for (let i = 0; i < items.length; i++) {
				if (items[i].classList.contains('item--hidden')) {
					items[i].classList.remove('item--hidden');
				}
			}
			if (countElement) {
				dropdown.control.removeChild(countElement);
			}
		}
	}

	findstr.hooks.addAction('findstrInit', 'findstr-search', () => {
		const dropdownFields = document.querySelectorAll(
			'.findstr-field.findstr-field-dropdown'
		);
		dropdownFields.forEach((fieldItem) => {
			const field = JSON.parse(fieldItem.dataset.field);
			const input = fieldItem.querySelector(
				'.findstrFieldContainer input[data-findstr]'
			);
			const values = input.value.split('||');
			const group = fieldItem.dataset.group;
			const choices = findstr.facets[field.source_name] || {};
			const hideEmpty = hideEmptyItems(field);

			const shouldDisable = isMultiselect(field);

			const options = [];
			Object.keys(choices).forEach((choice) => {
				const count = optionCount(field, choice, group);

				const disabled =
					count === 0 && !values.includes(choice) && shouldDisable;

				if (!hideEmpty || !disabled) {
					options.push({
						count,
						disabled,
						value: choice,
						/**
						 * Filter the field value label
						 *
						 * @hook findstrFieldValueLabel
						 *
						 * @param {string} label    - The label
						 * @param {string} key      - The key
						 * @param {Object} field    - The field object
						 * @param {Array}  selected - The selected values
						 *
						 * @return {string} The label
						 */
						text: findstr.hooks.applyFilters(
							'findstrFieldValueLabel',
							choice,
							field
						),
					});
				}
			});

			const maxItems = isMultiselect(field) ? null : 1;
			let plugins = {
				clear_button: {
					title: _x('Reset', 'dropdown clear button', 'findstr'),
				},
			};

			if (isMultiselect(field)) {
				plugins = ['checkbox_options'];
			}

			const inputElement = fieldItem.querySelector(
				'.findstrFieldContainer input'
			);

			const dropdown = new TomSelect(
				inputElement,

				/**
				 * Filter the dropdown select options.
				 * Options are passed to TomSelect, @see https://tom-select.js.org/docs/
				 *
				 * @hook findstrDropdownSelectOptions
				 *
				 * @param {Object} options   - The options
				 * @param {Object} field     - The field object
				 * @param {string} group     - The group
				 * @param {Object} fieldItem - The field item
				 *
				 * @return {Object} The options
				 */
				findstr.hooks.applyFilters(
					'findstrDropdownSelectOptions',
					{
						maxItems,
						options,
						plugins,
						persist: false,
						controlInput: '<input type="text" readonly="readonly">',
						placeholder: inputElement.placeholder || '',
						hidePlaceholder: false,
						sortField: [
							{ field: 'value' },
							{ field: '$order' },
							{ field: '$score' },
						],
						delimiter: '||',
						onInitialize() {
							manageItems(this);
						},
						render: {
							option(data, escape) {
								if (showCount(field)) {
									const count = data.count || 0;
									const ariaLabel = sprintf(
										/*  Translators: %s is the field value, %i is the results count */
										_x(
											'Number of results for %s : %i',
											'dropdown option',
											'findstr'
										),
										escape(data.text),
										count
									);
									return `<div>${escape(data.text)} <span class="filter-count" data-value="${escape(data.value)}" data-field="${field.id}" aria-label="${ariaLabel}">${escape(count)}</span></div>`;
								}
								return `<div>${escape(data.text)}</div>`;
							},
							item(data, escape) {
								return (
									'<span class="item">' +
									escape(data.text) +
									'</span>'
								);
							},
						},
					},
					field,
					fieldItem.dataset.group,
					fieldItem
				)
			);

			dropdown.on('item_add', function () {
				manageItems(dropdown);
			});
			dropdown.on('item_remove', function () {
				manageItems(dropdown);
			});

			findstr.hooks.addAction(
				'beforeSearch',
				'findstr-dropdown',
				(currentGroup, parameters, currentField, currentElement) => {
					const newQuery = buildSearchQuery(group, fieldItem);

					const query = {
						...newQuery,
						facets: ['*'],
						limit: 1,
					};
					const queryClone = JSON.parse(JSON.stringify(query)); //clone query;

					if (
						field.options?.multiselect &&
						field.options?.multiselectLogic === 'OR'
					) {
						delete query.filter.clauses[field.source_name];

						//update count, remove filters
						queryClone.filter = parseFilters(query.filter);
						queryClone.sort = parseSort(query.sort);
					} else {
						//update count, with filters
						queryClone.filter = parseFilters(newQuery.filter);
						queryClone.sort = parseSort(newQuery.sort);
					}

					getFacetDistribution(queryClone).then((facets) => {
						Object.keys(findstr.facets[field.source_name]).forEach(
							(choice) => {
								//manage excluded options
								if (
									field.options?.excludeOptions?.includes(
										choice
									)
								) {
									dropdown.removeOption(choice);
									return;
								}

								const count = optionCount(
									field,
									choice,
									group,
									facets
								);

								const disabled =
									count === 0 &&
									!dropdown.items.includes(choice) &&
									shouldDisable;

								const data = {
									count,
									value: choice,
									disabled,
									text: findstr.hooks.applyFilters(
										'findstrFieldValueLabel',
										choice,
										field
									),
								};

								dropdown.updateOption(choice, data);

								if (hideEmpty && disabled) {
									dropdown.removeOption(choice);
								} else if (!dropdown.getOption(choice)) {
									dropdown.addOption(data);
								}
							}
						);

						if (field.options?.multiselect && currentElement) {
							//this condition is added to prevent the dropdown from refreshing when the search is reset
							if (
								currentElement.dataset.findstrReseted !== 'true'
							) {
								dropdown.refreshOptions(
									field.id === currentField.id
								);
							}
						}
						manageItems(dropdown);
					});
				}
			);

			findstr.hooks.addAction(
				'afterSearchResults',
				'findstr-dropdown',
				(results, group, field, currentElement) => {
					if (currentElement) {
						currentElement.dataset.findstrReseted = 'false';
					}
				}
			);
		});
	});

	/**
	 *  Build search query based on select value
	 */
	findstr.hooks.addFilter(
		'findstrBuildSearchQuery',
		'findstr-search',
		(query, items, group) => {
			for (const [filter_name, inputs] of Object.entries(items)) {
				inputs.forEach((input) => {
					if (input.dataset.fieldType === 'dropdown' && input.value) {
						const field = JSON.parse(
							document.querySelector(
								`[data-id="${input.dataset.findstrId}"][data-group="${group}"]`
							).dataset.field
						);

						const values = input.value.split('||');

						//default case, single select
						query.filter.clauses[filter_name] = {
							value: values[0],
						};

						if (field.options.multiselect === true) {
							query.filter.clauses[filter_name].value = values;
							if (field.options.multiselectLogic === 'OR') {
								query.filter.clauses[filter_name].compare =
									'IN';
							} else {
								query.filter.clauses[filter_name].compare = '=';
							}
						}
					}
				});
			}

			return query;
		}
	);

	/**
	 * Reset Filters
	 */
	findstr.hooks.addAction('resetFilters', 'dropdownField', (group, query) => {
		findstr.groups[group].items.forEach((item) => {
			if ('dropdown' === item.field.type) {
				const targetValue =
					query.filter.clauses[item.field.source_name]?.value;
				item.tomselect.clear(true);
				if ('undefined' !== typeof targetValue) {
					item.tomselect.addItems(targetValue, true);
				}
			}
		});
	});

	/**
	 * Excluded filters
	 */
	findstr.hooks.addFilter(
		'findstrDropdownSelectOptions',
		'checkboxField',
		function (options, field) {
			if (field.options?.excludeOptions) {
				//remove the excluded options from the choices
				options.options = options.options.filter(
					(option) =>
						!field.options.excludeOptions.includes(option.value)
				);
			}
			return options;
		}
	);
});