导航菜单
登录 注册

背景与问题

最近在将 XEditor 插件兼容到 Typecho 1.3 时,我再次遇到了 Vditor 编辑器的一些功能限制。Vditor 虽然功能强大,但在某些细节实现上文档不够完善,公共方法也不够丰富。特别是当我需要判断光标是否位于行首时,发现官方提供的 getCursorPosition 方法返回的是像素坐标而非文本位置。

{
    top: 10.32432
    left: 10
}

这样的返回值对于判断光标在文本中的实际位置几乎没有帮助。翻阅 issue #1628 发现有人提出过类似需求,但官方解决方案并不明确。于是,我决定自己实现这一功能。

解决方案设计

第一步:获取光标所在节点

首先需要获取光标当前所在的 DOM 节点。通过浏览器原生的 Selection API 可以做到这一点:

let cursorElement, cursorRelativeCols;

function updateCursorElement() {
    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0) {
        cursorElement = null;
        return;
    }

    const range = selection.getRangeAt(0);
    const selectedNode = range.startContainer;

    // 处理文本节点和元素节点的差异
    cursorElement = selectedNode.nodeType === Node.TEXT_NODE
        ? selectedNode.parentElement
        : selectedNode;

    cursorRelativeCols = range.startOffset;
}

这段代码会在光标位置变化时更新 cursorElementcursorRelativeCols 两个变量,分别表示光标所在的元素节点和在当前文本节点中的偏移量。

第二步:绑定编辑器事件

为了让光标位置信息实时更新,需要在 Vditor 初始化时绑定相关事件:

new Vditor('vditor', {
    // ... 其他配置
    blur: updateCursorElement,
    focus: updateCursorElement,
    input: updateCursorElement
});

这样无论是通过键盘输入、鼠标点击还是失去/获得焦点,都会触发光标位置的更新。

第三步:分析编辑器DOM结构

观察 Vditor 生成的 HTML 结构,发现它使用特定的标记来表示换行和文本内容:

<div data-block="0">
    <span data-type="text">
解决因Caddy 伪静态规则引起 TeStore 插件页面404
下载方式:免费下载更新时间:2021-01-22 14:01:44文件大小:0.00下载次数:
</span> <span data-type="newline"><br><span style="display: none"></span></span> <span class="vditor-sv__marker">!</span> <!-- 更多内容... --> </div>

关键点在于:

  1. <span data-type="newline"> 表示换行标记
  2. 文本内容都被包裹在 <span> 元素中

第四步:实现行号列号计算

基于上述DOM结构,可以实现光标位置的计算:

getCursorPosition() {
    const position = editorInstance.getCursorPosition();
    if (!cursorElement) {
        return {
            ...position,
            line: 1
        };
    }
    
    // 计算行号:统计前面的换行标记数量
    let line = 0, prevEl = cursorElement.previousElementSibling;
    while (prevEl) {
        if (prevEl.getAttribute('data-type') === 'newline') {
            line++;
        }
        prevEl = prevEl.previousElementSibling;
    }
    
    // 计算列号:统计当前行前面的字符数
    let text = "";
    let el = cursorElement.closest('span:not([data-type="newline"])');
    let col = cursorRelativeCols;
    if (el) {
        // 向前遍历所有同级元素直到遇到换行标记
        let prevSibling = el.previousElementSibling;
        while (prevSibling && prevSibling.getAttribute('data-type') !== 'newline') {
            text = prevSibling.textContent + text;  // 累加前面的文本内容
            prevSibling = prevSibling.previousElementSibling;
        }
        col = cursorRelativeCols + text.length;
    }
    
    return {
        ...position,
        line: line + 1,  // 行号1开始
        column: col + 1  // 列号1开始
    }
}

技术细节解析

行号计算原理

行号的计算基于以下逻辑:

  1. 从当前光标所在元素开始,向前遍历所有兄弟节点
  2. 每遇到一个 data-type="newline" 的元素,行号加1
  3. 最终行号为遇到的换行标记数加1(因为第一行前面没有换行标记)

这种方法与VIM等编辑器的行号定位思路类似,只是实现方式不同。

列号计算原理

列号计算更为复杂:

  1. 首先找到光标所在的文本元素(跳过换行标记)
  2. 向前遍历同级元素,累加所有非换行元素的文本内容长度
  3. 最后加上光标在当前文本节点中的偏移量
  4. 结果加1转为1开始索引

边界情况处理

代码中已经考虑了多种边界情况:

  1. 没有选中任何内容时(!cursorElement)默认返回第一行
  2. 正确处理了文本节点和元素节点的差异
  3. 通过 closest() 方法确保只计算当前行内的元素
  4. 使用 previousElementSibling 而非 previousSibling 避免文本节点干扰

替代方案探讨

除了上述实现,还有其他可能的解决方案:

  1. 修改Vditor源码:直接添加行列号返回功能
  2. 使用contenteditable的Range API:更精确地控制选区
  3. 基于Markdown源码计算:同步编辑内容和原始Markdown文本的位置
  4. 使用第三方库:如CodeMirror或Monaco Editor的定位算法

不过考虑到与现有系统的集成成本,当前方案可能是最实用的。

总结

通过深入分析Vditor的DOM结构和结合浏览器原生API,我们成功实现了光标行列号的精确定位功能。这个方案不仅解决了判断光标是否在行首的需求,还为后续开发更多基于位置的编辑器功能奠定了基础。

最后

新版基于 Vditor 的 XEditor 插件也分享了,不过只有原生功能按钮,其他的要你自己实现了。

资源下载

资源为外部正规网站提供,本站不保存任何下载内容,请自行甄别安全性。
2025年06月27日,六月初三,星期五,在这里每天60秒读懂世界!
上一篇