@assistant-ui/react
Version:
Open-source TypeScript/React library for building production-grade AI chat experiences
84 lines (83 loc) • 3.78 kB
JavaScript
// 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