diff --git a/src/components/input/chip-input.tsx b/src/components/input/chip-input.tsx index c95b23c8f..d7d020cc6 100644 --- a/src/components/input/chip-input.tsx +++ b/src/components/input/chip-input.tsx @@ -1,6 +1,8 @@ -import { XIcon } from "@phosphor-icons/react"; +import { PencilSimpleIcon, XIcon } from "@phosphor-icons/react"; +import { motion, Reorder, useDragControls } from "motion/react"; import * as React from "react"; import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; import { useControlledState } from "@/hooks/use-controlled-state"; import { cn } from "@/utils/style"; @@ -10,6 +12,79 @@ type Props = Omit, "value" | "onChange"> & { onChange?: (value: string[]) => void; }; +type ChipItemProps = { + chip: string; + index: number; + isEditing: boolean; + onEdit: (index: number) => void; + onRemove: (index: number) => void; +}; + +function ChipItem({ chip, index, isEditing, onEdit, onRemove }: ChipItemProps) { + const controls = useDragControls(); + const [isHovered, setIsHovered] = React.useState(false); + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + { + e.preventDefault(); + controls.start(e); + }} + > + {chip} + + + + + + + + ); +} + export function ChipInput({ value, defaultValue = [], onChange, className, ...props }: Props) { const [chips, setChips] = useControlledState({ value, @@ -18,6 +93,7 @@ export function ChipInput({ value, defaultValue = [], onChange, className, ...pr }); const [input, setInput] = React.useState(""); + const [editingIndex, setEditingIndex] = React.useState(null); const inputRef = React.useRef(null); const addChip = React.useCallback( @@ -30,18 +106,82 @@ export function ChipInput({ value, defaultValue = [], onChange, className, ...pr [chips, setChips], ); + const updateChip = React.useCallback( + (index: number, newValue: string) => { + const trimmed = newValue.trim(); + if (!trimmed || index < 0 || index >= chips.length) return; + + // Check if the new value already exists at a different index + const existingIndex = chips.findIndex((c, i) => c === trimmed && i !== index); + if (existingIndex !== -1) return; + + const newChips = [...chips]; + newChips[index] = trimmed; + setChips(newChips); + }, + [chips, setChips], + ); + const removeChip = React.useCallback( (index: number) => { if (index < 0 || index >= chips.length) return; const newChips = chips.slice(0, index).concat(chips.slice(index + 1)); - setChips(Array.from(new Set(newChips))); + setChips(newChips); + + // If we were editing this chip, clear edit mode + if (editingIndex === index) { + setEditingIndex(null); + setInput(""); + } else if (editingIndex !== null && editingIndex > index) { + // Adjust editing index if we removed a chip before it + setEditingIndex(editingIndex - 1); + } }, - [chips, setChips], + [chips, setChips, editingIndex], + ); + + const handleEdit = React.useCallback( + (index: number) => { + setEditingIndex(index); + setInput(chips[index]); + inputRef.current?.focus(); + }, + [chips], + ); + + const handleReorder = React.useCallback( + (newOrder: string[]) => { + // Update editingIndex to follow the chip being edited + if (editingIndex !== null) { + const editingChip = chips[editingIndex]; + const newIndex = newOrder.indexOf(editingChip); + if (newIndex !== -1 && newIndex !== editingIndex) { + setEditingIndex(newIndex); + } + } + setChips(newOrder); + }, + [chips, editingIndex, setChips], ); const handleInputChange = React.useCallback( (e: React.ChangeEvent) => { const newValue = e.target.value; + + // When editing, don't split on comma - allow full text editing + if (editingIndex !== null) { + if (newValue.includes(",")) { + // Save current edit + updateChip(editingIndex, newValue.replace(",", "")); + setEditingIndex(null); + setInput(""); + } else { + setInput(newValue); + } + return; + } + + // When adding new chips, split on comma if (newValue.includes(",")) { const parts = newValue.split(","); parts.slice(0, -1).forEach(addChip); @@ -50,74 +190,65 @@ export function ChipInput({ value, defaultValue = [], onChange, className, ...pr setInput(newValue); } }, - [addChip], + [addChip, editingIndex, updateChip], ); - const removeLastChip = React.useCallback(() => { - if (chips.length === 0) return; - const newChips = chips.slice(0, -1); - setChips(newChips); - }, [chips, setChips]); - const handleKeyDown = React.useCallback( (e: React.KeyboardEvent) => { - if ((e.key === "Enter" || e.key === ",") && input.trim() !== "") { + if (e.key === "Enter" || e.key === ",") { e.preventDefault(); - addChip(input); + + if (editingIndex !== null) { + // Save edit + if (input.trim()) { + updateChip(editingIndex, input); + } + setEditingIndex(null); + setInput(""); + } else if (input.trim()) { + // Add new chip + addChip(input); + setInput(""); + } + } else if (e.key === "Escape" && editingIndex !== null) { + // Cancel edit + setEditingIndex(null); setInput(""); - } else if (e.key === "Backspace" && input === "") { - removeLastChip(); } }, - [input, addChip, removeLastChip], + [input, addChip, editingIndex, updateChip], ); - const handleWrapperClick = React.useCallback(() => { - inputRef.current?.focus(); - }, []); - return ( -
+ {chips.length > 0 && ( + + {chips.map((chip, idx) => ( + + ))} + )} - {...props} - > -
- {chips.map((chip, idx) => ( -
- - {chip} - - -
- ))} -
- +
+ +

Press Enter or comma to save your keyword

+
); }