const TagsInput = ({ tags, onChange, placeholder = "Add tags..." }) => { const [inputValue, setInputValue] = React.useState(''); const [error, setError] = React.useState(''); // Ensure tags is always an array const tagArray = Array.isArray(tags) ? tags : []; const addTag = (tagText) => { const cleaned = tagText.trim(); if (!cleaned) return; // Validate: no special characters that could break JSON if (/[<>"'`{}[\]]/.test(cleaned)) { setError('Tags cannot contain: < > " \' ` { } [ ]'); return; } // Dedupe: case-insensitive, and when adding multiple from comma-separated const existingLower = tagArray.map(t => String(t).toLowerCase()); const toAdd = cleaned.split(',').map(t => t.trim()).filter(Boolean).filter(t => !existingLower.includes(t.toLowerCase())); toAdd.forEach(t => existingLower.push(t.toLowerCase())); if (toAdd.length === 0) { setInputValue(''); return; } setError(''); onChange([...tagArray, ...toAdd]); setInputValue(''); }; const removeTag = (index) => { const newTags = tagArray.filter((_, i) => i !== index); onChange(newTags); }; const handleKeyDown = (e) => { if (e.key === 'Enter' || e.key === ',' || e.key === 'Tab') { e.preventDefault(); addTag(inputValue); } else if (e.key === 'Backspace' && !inputValue && tagArray.length > 0) { removeTag(tagArray.length - 1); } else if (e.key === 'Escape') { setInputValue(''); setError(''); } }; return (
{tagArray.map((tag, index) => ( {tag} ))} { setInputValue(e.target.value); setError(''); }} onKeyDown={handleKeyDown} onBlur={() => { if (inputValue.trim()) { addTag(inputValue); } }} placeholder={tagArray.length === 0 ? (placeholder || 'e.g. urgent, policy, 2025') : 'Add another…'} className="flex-1 min-w-[120px] py-1.5 px-0 border-0 focus:ring-0 focus:outline-none text-gray-900 placeholder-gray-400 outline-none text-sm" />
{error && (
{error}
)}
Press Enter or comma to add. Hover a tag to remove it.
); }; if (typeof window !== 'undefined') window.TagsInput = TagsInput;