From 0556d68a4e5bd8b2c19e9e3c9b1d596ea2d88926 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 23 Dec 2024 22:22:00 +0800 Subject: [PATCH] feat(ui): MultipleInput --- ui/src/components/ui/MultipleInput.tsx | 275 +++++++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 ui/src/components/ui/MultipleInput.tsx diff --git a/ui/src/components/ui/MultipleInput.tsx b/ui/src/components/ui/MultipleInput.tsx new file mode 100644 index 00000000..f9887b05 --- /dev/null +++ b/ui/src/components/ui/MultipleInput.tsx @@ -0,0 +1,275 @@ +import { forwardRef, useImperativeHandle, useMemo, useRef, type ChangeEvent } from "react"; +import { useTranslation } from "react-i18next"; +import { useControllableValue } from "ahooks"; +import { Button, Input, Space, type InputRef, type InputProps } from "antd"; +import { produce } from "immer"; +import { ArrowDown as ArrowDownIcon, ArrowUp as ArrowUpIcon, Minus as MinusIcon, Plus as PlusIcon } from "lucide-react"; + +export type MultipleInputProps = Omit & { + allowClear?: boolean; + defaultValue?: string[]; + maxCount?: number; + minCount?: number; + showSortButton?: boolean; + value?: string[]; + onChange?: (index: number, e: ChangeEvent) => void; + onCreate?: (index: number) => void; + onRemove?: (index: number) => void; + onSort?: (oldIndex: number, newIndex: number) => void; + onValueChange?: (value: string[]) => void; +}; + +const MultipleInput = ({ + allowClear = false, + disabled, + maxCount, + minCount, + showSortButton = true, + onChange, + onCreate, + onSort, + onRemove, + ...props +}: MultipleInputProps) => { + const { t } = useTranslation(); + + const itemRefs = useRef([]); + + const [value, setValue] = useControllableValue(props, { + valuePropName: "value", + defaultValue: [], + defaultValuePropName: "defaultValue", + trigger: "onValueChange", + }); + + const handleCreate = () => { + const newValue = produce(value, (draft) => { + draft.push(""); + }); + setValue(newValue); + setTimeout(() => itemRefs.current[newValue.length - 1]?.focus(), 0); + + onCreate?.(newValue.length - 1); + }; + + const handleInputChange = (index: number, e: ChangeEvent) => { + const newValue = produce(value, (draft) => { + draft[index] = e.target.value; + }); + setValue(newValue); + + onChange?.(index, e); + }; + + const handleInputBlur = (index: number) => { + if (!allowClear && !value[index]) { + const newValue = produce(value, (draft) => { + draft.splice(index, 1); + }); + setValue(newValue); + } + }; + + const handleClickUp = (index: number) => { + if (index === 0) { + return; + } + + const newValue = produce(value, (draft) => { + const temp = draft[index - 1]; + draft[index - 1] = draft[index]; + draft[index] = temp; + }); + setValue(newValue); + + onSort?.(index, index - 1); + }; + + const handleClickDown = (index: number) => { + if (index === value.length - 1) { + return; + } + + const newValue = produce(value, (draft) => { + const temp = draft[index + 1]; + draft[index + 1] = draft[index]; + draft[index] = temp; + }); + setValue(newValue); + + onSort?.(index, index + 1); + }; + + const handleClickAdd = (index: number) => { + const newValue = produce(value, (draft) => { + draft.splice(index + 1, 0, ""); + }); + setValue(newValue); + setTimeout(() => itemRefs.current[index + 1]?.focus(), 0); + + onCreate?.(index + 1); + }; + + const handleClickRemove = (index: number) => { + const newValue = produce(value, (draft) => { + draft.splice(index, 1); + }); + setValue(newValue); + + onRemove?.(index); + }; + + return value == null || value.length === 0 ? ( + + ) : ( + + {value.map((element, index) => { + const allowUp = index > 0; + const allowDown = index < value.length - 1; + const allowRemove = minCount == null || value.length > minCount; + const allowAdd = maxCount == null || value.length < maxCount; + + return ( + (itemRefs.current[index] = ref!)} + allowAdd={allowAdd} + allowClear={allowClear} + allowDown={allowDown} + allowRemove={allowRemove} + allowUp={allowUp} + disabled={disabled} + defaultValue={undefined} + showSortButton={showSortButton} + value={element} + onBlur={() => handleInputBlur(index)} + onChange={(val) => handleInputChange(index, val)} + onClickAdd={() => handleClickAdd(index)} + onClickDown={() => handleClickDown(index)} + onClickUp={() => handleClickUp(index)} + onClickRemove={() => handleClickRemove(index)} + onValueChange={undefined} + /> + ); + })} + + ); +}; + +type MultipleInputItemProps = Omit< + MultipleInputProps, + "defaultValue" | "maxCount" | "minCount" | "preset" | "value" | "onChange" | "onCreate" | "onRemove" | "onSort" | "onValueChange" +> & { + allowAdd: boolean; + allowRemove: boolean; + allowUp: boolean; + allowDown: boolean; + defaultValue?: string; + value?: string; + onChange?: (e: ChangeEvent) => void; + onClickAdd?: () => void; + onClickDown?: () => void; + onClickUp?: () => void; + onClickRemove?: () => void; + onValueChange?: (value: string) => void; +}; + +type MultipleInputItemInstance = { + focus: () => void; + blur: () => void; + select: () => void; +}; + +const MultipleInputItem = forwardRef( + ( + { + allowAdd, + allowClear, + allowDown, + allowRemove, + allowUp, + disabled, + showSortButton, + onChange, + onClickAdd, + onClickDown, + onClickUp, + onClickRemove, + ...props + }: MultipleInputItemProps, + ref + ) => { + const inputRef = useRef(null); + + const [value, setValue] = useControllableValue(props, { + valuePropName: "value", + defaultValue: "", + defaultValuePropName: "defaultValue", + trigger: "onValueChange", + }); + + const upBtn = useMemo(() => { + if (!showSortButton) return null; + return