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

时间:2011-11-14 21:34:48

标签: javascript ckeditor range

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

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

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

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

但是,这与将鼠标坐标转换为textarea中的光标位置不同,因为textareas具有固定的列宽和行高,CKEditor通过iframe呈现HTML。

基于this,看起来范围可能会应用于元素。

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

编辑: 一个示例,说明如何使用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等节点触发,但不针对文本节点触发。即使这样做,这些节点也代表不支持将光标设置到该文本块内鼠标位置的文本块。

4 个答案:

答案 0 :(得分:2)

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

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

获得元素后,您需要找出元素中的哪个字符被单击,然后创建一个合适的范围以将光标放在那里。正如您所说的那样,浏览器的变化使得这非常困难。此页面有点过时,但对范围进行了很好的讨论:http://www.quirksmode.org/dom/range_intro.html

范围无法告诉您他们在页面上的位置,因此您必须使用其他技术来查找点击了哪些文字。

我从未在javascript中看到过这方面的完整解决方案。几年前我曾经做过一次,但我没有想出一个我很满意的答案(一些非常难以处理的案例)。我使用的方法是一个可怕的黑客:插入文本然后使用它们执行二进制搜索,直到找到包含鼠标单击的最小可能跨度。 Spans不会更改布局,因此您可以使用span的position_x / y属性来查找它们包含的单击。

E.g。假设您在节点中有以下文本:

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

我们知道点击是在本段的某处。用段落将段落分成两半:

<p><span>Here is some p</span>aragraph text.</p>

如果跨度包含点击坐标,则在该半边继续二分搜索,否则搜索下半部分。

这适用于单行,但如果文本跨越多行,则必须先找到换行符,否则跨距可能会重叠。你还需要弄清楚当点击不在任何文本上但在元素中时要做什么 - 例如在段落中最后一行的末尾。

因为我在这个浏览器上工作得快得多。它们现在可能足够快,可以在每个字符周围添加s,然后围绕每两个字符等创建一个易于搜索的二叉树。您可以尝试这种方法 - 这样可以更容易地确定您正在使用哪条线。

TL; DR这是一个非常难的问题,如果有答案,可能不值得花时间来提出它。

答案 1 :(得分:1)

有两种方法可以做到这一点,就像每个WYSIWYG一样。

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

第二:   - 您尝试解析文本并将其放在半透明textarea或div上面的确切位置,但这里我们有两个问题:

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

2)您如何解决更新以解析您键入的每个错误字符或您在编辑器中执行的每个操作。

最后,这只是“对DOM树黑暗面的残酷冒险”,但是如果你选择第二种方式,那么你帖子中的代码就像魅力一样。

答案 2 :(得分:0)

我正在执行类似的任务,以允许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会移动插入符号以开始初始化,因此我在当前范围开始处创建了特殊跨度的“插入符号”元素,然后强制编辑器选择了该元素,然后将其删除。

答案 3 :(得分:0)

很抱歉碰到一个旧的线程,但是我想在这里发帖,以防其他人偶然发现这个问题,因为关于此的信息很少。我只需要编写一个针对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元素)以及最大和最小偏移量。然后,它将继续进行,遍历偏移量,将marker跨度元素与position: absolute; width: 0; height: 0; overflow: hidden;放置在每个偏移量处以探查其位置,移除它们并检查距离。根据大多数文本编辑器,它将首先在Y坐标上尽可能接近,然后在X坐标上移动。找到最接近的位置后,它将创建一个新选择并返回。