I would like to be able to convert the current mouse position to a range, in CKEditor in particular.
The CKEditor provides an API for setting the cursor according to a range:
var ranges = new CKEDITOR.dom.range( editor.document );
editor.getSelection().selectRanges( [ ranges ] );
Since CKEditor provides this API, the problem may be simplified by removing this requirement and just find a way to produce the range from the mouse coordinates over a div containing various HTML elements.
However, this is not the same as converting a mouse coordinate into the cursor position in a textarea since textareas have fixed column widths and row heights where the CKEditor renders HTML through an iframe.
Based on this, it looks like the range may be applied to elements.
How would you figure out the start/end range which is closest to the current mouse position?
Edit:
An example of how one might use the ckeditor API to select a range on the mouseup event.
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 );
});
The problem with the above example is that the event target node (e.data.$.target) is only firing for nodes such as HTML, BODY, or IMG but not for text nodes. Even if it did, these nodes represent chunks of text which wouldn't support setting the cursor to the position of the mouse within that chunk of text.
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.