import { Autocomplete as BaseAutocomplete } from '@base-ui/react/autocomplete'; import type { ChangeEvent, ReactNode, Ref } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { IoExpandOutline } from 'react-icons/io5'; import Button from '../../../../../common/components/buttons/Button'; import IconButton from '../../../../../common/components/buttons/IconButton'; import { type InputProps } from '../../../../../common/components/input/input/Input'; import Textarea from '../../../../../common/components/input/textarea/Textarea'; import Modal from '../../../../../common/components/modal/Modal'; import useCustomFields from '../../../../../common/hooks-query/useCustomFields'; import { cx } from '../../../../../common/utils/styleUtils'; import { makeAutoCompleteList } from './templateInput.utils'; import { useTemplateAutocomplete } from './useTemplateAutocomplete'; import inputStyle from '../../../../../common/components/input/input/Input.module.scss'; import style from './TemplateInput.module.scss'; interface TemplateInputProps extends Omit { ref?: Ref; value?: string; } interface TemplateEditorModalProps { autocompleteList: string[]; isOpen: boolean; onClose: () => void; onSave: (value: string) => void; value: string; } type TemplateAutocompleteState = ReturnType>; interface TemplateAutocompleteRootProps { autocomplete: TemplateAutocompleteState; children: ReactNode; value: string; } function assignRef(ref: Ref | undefined, value: T | null) { if (typeof ref === 'function') { ref(value); } else if (ref) { ref.current = value; } } function emitInputChange(name: string | undefined, value: string, onChange: InputProps['onChange']) { onChange?.({ target: { name, value }, currentTarget: { name, value }, } as ChangeEvent); } export default function TemplateInput({ className, disabled, fluid, height = 'medium', onChange, readOnly, ref, value, variant = 'subtle', ...rest }: TemplateInputProps) { const { data } = useCustomFields(); const inputRef = useRef(null); const [inputValue, setInputValue] = useState(value || ''); const [isExpanded, setIsExpanded] = useState(false); const autocompleteList = useMemo(() => { return makeAutoCompleteList(data); }, [data]); const updateInputValue = useCallback( (nextValue: string) => { setInputValue(nextValue); emitInputChange(rest.name, nextValue, onChange); }, [onChange, rest.name], ); const autocomplete = useTemplateAutocomplete(inputValue, autocompleteList, inputRef, updateInputValue); const { setCursorForValue } = autocomplete; // Keep the local autocomplete input in sync when react-hook-form resets or swaps field-array values. useEffect(() => { const nextValue = value || ''; setInputValue(nextValue); setCursorForValue(nextValue, nextValue.length); }, [setCursorForValue, value]); const setInputRef = useCallback( (element: HTMLInputElement | null) => { inputRef.current = element; assignRef(ref, element); }, [ref], ); const openExpandedEditor = () => { autocomplete.setShowSuggestions(false); setIsExpanded(true); }; const closeExpandedEditor = () => { setIsExpanded(false); }; const saveExpandedEditor = (nextValue: string) => { updateInputValue(nextValue); autocomplete.setCursorForValue(nextValue, nextValue.length); setIsExpanded(false); }; return ( <>
); } function TemplateAutocompleteRoot({ autocomplete, children, value }: TemplateAutocompleteRootProps) { return ( {children} ); } function TemplateEditorModal({ autocompleteList, isOpen, onClose, onSave, value }: TemplateEditorModalProps) { const expandedInputRef = useRef(null); const [draftValue, setDraftValue] = useState(value); const autocomplete = useTemplateAutocomplete(draftValue, autocompleteList, expandedInputRef, setDraftValue); const { setShowSuggestions } = autocomplete; // Reset the draft whenever the modal opens so cancel never leaks unsaved changes. useEffect(() => { if (!isOpen) { return; } setDraftValue(value); setShowSuggestions(false); }, [isOpen, setShowSuggestions, value]); const handleClose = () => { setShowSuggestions(false); onClose(); }; const handleSave = () => { setShowSuggestions(false); onSave(draftValue); }; return ( } /> } footerElements={ <> Start a template with {'{{'} to see autocomplete. } /> ); } function TemplateSuggestionPopup() { return ( {(suggestion: string) => ( {suggestion} )} ); }