UNPKG

@assistant-ui/react

Version:

Open-source TypeScript/React library for building production-grade AI chat experiences

84 lines (83 loc) • 3.78 kB
// src/utils/getSelectionMessageId.ts const QUOTE_SELECTABLE_SELECTOR = "[data-aui-quote-selectable]"; const getElement = (node) => { return node instanceof HTMLElement ? node : node?.parentElement ?? null; }; const findMessageElement = (node) => { let el = node instanceof HTMLElement ? node : node?.parentElement ?? null; while (el) { if (el.getAttribute("data-message-id")) return el; el = el.parentElement; } return null; }; const isExcluded = (marker) => { return marker.getAttribute("data-aui-quote-selectable") === "false"; }; const hasQuoteSelectableRegion = (messageElement) => { if (messageElement.matches(QUOTE_SELECTABLE_SELECTOR) && !isExcluded(messageElement)) return true; for (const marker of messageElement.querySelectorAll(QUOTE_SELECTABLE_SELECTOR)) if (!isExcluded(marker)) return true; return false; }; const findQuoteMarker = (node, messageElement) => { const marker = getElement(node)?.closest(QUOTE_SELECTABLE_SELECTOR); if (!(marker instanceof HTMLElement)) return null; if (!messageElement.contains(marker)) return null; return marker; }; const normalizeRangeEnd = (range) => { if (range.endOffset !== 0 || range.collapsed) return range; const walker = range.endContainer.ownerDocument?.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); if (!walker) return range; walker.currentNode = range.endContainer; while (walker.previousNode()) { const node = walker.currentNode; const offset = node instanceof Text ? node.length : node.childNodes.length; if (range.comparePoint(node, offset) < 0) return range; const marker = (node instanceof Element ? node : node.parentElement)?.closest(QUOTE_SELECTABLE_SELECTOR); if (marker && isExcluded(marker) && !node.contains(range.endContainer)) return range; if (!(node instanceof Text) || !node.data.trim()) continue; const normalized = range.cloneRange(); normalized.setEnd(node, offset); return normalized.collapsed ? range : normalized; } return range; }; const intersectsExcluded = (scope, ranges) => { for (const marker of scope.querySelectorAll(QUOTE_SELECTABLE_SELECTOR)) { if (!isExcluded(marker)) continue; if (ranges.some((range) => range.intersectsNode(marker))) return true; } return false; }; const getSelectionMessageId = (selection, root) => { let { anchorNode, focusNode } = selection; if (!anchorNode || !focusNode) return null; const ranges = Array.from({ length: selection.rangeCount }, (_, i) => selection.getRangeAt(i)); if (ranges.length === 1) { const range = normalizeRangeEnd(ranges[0]); if (range !== ranges[0]) { ranges[0] = range; anchorNode = range.startContainer; focusNode = range.endContainer; } } const anchorMessageElement = findMessageElement(anchorNode); const focusMessageElement = findMessageElement(focusNode); if (!anchorMessageElement || anchorMessageElement !== focusMessageElement) return null; if (root && !root.contains(anchorMessageElement)) return null; const messageId = anchorMessageElement.getAttribute("data-message-id"); if (!messageId) return null; const anchorMarker = findQuoteMarker(anchorNode, anchorMessageElement); const focusMarker = findQuoteMarker(focusNode, anchorMessageElement); if (anchorMarker && isExcluded(anchorMarker)) return null; if (focusMarker && isExcluded(focusMarker)) return null; if (hasQuoteSelectableRegion(anchorMessageElement)) { if (!anchorMarker || anchorMarker !== focusMarker) return null; } const scope = anchorMarker ?? anchorMessageElement; for (const { commonAncestorContainer } of ranges) if (!scope.contains(commonAncestorContainer)) return null; return intersectsExcluded(scope, ranges) ? null : messageId; }; export { getSelectionMessageId }; //# sourceMappingURL=getSelectionMessageId.js.map