JavaScript将鼠标位置转换为选择范围

JavaScript convert mouse position to selection range

本文关键字:选择 范围 转换 位置 鼠标 JavaScript      更新时间:2023-09-26

我希望能够将当前鼠标位置转换为范围,特别是在CKEditor中。

CKEditor提供了一个API,用于根据范围设置游标:

var ranges = new CKEDITOR.dom.range( editor.document );
editor.getSelection().selectRanges( [ ranges ] );

由于CKEditor提供了这个API,通过删除这个要求,并找到一种方法从包含各种HTML元素的div上的鼠标坐标生成范围,可以简化问题。

然而,这与将鼠标坐标转换为文本区域中的光标位置不同,因为文本区域具有固定的列宽和行高,其中CKEditor通过iframe呈现HTML。

基于此,看起来该范围可以应用于元素。

如何计算最接近当前鼠标位置的开始/结束范围?

编辑:下面是如何使用ckeditor API在mouseup事件上选择范围的示例。

editor.document.on('mouseup', function(e) {
    this.focus();
    var node = e.data.$.target;
    var range = new CKEDITOR.dom.range( this.document );
    range.setStart(new CKEDITOR.dom.node(node), 0);
    range.collapse();
    var ranges = [];
    ranges.push(range);
    this.getSelection().selectRanges( ranges );
});
上面例子的问题是事件目标节点(e.data.$.target)只对HTML、BODY或IMG这样的节点触发,而对文本节点则不触发。即使有,这些节点表示的文本块也不支持将光标设置到该文本块中的鼠标位置。

你想做的事情在浏览器中真的很难。我不熟悉ckeditor特别,但常规的javascript允许你选择文本使用范围,所以我不认为它添加任何特别的。您必须找到包含点击的浏览器元素,然后在被点击的元素中找到字符。

检测浏览器元素很简单:您需要在每个元素上注册处理程序,或者使用事件的目标字段。有很多关于这方面的信息,如果这就是你遇到的问题,请在stackoverflow上问一个更具体的问题。

一旦有了元素,您需要找出元素中的哪个字符被单击,然后创建一个适当的范围将光标放置在那里。正如你链接到的帖子所述,浏览器的变化使这真的很难。这个页面有点过时了,但是对范围有很好的讨论:http://www.quirksmode.org/dom/range_intro.html

range不能告诉你它们在页面上的位置,所以你必须使用另一种技术来找出被点击的文本。

我从来没有在javascript中看到过一个完整的解决方案。几年前我研究过一个问题,但我没有得到一个令我满意的答案(一些非常困难的边缘情况)。我使用的方法是一个可怕的hack:将span插入文本中,然后使用它们执行二进制搜索,直到找到包含鼠标单击的最小可能的span。span不会改变布局,所以你可以使用span的position_x/y属性来确定它们是否包含点击。

。假设在一个节点中有以下文本:

<p>Here is some paragraph text.</p>

我们知道点击是在这段的某个地方。用span:

将段落分成两段
<p><span>Here is some p</span>aragraph text.</p>

如果span中包含点击坐标,则继续在这一半中进行二进制搜索,否则搜索后一半。

这对于单行非常有效,但如果文本跨越多行,则必须首先找到换行符,否则跨距可能重叠。你还必须弄清楚,当点击不是在任何文本上,而是在元素中——例如,在段落的最后一行结束后,该怎么做。

自从我在这个浏览器上工作以来,速度已经快了很多。它们现在可能足够快了,可以在每个字符周围加上s,然后在每两个字符周围加上s,以此来创建一个易于搜索的二叉树。您可以尝试这种方法——它会使您更容易确定您正在处理的是哪一行。

这是一个非常困难的问题,如果有答案,可能不值得你花时间去想。

很抱歉撞了一个旧线程,但我想在这里发布这个以防其他人偶然发现这个问题,因为关于这个问题的信息很少。我只需要为Outlook for web用户脚本编写一个函数,因为它们覆盖了默认的拖放功能,并在撰写框中破坏了它。这是我想到的解决方案:

function rangeFromCoord(x, y) {
    const closest = {
        offset: 0,
        xDistance: Infinity,
        yDistance: Infinity,
    };
    const {
        minOffset,
        maxOffset,
        element,
    } = (() => {
        const range = document.createRange();
        range.selectNodeContents(document.elementFromPoint(x, y));
        return {
            element: range.startContainer,
            minOffset: range.startOffset,
            maxOffset: range.endOffset,
        };
    })();
    for(let i = minOffset; i <= maxOffset; i++) {
        const range = document.createRange();
        range.setStart(element, i);
        range.setEnd(element, i);
        const marker = document.createElement("span");
        marker.style.width = "0";
        marker.style.height = "0";
        marker.style.position = "absolute";
        marker.style.overflow = "hidden";
        range.insertNode(marker);
        const rect = marker.getBoundingClientRect();
        const distX = Math.abs(x - rect.left);
        const distY = Math.abs(y - rect.top);
        marker.remove();
        if(closest.yDistance > distY) {
            closest.offset = i;
            closest.xDistance = distX;
            closest.yDistance = distY;
        } else if(closest.yDistance === distY) {
            if(closest.xDistance > distX) {
                closest.offset = i;
                closest.xDistance = distX;
                closest.yDistance = distY;
            }
        }
    }
    const range = document.createRange();
    range.setStart(element, closest.offset);
    range.setEnd(element, closest.offset);
    return range;
}

您所要做的就是传递客户端坐标,函数将自动选择该位置上最具体的元素。它将使用该选择来获取浏览器使用的父元素(最明显的是contenteditable元素),以及最大和最小偏移量。然后它将继续,迭代通过偏移量,在每个偏移量放置markerposition: absolute; width: 0; height: 0; overflow: hidden;的跨度元素以探测它们的位置,删除它们,并检查距离。根据大多数文本编辑器,它将首先尽可能靠近Y坐标,然后移动到X坐标。一旦找到最近的位置,它将创建一个新的选择并返回它。

有两种方法可以做到这一点,就像每个所见即所得一样。

:你放弃了,因为它太难了,它最终会成为浏览器杀手;

第二:

-你试着解析文本,并把它放在确切的地方在一个半透明的文本区或div以上的原始,但在这里我们有两个问题:

1)如何解析动态数据块以仅获取文本并确保将其映射到实际内容的确切位置

2)你如何解决更新解析你输入的每一个字符或你在编辑器中做的每一个动作。

最后,这只是一个"残酷的奥德赛到DOM树的黑暗面",但如果你选择第二种方式,那么你的帖子中的代码将像魅力一样工作。

我正在做一个类似的任务,允许TinyMCE(内联模式)在鼠标单击位置使用插入符号进行初始化。以下代码至少可以在最新的Firefox和Chrome中运行:

let contentElem  = $('#editorContentRootElem');
let editorConfig = { inline: true, forced_root_block: false };
let onFirstFocus = () => {
  contentElem.off('click focus', onFirstFocus);
  setTimeout(() => {
    let uniqueId = 'uniqueCaretId';
    let range    = document.getSelection().getRangeAt(0);
    let caret    = document.createElement("span");
    range.surroundContents(caret);
    caret.outerHTML = `<span id="${uniqueId}" contenteditable="false"></span>`;
    editorConfig.setup = (editor) => {
      this.editor = editor;
      editor.on('init', () => {
        var caret = $('#' + uniqueId)[0];
        if (!caret) return;
        editor.selection.select(caret);
        editor.selection.collapse(false);
        caret.parentNode.removeChild(caret);
      });
    };
    tinymce.init(editorConfig);         
  }, 0); // after redraw
}; // onFirstFocus
contentElem.on('click focus', onFirstFocus);

似乎鼠标单击/焦点事件并重新绘制(setTimeout ms 0)后,document.getSelection().getRangeAt(0)返回有效的光标范围。我们可以把它用于任何目的。TinyMCE将插入符号移动到初始化时开始,所以我在当前范围开始处创建了特殊的span '插入符号'元素,然后强制编辑器选择它,然后删除它。