import {useState, useRef, useEffect, useMemo} from '@wordpress/element'; import {__} from '@wordpress/i18n'; import './TokenSelect.scss'; import {Popover} from '@wordpress/components'; const TokenSelect = ( { options, selectedOptions, // Expects array when multi, but single object or null when single onChange, onSearch, placeholder, isLoading, isSingle = false, className = '', tokenClassName = '', disabled = false, } ) => { const [isOpen, setIsOpen] = useState( false ); const [query, setQuery] = useState( '' ); const containerRef = useRef( null ); const inputRef = useRef( null ); // Normalise selectedOptions to an array for internal workflow const selection = useMemo( () => { if ( !selectedOptions ) { return []; } return Array.isArray( selectedOptions ) ? selectedOptions : [selectedOptions]; }, [selectedOptions] ); // Helper: get the class of a group for a specific value const getGroupClass = ( optionValue ) => { const isGrouped = options.length > 0 && options[0].options; if ( !isGrouped ) return ''; for ( const group of options ) { if ( group.options.some( opt => opt.value === optionValue ) ) { return group.class || ''; } } return ''; }; // Close when clicking outside. useEffect( () => { const handleClickOutside = ( event ) => { const popoverContent = document.querySelector( '.adpresso-token-select__popover-content' ); if ( containerRef.current && !containerRef.current.contains( event.target ) && (!popoverContent || !popoverContent.contains( event.target )) ) { setIsOpen( false ); setQuery( '' ); } }; document.addEventListener( 'mousedown', handleClickOutside ); return () => document.removeEventListener( 'mousedown', handleClickOutside ); }, [] ); const handleRemoveOption = ( optionToRemove ) => { if ( isSingle ) { onChange( null ); } else { onChange( selection.filter( opt => opt.value !== optionToRemove.value ) ); } inputRef.current?.focus(); }; const handleSelectOption = ( optionToAdd ) => { if ( isSingle ) { onChange( [optionToAdd] ); setIsOpen( false ); } else { onChange( [...selection, optionToAdd] ); setQuery( '' ); onSearch && onSearch( '' ); inputRef.current?.focus(); // Focus back for another input. } }; // Filter logic supports flat lists and groups const filteredOptions = useMemo( () => { const q = query.toLowerCase(); const isSelected = ( opt ) => selection.some( sel => sel.value === opt.value ); const matchesQuery = ( opt ) => opt.label.toLowerCase().includes( q ); const isGrouped = options.length > 0 && options[0].options; if ( isGrouped ) { return options.map( group => { const groupOptions = group.options.filter( opt => !isSelected( opt ) && matchesQuery( opt ) ); return groupOptions.length > 0 ? {...group, options: groupOptions} : null; } ).filter( Boolean ); } else { // Flat list return options.filter( opt => !isSelected( opt ) && matchesQuery( opt ) ); } }, [options, selection, query] ); return (
inputRef.current?.focus()}> {selection.map( option => { const groupClass = getGroupClass( option.value ); return ( {option.label} ); } )} { setQuery( e.target.value ); onSearch && onSearch( e.target.value ); setIsOpen( true ); }} onFocus={() => setIsOpen( true )} placeholder={selection.length === 0 ? placeholder : ''} />
{/* --- Dropdown Menu --- */} {isOpen && ( setIsOpen( false )} position="left center" noArrow={true} className="adpresso-token-select__popover" focusOnMount={false} >
    {isLoading &&
  • {__( 'Loading...', 'adpresso' )}
  • } {!isLoading && filteredOptions.length === 0 && (
  • {__( 'No results found.', 'adpresso' )}
  • )} {!isLoading && filteredOptions.map( ( item, index ) => { if ( item.options ) { return (
  • {item.label}
      {item.options.map( opt => (
    • handleSelectOption( { ...opt, groupClass: item.class } )} > {opt.label}
    • ) )}
  • ); } return (
  • handleSelectOption( item )} > {item.label}
  • ); } )}
)}
); }; export default TokenSelect;