@nopendsui/tags-input
Version:
Tags Input is a component that allows users to input tags.
739 lines (732 loc) • 24.9 kB
JavaScript
'use client';
'use strict';
var React = require('react');
var shared = require('@nopendsui/shared');
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespace(React);
var ROOT_NAME = "TagsInputRoot";
var [TagsInputProvider, useTagsInput] = shared.createContext(ROOT_NAME);
var TagsInputRoot = React__namespace.forwardRef((props, ref) => {
const {
value: valueProp,
defaultValue,
onValueChange,
onValidate,
onInvalid,
displayValue = (value2) => value2.toString(),
addOnPaste = false,
addOnTab = false,
disabled = false,
editable = false,
loop = false,
blurBehavior,
delimiter = ",",
max = Number.POSITIVE_INFINITY,
readOnly = false,
required = false,
name,
children,
dir: dirProp,
id: idProp,
...rootProps
} = props;
const [value = [], setValue] = shared.useControllableState({
prop: valueProp,
defaultProp: defaultValue,
onChange: onValueChange
});
const [highlightedIndex, setHighlightedIndex] = React__namespace.useState(
null
);
const [editingIndex, setEditingIndex] = React__namespace.useState(null);
const [isInvalidInput, setIsInvalidInput] = React__namespace.useState(false);
const collectionRef = React__namespace.useRef(null);
const inputRef = React__namespace.useRef(null);
const id = shared.useId();
const inputId = shared.useId();
const labelId = shared.useId();
const dir = shared.useDirection(dirProp);
const { getEnabledItems } = shared.useItemCollection({
ref: collectionRef
});
const { isFormControl, onTriggerChange } = shared.useFormControl();
const composedRef = shared.useComposedRefs(
ref,
collectionRef,
(node) => onTriggerChange(node)
);
const onItemAdd = React__namespace.useCallback(
(textValue, options) => {
if (disabled || readOnly) return false;
if (addOnPaste && options?.viaPaste) {
const splitValues = textValue.split(delimiter).map((v) => v.trim()).filter(Boolean);
if (value.length + splitValues.length > max && max > 0) {
onInvalid?.(textValue);
return false;
}
let newValues2 = [];
for (const v of splitValues) {
if (value.includes(v)) {
onInvalid?.(v);
}
}
newValues2 = [...new Set(splitValues.filter((v) => !value.includes(v)))];
const validValues = newValues2.filter(
(v) => !onValidate || onValidate(v)
);
if (validValues.length === 0) return false;
setValue([...value, ...validValues]);
return true;
}
if (value.length >= max && max > 0) {
onInvalid?.(textValue);
return false;
}
const trimmedValue = textValue.trim();
if (onValidate && !onValidate(trimmedValue)) {
setIsInvalidInput(true);
onInvalid?.(trimmedValue);
return false;
}
const exists = value.some((v) => {
const valueToCompare = v;
return valueToCompare === trimmedValue;
});
if (exists) {
setIsInvalidInput(true);
onInvalid?.(trimmedValue);
return true;
}
const newValue = trimmedValue;
const newValues = [...value, newValue];
setValue(newValues);
setHighlightedIndex(null);
setEditingIndex(null);
setIsInvalidInput(false);
return true;
},
[
value,
max,
addOnPaste,
delimiter,
setValue,
onInvalid,
onValidate,
disabled,
readOnly
]
);
const onItemUpdate = React__namespace.useCallback(
(index, newTextValue) => {
if (disabled || readOnly) return;
if (index !== -1) {
const trimmedValue = newTextValue.trim();
const exists = value.some((v, i) => {
if (i === index) return false;
const valueToCompare = v;
return valueToCompare === trimmedValue;
});
if (exists) {
setIsInvalidInput(true);
onInvalid?.(trimmedValue);
return;
}
if (onValidate && !onValidate(trimmedValue)) {
setIsInvalidInput(true);
onInvalid?.(trimmedValue);
return;
}
const updatedValue = displayValue(trimmedValue);
const newValues = [...value];
newValues[index] = updatedValue;
setValue(newValues);
setHighlightedIndex(index);
setEditingIndex(null);
setIsInvalidInput(false);
requestAnimationFrame(() => inputRef.current?.focus());
}
},
[value, setValue, displayValue, onInvalid, onValidate, disabled, readOnly]
);
const onItemRemove = React__namespace.useCallback(
(index) => {
if (disabled || readOnly) return;
if (index !== -1) {
const newValues = [...value];
newValues.splice(index, 1);
setValue(newValues);
setHighlightedIndex(null);
setEditingIndex(null);
inputRef.current?.focus();
}
},
[value, setValue, disabled, readOnly]
);
const onItemLeave = React__namespace.useCallback(() => {
setHighlightedIndex(null);
setEditingIndex(null);
inputRef.current?.focus();
}, []);
const onInputKeydown = React__namespace.useCallback(
(event) => {
const target = event.target;
if (!(target instanceof HTMLInputElement)) return;
const isArrowLeft = event.key === "ArrowLeft" && dir === "ltr" || event.key === "ArrowRight" && dir === "rtl";
const isArrowRight = event.key === "ArrowRight" && dir === "ltr" || event.key === "ArrowLeft" && dir === "rtl";
if (target.value && target.selectionStart !== 0) {
setHighlightedIndex(null);
setEditingIndex(null);
return;
}
function findNextEnabledIndex(currentIndex, direction) {
const collectionElement = collectionRef.current;
if (!collectionElement) return null;
const enabledItems = getEnabledItems();
const enabledIndices = enabledItems.map((_, index) => index);
if (enabledIndices.length === 0) return null;
if (currentIndex === null) {
return direction === "prev" ? enabledIndices[enabledIndices.length - 1] ?? null : enabledIndices[0] ?? null;
}
const currentEnabledIndex = enabledIndices.indexOf(currentIndex);
if (direction === "next") {
return currentEnabledIndex >= enabledIndices.length - 1 ? loop ? enabledIndices[0] ?? null : null : enabledIndices[currentEnabledIndex + 1] ?? null;
}
return currentEnabledIndex <= 0 ? loop ? enabledIndices[enabledIndices.length - 1] ?? null : null : enabledIndices[currentEnabledIndex - 1] ?? null;
}
switch (event.key) {
case "Delete":
case "Backspace": {
if (target.selectionStart !== 0 || target.selectionEnd !== 0) break;
if (highlightedIndex !== null) {
const newIndex = findNextEnabledIndex(highlightedIndex, "prev");
onItemRemove(highlightedIndex);
setHighlightedIndex(newIndex);
event.preventDefault();
} else if (event.key === "Backspace" && value.length > 0) {
const lastIndex = findNextEnabledIndex(null, "prev");
setHighlightedIndex(lastIndex);
event.preventDefault();
}
break;
}
case "Enter": {
if (highlightedIndex !== null && editable && !disabled) {
setEditingIndex(highlightedIndex);
event.preventDefault();
return;
}
break;
}
case "ArrowLeft":
case "ArrowRight": {
if (target.selectionStart === 0 && isArrowLeft && highlightedIndex === null && value.length > 0) {
const lastIndex = findNextEnabledIndex(null, "prev");
setHighlightedIndex(lastIndex);
event.preventDefault();
} else if (highlightedIndex !== null) {
const nextIndex = findNextEnabledIndex(
highlightedIndex,
isArrowLeft ? "prev" : "next"
);
if (nextIndex !== null) {
setHighlightedIndex(nextIndex);
event.preventDefault();
} else if (isArrowRight) {
setHighlightedIndex(null);
requestAnimationFrame(() => target.setSelectionRange(0, 0));
}
}
break;
}
case "Home": {
if (highlightedIndex !== null) {
const firstIndex = findNextEnabledIndex(null, "next");
setHighlightedIndex(firstIndex);
event.preventDefault();
}
break;
}
case "End": {
if (highlightedIndex !== null) {
const lastIndex = findNextEnabledIndex(null, "prev");
setHighlightedIndex(lastIndex);
event.preventDefault();
}
break;
}
case "Escape": {
setHighlightedIndex(null);
setEditingIndex(null);
requestAnimationFrame(() => target.setSelectionRange(0, 0));
break;
}
}
},
[
highlightedIndex,
value,
onItemRemove,
dir,
editable,
disabled,
loop,
getEnabledItems
]
);
const getIsClickedInEmptyRoot = React__namespace.useCallback((target) => {
return collectionRef.current?.contains(target) && !target.hasAttribute(shared.DATA_ITEM_ATTR) && target.tagName !== "INPUT";
}, []);
return /* @__PURE__ */ React__namespace.createElement(
TagsInputProvider,
{
value,
onValueChange: setValue,
onItemAdd,
onItemRemove,
onItemUpdate,
onInputKeydown,
highlightedIndex,
setHighlightedIndex,
editingIndex,
setEditingIndex,
displayValue,
onItemLeave,
inputRef,
isInvalidInput,
addOnPaste,
addOnTab,
disabled,
editable,
loop,
readOnly,
blurBehavior,
delimiter,
max,
dir,
id,
inputId,
labelId
},
/* @__PURE__ */ React__namespace.createElement(
shared.Primitive.div,
{
id,
"data-disabled": disabled ? "" : void 0,
"data-invalid": isInvalidInput ? "" : void 0,
"data-readonly": readOnly ? "" : void 0,
dir,
...rootProps,
ref: composedRef,
onClick: shared.composeEventHandlers(rootProps.onClick, (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
if (getIsClickedInEmptyRoot(target) && document.activeElement !== inputRef.current) {
event.currentTarget.focus();
inputRef.current?.focus();
}
}),
onMouseDown: shared.composeEventHandlers(rootProps.onMouseDown, (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
if (getIsClickedInEmptyRoot(target)) {
event.preventDefault();
}
}),
onBlur: shared.composeEventHandlers(rootProps.onBlur, (event) => {
if (event.relatedTarget !== inputRef.current && !collectionRef.current?.contains(event.relatedTarget)) {
requestAnimationFrame(() => setHighlightedIndex(null));
}
})
},
typeof children === "function" ? /* @__PURE__ */ React__namespace.createElement(React__namespace.Fragment, null, children({ value })) : children,
isFormControl && name && /* @__PURE__ */ React__namespace.createElement(
shared.VisuallyHiddenInput,
{
type: "hidden",
control: collectionRef.current,
name,
value,
disabled,
required
}
)
)
);
});
TagsInputRoot.displayName = ROOT_NAME;
var Root = TagsInputRoot;
var LABEL_NAME = "TagsInputLabel";
var TagsInputLabel = React__namespace.forwardRef(
(props, ref) => {
const context = useTagsInput(LABEL_NAME);
return /* @__PURE__ */ React__namespace.createElement(
shared.Primitive.label,
{
id: context.labelId,
htmlFor: context.inputId,
...props,
ref
}
);
}
);
TagsInputLabel.displayName = LABEL_NAME;
var Label = TagsInputLabel;
var ITEM_NAME = "TagsInputItem";
var [TagsInputItemProvider, useTagsInputItem] = shared.createContext(ITEM_NAME);
var TagsInputItem = React__namespace.forwardRef(
(props, ref) => {
const { value, disabled, ...itemProps } = props;
const pointerTypeRef = React__namespace.useRef("touch");
const context = useTagsInput(ITEM_NAME);
const id = shared.useId();
const textId = `${id}text`;
const index = context.value.indexOf(value);
const isHighlighted = index === context.highlightedIndex;
const isEditing = index === context.editingIndex;
const itemDisabled = disabled || context.disabled;
const displayValue = context.displayValue(value);
const onItemSelect = React__namespace.useCallback(() => {
context.setHighlightedIndex(index);
context.inputRef.current?.focus();
}, [context.setHighlightedIndex, context.inputRef, index]);
return /* @__PURE__ */ React__namespace.createElement(
TagsInputItemProvider,
{
id,
value,
index,
isHighlighted,
isEditing,
disabled: itemDisabled,
textId,
displayValue
},
/* @__PURE__ */ React__namespace.createElement(
shared.Primitive.div,
{
id,
"aria-labelledby": textId,
"aria-current": isHighlighted,
"aria-disabled": itemDisabled,
...{ [shared.DATA_ITEM_ATTR]: "" },
"data-state": isHighlighted ? "active" : "inactive",
"data-highlighted": isHighlighted ? "" : void 0,
"data-editing": isEditing ? "" : void 0,
"data-editable": context.editable ? "" : void 0,
"data-disabled": itemDisabled ? "" : void 0,
...itemProps,
ref,
onClick: shared.composeEventHandlers(itemProps.onClick, (event) => {
event.stopPropagation();
if (!isEditing && pointerTypeRef.current !== "mouse") {
onItemSelect();
}
}),
onDoubleClick: shared.composeEventHandlers(itemProps.onDoubleClick, () => {
if (context.editable && !itemDisabled) {
requestAnimationFrame(() => context.setEditingIndex(index));
}
}),
onPointerUp: shared.composeEventHandlers(itemProps.onPointerUp, () => {
if (pointerTypeRef.current === "mouse") onItemSelect();
}),
onPointerDown: shared.composeEventHandlers(
itemProps.onPointerDown,
(event) => pointerTypeRef.current = event.pointerType
),
onPointerMove: shared.composeEventHandlers(
itemProps.onPointerMove,
(event) => {
pointerTypeRef.current = event.pointerType;
if (disabled) {
context.onItemLeave();
} else if (pointerTypeRef.current === "mouse") {
event.currentTarget.focus({ preventScroll: true });
}
}
),
onPointerLeave: shared.composeEventHandlers(
itemProps.onPointerLeave,
(event) => {
if (event.currentTarget === document.activeElement) {
context.onItemLeave();
}
}
)
}
)
);
}
);
TagsInputItem.displayName = ITEM_NAME;
var Item = TagsInputItem;
var INPUT_NAME = "TagsInputInput";
var TagsInputInput = React__namespace.forwardRef(
(props, ref) => {
const { autoFocus, ...inputProps } = props;
const context = useTagsInput(INPUT_NAME);
const onTab = React__namespace.useCallback(
(event) => {
if (!context.addOnTab) return;
onCustomKeydown(event);
},
[context.addOnTab]
);
const onCustomKeydown = React__namespace.useCallback(
(event) => {
if (event.defaultPrevented) return;
const value = event.currentTarget.value;
if (!value) return;
const isAdded = context.onItemAdd(value);
if (isAdded) {
event.currentTarget.value = "";
context.setHighlightedIndex(null);
}
event.preventDefault();
},
[context.onItemAdd, context.setHighlightedIndex]
);
React__namespace.useEffect(() => {
if (!autoFocus) return;
const animationFrameId = requestAnimationFrame(
() => context.inputRef.current?.focus()
);
return () => cancelAnimationFrame(animationFrameId);
}, [autoFocus, context.inputRef]);
return /* @__PURE__ */ React__namespace.createElement(
shared.Primitive.input,
{
type: "text",
id: context.inputId,
autoCapitalize: "off",
autoComplete: "off",
autoCorrect: "off",
spellCheck: "false",
autoFocus,
"aria-labelledby": context.labelId,
"aria-readonly": context.readOnly,
"data-invalid": context.isInvalidInput ? "" : void 0,
dir: context.dir,
disabled: context.disabled,
readOnly: context.readOnly,
...inputProps,
ref: shared.composeRefs(context.inputRef, ref),
onBlur: shared.composeEventHandlers(inputProps.onBlur, (event) => {
if (context.readOnly) return;
if (context.blurBehavior === "add") {
const value = event.target.value;
if (value) {
const isAdded = context.onItemAdd(value);
if (isAdded) event.target.value = "";
}
}
if (context.blurBehavior === "clear") {
event.target.value = "";
}
}),
onChange: shared.composeEventHandlers(inputProps.onChange, (event) => {
if (context.readOnly) return;
const target = event.target;
if (!(target instanceof HTMLInputElement)) return;
const delimiter = context.delimiter;
if (delimiter === target.value.slice(-1)) {
const value = target.value.slice(0, -1);
target.value = "";
if (value) {
context.onItemAdd(value);
context.setHighlightedIndex(null);
}
}
}),
onKeyDown: shared.composeEventHandlers(inputProps.onKeyDown, (event) => {
if (context.readOnly) return;
if (event.key === "Enter") onCustomKeydown(event);
if (event.key === "Tab") onTab(event);
context.onInputKeydown(event);
if (event.key.length === 1) context.setHighlightedIndex(null);
}),
onPaste: shared.composeEventHandlers(inputProps.onPaste, (event) => {
if (context.readOnly) return;
if (context.addOnPaste) {
event.preventDefault();
const value = event.clipboardData.getData("text");
context.onItemAdd(value, { viaPaste: true });
context.setHighlightedIndex(null);
}
})
}
);
}
);
TagsInputInput.displayName = INPUT_NAME;
var Input = TagsInputInput;
var ITEM_DELETE_NAME = "TagsInputItemDelete";
var TagsInputItemDelete = React__namespace.forwardRef((props, ref) => {
const context = useTagsInput(ITEM_DELETE_NAME);
const itemContext = useTagsInputItem(ITEM_DELETE_NAME);
const disabled = itemContext.disabled || context.disabled;
if (itemContext.isEditing) return null;
return /* @__PURE__ */ React__namespace.createElement(
shared.Primitive.button,
{
type: "button",
tabIndex: disabled ? void 0 : -1,
"aria-labelledby": itemContext.textId,
"aria-controls": itemContext.id,
"aria-current": itemContext.isHighlighted,
"data-state": itemContext.isHighlighted ? "active" : "inactive",
"data-disabled": disabled ? "" : void 0,
...props,
ref,
onClick: shared.composeEventHandlers(props.onClick, () => {
if (disabled) return;
const index = context.value.findIndex((i) => i === itemContext.value);
context.onItemRemove(index);
})
}
);
});
TagsInputItemDelete.displayName = ITEM_DELETE_NAME;
var ItemDelete = TagsInputItemDelete;
var ITEM_TEXT_NAME = "TagsInputItemText";
var TagsInputItemText = React__namespace.forwardRef((props, ref) => {
const { children, ...itemTextProps } = props;
const context = useTagsInput(ITEM_TEXT_NAME);
const itemContext = useTagsInputItem(ITEM_TEXT_NAME);
if (itemContext.isEditing && context.editable && !itemContext.disabled) {
return /* @__PURE__ */ React__namespace.createElement(TagsInputEditableItemText, null);
}
return /* @__PURE__ */ React__namespace.createElement(shared.Primitive.span, { id: itemContext.textId, ...itemTextProps, ref }, children ?? itemContext.displayValue);
});
var ItemText = TagsInputItemText;
function TagsInputEditableItemText() {
const context = useTagsInput(ITEM_TEXT_NAME);
const itemContext = useTagsInputItem(ITEM_TEXT_NAME);
const [editValue, setEditValue] = React__namespace.useState(itemContext.displayValue);
const onBlur = React__namespace.useCallback(() => {
setEditValue(itemContext.displayValue);
context.setEditingIndex(null);
}, [context.setEditingIndex, itemContext.displayValue]);
const onChange = React__namespace.useCallback(
(event) => {
const target = event.target;
target.style.width = "0";
target.style.width = `${target.scrollWidth + 4}px`;
setEditValue(event.target.value);
},
[]
);
const onFocus = React__namespace.useCallback(
(event) => {
event.target.select();
event.target.style.width = "0";
event.target.style.width = `${event.target.scrollWidth + 4}px`;
},
[]
);
const onKeyDown = React__namespace.useCallback(
(event) => {
if (event.key === "Enter") {
const index = context.value.findIndex((v) => v === itemContext.value);
context.onItemUpdate(index, editValue);
} else if (event.key === "Escape") {
setEditValue(itemContext.displayValue);
context.setEditingIndex(null);
context.inputRef.current?.focus();
}
event.stopPropagation();
},
[
context.value,
context.onItemUpdate,
context.setEditingIndex,
itemContext.displayValue,
context.inputRef.current?.focus,
editValue,
itemContext.value
]
);
return /* @__PURE__ */ React__namespace.createElement(
shared.Primitive.input,
{
type: "text",
autoCapitalize: "off",
autoComplete: "off",
autoCorrect: "off",
spellCheck: "false",
autoFocus: true,
"aria-describedby": itemContext.textId,
value: editValue,
onChange,
onKeyDown,
onFocus,
onBlur,
style: {
outline: "none",
background: "inherit",
border: "none",
font: "inherit",
color: "inherit",
padding: 0,
minWidth: "1ch"
}
}
);
}
var CLEAR_NAME = "TagsInputClear";
var TagsInputClear = React__namespace.forwardRef(
(props, ref) => {
const { forceMount, ...clearProps } = props;
const context = useTagsInput(CLEAR_NAME);
return /* @__PURE__ */ React__namespace.createElement(shared.Presence, { present: forceMount || context.value.length > 0 }, /* @__PURE__ */ React__namespace.createElement(
shared.Primitive.button,
{
type: "button",
"aria-disabled": context.disabled,
"data-state": context.value.length > 0 ? "visible" : "invisible",
"data-disabled": context.disabled ? "" : void 0,
...clearProps,
ref,
onClick: shared.composeEventHandlers(props.onClick, () => {
if (context.disabled) return;
context.onValueChange([]);
context.inputRef.current?.focus();
})
}
));
}
);
TagsInputClear.displayName = CLEAR_NAME;
var Clear = TagsInputClear;
exports.Clear = Clear;
exports.Input = Input;
exports.Item = Item;
exports.ItemDelete = ItemDelete;
exports.ItemText = ItemText;
exports.Label = Label;
exports.Root = Root;
exports.TagsInputClear = TagsInputClear;
exports.TagsInputInput = TagsInputInput;
exports.TagsInputItem = TagsInputItem;
exports.TagsInputItemDelete = TagsInputItemDelete;
exports.TagsInputItemText = TagsInputItemText;
exports.TagsInputLabel = TagsInputLabel;
exports.TagsInputRoot = TagsInputRoot;