// 创建全局唯一 marker
const globalMarker = (() => {
const marker = document.createElement('span');
marker.textContent = '\u200b';
marker.style.cssText = 'position: absolute; visibility: hidden; pointer-events: none;';
return marker;
})();
// 获取光标位置
function getCursorPosition() {
const sel = window.getSelection();
if (!sel.rangeCount) return null;
// 克隆并 collapse Range
const range = sel.getRangeAt(0).cloneRange();
range.collapse(true);
// 找到可编辑容器,用于取行高
let hitNode = sel.focusNode;
if (hitNode.nodeType === Node.TEXT_NODE) hitNode = hitNode.parentElement;
const editableEl = hitNode.closest('[contenteditable="true"]');
const style = editableEl ? window.getComputedStyle(editableEl) : null;
// 优先用 line-height,否则 fallback 到 font-size * 1.625 或 26px
const rawLineH = style && parseFloat(style.lineHeight)
|| (style && parseFloat(style.fontSize) * 1.625)
|| 26;
const lineH = rawLineH;
const paddingLeft = style ? parseFloat(style.paddingLeft) || 0 : 0;
// 尝试浏览器原生的 clientRects
const rects = Array.from(range.getClientRects());
let baseRect, x;
if (rects.length) {
baseRect = rects[rects.length - 1];
} else {
// 先判断是否是段落空行,段落空行直接通过段落获取
const paragraph = findParentParagraph(range.startContainer);
if (paragraph && !paragraph.textContent.replace(/[\u200B-\u200D\uFEFF]/g, '').trim()) {
baseRect = paragraph.getBoundingClientRect();
const style = window.getComputedStyle(paragraph);
x = baseRect.left + parseFloat(style.paddingLeft);
} else {
// 回退:插 marker 测一次
range.insertNode(globalMarker);
baseRect = globalMarker.getBoundingClientRect();
globalMarker.remove();
}
}
// 计算高度:统一用行高 * 比例
const height = lineH * cursorHeightRelativeToLineHeight;
// 计算 y:把原生/marker 获取的 rect.top 对齐到行高
// rectTop + (rect.height - height)/2 可能让光标在垂直居中
const gap = (baseRect.height - height) / 2;
const y = baseRect.top + gap;
// x 贴在文字末尾
x = x ? x : baseRect.right;
return baseRect.width + baseRect.height > 0 ? { x, y, height } : null;
}
