import React, { useRef, useState } from 'react'; import { VevCondition } from '@vev/interactions'; import { VevProps } from '@vev/utils'; import { SilkeTextFieldOutline, SilkeTextFieldOutlineProps, } from '../silke-text-field/silke-text-field-outline'; import { SilkeBox } from '../silke-box'; import { SilkeText } from '../silke-text'; import { SilkeTextFieldItem } from '../silke-text-field'; import { SilkePopover } from '../silke-popover'; import { SilkeConditionsInputOptions } from './silke-conditions-input-options'; import { ConditionSuggestion, VevConditionSuggestion } from './types'; import { useSilkeCSSContext } from '../silke-css-number-field'; import { SilkeConditionsTag } from './silke-conditions-tag'; import { booleanSuggestions, getCombinatorSuggestions, getOperatorSuggestions, getSuggestions, getSystemSuggestionOptions, getWidgetOptionType, getWidgetSuggestionOptions, } from './utils'; import { SilkeButton } from '../silke-button'; import styles from './silke-conditions-input.scss'; export type SilkeConditionsInputProps = SilkeTextFieldOutlineProps & { conditions: VevCondition; placeholder?: string; label?: string; systemOptions?: VevConditionSuggestion[]; widgetOptions?: VevProps[]; disabled?: boolean; onClear?: () => void; onChange: (value: VevCondition) => void; }; export function SilkeConditionsInput(props: SilkeConditionsInputProps) { const { conditions = [], systemOptions, widgetOptions, placeholder, disabled, onChange } = props; const [value, setValue] = useState(''); const [showSuggestions, setShowSuggestions] = useState(false); const [highlightedSuggestion, setHighlightedSuggestion] = useState(-1); const wrapperRef = useRef(null); const context = useSilkeCSSContext(); const variables = context.variablesv2; // Determine if we're inputting a, operator or b (or a combinator) const currentInputType = React.useMemo(() => { if (conditions.length % 4 === 0) return 'a'; if (conditions.length % 4 === 1) return 'operator'; if (conditions.length % 4 === 2) return 'b'; return 'combinator'; }, [conditions]); // Get suggestions based on current input type const suggestionItems: ConditionSuggestion[] = React.useMemo(() => { // Check if we should display a list of options (if a is of type select) if (currentInputType === 'b') { const aValue = conditions[conditions.length - 2]; const systemItems = getSystemSuggestionOptions(aValue, systemOptions); if (systemItems && systemItems.length > 0) { return systemItems; } const widgetItems = getWidgetSuggestionOptions(aValue, widgetOptions); if (widgetItems && widgetItems.length > 0) { return widgetItems; } // Check if A value is of type boolean, if so return true/false as suggestions if (getWidgetOptionType(aValue, widgetOptions) === 'boolean') { return booleanSuggestions; } // If we have a value, but no suggestions, return any variable suggestions of same type return getSuggestions(value || '', variables); } if (currentInputType === 'combinator') { return getCombinatorSuggestions(value || ''); } if (currentInputType === 'operator') { // Check if A value is of type boolean, if so limit operators to is/is not const onlyTrueFalse = getWidgetOptionType(conditions[conditions.length - 1], widgetOptions) === 'boolean'; return getOperatorSuggestions(value || '', onlyTrueFalse); } return getSuggestions(value || '', variables, systemOptions, widgetOptions); }, [currentInputType, value, conditions, variables, systemOptions, widgetOptions]); const handleBackspace = (e: React.KeyboardEvent) => { // If we have a value in the input field, abort! if (value) return; // Otherwise, delete last item in the condition e.stopPropagation(); e.preventDefault(); // Check if command key is pressed if (e.metaKey) { onClear(); return; } const newConditions = conditions.slice(0, -1); onChange(newConditions); }; // A new item has been selected/added const handleAddItem = (item: string | number | boolean, isBlurEvent?: boolean) => { const newConditions = [...conditions, item]; onChange(newConditions); // Set focus back to input (if not a blur event) if (!isBlurEvent) focusInput(); }; // A new suggestion has been selected/added const handleAddSuggestion = (item: ConditionSuggestion, isBlurEvent?: boolean) => { const newItems = [typeof item === 'string' ? item : item.value]; const prefix = typeof item === 'string' ? null : item.prefix || null; const suffix = typeof item === 'string' ? null : item.suffix || null; if (prefix) newItems.unshift(prefix); if (suffix) newItems.push(suffix); const newConditions = [...conditions, ...newItems]; onChange(newConditions); // Reset input value onChangeValue(''); // Set focus back to input (if not a blur event) if (!isBlurEvent) focusInput(); }; const focusInput = () => { // Find element in SilkeTextField const input = wrapperRef.current?.querySelector('input'); if (input) { setTimeout(() => input.focus()); } }; const onChangeValue = (newValue: string) => { setValue(newValue); setHighlightedSuggestion(1); }; const onClear = () => { onChange([]); props.onClear?.(); }; const selectNext = React.useCallback(() => { if (highlightedSuggestion === -1 || highlightedSuggestion + 1 >= suggestionItems.length) { setHighlightedSuggestion(1); } else { if (typeof suggestionItems[highlightedSuggestion + 1] === 'string') { if (highlightedSuggestion + 2 >= suggestionItems.length) { setHighlightedSuggestion(1); } else { setHighlightedSuggestion(highlightedSuggestion + 2); } } else { setHighlightedSuggestion(highlightedSuggestion + 1); } } }, [highlightedSuggestion, suggestionItems]); const selectPrev = React.useCallback(() => { if (highlightedSuggestion === -1 || highlightedSuggestion - 1 === -1) { setHighlightedSuggestion(suggestionItems.length - 1); } else { if (typeof suggestionItems[highlightedSuggestion - 1] === 'string') { if (highlightedSuggestion - 2 < 0) { setHighlightedSuggestion(suggestionItems.length - 1); } else { setHighlightedSuggestion(highlightedSuggestion - 2); } } else { setHighlightedSuggestion(highlightedSuggestion - 1); } } }, [highlightedSuggestion, suggestionItems]); const onConfirmSelected = () => { const suggestedItem = suggestionItems[highlightedSuggestion]; if (suggestedItem) { handleAddSuggestion(suggestedItem); onChangeValue(''); } else if (value) { handleAddItem(value); onChangeValue(''); } }; const onFocusInput = () => { setShowSuggestions(true); setHighlightedSuggestion(1); }; const onBlurInput = () => { // Check if we should add the current value as a new item if (value) { handleAddItem(value, true); onChangeValue(''); } setShowSuggestions(false); setHighlightedSuggestion(-1); }; return ( If {conditions?.map((condition, index) => ( ))} onChangeValue(e.target.value)} onKeyDown={(e: React.KeyboardEvent) => { switch (e.key) { case 'Enter': { onConfirmSelected(); break; } case ' ': { e.preventDefault(); onConfirmSelected(); break; } case 'ArrowUp': selectPrev(); break; case 'ArrowDown': { selectNext(); break; } case 'Backspace': { handleBackspace(e); break; } } }} onFocus={onFocusInput} onBlur={onBlurInput} /> {Boolean(conditions.length) && ( )} ); } SilkeConditionsInput.displayName = 'SilkeConditionsInput';