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 (