@diceui/mention
Advanced tools
+0
-0
@@ -0,0 +0,0 @@ MIT License |
+2
-2
| { | ||
| "name": "@diceui/mention", | ||
| "version": "0.2.0", | ||
| "version": "0.3.0", | ||
| "type": "module", | ||
@@ -52,3 +52,3 @@ "publishConfig": { | ||
| "tsup": "^8.3.5", | ||
| "typescript": "^5.7.2" | ||
| "typescript": "^5.7.3" | ||
| }, | ||
@@ -55,0 +55,0 @@ "dependencies": { |
-1271
| 'use client'; | ||
| 'use strict'; | ||
| var shared = require('@diceui/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.BubbleInput, | ||
| { | ||
| 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] === " "; | ||
| } | ||
| const textBetween = input.value.slice(m.end, cursorPosition); | ||
| return /^\s*$/.test(textBetween); | ||
| }); | ||
| if (mentionBeforeCursor) { | ||
| const hasTrailingSpace = input.value[mentionBeforeCursor.end] === " "; | ||
| 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 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, | ||
| onChange: shared.composeEventHandlers(props.onChange, onChange), | ||
| onClick: shared.composeEventHandlers(props.onClick, onClick), | ||
| onPointerDown: shared.composeEventHandlers( | ||
| props.onPointerDown, | ||
| onPointerDown | ||
| ), | ||
| onCut: shared.composeEventHandlers(props.onCut, onCut), | ||
| onFocus: shared.composeEventHandlers(props.onFocus, onFocus), | ||
| onKeyDown: shared.composeEventHandlers(props.onKeyDown, onKeyDown), | ||
| onPaste: shared.composeEventHandlers(props.onPaste, onPaste), | ||
| 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 | ||
| }); | ||
| if (!forceMount && !context.open) return null; | ||
| 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.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, | ||
| { | ||
| ...{ [shared.DATA_ITEM_ATTR]: "" }, | ||
| id, | ||
| role: "option", | ||
| "aria-selected": isSelected, | ||
| "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 input = context.inputRef.current; | ||
| if (!input) return; | ||
| const selectionStart = input.selectionStart ?? 0; | ||
| const lastTriggerIndex = input.value.lastIndexOf( | ||
| context.trigger, | ||
| selectionStart | ||
| ); | ||
| if (lastTriggerIndex !== -1) { | ||
| context.onMentionAdd(value, lastTriggerIndex); | ||
| } | ||
| input.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.pointerType === "mouse") { | ||
| 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.MentionItem = MentionItem; | ||
| exports.MentionLabel = MentionLabel; | ||
| exports.MentionPortal = MentionPortal; | ||
| exports.MentionRoot = MentionRoot; | ||
| exports.Portal = Portal; | ||
| exports.Root = Root; |
-115
| import { AnchorPositionerProps, Primitive, PointerDownOutsideEvent, Direction, PortalProps } from '@diceui/shared'; | ||
| import * as React from 'react'; | ||
| interface MentionContentProps extends AnchorPositionerProps, React.ComponentPropsWithoutRef<typeof Primitive.div> { | ||
| /** | ||
| * Event handler called when the `Escape` key is pressed. | ||
| * | ||
| * Can be used to prevent input value from being reset on `Escape` key press. | ||
| */ | ||
| onEscapeKeyDown?: (event: KeyboardEvent) => void; | ||
| /** | ||
| * Event handler called when a `pointerdown` event happens outside of the content. | ||
| * | ||
| * Can be used to prevent the content from closing when the pointer is outside of the content. | ||
| */ | ||
| onPointerDownOutside?: (event: PointerDownOutsideEvent) => void; | ||
| } | ||
| declare const MentionContent: React.ForwardRefExoticComponent<MentionContentProps & React.RefAttributes<HTMLDivElement>>; | ||
| declare const Content: React.ForwardRefExoticComponent<MentionContentProps & React.RefAttributes<HTMLDivElement>>; | ||
| interface MentionRootProps extends Omit<React.ComponentPropsWithoutRef<typeof Primitive.div>, "value" | "defaultValue"> { | ||
| /** The currently selected value. */ | ||
| value?: string[]; | ||
| /** The default selected value. */ | ||
| defaultValue?: string[]; | ||
| /** Event handler called when a mention item is selected. */ | ||
| onValueChange?: (value: string[]) => void; | ||
| /** Whether the mention menu is open. */ | ||
| open?: boolean; | ||
| /** The default open state. */ | ||
| defaultOpen?: boolean; | ||
| /** Event handler called when the open state changes. */ | ||
| onOpenChange?: (open: boolean) => void; | ||
| /** The current input value. */ | ||
| inputValue?: string; | ||
| /** Event handler called when the input value changes. */ | ||
| onInputValueChange?: (value: string) => void; | ||
| /** The character that activates the mention menu when typed. */ | ||
| trigger?: string; | ||
| /** The direction the mention should open. */ | ||
| dir?: Direction; | ||
| /** Whether the mention is disabled. */ | ||
| disabled?: boolean; | ||
| /** | ||
| * Event handler called when the filter is applied. | ||
| * Can be used to prevent the default filtering behavior. | ||
| */ | ||
| onFilter?: (options: string[], term: string) => string[]; | ||
| /** | ||
| * Whether the mention uses exact string matching or fuzzy matching. | ||
| * When onFilter is provided, this prop is ignored. | ||
| * @default false | ||
| */ | ||
| exactMatch?: boolean; | ||
| /** | ||
| * Whether the mention loops through items. | ||
| * @default false | ||
| */ | ||
| loop?: boolean; | ||
| /** | ||
| * Whether the mention is modal. | ||
| * @default false | ||
| */ | ||
| modal?: boolean; | ||
| /** | ||
| * Whether the mention is read-only. | ||
| * @default false | ||
| */ | ||
| readonly?: boolean; | ||
| /** | ||
| * Whether the mention is required in a form context. | ||
| * @default false | ||
| */ | ||
| required?: boolean; | ||
| /** The name of the mention when used in a form. */ | ||
| name?: string; | ||
| } | ||
| declare const MentionRoot: React.ForwardRefExoticComponent<MentionRootProps & React.RefAttributes<HTMLDivElement>>; | ||
| declare const Root: React.ForwardRefExoticComponent<MentionRootProps & React.RefAttributes<HTMLDivElement>>; | ||
| interface MentionLabelProps extends React.ComponentPropsWithoutRef<typeof Primitive.label> { | ||
| } | ||
| declare const MentionLabel: React.ForwardRefExoticComponent<MentionLabelProps & React.RefAttributes<HTMLLabelElement>>; | ||
| declare const Label: React.ForwardRefExoticComponent<MentionLabelProps & React.RefAttributes<HTMLLabelElement>>; | ||
| interface MentionInputProps extends React.ComponentPropsWithoutRef<typeof Primitive.input> { | ||
| } | ||
| declare const MentionInput: React.ForwardRefExoticComponent<MentionInputProps & React.RefAttributes<HTMLInputElement>>; | ||
| declare const Input: React.ForwardRefExoticComponent<MentionInputProps & React.RefAttributes<HTMLInputElement>>; | ||
| interface MentionPortalProps extends Pick<PortalProps, "container" | "children"> { | ||
| } | ||
| declare const MentionPortal: React.ForwardRefExoticComponent<MentionPortalProps & React.RefAttributes<HTMLDivElement>>; | ||
| declare const Portal: React.ForwardRefExoticComponent<MentionPortalProps & React.RefAttributes<HTMLDivElement>>; | ||
| interface MentionItemProps extends React.ComponentPropsWithoutRef<typeof Primitive.div> { | ||
| /** | ||
| * The value of the item. | ||
| * | ||
| * Cannot be an empty string. | ||
| */ | ||
| value: string; | ||
| /** | ||
| * The label of the item. By default value is used as label. | ||
| * | ||
| * Override the text value for mention item in the input. | ||
| */ | ||
| label?: string; | ||
| /** Whether the item is disabled. */ | ||
| disabled?: boolean; | ||
| } | ||
| declare const MentionItem: React.ForwardRefExoticComponent<MentionItemProps & React.RefAttributes<HTMLDivElement>>; | ||
| declare const Item: React.ForwardRefExoticComponent<MentionItemProps & React.RefAttributes<HTMLDivElement>>; | ||
| export { Content, Input, Item, Label, MentionContent, type MentionContentProps, MentionInput, type MentionInputProps, MentionItem, type MentionItemProps, MentionLabel, type MentionLabelProps, MentionPortal, type MentionPortalProps, MentionRoot, type MentionRootProps, Portal, Root }; |
-115
| import { AnchorPositionerProps, Primitive, PointerDownOutsideEvent, Direction, PortalProps } from '@diceui/shared'; | ||
| import * as React from 'react'; | ||
| interface MentionContentProps extends AnchorPositionerProps, React.ComponentPropsWithoutRef<typeof Primitive.div> { | ||
| /** | ||
| * Event handler called when the `Escape` key is pressed. | ||
| * | ||
| * Can be used to prevent input value from being reset on `Escape` key press. | ||
| */ | ||
| onEscapeKeyDown?: (event: KeyboardEvent) => void; | ||
| /** | ||
| * Event handler called when a `pointerdown` event happens outside of the content. | ||
| * | ||
| * Can be used to prevent the content from closing when the pointer is outside of the content. | ||
| */ | ||
| onPointerDownOutside?: (event: PointerDownOutsideEvent) => void; | ||
| } | ||
| declare const MentionContent: React.ForwardRefExoticComponent<MentionContentProps & React.RefAttributes<HTMLDivElement>>; | ||
| declare const Content: React.ForwardRefExoticComponent<MentionContentProps & React.RefAttributes<HTMLDivElement>>; | ||
| interface MentionRootProps extends Omit<React.ComponentPropsWithoutRef<typeof Primitive.div>, "value" | "defaultValue"> { | ||
| /** The currently selected value. */ | ||
| value?: string[]; | ||
| /** The default selected value. */ | ||
| defaultValue?: string[]; | ||
| /** Event handler called when a mention item is selected. */ | ||
| onValueChange?: (value: string[]) => void; | ||
| /** Whether the mention menu is open. */ | ||
| open?: boolean; | ||
| /** The default open state. */ | ||
| defaultOpen?: boolean; | ||
| /** Event handler called when the open state changes. */ | ||
| onOpenChange?: (open: boolean) => void; | ||
| /** The current input value. */ | ||
| inputValue?: string; | ||
| /** Event handler called when the input value changes. */ | ||
| onInputValueChange?: (value: string) => void; | ||
| /** The character that activates the mention menu when typed. */ | ||
| trigger?: string; | ||
| /** The direction the mention should open. */ | ||
| dir?: Direction; | ||
| /** Whether the mention is disabled. */ | ||
| disabled?: boolean; | ||
| /** | ||
| * Event handler called when the filter is applied. | ||
| * Can be used to prevent the default filtering behavior. | ||
| */ | ||
| onFilter?: (options: string[], term: string) => string[]; | ||
| /** | ||
| * Whether the mention uses exact string matching or fuzzy matching. | ||
| * When onFilter is provided, this prop is ignored. | ||
| * @default false | ||
| */ | ||
| exactMatch?: boolean; | ||
| /** | ||
| * Whether the mention loops through items. | ||
| * @default false | ||
| */ | ||
| loop?: boolean; | ||
| /** | ||
| * Whether the mention is modal. | ||
| * @default false | ||
| */ | ||
| modal?: boolean; | ||
| /** | ||
| * Whether the mention is read-only. | ||
| * @default false | ||
| */ | ||
| readonly?: boolean; | ||
| /** | ||
| * Whether the mention is required in a form context. | ||
| * @default false | ||
| */ | ||
| required?: boolean; | ||
| /** The name of the mention when used in a form. */ | ||
| name?: string; | ||
| } | ||
| declare const MentionRoot: React.ForwardRefExoticComponent<MentionRootProps & React.RefAttributes<HTMLDivElement>>; | ||
| declare const Root: React.ForwardRefExoticComponent<MentionRootProps & React.RefAttributes<HTMLDivElement>>; | ||
| interface MentionLabelProps extends React.ComponentPropsWithoutRef<typeof Primitive.label> { | ||
| } | ||
| declare const MentionLabel: React.ForwardRefExoticComponent<MentionLabelProps & React.RefAttributes<HTMLLabelElement>>; | ||
| declare const Label: React.ForwardRefExoticComponent<MentionLabelProps & React.RefAttributes<HTMLLabelElement>>; | ||
| interface MentionInputProps extends React.ComponentPropsWithoutRef<typeof Primitive.input> { | ||
| } | ||
| declare const MentionInput: React.ForwardRefExoticComponent<MentionInputProps & React.RefAttributes<HTMLInputElement>>; | ||
| declare const Input: React.ForwardRefExoticComponent<MentionInputProps & React.RefAttributes<HTMLInputElement>>; | ||
| interface MentionPortalProps extends Pick<PortalProps, "container" | "children"> { | ||
| } | ||
| declare const MentionPortal: React.ForwardRefExoticComponent<MentionPortalProps & React.RefAttributes<HTMLDivElement>>; | ||
| declare const Portal: React.ForwardRefExoticComponent<MentionPortalProps & React.RefAttributes<HTMLDivElement>>; | ||
| interface MentionItemProps extends React.ComponentPropsWithoutRef<typeof Primitive.div> { | ||
| /** | ||
| * The value of the item. | ||
| * | ||
| * Cannot be an empty string. | ||
| */ | ||
| value: string; | ||
| /** | ||
| * The label of the item. By default value is used as label. | ||
| * | ||
| * Override the text value for mention item in the input. | ||
| */ | ||
| label?: string; | ||
| /** Whether the item is disabled. */ | ||
| disabled?: boolean; | ||
| } | ||
| declare const MentionItem: React.ForwardRefExoticComponent<MentionItemProps & React.RefAttributes<HTMLDivElement>>; | ||
| declare const Item: React.ForwardRefExoticComponent<MentionItemProps & React.RefAttributes<HTMLDivElement>>; | ||
| export { Content, Input, Item, Label, MentionContent, type MentionContentProps, MentionInput, type MentionInputProps, MentionItem, type MentionItemProps, MentionLabel, type MentionLabelProps, MentionPortal, type MentionPortalProps, MentionRoot, type MentionRootProps, Portal, Root }; |
-1238
| 'use client'; | ||
| import { createContext, useId, useCollection, useFormControl, composeRefs, useDirection, useControllableState, useFilterStore, useListHighlighting, Primitive, BubbleInput, useComposedRefs, useCallbackRef, composeEventHandlers, Portal as Portal$1, useAnchorPositioner, visuallyHidden, useDismiss, useScrollLock, useIsomorphicLayoutEffect, DATA_ITEM_ATTR } from '@diceui/shared'; | ||
| import * as React4 from 'react'; | ||
| import { FloatingFocusManager } from '@floating-ui/react'; | ||
| // src/mention-root.tsx | ||
| function getDataState(open) { | ||
| return open ? "open" : "closed"; | ||
| } | ||
| var ROOT_NAME = "MentionRoot"; | ||
| var [MentionProvider, useMentionContext] = createContext(ROOT_NAME); | ||
| var MentionRoot = React4.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.useRef(null); | ||
| const inputRef = React4.useRef(null); | ||
| const inputId = useId(); | ||
| const labelId = useId(); | ||
| const listId = useId(); | ||
| const { collectionRef, itemMap, getItems, onItemRegister } = useCollection(); | ||
| const { isFormControl, onTriggerChange } = useFormControl(); | ||
| const composedRef = composeRefs( | ||
| forwardedRef, | ||
| collectionRef, | ||
| (node) => onTriggerChange(node) | ||
| ); | ||
| const dir = useDirection(dirProp); | ||
| const [open = false, setOpen] = useControllableState({ | ||
| prop: openProp, | ||
| defaultProp: defaultOpen, | ||
| onChange: onOpenChangeProp | ||
| }); | ||
| const [value = [], setValue] = useControllableState({ | ||
| prop: valueProp, | ||
| defaultProp: defaultValue, | ||
| onChange: onValueChange | ||
| }); | ||
| const [inputValue = "", setInputValue] = useControllableState({ | ||
| prop: inputValueProp, | ||
| defaultProp: "", | ||
| onChange: onInputValueChange | ||
| }); | ||
| const [trigger, setTrigger] = React4.useState(triggerProp); | ||
| const [virtualAnchor, setVirtualAnchor] = React4.useState(null); | ||
| const [highlightedItem, setHighlightedItem] = React4.useState(null); | ||
| const [mentions, setMentions] = React4.useState([]); | ||
| const [isPasting, setIsPasting] = React4.useState(false); | ||
| const { filterStore, onItemsFilter, getIsItemVisible } = useFilterStore({ | ||
| itemMap, | ||
| onFilter, | ||
| exactMatch, | ||
| onCallback: (itemCount) => { | ||
| if (itemCount === 0) { | ||
| setOpen(false); | ||
| setHighlightedItem(null); | ||
| setVirtualAnchor(null); | ||
| } | ||
| } | ||
| }); | ||
| const getEnabledItems = React4.useCallback(() => { | ||
| return getItems().filter((item) => !item.disabled); | ||
| }, [getItems]); | ||
| const onOpenChange = React4.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 } = useListHighlighting({ | ||
| highlightedItem, | ||
| onHighlightedItemChange: setHighlightedItem, | ||
| getItems: React4.useCallback(() => { | ||
| return getItems().filter( | ||
| (item) => !item.disabled && getIsItemVisible(item.value) | ||
| ); | ||
| }, [getItems, getIsItemVisible]), | ||
| getIsItemSelected: (item) => value.includes(item.value), | ||
| loop | ||
| }); | ||
| const onMentionAdd = React4.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.useCallback( | ||
| (mentionsToRemove) => { | ||
| setMentions( | ||
| (prev) => prev.filter( | ||
| (mention) => !mentionsToRemove.some((m) => m.value === mention.value) | ||
| ) | ||
| ); | ||
| }, | ||
| [] | ||
| ); | ||
| return /* @__PURE__ */ React4.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.createElement(Primitive.div, { ref: composedRef, ...rootProps }, children, isFormControl && name && /* @__PURE__ */ React4.createElement( | ||
| BubbleInput, | ||
| { | ||
| 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.forwardRef( | ||
| (props, forwardedRef) => { | ||
| const context = useMentionContext(LABEL_NAME); | ||
| return /* @__PURE__ */ React4.createElement( | ||
| 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.memo( | ||
| React4.forwardRef( | ||
| (props, forwardedRef) => { | ||
| const { style, ...highlighterProps } = props; | ||
| const context = useMentionContext(HIGHLIGHTER_NAME); | ||
| const highlighterRef = React4.useRef(null); | ||
| const composedRef = useComposedRefs(forwardedRef, highlighterRef); | ||
| const [inputStyle, setInputStyle] = React4.useState(); | ||
| const onInputStyleChangeCallback = useCallbackRef(setInputStyle); | ||
| const onInputStyleChange = React4.useCallback(() => { | ||
| const inputElement = context.inputRef.current; | ||
| if (!inputElement) return; | ||
| const computedStyle = window.getComputedStyle(inputElement); | ||
| onInputStyleChangeCallback(computedStyle); | ||
| }, [context.inputRef, onInputStyleChangeCallback]); | ||
| const onSyncScrollAndResize = React4.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.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.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.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.createElement("span", { key: `text-${lastIndex}` }, value.slice(lastIndex, start)) | ||
| ); | ||
| } | ||
| segments.push( | ||
| /* @__PURE__ */ React4.createElement("span", { key: `mention-${start}`, "data-tag": "" }, value.slice(start, end)) | ||
| ); | ||
| lastIndex = end; | ||
| } | ||
| if (lastIndex < value.length) { | ||
| segments.push( | ||
| /* @__PURE__ */ React4.createElement("span", { key: `text-end-${value.length}` }, value.slice(lastIndex)) | ||
| ); | ||
| } | ||
| segments.push(/* @__PURE__ */ React4.createElement("span", { key: "space" }, "\xA0")); | ||
| return segments; | ||
| }, [context.inputRef, context.mentions]); | ||
| if (!inputStyle) return null; | ||
| return /* @__PURE__ */ React4.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.forwardRef( | ||
| (props, forwardedRef) => { | ||
| const context = useMentionContext(INPUT_NAME); | ||
| const composedRef = useComposedRefs(forwardedRef, context.inputRef); | ||
| const getTextWidth = React4.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.useCallback((input) => { | ||
| const style = window.getComputedStyle(input); | ||
| return Number.parseInt(style.lineHeight) || input.offsetHeight; | ||
| }, []); | ||
| const calculatePosition = React4.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.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.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.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.useCallback( | ||
| (event) => { | ||
| onMentionUpdate(event.currentTarget); | ||
| }, | ||
| [onMentionUpdate] | ||
| ); | ||
| const onCut = React4.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.useCallback( | ||
| (event) => { | ||
| onMentionUpdate(event.currentTarget); | ||
| }, | ||
| [onMentionUpdate] | ||
| ); | ||
| const onKeyDown = React4.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] === " "; | ||
| } | ||
| const textBetween = input.value.slice(m.end, cursorPosition); | ||
| return /^\s*$/.test(textBetween); | ||
| }); | ||
| if (mentionBeforeCursor) { | ||
| const hasTrailingSpace = input.value[mentionBeforeCursor.end] === " "; | ||
| 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 onSelect = React4.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.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.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.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React4.createElement(MentionHighlighter, null), /* @__PURE__ */ React4.createElement( | ||
| 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, | ||
| onChange: composeEventHandlers(props.onChange, onChange), | ||
| onClick: composeEventHandlers(props.onClick, onClick), | ||
| onPointerDown: composeEventHandlers( | ||
| props.onPointerDown, | ||
| onPointerDown | ||
| ), | ||
| onCut: composeEventHandlers(props.onCut, onCut), | ||
| onFocus: composeEventHandlers(props.onFocus, onFocus), | ||
| onKeyDown: composeEventHandlers(props.onKeyDown, onKeyDown), | ||
| onPaste: composeEventHandlers(props.onPaste, onPaste), | ||
| onSelect: composeEventHandlers(props.onSelect, onSelect) | ||
| } | ||
| )); | ||
| } | ||
| ); | ||
| MentionInput.displayName = INPUT_NAME; | ||
| var Input = MentionInput; | ||
| var PORTAL_NAME = "MentionPortal"; | ||
| var MentionPortal = React4.forwardRef( | ||
| (props, forwardedRef) => { | ||
| const { container, ...portalProps } = props; | ||
| return /* @__PURE__ */ React4.createElement( | ||
| Portal$1, | ||
| { | ||
| container, | ||
| ...portalProps, | ||
| ref: forwardedRef, | ||
| asChild: true | ||
| } | ||
| ); | ||
| } | ||
| ); | ||
| MentionPortal.displayName = PORTAL_NAME; | ||
| var Portal = MentionPortal; | ||
| var CONTENT_NAME = "MentionContent"; | ||
| var [MentionContentProvider, useMentionContentContext] = createContext(CONTENT_NAME); | ||
| var MentionContent = React4.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.useMemo(() => { | ||
| if (context.dir !== "rtl") return align; | ||
| return align === "start" ? "end" : align === "end" ? "start" : align; | ||
| }, [align, context.dir]); | ||
| const positionerContext = 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 = useComposedRefs( | ||
| forwardedRef, | ||
| (node) => positionerContext.refs.setFloating(node) | ||
| ); | ||
| const composedStyle = React4.useMemo(() => { | ||
| return { | ||
| ...style, | ||
| ...positionerContext.floatingStyles, | ||
| ...!context.open && forceMount ? { visibility: "hidden" } : {}, | ||
| // Hide content visually during pasting while keeping items registered | ||
| ...context.isPasting ? visuallyHidden : {} | ||
| }; | ||
| }, [ | ||
| style, | ||
| positionerContext.floatingStyles, | ||
| forceMount, | ||
| context.open, | ||
| context.isPasting | ||
| ]); | ||
| 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 | ||
| }); | ||
| useScrollLock({ | ||
| referenceElement: context.inputRef.current, | ||
| enabled: context.open && context.modal | ||
| }); | ||
| if (!forceMount && !context.open) return null; | ||
| return /* @__PURE__ */ React4.createElement( | ||
| MentionContentProvider, | ||
| { | ||
| side, | ||
| align: rtlAwareAlign, | ||
| arrowStyles: positionerContext.arrowStyles, | ||
| arrowDisplaced: positionerContext.arrowDisplaced, | ||
| onArrowChange: positionerContext.onArrowChange, | ||
| forceMount | ||
| }, | ||
| /* @__PURE__ */ React4.createElement( | ||
| FloatingFocusManager, | ||
| { | ||
| context: positionerContext.context, | ||
| modal: false, | ||
| initialFocus: context.inputRef, | ||
| returnFocus: false, | ||
| disabled: !context.open, | ||
| visuallyHiddenDismiss: true | ||
| }, | ||
| /* @__PURE__ */ React4.createElement( | ||
| 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] = createContext(ITEM_NAME); | ||
| var MentionItem = React4.forwardRef( | ||
| (props, forwardedRef) => { | ||
| const { value, label: labelProp, disabled = false, ...itemProps } = props; | ||
| const context = useMentionContext(ITEM_NAME); | ||
| const [itemNode, setItemNode] = React4.useState(null); | ||
| const composedRef = composeRefs(forwardedRef, (node) => setItemNode(node)); | ||
| const id = useId(); | ||
| const label = labelProp ?? value; | ||
| const isDisabled = disabled || context.disabled; | ||
| const isSelected = context.value.includes(value); | ||
| 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.createElement(MentionItemProvider, { label, value, disabled: isDisabled }, /* @__PURE__ */ React4.createElement( | ||
| Primitive.div, | ||
| { | ||
| ...{ [DATA_ITEM_ATTR]: "" }, | ||
| id, | ||
| role: "option", | ||
| "aria-selected": isSelected, | ||
| "data-selected": isSelected ? "" : void 0, | ||
| "data-highlighted": context.highlightedItem?.ref.current?.id === id ? "" : void 0, | ||
| "data-disabled": isDisabled ? "" : void 0, | ||
| ...itemProps, | ||
| ref: composedRef, | ||
| onClick: composeEventHandlers(itemProps.onClick, () => { | ||
| if (isDisabled) return; | ||
| const input = context.inputRef.current; | ||
| if (!input) return; | ||
| const selectionStart = input.selectionStart ?? 0; | ||
| const lastTriggerIndex = input.value.lastIndexOf( | ||
| context.trigger, | ||
| selectionStart | ||
| ); | ||
| if (lastTriggerIndex !== -1) { | ||
| context.onMentionAdd(value, lastTriggerIndex); | ||
| } | ||
| input.focus(); | ||
| }), | ||
| onPointerDown: 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.pointerType === "mouse") { | ||
| event.preventDefault(); | ||
| } | ||
| } | ||
| ), | ||
| onPointerMove: composeEventHandlers(itemProps.onPointerMove, () => { | ||
| if (isDisabled || !itemNode) return; | ||
| context.onHighlightedItemChange({ | ||
| ref: { current: itemNode }, | ||
| label, | ||
| value, | ||
| disabled: isDisabled | ||
| }); | ||
| }) | ||
| } | ||
| )); | ||
| } | ||
| ); | ||
| MentionItem.displayName = ITEM_NAME; | ||
| var Item = MentionItem; | ||
| export { Content, Input, Item, Label, MentionContent, MentionInput, MentionItem, MentionLabel, MentionPortal, MentionRoot, Portal, Root }; |
Empty package
Supply chain riskPackage does not contain any code. It may be removed, is name squatting, or the result of a faulty package publish.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
2448
-97.69%2
-66.67%0
-100%2
Infinity%