UNPKG

@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
'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