front_fields_datepicker.js

import { _x } from '@wordpress/i18n';
import { WcDatepicker } from 'wc-datepicker/dist/components/wc-datepicker';
import 'wc-datepicker/dist/themes/light.css';

customElements.define( 'wc-datepicker', WcDatepicker );

const findstr = window.findstr || {};
const moment = window.moment || {};

const labels = {
	clearButton: _x( 'Clear value', 'datepicker labels', 'findstr' ),
	monthSelect: _x( 'Select month', 'datepicker labels', 'findstr' ),
	nextMonthButton: _x( 'Next month', 'datepicker labels', 'findstr' ),
	nextYearButton: _x( 'Next year', 'datepicker labels', 'findstr' ),
	picker: _x( 'Choose date', 'datepicker labels', 'findstr' ),
	previousMonthButton: _x( 'Previous month', 'datepicker labels', 'findstr' ),
	previousYearButton: _x( 'Previous year', 'datepicker labels', 'findstr' ),
	todayButton: _x( 'Show today', 'datepicker labels', 'findstr' ),
	yearSelect: _x( 'Select year', 'datepicker labels', 'findstr' ),
};

const DEFAULT_DATE_FORMAT = _x(
	'YYYY-MM-DD',
	'datepicker input date format',
	'findstr'
);

document.addEventListener( 'findstrLoaded', async function ( e ) {
	/**
	 * On load
	 */
	const datePickers = document.querySelectorAll(
		'[id^="findstr-datepicker-"]'
	);

	if ( datePickers.length > 0 ) {
		datePickers.forEach( ( datepicker ) => {
			datepicker.labels = labels;
		} );
	}

	findstr.hooks.addAction( 'findstrInit', 'datepicker', async function () {
		const datePickerFields = document.querySelectorAll(
			'.findstr-field.findstr-field-datepicker'
		);
		for ( const fieldItem of datePickerFields ) {
			const field = JSON.parse( fieldItem.dataset.field );
			const group = fieldItem.dataset.group;

			field.source_name = field.source_name + '_timestamp';

			let facetDates = [];

			if ( field.options.showEvents ) {
				//get group default query
				const defaultQuery = findstr.groups[ group ].defaultQuery;
				defaultQuery.facets = [ field.source_name ];

				//do search to get facet dates
				facetDates = await findstr
					.search( '', defaultQuery )
					.then( ( response ) => {
						const datesTimestamp =
							response?.facetDistribution?.[
								field.source_name
							] || {};
						return Object.keys( datesTimestamp ).map(
							( timestamp ) => {
								return new Date(
									timestamp * 1000
								).toISOString().split( 'T' )[ 0 ];
							}
						);
					} );
			}

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

			//set selected dates
			const selectedDatesTimeStamp =
				JSON.parse( input.dataset.findstrSelectedDates ) || [];
			const selectedDates = selectedDatesTimeStamp.map( ( timestamp ) => {
				const offset = new Date().getTimezoneOffset() * 60;
				return new Date( ( parseInt( timestamp ) + offset ) * 1000 );
			} );

			const currentDatepicker = document.getElementById(
				'findstr-datepicker-' +
					fieldItem.dataset.id +
					'-' +
					fieldItem.dataset.group
			);

			if ( selectedDates.length > 0 ) {
				currentDatepicker.value =
					field.options.dateType === 'single'
						? new Date( selectedDates[ 0 ] )
						: [
								new Date( selectedDates[ 0 ] ),
								new Date( selectedDates[ 1 ] ),
						  ];
			}

			updateCalendarOccurrences( currentDatepicker, facetDates );

			const datepickerWrapper = document.querySelector(
				`.findstr-field-datepicker[data-id="${ fieldItem.dataset.id }"][data-group="${ fieldItem.dataset.group }"]`
			);
			const datepickerInput = datepickerWrapper.querySelector(
				'input[data-field-type="datepicker"]'
			);
			const statusElement = document.getElementById(
				'findstr-datepicker-status-' +
					fieldItem.dataset.id +
					'-' +
					fieldItem.dataset.group
			);

			const openDatepicker = () => {
				if ( field.options?.inline !== true ) {
					const datepicker = document.getElementById(
						'findstr-datepicker-' +
							fieldItem.dataset.id +
							'-' +
							fieldItem.dataset.group
					);
					if ( datepicker ) {
						datepicker.style.position = 'absolute';
						datepicker.style.display = 'block';
						datepickerInput.setAttribute( 'aria-expanded', 'true' );
						const today = datepickerWrapper.querySelector(
							'.wc-datepicker__date--today'
						);
						statusElement.textContent = '';
						if ( today ) {
							today.focus();
						}
					}
				}
			};
			const closeDatepicker = () => {
				const datepicker = document.getElementById(
					'findstr-datepicker-' +
						fieldItem.dataset.id +
						'-' +
						fieldItem.dataset.group
				);
				if ( datepicker ) {
					datepickerInput.setAttribute( 'aria-expanded', 'false' );
					datepicker.style.display = 'none';
					datepicker.style.position = 'relative';
					if ( datepickerInput.value ) {
						statusElement.textContent =
							_x(
								'Selected dates:',
								'datepicker aria live',
								'findstr'
							) +
							' ' +
							datepickerInput.value;
					}
				}
			};
			datepickerInput.addEventListener( 'click', ( event ) => {
				openDatepicker();
			} );

			datepickerInput.addEventListener( 'keyup', ( event ) => {
				if ( event.key === 'Enter' ) {
					openDatepicker();
				}
			} );

			datepickerWrapper.addEventListener( 'focusout', () => {
				setTimeout( () => {
					if (
						field.options?.inline !== true &&
						! datepickerWrapper.contains( document.activeElement )
					) {
						closeDatepicker();
					}
				}, 0 );
			} );

			document.addEventListener( 'keyup', ( event ) => {
				if ( event.key === 'Escape' ) {
					closeDatepicker();
				}
			} );

			currentDatepicker.addEventListener( 'selectDate', ( event ) => {
				if ( ! event.detail ) {
					input.dataset.findstrSelectedDates = '';
					input.value = '';
					input.dispatchEvent( new Event( 'input' ) );
					return;
				}

				/**
				 * Filter the input field date format for the datepicker (single or range).
				 *
				 * @hook findstrDatePickerInputFormat
				 *
				 * @param {string} format - The default date format (e.g., 'YYYY-MM-DD')
				 * @param {Object} field  - The datepicker field object
				 *
				 * @return {string} The date format to use for the input display
				 */
				const inputFormat = findstr.hooks.applyFilters(
					'findstrDatePickerInputFormat',
					DEFAULT_DATE_FORMAT,
					field
				);

				if ( Array.isArray( event.detail ) ) {
					const [ startDate, endDate ] = event.detail;
					const startTimestamp = moment( startDate )
						.utcOffset( 0, true )
						.unix();
					const endTimestamp = moment( endDate )
						.utcOffset( 0, true )
						.unix();
					input.dataset.findstrSelectedDates = JSON.stringify( [
						startTimestamp,
						endTimestamp,
					] );
					/**
					 * Filter the separator string used between start and end dates in the input field.
					 *
					 * @hook findstrDatePickerRangeSeparator
					 *
					 * @param {string} separator - The default separator string (e.g., 'to')
					 *
					 * @return {string} The separator string to use between dates
					 */
					input.value = `${ moment( startDate ).format(
						inputFormat
					) } ${ findstr.hooks.applyFilters(
						'findstrDatePickerRangeSeparator',
						_x( 'to', 'date range separator', 'findstr' )
					) } ${ moment( endDate ).format( inputFormat ) }`;
				} else {
					const selectedDate = event.detail;
					const timestamp = moment( selectedDate )
						.utcOffset( 0, true )
						.unix();
					input.dataset.findstrSelectedDates = JSON.stringify( [
						timestamp,
					] );
					input.value = moment( selectedDate ).format( inputFormat );
				}
				input.dispatchEvent( new Event( 'input' ) );
			} );

			currentDatepicker.addEventListener( 'changeMonth', ( event ) => {
				setTimeout( () => {
					updateCalendarOccurrences( currentDatepicker, facetDates );
				} );
			} );
		}
	} );

	/**
	 *  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 === 'datepicker' &&
						input.value
					) {
						const field = findstr.getField(
							input.dataset.findstrId,
							group
						);

						if ( ! input.dataset.findstrSelectedDates ) {
							return;
						}

						const date = input.dataset.findstrSelectedDates
							? JSON.parse( input.dataset.findstrSelectedDates )
							: [];

						const isSingleDate =
							field.options.dateType === 'single';
						const behavior = field.options.singleDateBehavior;

						if ( ! date[ 1 ] ) {
							date[ 1 ] = moment( date[ 0 ] * 1000 )
								.utc()
								.endOf( 'day' )
								.unix();
						} else {
							date[ 1 ] = moment( date[ 1 ] * 1000 )
								.utc()
								.endOf( 'day' )
								.unix();
						}

						switch ( behavior ) {
							case 'after':
								query.filter.clauses[ filter_name ] = {
									value: date[ 0 ],
									compare: '>=',
								};
								break;
							case 'before':
								query.filter.clauses[ filter_name ] = {
									value: moment( date[ 0 ] * 1000 )
										.utc()
										.endOf( 'day' )
										.unix(),
									compare: '<=',
								};
								break;
							default:
								query.filter.clauses[ filter_name ] = {
									value: date,
									compare: 'BETWEEN',
								};
								break;
						}
					}
				} );
			}

			return query;
		}
	);

 findstr.hooks.addAction(
		'resetFilters',
		'findstr-search',
		( group, query ) => {
			findstr.groups[ group ].items.forEach( ( item ) => {
				if ( item.dataset.fieldType === 'datepicker' ) {
					const datepicker = document.getElementById(
						'findstr-datepicker-' + item.dataset.findstrId + '-' + item.dataset.findstrGroup
					);
					datepicker.startDate = new Date();
					datepicker.value = undefined;
					item.dataset.findstrSelectedDates = '';
					item.value = '';
				}
			} );
		}
	);
} );

function updateCalendarOccurrences( currentDatepicker, facetDates ) {
	const calendar = currentDatepicker.querySelectorAll(
		'.wc-datepicker__calendar td'
	);
	calendar.forEach( ( td ) => {
		const date = td.dataset.date;
		if ( facetDates.includes( date ) ) {
			td.classList.add( 'has-occurrence' );
			const eventsSpan = document.createElement( 'span' );
			eventsSpan.classList.add(
				'occurrence-dot',
				'visually-hidden',
				'sc-wc-datepicker'
			);
			/**
			 * Filter the aria-label for the occurrence dot in the datepicker.
			 *
			 * @hook findstrDatePickerOccurrenceLabel
			 *
			 * @param {string} label - The default aria-label for the occurrence dot
			 *
			 * @return {string} The aria-label to use for the occurrence dot
			 */
			eventsSpan.setAttribute(
				'aria-label',
				findstr.hooks.applyFilters(
					'findstrDatePickerOccurrenceLabel',
					_x(
						'Occurrences available on this date',
						'datepicker aria label',
						'findstr'
					)
				)
			);
			td.appendChild( eventsSpan );
		} else {
			td.classList.remove( 'has-occurrence' );
			const existingSpan = td.querySelector( '.occurrence-dot' );
			if ( existingSpan ) {
				td.removeChild( existingSpan );
			}
		}
	} );
}