JavaScript convert mouse position to selection range

旧街凉风 提交于 2019-12-04 00:20:59

What you're trying to do is really hard in a browser. I'm not familar with ckeditor in particular, but regular javascript allows you to select text using a range so I don't think it's adding anything special. You have to find the browser element that contains the click, then find the character within the element that was clicked.

Detecting the browser element is the easy bit: you need to either register your handler on every element, or use the event's target field. There is lots of info on this out there, ask a more specific question on stackoverflow if that's what you're having trouble with.

Once you have the element you need to find out which character within the element was clicked, then create an appropriate range to put the cursor there. As the post you linked to stated, browser variations make this really hard. This page is a bit dated, but has a good discussion of ranges: http://www.quirksmode.org/dom/range_intro.html

Ranges can't tell you their positions on the page, so you'll have to use another technique to find out what bit of text was clicked.

I've never seen a complete solution to this in javascript. A few years ago I worked on one but I didn't come up with an answer I was happy with (some really hard edge cases). The approach I used was a horrible hack: insert spans into the text then use them to perform binary search until you find the smallest possible span containing the mouse click. Spans don't change the layout, so you can use the span's position_x/y properties to find out they contain the click.

E.g. suppose you have the following text in a node:

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

We know the click was somewhere in this paragraph. Split the paragraph in half with a span:

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

If the span contains the click coordinates, continue binary search in that half, otherwise search the second half.

This works great for single lines, but if the text spans multiple lines you have to first find line breaks, or the spans can overlap. You also have to work out what to do when the click wasn't on any text but was in the element --- past the end of the last line in a paragraph for example.

Since I worked on this browsers have got a lot faster. They're probably fast enough now to add s around each character, then around each two characters etc to create a binary tree which is easy to search. You could try this approach - it would make it much easier to work out which line you're working on.

TL;DR this is a really hard problem and if there is an answer, it might not be worth your time to come up with it.

There are two ways of doing this, just like every WYSIWYG does.

First: - you give up because it is too hard and it will end up to be a browser killer;

Second: - you try to parse the text and put it in the exact place in a semitransparent textarea or div above the original, but here we have two problems:

1) how would you parse the dynamic chunks of data to get only the text and to be sure you map it over the exact position of the actual content

2) how would you solve the update to parse for every darn character you type or every action you do in the editor.

In the end this is just a "A brutal odyssey to the dark side of the DOM tree", but if you choose the second way, than the code from your post will work like a charm.

I was working on a similar task to allow TinyMCE (inline mode) to initialize with a caret placed in mouse click position. The following code works in the latest Firefox and Chrome, at least:

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);

Explanation

It seems that after mouse click/focus event and redraw (setTimeout ms 0) document.getSelection().getRangeAt(0) returns valid cursor range. We can use it for any purpose. TinyMCE moves caret to start on initialization, so I create special span 'caret' element at current range start and later force editor to select it, then remove it.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!