|
|
@@ -0,0 +1,148 @@ |
|
|
|
import * as React from "react"; |
|
|
|
|
|
|
|
const useDidUpdateEffect = (fn: () => void, inputs: unknown[]) => { |
|
|
|
const didMountRef = React.useRef(false); |
|
|
|
|
|
|
|
React.useEffect(() => { |
|
|
|
if (didMountRef.current) fn(); |
|
|
|
else didMountRef.current = true; |
|
|
|
}, inputs); |
|
|
|
} |
|
|
|
|
|
|
|
interface TagProps { |
|
|
|
text: string; |
|
|
|
remove: any; |
|
|
|
disabled?: boolean; |
|
|
|
className?: string; |
|
|
|
} |
|
|
|
|
|
|
|
export const Tag = ({ text, remove, disabled }: TagProps) => { |
|
|
|
const handleOnRemove: React.MouseEventHandler<HTMLButtonElement> = (e) => { |
|
|
|
e.stopPropagation(); |
|
|
|
remove(text); |
|
|
|
}; |
|
|
|
|
|
|
|
return ( |
|
|
|
<span className="text-xs p-2 select-none font-inherit"> |
|
|
|
<span>{text}</span> |
|
|
|
{!disabled && ( |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
onClick={handleOnRemove} |
|
|
|
aria-label={`remove ${text}`} |
|
|
|
> |
|
|
|
✕ |
|
|
|
</button> |
|
|
|
)} |
|
|
|
</span> |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
export interface TagsInputProps { |
|
|
|
name?: string; |
|
|
|
placeHolder?: string; |
|
|
|
value?: string[]; |
|
|
|
onChange?: (tags: string[]) => void; |
|
|
|
onBlur?: any; |
|
|
|
separators?: string[]; |
|
|
|
disableBackspaceRemove?: boolean; |
|
|
|
onExisting?: (tag: string) => void; |
|
|
|
onRemoved?: (tag: string) => void; |
|
|
|
disabled?: boolean; |
|
|
|
isEditOnRemove?: boolean; |
|
|
|
beforeAddValidate?: (tag: string, existingTags: string[]) => boolean; |
|
|
|
onKeyUp?: (e: React.KeyboardEvent<HTMLInputElement>) => void; |
|
|
|
classNames?: { |
|
|
|
input?: string; |
|
|
|
tag?: string; |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
const defaultSeparators = ["Enter"]; |
|
|
|
|
|
|
|
export const TagsInput = ({ |
|
|
|
name, |
|
|
|
placeHolder, |
|
|
|
value, |
|
|
|
onChange, |
|
|
|
onBlur, |
|
|
|
separators, |
|
|
|
disableBackspaceRemove, |
|
|
|
onExisting, |
|
|
|
onRemoved, |
|
|
|
disabled, |
|
|
|
isEditOnRemove, |
|
|
|
beforeAddValidate, |
|
|
|
onKeyUp, |
|
|
|
classNames, |
|
|
|
}: TagsInputProps) => { |
|
|
|
const [tags, setTags] = React.useState<any>(value || []); |
|
|
|
|
|
|
|
useDidUpdateEffect(() => { |
|
|
|
onChange && onChange(tags); |
|
|
|
}, [tags]); |
|
|
|
|
|
|
|
useDidUpdateEffect(() => { |
|
|
|
if (JSON.stringify(value) !== JSON.stringify(tags)) { |
|
|
|
setTags(value); |
|
|
|
} |
|
|
|
}, [value]); |
|
|
|
|
|
|
|
const handleOnKeyUp: React.KeyboardEventHandler<HTMLInputElement> = (e) => { |
|
|
|
e.stopPropagation(); |
|
|
|
|
|
|
|
const text = e.currentTarget.value; |
|
|
|
|
|
|
|
if ( |
|
|
|
!text && |
|
|
|
!disableBackspaceRemove && |
|
|
|
tags.length && |
|
|
|
e.key === "Backspace" |
|
|
|
) { |
|
|
|
e.currentTarget.value = isEditOnRemove ? `${tags.at(-1)} ` : ""; |
|
|
|
setTags([...tags.slice(0, -1)]); |
|
|
|
} |
|
|
|
|
|
|
|
if (text && (separators || defaultSeparators).includes(e.key)) { |
|
|
|
e.preventDefault(); |
|
|
|
if (beforeAddValidate && !beforeAddValidate(text, tags)) return; |
|
|
|
|
|
|
|
if (tags.includes(text)) { |
|
|
|
onExisting && onExisting(text); |
|
|
|
return; |
|
|
|
} |
|
|
|
setTags([...tags, text]); |
|
|
|
e.currentTarget.value = ""; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const onTagRemove = (text: string) => { |
|
|
|
setTags(tags.filter((tag: string) => tag !== text)); |
|
|
|
onRemoved && onRemoved(text); |
|
|
|
}; |
|
|
|
|
|
|
|
return ( |
|
|
|
<div aria-labelledby={name} className="rti--container"> |
|
|
|
{tags.map((tag: string) => ( |
|
|
|
<Tag |
|
|
|
key={tag} |
|
|
|
className={classNames?.tag} |
|
|
|
text={tag} |
|
|
|
remove={onTagRemove} |
|
|
|
disabled={disabled} |
|
|
|
/> |
|
|
|
))} |
|
|
|
|
|
|
|
<input |
|
|
|
className="peer bg-transparent font-inherit focus:outline-0" |
|
|
|
type="text" |
|
|
|
name={name} |
|
|
|
placeholder={placeHolder} |
|
|
|
onKeyDown={handleOnKeyUp} |
|
|
|
onBlur={onBlur} |
|
|
|
disabled={disabled} |
|
|
|
onKeyUp={onKeyUp} |
|
|
|
/> |
|
|
|
</div> |
|
|
|
); |
|
|
|
}; |