@nopendsui/mention
Version:
Mention is a component that allows to mention items in a list by a trigger character.
1,325 lines (1,318 loc) • 50.2 kB
JavaScript
'use client';
'use strict';
var shared = require('@nopendsui/shared');
var React4 = require('react');
var react = require('@floating-ui/react');
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 React4__namespace = /*#__PURE__*/_interopNamespace(React4);
// src/mention-root.tsx
function getDataState(open) {
return open ? "open" : "closed";
}
var ROOT_NAME = "MentionRoot";
var [MentionProvider, useMentionContext] = shared.createContext(ROOT_NAME);
var MentionRoot = React4__namespace.forwardRef(
(props, forwardedRef) => {
const {
children,
open: openProp,
defaultOpen = false,
onOpenChange: onOpenChangeProp,
inputValue: inputValueProp,
onInputValueChange,
value: valueProp,
defaultValue,
onValueChange,
onFilter,
trigger: triggerProp = "@",
dir: dirProp,
disabled = false,
exactMatch = false,
loop = false,
modal = false,
readonly = false,
required = false,
name,
...rootProps
} = props;
const listRef = React4__namespace.useRef(null);
const inputRef = React4__namespace.useRef(null);
const inputId = shared.useId();
const labelId = shared.useId();
const listId = shared.useId();
const { collectionRef, itemMap, getItems, onItemRegister } = shared.useCollection();
const { isFormControl, onTriggerChange } = shared.useFormControl();
const composedRef = shared.composeRefs(
forwardedRef,
collectionRef,
(node) => onTriggerChange(node)
);
const dir = shared.useDirection(dirProp);
const [open = false, setOpen] = shared.useControllableState({
prop: openProp,
defaultProp: defaultOpen,
onChange: onOpenChangeProp
});
const [value = [], setValue] = shared.useControllableState({
prop: valueProp,
defaultProp: defaultValue,
onChange: onValueChange
});
const [inputValue = "", setInputValue] = shared.useControllableState({
prop: inputValueProp,
defaultProp: "",
onChange: onInputValueChange
});
const [trigger, setTrigger] = React4__namespace.useState(triggerProp);
const [virtualAnchor, setVirtualAnchor] = React4__namespace.useState(null);
const [highlightedItem, setHighlightedItem] = React4__namespace.useState(null);
const [mentions, setMentions] = React4__namespace.useState([]);
const [isPasting, setIsPasting] = React4__namespace.useState(false);
const { filterStore, onItemsFilter, getIsItemVisible } = shared.useFilterStore({
itemMap,
onFilter,
exactMatch,
onCallback: (itemCount) => {
if (itemCount === 0) {
setOpen(false);
setHighlightedItem(null);
setVirtualAnchor(null);
}
}
});
const getEnabledItems = React4__namespace.useCallback(() => {
return getItems().filter((item) => !item.disabled);
}, [getItems]);
const onOpenChange = React4__namespace.useCallback(
(open2) => {
if (open2 && filterStore.search && filterStore.itemCount === 0) {
return;
}
setOpen(open2);
if (open2) {
requestAnimationFrame(() => {
const items = getEnabledItems();
const firstItem = items[0] ?? null;
setHighlightedItem(firstItem);
});
} else {
setHighlightedItem(null);
setVirtualAnchor(null);
}
},
[setOpen, getEnabledItems, filterStore]
);
const { onHighlightMove } = shared.useListHighlighting({
highlightedItem,
onHighlightedItemChange: setHighlightedItem,
getItems: React4__namespace.useCallback(() => {
return getItems().filter(
(item) => !item.disabled && getIsItemVisible(item.value)
);
}, [getItems, getIsItemVisible]),
getIsItemSelected: (item) => value.includes(item.value),
loop
});
const onMentionAdd = React4__namespace.useCallback(
(payloadValue, triggerIndex) => {
const input = inputRef.current;
if (!input) return;
const mentionLabel = getEnabledItems().find((item) => item.value === payloadValue)?.label ?? payloadValue;
const mentionText = `${trigger}${mentionLabel}`;
const beforeTrigger = input.value.slice(0, triggerIndex);
const afterSearchText = input.value.slice(
input.selectionStart ?? triggerIndex
);
const newValue = `${beforeTrigger}${mentionText} ${afterSearchText}`;
const newMention = {
value: payloadValue,
start: triggerIndex,
end: triggerIndex + mentionText.length
};
setMentions((prev) => [...prev, newMention]);
input.value = newValue;
setInputValue(newValue);
setValue((prev) => [...prev ?? [], payloadValue]);
const newCursorPosition = triggerIndex + mentionText.length + 1;
input.setSelectionRange(newCursorPosition, newCursorPosition);
setOpen(false);
setHighlightedItem(null);
filterStore.search = "";
},
[trigger, setInputValue, setValue, setOpen, getEnabledItems, filterStore]
);
const onMentionsRemove = React4__namespace.useCallback(
(mentionsToRemove) => {
setMentions(
(prev) => prev.filter(
(mention) => !mentionsToRemove.some((m) => m.value === mention.value)
)
);
},
[]
);
return /* @__PURE__ */ React4__namespace.createElement(
MentionProvider,
{
open,
onOpenChange,
inputValue,
onInputValueChange: setInputValue,
value,
onValueChange: setValue,
virtualAnchor,
onVirtualAnchorChange: setVirtualAnchor,
trigger,
onTriggerChange: setTrigger,
getEnabledItems,
onItemRegister,
filterStore,
onFilter,
onItemsFilter,
getIsItemVisible,
highlightedItem,
onHighlightedItemChange: setHighlightedItem,
onHighlightMove,
mentions,
onMentionsChange: setMentions,
onMentionAdd,
onMentionsRemove,
isPasting,
onIsPastingChange: setIsPasting,
dir,
disabled,
exactMatch,
loop,
modal,
readonly,
inputRef,
listRef,
inputId,
labelId,
listId
},
/* @__PURE__ */ React4__namespace.createElement(shared.Primitive.div, { ref: composedRef, ...rootProps }, children, isFormControl && name && /* @__PURE__ */ React4__namespace.createElement(
shared.VisuallyHiddenInput,
{
type: "hidden",
control: collectionRef.current,
name,
value,
disabled,
readOnly: readonly,
required
}
))
);
}
);
MentionRoot.displayName = ROOT_NAME;
var Root = MentionRoot;
var LABEL_NAME = "MentionLabel";
var MentionLabel = React4__namespace.forwardRef(
(props, forwardedRef) => {
const context = useMentionContext(LABEL_NAME);
return /* @__PURE__ */ React4__namespace.createElement(
shared.Primitive.label,
{
ref: forwardedRef,
id: context.labelId,
htmlFor: context.inputId,
...props
}
);
}
);
MentionLabel.displayName = LABEL_NAME;
var Label = MentionLabel;
var HIGHLIGHTER_NAME = "MentionHighlighter";
var defaultHighlighterStyle = {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
color: "transparent",
whiteSpace: "pre-wrap",
wordWrap: "break-word",
pointerEvents: "none",
userSelect: "none",
overflow: "hidden",
width: "100%"
};
var MentionHighlighter = React4__namespace.memo(
React4__namespace.forwardRef(
(props, forwardedRef) => {
const { style, ...highlighterProps } = props;
const context = useMentionContext(HIGHLIGHTER_NAME);
const highlighterRef = React4__namespace.useRef(null);
const composedRef = shared.useComposedRefs(forwardedRef, highlighterRef);
const [inputStyle, setInputStyle] = React4__namespace.useState();
const onInputStyleChangeCallback = shared.useCallbackRef(setInputStyle);
const onInputStyleChange = React4__namespace.useCallback(() => {
const inputElement = context.inputRef.current;
if (!inputElement) return;
const computedStyle = window.getComputedStyle(inputElement);
onInputStyleChangeCallback(computedStyle);
}, [context.inputRef, onInputStyleChangeCallback]);
const onSyncScrollAndResize = React4__namespace.useCallback(() => {
const inputElement = context.inputRef.current;
const highlighterElement = highlighterRef.current;
if (!inputElement || !highlighterElement) return;
requestAnimationFrame(() => {
highlighterElement.scrollTop = inputElement.scrollTop;
highlighterElement.scrollLeft = inputElement.scrollLeft;
highlighterElement.style.height = `${inputElement.offsetHeight}px`;
});
}, [context.inputRef]);
React4__namespace.useEffect(() => {
const inputElement = context.inputRef.current;
if (!inputElement) return;
onInputStyleChange();
function onResize() {
onInputStyleChange();
onSyncScrollAndResize();
}
const resizeObserver = new ResizeObserver(onResize);
const mutationObserver = new MutationObserver((mutations) => {
if (mutations.some(
(m) => m.type === "attributes" && m.attributeName === "class"
)) {
onResize();
}
});
inputElement.addEventListener("scroll", onSyncScrollAndResize, {
passive: true
});
window.addEventListener("resize", onSyncScrollAndResize, {
passive: true
});
resizeObserver.observe(inputElement);
mutationObserver.observe(inputElement, {
attributes: true,
attributeFilter: ["class"]
});
return () => {
inputElement.removeEventListener("scroll", onSyncScrollAndResize);
window.removeEventListener("resize", onSyncScrollAndResize);
resizeObserver.disconnect();
mutationObserver.disconnect();
};
}, [context.inputRef, onInputStyleChange, onSyncScrollAndResize]);
const highlighterStyle = React4__namespace.useMemo(() => {
if (!inputStyle) return defaultHighlighterStyle;
return {
...defaultHighlighterStyle,
fontStyle: inputStyle.fontStyle,
fontVariant: inputStyle.fontVariant,
fontWeight: inputStyle.fontWeight,
fontSize: inputStyle.fontSize,
lineHeight: inputStyle.lineHeight,
fontFamily: inputStyle.fontFamily,
letterSpacing: inputStyle.letterSpacing,
textTransform: inputStyle.textTransform,
textIndent: inputStyle.textIndent,
padding: inputStyle.padding,
borderWidth: inputStyle.borderWidth,
borderStyle: inputStyle.borderStyle,
borderColor: "currentColor",
borderRadius: inputStyle.borderRadius,
boxSizing: inputStyle.boxSizing,
wordBreak: inputStyle.wordBreak,
overflowWrap: inputStyle.overflowWrap,
direction: context.dir,
...style
};
}, [inputStyle, style, context.dir]);
const onSegmentsRender = React4__namespace.useCallback(() => {
const inputElement = context.inputRef.current;
if (!inputElement) return null;
const { value } = inputElement;
const segments = [];
let lastIndex = 0;
for (const { start, end } of context.mentions) {
if (start > lastIndex) {
segments.push(
/* @__PURE__ */ React4__namespace.createElement("span", { key: `text-${lastIndex}` }, value.slice(lastIndex, start))
);
}
segments.push(
/* @__PURE__ */ React4__namespace.createElement("span", { key: `mention-${start}`, "data-tag": "" }, value.slice(start, end))
);
lastIndex = end;
}
if (lastIndex < value.length) {
segments.push(
/* @__PURE__ */ React4__namespace.createElement("span", { key: `text-end-${value.length}` }, value.slice(lastIndex))
);
}
segments.push(/* @__PURE__ */ React4__namespace.createElement("span", { key: "space" }, "\xA0"));
return segments;
}, [context.inputRef, context.mentions]);
if (!inputStyle) return null;
return /* @__PURE__ */ React4__namespace.createElement(
"div",
{
...highlighterProps,
ref: composedRef,
dir: context.dir,
style: highlighterStyle
},
onSegmentsRender()
);
}
),
(prevProps, nextProps) => prevProps.style === nextProps.style && Object.keys(prevProps).every(
(key) => prevProps[key] === nextProps[key]
)
);
MentionHighlighter.displayName = HIGHLIGHTER_NAME;
// src/mention-input.tsx
var INPUT_NAME = "MentionInput";
var SEPARATORS_PATTERN = /[-_\s./\\|:;,]+/g;
var UNWANTED_CHARS = /[^\p{L}\p{N}\s]/gu;
var MentionInput = React4__namespace.forwardRef(
(props, forwardedRef) => {
const context = useMentionContext(INPUT_NAME);
const composedRef = shared.useComposedRefs(forwardedRef, context.inputRef);
const getTextWidth = React4__namespace.useCallback(
(text, input) => {
const style = window.getComputedStyle(input);
const measureSpan = document.createElement("span");
measureSpan.style.cssText = `
position: absolute;
visibility: hidden;
white-space: pre;
font: ${style.font};
letter-spacing: ${style.letterSpacing};
text-transform: ${style.textTransform};
`;
measureSpan.textContent = text;
document.body.appendChild(measureSpan);
const width = measureSpan.offsetWidth;
document.body.removeChild(measureSpan);
return width;
},
[]
);
const getLineHeight = React4__namespace.useCallback((input) => {
const style = window.getComputedStyle(input);
return Number.parseInt(style.lineHeight) ?? input.offsetHeight;
}, []);
const calculatePosition = React4__namespace.useCallback(
(input, cursorPosition) => {
const rect = input.getBoundingClientRect();
const textBeforeCursor = input.value.slice(0, cursorPosition);
const lines = textBeforeCursor.split("\n");
const currentLine = lines.length - 1;
const currentLineText = lines[currentLine] ?? "";
const textWidth = getTextWidth(currentLineText, input);
const style = window.getComputedStyle(input);
const lineHeight = getLineHeight(input);
const paddingLeft = Number.parseFloat(
style.getPropertyValue("padding-left") ?? "0"
);
const paddingRight = Number.parseFloat(
style.getPropertyValue("padding-right") ?? "0"
);
const paddingTop = Number.parseFloat(
style.getPropertyValue("padding-top") ?? "0"
);
const containerWidth = input.clientWidth - paddingLeft - paddingRight;
const wrappedLines = Math.floor(textWidth / containerWidth);
const totalLines = currentLine + wrappedLines;
const scrollTop = input.scrollTop;
const scrollLeft = input.scrollLeft;
const effectiveTextWidth = textWidth % containerWidth;
const isRTL = context.dir === "rtl";
const x = isRTL ? Math.min(
rect.right - paddingRight - effectiveTextWidth + scrollLeft,
rect.right - 10
) : Math.min(
rect.left + paddingLeft + effectiveTextWidth - scrollLeft,
rect.right - 10
);
const y = rect.top + paddingTop + (totalLines * lineHeight - scrollTop);
return {
width: 0,
height: lineHeight,
x,
y,
top: y,
right: x,
bottom: y + lineHeight,
left: x,
toJSON() {
return this;
}
};
},
[getTextWidth, getLineHeight, context.dir]
);
const createVirtualElement = React4__namespace.useCallback(
(element, cursorPosition) => {
const virtualElement = {
getBoundingClientRect() {
return calculatePosition(element, cursorPosition);
},
getClientRects() {
const rect = this.getBoundingClientRect();
const rects = [rect];
Object.defineProperty(rects, "item", {
value: function(index) {
return this[index];
}
});
return rects;
}
};
context.onVirtualAnchorChange(virtualElement);
},
[context.onVirtualAnchorChange, calculatePosition]
);
const onMentionUpdate = React4__namespace.useCallback(
(element, selectionStart = null) => {
if (context.disabled || context.readonly) return false;
const currentPosition = selectionStart ?? element.selectionStart;
if (currentPosition === null) return false;
const value = element.value;
const lastTriggerIndex = value.lastIndexOf(
context.trigger,
currentPosition
);
if (lastTriggerIndex === -1) {
if (context.open) {
context.onOpenChange(false);
context.onHighlightedItemChange(null);
context.filterStore.search = "";
}
return false;
}
const isPartOfExistingMention = context.mentions.some(
(mention) => mention.start <= lastTriggerIndex && mention.end > lastTriggerIndex
);
if (isPartOfExistingMention) {
if (context.open) {
context.onOpenChange(false);
context.onHighlightedItemChange(null);
context.filterStore.search = "";
}
return false;
}
function getIsTriggerPartOfText() {
const textBeforeTrigger = value.slice(0, lastTriggerIndex);
const hasTextBeforeTrigger = /\S/.test(textBeforeTrigger);
if (!hasTextBeforeTrigger) return false;
const lastCharBeforeTrigger = textBeforeTrigger.slice(-1);
return lastCharBeforeTrigger !== " " && lastCharBeforeTrigger !== "\n";
}
if (getIsTriggerPartOfText()) {
if (context.open) {
context.onOpenChange(false);
context.onHighlightedItemChange(null);
context.filterStore.search = "";
}
return false;
}
const textAfterTrigger = value.slice(
lastTriggerIndex + 1,
currentPosition
);
const isValidMention = !textAfterTrigger.includes(" ");
const isCursorAfterTrigger = currentPosition > lastTriggerIndex;
const isImmediatelyAfterTrigger = currentPosition === lastTriggerIndex + 1;
const textAfterCursor = value.slice(currentPosition).trim();
const hasCompletedText = textAfterCursor.length > 0 && !textAfterCursor.startsWith(" ");
if (hasCompletedText) {
if (context.open) {
context.onOpenChange(false);
context.onHighlightedItemChange(null);
context.filterStore.search = "";
}
return false;
}
if (isValidMention && (isCursorAfterTrigger || isImmediatelyAfterTrigger)) {
createVirtualElement(element, lastTriggerIndex);
context.onOpenChange(true);
context.filterStore.search = isImmediatelyAfterTrigger ? "" : textAfterTrigger;
context.onItemsFilter();
return true;
}
if (context.open) {
context.onOpenChange(false);
context.onHighlightedItemChange(null);
context.filterStore.search = "";
}
return false;
},
[
context.open,
context.onOpenChange,
context.trigger,
createVirtualElement,
context.filterStore,
context.onItemsFilter,
context.onHighlightedItemChange,
context.disabled,
context.readonly,
context.mentions
]
);
const onChange = React4__namespace.useCallback(
(event) => {
if (context.disabled || context.readonly) return;
const input = event.target;
const newValue = input.value;
const cursorPosition = input.selectionStart ?? 0;
const prevValue = context.inputValue;
const insertedLength = newValue.length - prevValue.length;
if (insertedLength !== 0) {
context.onMentionsChange(
(prev) => prev.map((mention) => {
if (mention.start >= cursorPosition - (insertedLength > 0 ? insertedLength : 0)) {
return {
...mention,
start: mention.start + insertedLength,
end: mention.end + insertedLength
};
}
return mention;
})
);
}
context.onInputValueChange?.(newValue);
onMentionUpdate(input);
},
[
context.onInputValueChange,
context.inputValue,
context.onMentionsChange,
onMentionUpdate,
context.disabled,
context.readonly
]
);
const onClick = React4__namespace.useCallback(
(event) => {
onMentionUpdate(event.currentTarget);
},
[onMentionUpdate]
);
const onCut = React4__namespace.useCallback(
(event) => {
if (context.disabled || context.readonly) return;
const input = event.currentTarget;
const cursorPosition = input.selectionStart ?? 0;
const selectionEnd = input.selectionEnd ?? cursorPosition;
const hasSelection = cursorPosition !== selectionEnd;
if (!hasSelection) return;
const affectedMentions = context.mentions.filter(
(m) => m.start >= cursorPosition && m.start < selectionEnd || m.end > cursorPosition && m.end <= selectionEnd
);
if (affectedMentions.length > 0) {
requestAnimationFrame(() => {
const remainingValues = context.value.filter(
(v) => !affectedMentions.some((m) => m.value === v)
);
context.onValueChange?.(remainingValues);
context.onMentionsRemove(affectedMentions);
});
}
},
[
context.disabled,
context.readonly,
context.mentions,
context.value,
context.onValueChange,
context.onMentionsRemove
]
);
const onFocus = React4__namespace.useCallback(
(event) => {
onMentionUpdate(event.currentTarget);
},
[onMentionUpdate]
);
const onKeyDown = React4__namespace.useCallback(
(event) => {
const input = event.currentTarget;
const cursorPosition = input.selectionStart ?? 0;
const selectionEnd = input.selectionEnd ?? cursorPosition;
const hasSelection = cursorPosition !== selectionEnd;
if ((event.key === "ArrowLeft" || event.key === "ArrowRight") && !hasSelection && !event.shiftKey) {
const isCtrlOrCmd = event.metaKey || event.ctrlKey;
const isLeftArrow = event.key === "ArrowLeft";
const adjacentMention = context.mentions.find((m) => {
if (isLeftArrow) {
const textBetween2 = input.value.slice(m.end, cursorPosition);
const isOnlySpaces2 = /^\s*$/.test(textBetween2);
if (isCtrlOrCmd) {
return cursorPosition > m.start && // Cursor after mention start
(cursorPosition === m.end || // Cursor at mention end
cursorPosition > m.end && // Or after mention end with only spaces
isOnlySpaces2);
}
return cursorPosition === m.end || // At mention end
cursorPosition > m.end && // Or after mention with only spaces
cursorPosition <= m.end + 1 && isOnlySpaces2;
}
const textBetween = input.value.slice(cursorPosition, m.start);
const isOnlySpaces = /^\s*$/.test(textBetween);
if (isCtrlOrCmd) {
return cursorPosition >= m.start && cursorPosition < m.end || // Cursor inside mention
cursorPosition < m.start && // Or cursor before mention start
isOnlySpaces;
}
return cursorPosition === m.start || // At mention start
cursorPosition < m.start && // Or before mention with only spaces
cursorPosition >= m.start - 1 && isOnlySpaces;
});
if (adjacentMention) {
event.preventDefault();
const newPosition = isCtrlOrCmd ? isLeftArrow ? adjacentMention.start : adjacentMention.end : isLeftArrow ? cursorPosition > adjacentMention.end ? adjacentMention.end : adjacentMention.start : cursorPosition < adjacentMention.start ? adjacentMention.start : adjacentMention.end;
input.setSelectionRange(newPosition, newPosition);
return;
}
if (isCtrlOrCmd) return;
}
if ((event.key === "Backspace" || event.key === "Delete") && hasSelection) {
const newValue = input.value.slice(0, cursorPosition) + input.value.slice(selectionEnd);
const affectedMentions = context.mentions.filter(
(m) => m.start >= cursorPosition && m.start < selectionEnd || m.end > cursorPosition && m.end <= selectionEnd
);
if (affectedMentions.length > 0) {
event.preventDefault();
input.value = newValue;
context.onInputValueChange?.(newValue);
const remainingValues = context.value.filter(
(v) => !affectedMentions.some((m) => m.value === v)
);
context.onValueChange?.(remainingValues);
context.onMentionsRemove(affectedMentions);
input.setSelectionRange(cursorPosition, cursorPosition);
return;
}
}
if (event.key === "Backspace" && !context.open && !hasSelection) {
const isCtrlOrCmd = event.metaKey || event.ctrlKey;
const mentionBeforeCursor = context.mentions.find((m) => {
if (!isCtrlOrCmd) {
return cursorPosition === m.end || // Cursor exactly at end
cursorPosition === m.end + 1 && input.value[m.end] === " " || // Or after space
cursorPosition > m.start && cursorPosition <= m.end;
}
const textBetween = input.value.slice(m.end, cursorPosition);
return m.end <= cursorPosition && // Mention must end before or at cursor
/^\s*$/.test(textBetween);
});
if (mentionBeforeCursor) {
const hasTrailingSpace = input.value[mentionBeforeCursor.end] === " ";
const isCursorInsideMention = cursorPosition > mentionBeforeCursor.start && cursorPosition <= mentionBeforeCursor.end;
if (isCursorInsideMention || isCtrlOrCmd) {
event.preventDefault();
const newValue2 = input.value.slice(0, mentionBeforeCursor.start) + input.value.slice(
mentionBeforeCursor.end + (hasTrailingSpace ? 1 : 0)
);
input.value = newValue2;
context.onInputValueChange?.(newValue2);
const remainingValues2 = context.value.filter(
(v) => v !== mentionBeforeCursor.value
);
context.onValueChange?.(remainingValues2);
context.onMentionsRemove([mentionBeforeCursor]);
const newPosition2 = mentionBeforeCursor.start;
input.setSelectionRange(newPosition2, newPosition2);
return;
}
if (hasTrailingSpace && cursorPosition === mentionBeforeCursor.end + 1 && !isCtrlOrCmd) {
event.preventDefault();
const newValue2 = input.value.slice(0, mentionBeforeCursor.end) + input.value.slice(mentionBeforeCursor.end + 1);
input.value = newValue2;
context.onInputValueChange?.(newValue2);
input.setSelectionRange(
mentionBeforeCursor.end,
mentionBeforeCursor.end
);
return;
}
event.preventDefault();
const newValue = input.value.slice(0, mentionBeforeCursor.start) + input.value.slice(
mentionBeforeCursor.end + (hasTrailingSpace ? 1 : 0)
);
input.value = newValue;
context.onInputValueChange?.(newValue);
const remainingValues = context.value.filter(
(v) => v !== mentionBeforeCursor.value
);
context.onValueChange?.(remainingValues);
context.onMentionsRemove([mentionBeforeCursor]);
const newPosition = mentionBeforeCursor.start;
input.setSelectionRange(newPosition, newPosition);
return;
}
}
if (!context.open) return;
const isNavigationKey = [
"ArrowDown",
"ArrowUp",
"Enter",
"Escape",
"Tab",
"Home",
"End"
].includes(event.key);
if (isNavigationKey && event.key !== "Tab") {
event.preventDefault();
}
function onMenuClose() {
context.onOpenChange(false);
context.onHighlightedItemChange(null);
context.filterStore.search = "";
}
function onItemSelect() {
if (context.disabled || context.readonly || !context.highlightedItem)
return;
const value = context.highlightedItem.value;
if (!value) return;
const lastTriggerIndex = input.value.lastIndexOf(
context.trigger,
cursorPosition
);
if (lastTriggerIndex !== -1) {
context.onMentionAdd(value, lastTriggerIndex);
}
}
switch (event.key) {
case "Enter": {
if (!context.highlightedItem) {
onMenuClose();
return;
}
event.preventDefault();
onItemSelect();
break;
}
case "Tab": {
if (context.modal) {
event.preventDefault();
onItemSelect();
return;
}
onMenuClose();
break;
}
case "ArrowDown": {
if (context.readonly) return;
context.onHighlightMove(context.highlightedItem ? "next" : "first");
break;
}
case "ArrowUp": {
if (context.readonly) return;
context.onHighlightMove(context.highlightedItem ? "prev" : "last");
break;
}
case "Home": {
if (event.metaKey || event.ctrlKey) return;
if (context.readonly) return;
event.preventDefault();
context.onHighlightMove("first");
break;
}
case "End": {
if (event.metaKey || event.ctrlKey) return;
if (context.readonly) return;
event.preventDefault();
context.onHighlightMove("last");
break;
}
case "Escape": {
onMenuClose();
break;
}
}
},
[
context.open,
context.onOpenChange,
context.value,
context.onValueChange,
context.onInputValueChange,
context.trigger,
context.highlightedItem,
context.onHighlightedItemChange,
context.onHighlightMove,
context.filterStore,
context.mentions,
context.onMentionAdd,
context.onMentionsRemove,
context.disabled,
context.readonly,
context.modal
]
);
const onBeforeInput = React4__namespace.useCallback(
(event) => {
if (context.disabled || context.readonly) return;
const input = event.currentTarget;
const cursorPosition = input.selectionStart ?? 0;
if (event.inputType === "deleteContentBackward") {
const mentionAtCursor = context.mentions.find(
(m) => cursorPosition > m.start && cursorPosition <= m.end
);
if (mentionAtCursor) {
event.preventDefault();
const hasTrailingSpace = input.value[mentionAtCursor.end] === " ";
const newValue = input.value.slice(0, mentionAtCursor.start) + input.value.slice(
mentionAtCursor.end + (hasTrailingSpace ? 1 : 0)
);
input.value = newValue;
context.onInputValueChange?.(newValue);
const remainingValues = context.value.filter(
(v) => v !== mentionAtCursor.value
);
context.onValueChange?.(remainingValues);
context.onMentionsRemove([mentionAtCursor]);
const newPosition = mentionAtCursor.start;
input.setSelectionRange(newPosition, newPosition);
}
}
},
[
context.disabled,
context.readonly,
context.mentions,
context.value,
context.onValueChange,
context.onInputValueChange,
context.onMentionsRemove
]
);
const onSelect = React4__namespace.useCallback(() => {
if (context.disabled || context.readonly) return;
const inputElement = context.inputRef.current;
if (!inputElement) return;
onMentionUpdate(inputElement);
}, [context.disabled, context.readonly, context.inputRef, onMentionUpdate]);
const onPointerDown = React4__namespace.useCallback(
(event) => {
if (context.disabled || context.readonly) return;
const input = event.currentTarget;
const rect = input.getBoundingClientRect();
const style = window.getComputedStyle(input);
const paddingLeft = Number.parseFloat(style.paddingLeft);
const clickX = event.clientX - rect.left - paddingLeft;
const textWidth = getTextWidth(
input.value.slice(0, input.value.length),
input
);
const charWidth = textWidth / input.value.length;
const approximateClickPosition = Math.round(clickX / charWidth);
const clickedMention = context.mentions.find(
(mention) => approximateClickPosition >= mention.start && approximateClickPosition < mention.end
);
if (clickedMention) {
event.preventDefault();
requestAnimationFrame(() => {
input.setSelectionRange(clickedMention.end, clickedMention.end);
});
}
},
[context.disabled, context.readonly, context.mentions, getTextWidth]
);
const onPaste = React4__namespace.useCallback(
(event) => {
if (context.disabled || context.readonly) return;
const inputElement = event.currentTarget;
const pastedText = event.clipboardData.getData("text");
const cursorPosition = inputElement.selectionStart ?? 0;
const selectionEnd = inputElement.selectionEnd ?? cursorPosition;
const triggerIndex = pastedText.indexOf(context.trigger);
if (triggerIndex === -1) return;
event.preventDefault();
const parts = pastedText.split(context.trigger);
let newText = "";
if (parts[0]) {
newText += parts[0];
}
function normalizeWithGaps(str) {
if (!str) return "";
if (typeof str !== "string") return "";
let normalized;
try {
normalized = str.toLowerCase().normalize("NFC").replace(UNWANTED_CHARS, " ").replace(SEPARATORS_PATTERN, " ").trim().replace(/\s+/g, "");
} catch (_err) {
normalized = str.toLowerCase().normalize("NFC").replace(/[^a-z0-9\s]/g, " ").trim().replace(/\s+/g, "");
}
return normalized;
}
requestAnimationFrame(async () => {
context.onIsPastingChange(true);
context.onOpenChange(true);
await new Promise((resolve) => requestAnimationFrame(resolve));
const items = context.getEnabledItems();
const newMentions = [];
const newValues = [...context.value];
const trailingSpaces = pastedText.match(/\s+$/)?.[0] ?? "";
for (let i = 1; i < parts.length; i++) {
const part = parts[i];
if (!part) continue;
const words = part.split(/(\s+)/);
let mentionText = "";
let spaces = "";
let remainingText = "";
let foundValidMention = false;
for (let wordCount = words.length; wordCount > 0; wordCount--) {
const candidateWords = words.slice(0, wordCount).filter((_, index) => index % 2 === 0);
const candidateText = candidateWords.join(" ").trim();
if (!candidateText) continue;
const mentionItem = items.find(
(item) => normalizeWithGaps(item.value) === normalizeWithGaps(candidateText)
);
if (mentionItem) {
mentionText = candidateText;
const usedWordCount = candidateWords.length;
const usedSegments = usedWordCount * 2 - 1;
const nextSegmentIndex = usedSegments;
const nextSegment = words[nextSegmentIndex];
const afterNextSegment = words[nextSegmentIndex + 1];
if (nextSegment?.match(/^\s+/) && afterNextSegment) {
spaces = nextSegment;
remainingText = words.slice(nextSegmentIndex + 1).join("");
} else {
spaces = "";
remainingText = words.slice(nextSegmentIndex).join("");
}
foundValidMention = true;
break;
}
}
const mentionStartPosition = cursorPosition + newText.length;
if (foundValidMention) {
const mentionItem = items.find(
(item) => normalizeWithGaps(item.value) === normalizeWithGaps(mentionText)
);
if (mentionItem) {
const mentionLabel = `${context.trigger}${mentionItem.label}`;
const shouldAddTrailingSpaces = i === parts.length - 1 && !remainingText;
newText += mentionLabel + spaces + remainingText + (shouldAddTrailingSpaces ? trailingSpaces : "");
newValues.push(mentionItem.value);
newMentions.push({
value: mentionItem.value,
start: mentionStartPosition,
end: mentionStartPosition + mentionLabel.length
});
}
} else {
const firstWord = words[0] ?? "";
const spaceSegment = words[1] ?? "";
spaces = spaceSegment?.match(/^\s+/) ? spaceSegment : "";
remainingText = words.slice(2).join("");
const shouldAddTrailingSpaces = i === parts.length - 1;
newText += `${context.trigger}${firstWord}${spaces}${remainingText}${shouldAddTrailingSpaces ? trailingSpaces : ""}`;
}
}
const finalValue = inputElement.value.slice(0, cursorPosition) + newText + inputElement.value.slice(selectionEnd);
inputElement.value = finalValue;
context.onInputValueChange(finalValue);
if (newMentions.length > 0) {
context.onValueChange(newValues);
context.onMentionsChange((prev) => [...prev, ...newMentions]);
}
const newCursorPosition = cursorPosition + newText.length;
inputElement.setSelectionRange(newCursorPosition, newCursorPosition);
context.onIsPastingChange(false);
context.onOpenChange(false);
});
},
[
context.trigger,
context.onOpenChange,
context.onInputValueChange,
context.value,
context.onValueChange,
context.getEnabledItems,
context.onMentionsChange,
context.onIsPastingChange,
context.disabled,
context.readonly
]
);
return /* @__PURE__ */ React4__namespace.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React4__namespace.createElement(MentionHighlighter, null), /* @__PURE__ */ React4__namespace.createElement(
shared.Primitive.input,
{
role: "combobox",
id: context.inputId,
autoComplete: "off",
"aria-expanded": context.open,
"aria-controls": context.listId,
"aria-labelledby": context.labelId,
"aria-autocomplete": "list",
"aria-activedescendant": context.highlightedItem?.ref.current?.id,
"aria-disabled": context.disabled,
"aria-readonly": context.readonly,
disabled: context.disabled,
readOnly: context.readonly,
dir: context.dir,
...props,
ref: composedRef,
onBeforeInput: shared.composeEventHandlers(
props.onBeforeInput,
onBeforeInput
),
onChange: shared.composeEventHandlers(props.onChange, onChange),
onClick: shared.composeEventHandlers(props.onClick, onClick),
onCut: shared.composeEventHandlers(props.onCut, onCut),
onFocus: shared.composeEventHandlers(props.onFocus, onFocus),
onKeyDown: shared.composeEventHandlers(props.onKeyDown, onKeyDown),
onPaste: shared.composeEventHandlers(props.onPaste, onPaste),
onPointerDown: shared.composeEventHandlers(
props.onPointerDown,
onPointerDown
),
onSelect: shared.composeEventHandlers(props.onSelect, onSelect)
}
));
}
);
MentionInput.displayName = INPUT_NAME;
var Input = MentionInput;
var PORTAL_NAME = "MentionPortal";
var MentionPortal = React4__namespace.forwardRef(
(props, forwardedRef) => {
const { container, ...portalProps } = props;
return /* @__PURE__ */ React4__namespace.createElement(
shared.Portal,
{
container,
...portalProps,
ref: forwardedRef,
asChild: true
}
);
}
);
MentionPortal.displayName = PORTAL_NAME;
var Portal = MentionPortal;
var CONTENT_NAME = "MentionContent";
var [MentionContentProvider, useMentionContentContext] = shared.createContext(CONTENT_NAME);
var MentionContent = React4__namespace.forwardRef(
(props, forwardedRef) => {
const {
side = "bottom",
sideOffset = 4,
align = "start",
alignOffset = 0,
arrowPadding = 0,
collisionBoundary,
collisionPadding,
sticky = "partial",
strategy = "absolute",
avoidCollisions = true,
fitViewport = false,
forceMount = false,
hideWhenDetached = false,
trackAnchor = true,
onEscapeKeyDown,
onPointerDownOutside,
style,
...contentProps
} = props;
const context = useMentionContext(CONTENT_NAME);
const rtlAwareAlign = React4__namespace.useMemo(() => {
if (context.dir !== "rtl") return align;
return align === "start" ? "end" : align === "end" ? "start" : align;
}, [align, context.dir]);
const positionerContext = shared.useAnchorPositioner({
open: context.open,
onOpenChange: context.onOpenChange,
anchorRef: context.virtualAnchor,
side,
sideOffset,
align: rtlAwareAlign,
alignOffset,
arrowPadding,
collisionBoundary,
collisionPadding,
sticky,
strategy,
avoidCollisions,
disableArrow: true,
fitViewport,
hideWhenDetached,
trackAnchor
});
const composedRef = shared.useComposedRefs(
forwardedRef,
(node) => positionerContext.refs.setFloating(node)
);
const composedStyle = React4__namespace.useMemo(() => {
return {
...style,
...positionerContext.floatingStyles,
...!context.open && forceMount ? { visibility: "hidden" } : {},
// Hide content visually during pasting while keeping items registered
...context.isPasting ? shared.visuallyHidden : {}
};
}, [
style,
positionerContext.floatingStyles,
forceMount,
context.open,
context.isPasting
]);
shared.useDismiss({
enabled: context.open,
onDismiss: () => context.onOpenChange(false),
refs: [context.listRef, context.inputRef],
onFocusOutside: (event) => event.preventDefault(),
onEscapeKeyDown,
onPointerDownOutside,
disableOutsidePointerEvents: context.open && context.modal,
preventScrollDismiss: context.open
});
shared.useScrollLock({
referenceElement: context.inputRef.current,
enabled: context.open && context.modal
});
return /* @__PURE__ */ React4__namespace.createElement(
MentionContentProvider,
{
side,
align: rtlAwareAlign,
arrowStyles: positionerContext.arrowStyles,
arrowDisplaced: positionerContext.arrowDisplaced,
onArrowChange: positionerContext.onArrowChange,
forceMount
},
/* @__PURE__ */ React4__namespace.createElement(
react.FloatingFocusManager,
{
context: positionerContext.context,
modal: false,
initialFocus: context.inputRef,
returnFocus: false,
disabled: !context.open,
visuallyHiddenDismiss: true
},
/* @__PURE__ */ React4__namespace.createElement(shared.Presence, { present: forceMount || context.open }, /* @__PURE__ */ React4__namespace.createElement(
shared.Primitive.div,
{
ref: composedRef,
role: "listbox",
"aria-orientation": "vertical",
"data-state": getDataState(context.open),
dir: context.dir,
...positionerContext.getFloatingProps(contentProps),
style: composedStyle
}
))
)
);
}
);
MentionContent.displayName = CONTENT_NAME;
var Content = MentionContent;
var ITEM_NAME = "MentionItem";
var [MentionItemProvider, useMentionItemContext] = shared.createContext(ITEM_NAME);
var MentionItem = React4__namespace.forwardRef(
(props, forwardedRef) => {
const { value, label: labelProp, disabled = false, ...itemProps } = props;
const context = useMentionContext(ITEM_NAME);
const [itemNode, setItemNode] = React4__namespace.useState(null);
const composedRef = shared.composeRefs(forwardedRef, (node) => setItemNode(node));
const id = shared.useId();
const label = labelProp ?? value;
const isDisabled = disabled || context.disabled;
const isSelected = context.value.includes(value);
shared.useIsomorphicLayoutEffect(() => {
if (value === "") {
throw new Error(`\`${ITEM_NAME}\` value cannot be an empty string`);
}
return context.onItemRegister({
ref: { current: itemNode },
value,
label,
disabled: isDisabled
});
}, [label, value, isDisabled, itemNode, context.onItemRegister]);
const isVisible = context.getIsItemVisible(value);
if (!isVisible) return null;
return /* @__PURE__ */ React4__namespace.createElement(MentionItemProvider, { label, value, disabled: isDisabled }, /* @__PURE__ */ React4__namespace.createElement(
shared.Primitive.div,
{
role: "option",
id,
"aria-selected": isSelected,
...{ [shared.DATA_ITEM_ATTR]: "" },
"data-selected": isSelected ? "" : void 0,
"data-highlighted": context.highlightedItem?.ref.current?.id === id ? "" : void 0,
"data-disabled": isDisabled ? "" : void 0,
...itemProps,
ref: composedRef,
onClick: shared.composeEventHandlers(itemProps.onClick, () => {
if (isDisabled) return;
const inputElement = context.inputRef.current;
if (!inputElement) return;
const selectionStart = inputElement.selectionStart ?? 0;
const lastTriggerIndex = inputElement.value.lastIndexOf(
context.trigger,
selectionStart
);
if (lastTriggerIndex !== -1) {
context.onMentionAdd(value, lastTriggerIndex);
}
inputElement.focus();
}),
onPointerDown: shared.composeEventHandlers(
itemProps.onPointerDown,
(event) => {
if (isDisabled) return;
const target = event.target;
if (!(target instanceof HTMLElement)) return;
if (target.hasPointerCapture(event.pointerId)) {
target.releasePointerCapture(event.pointerId);
}
if (event.button === 0 && event.ctrlKey === false) {
event.preventDefault();
}
}
),
onPointerMove: shared.composeEventHandlers(itemProps.onPointerMove, () => {
if (isDisabled || !itemNode) return;
context.onHighlightedItemChange({
ref: { current: itemNode },
label,
value,
disabled: isDisabled
});
})
}
));
}
);
MentionItem.displayName = ITEM_NAME;
var Item = MentionItem;
exports.Content = Content;
exports.Input = Input;
exports.Item = Item;
exports.Label = Label;
exports.MentionContent = MentionContent;
exports.MentionInput = MentionInput;
exports