preserve cursor position when changing innerHTML in a contenteditable div - javascript

I'm building an Editor that does syntax highlighting on code. My current approach is to just take the text in the editor (a div with contenteditable set to true) and check for regex matches in the string. I then replace the matches with <span> elements and apply some styles to those span elements. Then I replace the complete text in the divider with the new text using the .innerHTML attribute. This works just fine, but I have to type backwards, because upon inserting the text, my cursor is reset to position zero. I tried recording the value of selectionStart before inserting and then doing
element.selectionStart = oldSelectionStart + 1;
but it didn't work. I assume this is because of chrome's render pipeline, where JavaScript is run before rendering the page, and the cursor is reset upon render, not upon set... Can anyone help? How do I manage to keep the cursor where it was?

element.selectionStart works for input elements, not for contentEditable elements. You should try creating a Range object, set its startContainer & startOffset and collapse it to set caret at the required position. Refer to [https://developer.mozilla.org/en-US/docs/Web/API/Range] for details.
PS: If you would like to set your cursor to the 'end of line', you could do it easily with range.selectNode(requiredNode) and range.collapse(true) to move the caret to the end of node, i.e., line

Related

How do I stop cursor moving to the start every time innerHTML is changed?

I have a function that separates words into separate span tags and updates the div I am typing into but every time I update the innerHTML the cursor moves to the start of the box so the next character I type outputs behind the last one instead of after it. Could someone tell me how I can stop the cursor from doing this?
Here is my code
const editorDiv = document.getElementById('editor');
function wrapWords(str, tmpl) { //separates words into <span>
return str.replace(/\w+/g, tmpl || "<span>$&</span>");
}
editorDiv.addEventListener('keydown', function(event) {
editorDiv.innerHTML = wrapWords(editorDiv.innerText)
});
<div id="editor" contenteditable="true">
<span>hello</span>
</div>
When I type into the div text looks like this:
.siht ekil skool txeT
One way I have solved this in the past is to use 2 seperate elements, positioned on top of each other, with position: absolute, so that they overlap in a pixel-perfect manner.
The element on top is a regular input element, and its text color is set to transparent.
The element behind it contains the styled text (spans etc.)
This way, you can replace the styled content as the user types, without interfering with the input element's cursor at all.
Sorry that I don't have an 'essential code snippet' at hand, but I hope you get the idea. It did work well in practice for me.

Set the caret at a specific point in a dynamically added HTML

I have a contenteditable div
<div contenteditable="true" class="editable"></div>
I append some html to it using :
$('.editable').html('<p><br></p>');
How I can set the caret before <br> ?
Setting cursor position in content editables is tricky business. The handling of cursor is browser dependent.
There are range and selection apis which are quite inconsistent over different browsers.
This is how to play with cursors:
Create a range object by document.createRange()
select the specified position by range methods (see the api)
Get selection object by selection=window.getSelection()
remove all existing selection by selection.removeAllRanges();
add the range to the selection by selection.addRange(range);
In this situations there is no text content in paragraph element, so you cannot place cursor inside it. You can either select the node by range.selectNode or you can place the cursor before/after the element.
Placing the cursor before or after will also behave weirdly in different browsers especially in edge cases (like in empty div or first or last postion).
You have to clearly specify what you want to achieve in the UI. Then I can suggest some logic to accomplish that.

How to get the bounding rect of selected text inside an <input>?

I can use document.createRange() to get a range of individual characters in a textnode, then Range.getBoundingClientRect() to get the position on the screen of those characters. I want to do the same thing for text in an input element, which used to be possible in IE only with createTextRange.
This jsfiddle sums up the problem - http://jsfiddle.net/sobu5ug2/2/
I can't use the same method for an input because Range.setStart/End only works on the element level, not the character level, for an input. I also tried window.getSelection().getRangeAt(0).getBoundingClientRect() but that returns a bounding rect of all 0's. I'd like to do this without actually selecting the text, if possible.
Is there any way to do this in a modern browser? Thanks.
Here is an example how to do it:
https://jh3y.medium.com/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a
You need to create a div, copy all styles of the input element into that div, copy text up to the selection into that div, add a span inside that div with the selection and then you can measure this span relative to the div before removing the div again.

Retain cursor position in contenteditable

I am trying to create a richtext area using contenteditable. The parser for the highlighting returns a html string which I replace the content of the element with (perhaps ineffective, thats the way it has to be for now).
The problem with contenteditable is that the cursor position is not retained after something like this, which I can understand.
Is there a way to retain this position? I could make the parser return a element representing where the caret should be (given the cursors offset in the text) if this would aid in a solution.
Thanks in advance.
I could make the parser return a element representing where the caret should be
That would be enough. Suppose you parse the generated HTML and now you have a reference to the caret-should-be-here element in sentinel.
Use document.createRange() to create a DOM range. Let here be the new range. Call here.selectNode(sentinel) to have the range surround the element.
Call here.deleteContents() to remove the dummy element.
Call window.getSelection().removeAllRanges() to clear the current selection, which has gotten messed up by the HTML change.
Call window.getSelection().addRange(here) to put the cursor where the element used to be.
is sentinel my anchor element?
Yeah, I guess.
How do I fetch the cursor position in the string? ... I want the offset from the start of the string.
Let's start with the cursor position. The zeroth range of the window's selection should be the cursor's position. That is, the range's start and end are in the same place, at the cursor. However, these locations are expressed in a way that's geared toward DOM trees, rather than for string and offsets. They have a (start|end)Container and a (start|end)Offset. Check the DOM spec for what these mean.
You're interested in some sort of string offset. As I interpret it, this would be like, if you took a plaintext-only version of the subtree, what index corresponds to the range? There are multiple ways to define a plaintext version of a subtree. One is what the textContent property returns, where foo<br>bar gives "foobar". Some browsers define innerText, where foo<br>bar gives "foor\nbar". You've probably picked the one that you'll be using, but it's not stated in the question.
Anyway, here's an idea, but it might not be the right kind of offset for your app.
Set the window selection to a single range going from the beginning (wherever index 0 should be) to the cursor position.
Read window.getSelection().toString().length. In the browsers I've developed for, toString on a selection object gives results comparable to innerText.

Get exact caret position

How can I get the current caret position in tinyMCE? Referring to this question "Get cursor position or number of line on which the cursor is in TinyMCE" there is mentioned how to get the line number in tinyMCE, however I can't find no reference on how to get the actual caret position? I need this since I am using tinyMCE as a document for real-time collaboration therefore I need to know the position of the caret for clients writing in the same document so that I broadcast the position on some events (keystroke, click, etc..) and paint custom carets on the position for one client to know where the other client is writing or editing.
The only solution I found was to add a span after the content, select it and remove it as follows:
<span id="caret_pos_holder"></span>
Then once inserted, do this...
ed.selection.select(ed.dom.select('span#caret_pos_holder')[0]); //select the span
ed.dom.remove(ed.dom.select('span#caret_pos_holder')[0]); //remove the span
Is this the only way around it?
EDIT: Actually the above only sets the position of the cursor to the end of the content, something which I already took care of using bookmarks.
Is it possible that there is nothing that can get the cursor's position?
var sel = ed.selection.getSel();
will return the current selection as a selection object. You can then query sel.anchorNode and sel.anchorOffset to get current caret position.
Broadcasting this data to the other client(s) would be easier than trying to extract the position as a number, but if that's really necessary, you could climb the DOM from the position given above.

Categories