import * as React from 'react'; import * as TextControlBase from '@tesseract-design/web-base-textcontrol'; import * as BadgeBase from '@tesseract-design/web-base-badge'; export type TagInputProps = Omit, 'size' | 'style'> & { /** * Short textual description indicating the nature of the component's value. */ label?: React.ReactNode, /** * Short textual description as guidelines for valid input values. */ hint?: React.ReactNode, /** * Size of the component. */ size?: TextControlBase.TextControlSize, /** * Additional description, usually graphical, indicating the nature of the component's value. */ indicator?: React.ReactNode, /** * Should the component display a border? */ border?: boolean, /** * Should the component occupy the whole width of its parent? */ block?: boolean, /** * Style of the component. */ style?: TextControlBase.TextControlStyle, /** * Is the label hidden? */ hiddenLabel?: boolean, enhanced?: boolean, separator?: string, } export const TagInput = React.forwardRef((( { label = '', hint = '', indicator = null, size = TextControlBase.TextControlSize.MEDIUM, border = false, block = false, style = TextControlBase.TextControlStyle.DEFAULT, hiddenLabel = false, onInput, enhanced = false, defaultValue = '', separator = ',', onFocus, onBlur, onSelect, className: _className, placeholder: _placeholder, as: _as, ...etcProps }: TagInputProps, forwardedRef, ) => { const [hydrated, setHydrated] = React.useState(false); const [focused, setFocused] = React.useState(false); const [selectionStart, setSelectionStart] = React.useState(0); const [selectionEnd, setSelectionEnd] = React.useState(0); const [viewValue, setViewValue] = React.useState(() => { const theDefaultValue = !Array.isArray(defaultValue) ? [defaultValue.toString(), ''] : [...defaultValue, '']; return theDefaultValue.filter(v => v.length > 0); }); const defaultRef = React.useRef(null); const effectiveRef = forwardedRef ?? defaultRef; const styleArgs = React.useMemo(() => ({ block, border, size, indicator: Boolean(indicator), style, resizable: true, predefinedValues: false, }), [block, border, size, indicator, style]); const renderEnhanced = React.useMemo(() => enhanced && hydrated, [enhanced, hydrated]); const handleInput: React.FormEventHandler = (e) => { const target = e.target as HTMLInputElement; setViewValue(target.value.split(separator)) if (onInput) { onInput(e) } } const handleFocus: React.FocusEventHandler = (e) => { setFocused(true); if (onFocus) { onFocus(e); } } const handleBlur: React.FocusEventHandler = (e) => { setFocused(false); if (onBlur) { onBlur(e); } } const handleSelect: React.ReactEventHandler = (e) => { const target = e.target as HTMLInputElement; const newSelectionStart = target.selectionStart ?? 0; const newSelectionEnd = target.selectionEnd ?? 0; const newDirection = Math.sign(newSelectionStart - selectionStart) > 0 ? 'forward' : 'backward' setSelectionStart(newSelectionStart); setSelectionEnd(newSelectionEnd); console.log(newDirection); const lastSeparatorIndex = target.value.lastIndexOf(separator); const separatorStartRaw = newSelectionStart > lastSeparatorIndex ? newSelectionStart : target.value.slice(0, newSelectionEnd).lastIndexOf(separator) + separator?.length; const separatorEndRaw = newSelectionEnd > lastSeparatorIndex ? newSelectionEnd : target.value.slice(0, newSelectionEnd).length + target.value.slice(newSelectionEnd).indexOf(separator); let separatorStart = 0; let separatorEnd; if (lastSeparatorIndex > -1) { separatorStart = separatorStartRaw > -1 ? separatorStartRaw : 0; } separatorEnd = separatorEndRaw; if (newSelectionStart <= target.value.lastIndexOf(separator)) { if (newSelectionStart === newSelectionEnd && newSelectionStart === separatorStart && newDirection === 'backward') { target.selectionStart = newSelectionStart - separator?.length; target.selectionEnd = newSelectionStart - separator?.length; target.selectionDirection = newDirection; } else if (newSelectionStart === newSelectionEnd && newSelectionEnd === separatorEnd && newDirection === 'forward') { target.selectionStart = newSelectionEnd + separator?.length; target.selectionEnd = newSelectionEnd + separator?.length; target.selectionDirection = newDirection; } else { target.selectionStart = separatorStart; target.selectionEnd = separatorEnd; target.selectionDirection = 'backward'; } } if (onSelect) { onSelect(e); } } const focusOnInput: React.MouseEventHandler = (e) => { e.preventDefault(); if (typeof effectiveRef === 'function') { return; } if (effectiveRef !== null && effectiveRef.current) { effectiveRef.current.focus(); } } const tags = React.useMemo(() => viewValue.slice(0, -1), [viewValue]) const inputText = React.useMemo(() => viewValue.slice(-1)[0] ?? '', [viewValue]) React.useEffect(() => { setHydrated(true) }, []); return (
{tags.map(v => (
))} { inputText.lastIndexOf(separator) < 0 && ( <> { inputText.slice(0, selectionStart - tags.join(separator).length - separator?.length) }
{ inputText.slice(selectionStart - tags.join(separator).length - separator?.length, selectionEnd - tags.join(separator).length - separator?.length) }
{ inputText.slice(selectionEnd - tags.join(separator).length - separator?.length) } ) } { inputText.lastIndexOf(separator) >= 0 && ( <> { inputText.slice(0, selectionStart - tags.join(separator).length) }
{ inputText.slice(selectionStart - tags.join(separator).length - 1, selectionEnd - tags.join(separator).length - 1) }
{ inputText.slice(selectionEnd - tags.join(separator).length - 1) } ) }
{ border && ( ) } { label && !hiddenLabel && (
{label}
) } {hint && (
{hint}
)} {indicator && (
{indicator}
)}
); })); TagInput.displayName = 'TagInput';