export function elementAt(node ) {
if (!node) return null;
if (node instanceof Element) return node;
return node.parentElement;
}
export function currentRanges() {
const selection = window.getSelection();
if (!selection) return [];
const out = [];
for (let i = 0; i < selection.rangeCount; i += 1) {
out.push(selection.getRangeAt(i).cloneRange());
}
return out;
}
export function rangeScope(range , fallback ) {
const root = range.commonAncestorContainer;
const element = root instanceof Element ? root : root.parentElement;
return element && fallback.contains(element) ? element : fallback;
}
export function elementsIn(range , fallback ) {
const out = [];
const scope = rangeScope(range, fallback);
for (let el = scope; el && el !== fallback; el = el.parentElement) {
out.push(el);
}
const walker = document.createTreeWalker(scope, NodeFilter.SHOW_ELEMENT, null);
let node = walker.nextNode();
while (node) {
const el = node ;
if (range.intersectsNode(el)) out.push(el);
node = walker.nextNode();
}
return out;
}
export function textNodesIn(range , fallback ) {
const out = [];
const scope = rangeScope(range, fallback);
const walker = document.createTreeWalker(scope, NodeFilter.SHOW_TEXT, null);
let node = walker.nextNode();
while (node) {
if (range.intersectsNode(node) && node.textContent && node.textContent.length > 0) {
out.push(node);
}
node = walker.nextNode();
}
return out;
}
export function closestTag(el , tagName , boundary ) {
if (!tagName) return null;
const upper = tagName.toUpperCase();
let current = el;
while (current && current !== boundary) {
if (current.tagName === upper) return current;
current = current.parentElement;
}
return null;
}
export function selectionFullyMarked(
tagName ,
boundary ,
) {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return false;
if (selection.isCollapsed) {
const el = elementAt(selection.anchorNode);
return el !== null && closestTag(el, tagName, boundary) !== null;
}
const ranges = currentRanges();
if (ranges.length === 0) return false;
for (const range of ranges) {
const nodes = textNodesIn(range, boundary);
if (nodes.length === 0) return false;
for (const node of nodes) {
const el = elementAt(node);
if (!el || closestTag(el, tagName, boundary) === null) return false;
}
}
return true;
}
export function markedInSelection(tagName , fallback ) {
const out = [];
const seen = new Set ();
const upper = tagName.toUpperCase();
const collect = (range ) => {
for (const el of elementsIn(range, fallback)) {
if (el.tagName === upper && !seen.has(el)) {
seen.add(el);
out.push(el);
}
}
};
for (const range of currentRanges()) {
if (fallback.contains(range.commonAncestorContainer)) collect(range);
}
const selection = window.getSelection();
if (selection?.isCollapsed) {
const el = elementAt(selection.anchorNode);
if (el && fallback.contains(el) && el.tagName === upper && !seen.has(el)) {
seen.add(el);
out.push(el);
}
}
return out;
}
export function placeCaret(node , offset ) {
if (!node.isConnected) return;
const range = document.createRange();
try {
range.setStart(node, Math.max(0, Math.min(offset, node.textContent?.length ?? 0)));
range.collapse(true);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
} catch {
}
}