Keep caret position in contenteditable after editing the content via jscript - javascript

I've got a contenteditable div which's content is being edited via javascript after each textchange(textchange.js) like adding html tags(only span-tags for changing the color of some words) and adding or removing some whitespaces here and there, but my problem is since the content is being changed while the user is editing it, the caret changes its position after every keypress, which makes it nearly impossible to write a single word.
I'm at the moment searching for a way to prevent this jumping around, I've already thought of adding a special char which wouldn't be used anyway as a kind of marker at the position of the caret before editing it, removing it when finished and putting the caret back to this position, but since I'm using regex a lot(currently about 25 times after each textchange) this special character would ruin nearly every single one of them and I would have to add something like \x40? every where, which would not look nice and clear at all:
/\s<span class="b0">hello\sworld</span>/g
to:
/\s\x40?<span class="b0">\x40?h\x40?e\x40?l\x40?l\x40?o\x40?\s\x40?w\x40?o\x40?r\x40?l\x40?d\x40?</span>/g
I don't know if it helps but here is an example on how the content is changed(after each keypress):
foo +++ <span class="c3">bar</span> - baz -<span class="c0">qux</span>
to:
<span class="c1">foo</span> + <span class="c3">bar</span> - <span class="c1">baz</span> * <span class="c0">qux</span>
I'd be grateful for every advice, tip or hint on how to solve this problem, or a better way to do this marker-thing.
Thank you :)

Probably this is not the best solution, but I created 2 divs, one to display the text and other to edit it, the second one above the first one and with some level of transparency.
Like this:
<div style="position: absolute; left: 48px; top: 16px;" unselectable="on" onselectstart="return false">
<code id="code_show">Type here.</code>
</div>
<div style="position: absolute; left: 48px; top: 16px;">
<span style="opacity:0.33"><code id="code_area" contenteditable="true" onkeyup="colorize();">Type here.</code></span>
</div>
The onselectstart="return false" and the unselectable="on" ensure that the first one is not selectable. The onkeyup="colorize();" ensures that the javascript function colorize is called everytime that the user presses a key to edit the text.
So you have to define it to update the contents of code_show to match the ones in code_area but with colors.
For instance, this code paints every word RED in red color:
<script>
function colorize(){
var code_area= document.getElementById('code_area');
var code_show= document.getElementById('code_show');
var inner= code_area.innerHTML;
inner= inner.replace(/RED/g,"<span style=\"color: red\">RED</span>");
code_show.innerHTML= inner;
}
</script>

You could create an abstraction that would add special characters to otherwise readable regexes. It's simple and presumably requires minimal changes to your current code.
There might be a more elegant solution though. If you never replace text that has a caret in a middle of it - as in repl|aced - you could split the original string into two and apply the filter on both parts. The caret position remains at the division, that is at length of the first gist.
If you do however need to replace even at caret position, you could start with the split anyway. After that, run the filters again, one by one. If the filter matched, move caret to a predetermined relative position - say to replaced ++|+ (pipe denoting the caret) with ××. You could even let it be, which would result in ××|. Or have a hardcoded relative caret move for each filter, which is great if you do code replacements, because you can move the caret to proper location (say you replace <|i></i> with <em>|<em>; notice the caret conveniently moved to where the user would probably want it).

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.

Multiple contentEditable, unable to move carret to end of span with arrow keys

I have multiple spans with content editable property set to true, like this:
<span contentEditable='true'> value</span><span contentEditable='true'> value</span><span contentEditable='true'> value</span>
https://jsfiddle.net/du7g39cz/
Problem is that when I am using arrow keys to navigate around span element, I can not reach end of individual span as blur event gets called when caret reaches last symbol.
I can reproduce this behavior on all browsers apart MS Edge.
I must note that I wouldn't like to keep only one content editable parent, as this would easily let user to delete whole paragraph, intention is to let user edit only one word at a time.
It seems like a browser bug. It is only happening in Firefox for me, however, adding any amount of padding to the spans seems to correct the issue:
span[contentEditable] { padding: 1px; }
https://jsfiddle.net/jimbo2150/du7g39cz/2/

Contenteditable div set caret after replacing text with HTML

I'm trying to make a web-based code editor, and I came up with a way to sweep the entire text and replace anything that matches my super simple regex to colored HTML. See it here:
$("#text-area").on('keyup', function(e){
var html = $("#text-area").html();
var filter = html.replace(/function/g, '<span style="color:pink;">function</span>');
$("#text-area").html(filter);
});
I spent hours digging stackoverflow and found similar situations, but subtly different that I can't apply to my situation here. Here is the problem:
1) Once I sweep the text and replace selective words to colored HTML, the caret is returned to the beginning of the div.
2) Since it is a code-editor(reading other files), I need white-space: pre or pre-wrap. But then caret positions are the same for any number of line breaks, so the caret doesn't move and new lines are just added below the caret, and from any new empty line it jumps up to whichever line isn't empty.
I have the simplified version of my situation in the JSFiddle
There is better ways but this is the close solution I got to yours.
Have something have something like when you detect the word you like:
<div>
<span class="normal_text"> First part </span>
<span style="color:pink;">function</span>
<span class="normal_text"> | </span>
</div>
And in the end put the selection equal to range with offset 0 and focus node as the last span. Your function looking for the word "function" should also be listening the last span only to repeat everything.

How to measure word/caret position in Google Docs?

For those who haven't worked with the Google Docs editor here's a short explanation of how it works:
Google Docs has no visible editable textarea or contentEditable elements.
Google Docs listens for keydown/press/up in a separate iFrame where they place the OS cursor for event listening.
When the iFrame catches an event Google handles it by performing the equivalent operations on the visible document.
The "caret" in Google Docs is a DIV that is styled and scripted to look and act like an OS cursor.
With that out of the way, here's my request:
I'm working on a plugin that interacts with the Google Doc and I need to be able to do two things:
Highlight words with an opaque overlay DIV.
Determine cursor position inside a word.
I've been exhausting a lot of ideas about just how to handle this, but so far I've only manage to get a buggy solution for the latter problem (I perform a backspace, determine where the text changed and undo the backspace).
I'm looking for all the best ideas you can come up with to solve these problems. They don't need to be cross browser, but they do need to be able to be turned into something robust that will also handle things such as font size changed mid line.
A little bit of extra info explaining what a Google Doc looks like in HTML:
<wrapper> // Simplified wrapper containing margins, pagination and similar
<div class="kix-paragraphrenderer"> // single DIV per page wrapping all content
// Multiple paragraphs separated by linebreak created by Enter key:
<div class="kix-paragraphrendeder">...</div>
<div class="kix-paragraphrendeder">...</div>
<div class="kix-paragraphrendeder">
// Multiple wrapper divs created by Google's word wrapping:
<div class="kix-lineview">...</div>
<div class="kix-lineview">...</div>
<div class="kix-lineview">
// Single inner wrapper, still full width of first wrapper paragraph:
<div class="kix-lineview-content">
// Single wrapper SPAN containing full text of the line, but not display:block
<span class="kix-lineview-text-block">
// Multiple spans, one per new font change such as normal/bold text,
// change in font size, indentation and similar:
<span>This is normal text</span>
<span style="font-size:40px; padding-left:4px;">This larger text.</span>
<span style="font-weight:bold; padding-left:10px;">This is bold text</span>
<span style="padding-left:4px;">More normal text</span>
</span>
</div>
</div>
</div>
</div>
</wrapper>
After more tinkering I came to the conclusion that it is extremely troublesome - if not impossible - to try and programmatically determine cursor position with regard to a letter inside a <span>, simply because the <span> is the smallest element that is measurable (correct me if I am wrong).
So how to solve the problem? Here's what I ended up doing:
I create an offscreen positioned <div>
I get the text of the current paragraph (<div class="kix-paragraphrenderer">) - I could get the entire text, but wanted to limit the computational load.
I extract each single character of the paragraph by looping through its children in the following way:
Loop through linveviews of the paragraph (<div class="kix-lineview">)
Get the lineview content (<div class="kix-lineview-content">)
Loop through text blocks of the lineview content (<span class="kix-lineview-text-block">)
Loop through <span>'s of the text block
Loop through innerText of the <span>
I append each character in my offscreen <div> with the currently applied style extracted from style.cssText of the current <span>
For each character appended I measure the width of the <div> and save this in an array. I now have a position of each single character.
I measure the position of the cursor relative to my widths and voila - I know where the cursor is positioned in the text.
This is obviously a bit simplied (I left out details about margins and paddings of the different elements), but it covers the idea behind how it's possible to get the cursor position.
It works quite well, but there are many pitfalls and a lot of measuring required. On top of that it's also required to post-parse the text if you want to use it for anything, since tabs, spaces and linebreaks aren't always included in innerText (depending on where these are in the text, Google may or may not make them through positioning of new elements).
I made something like Kix two years ago Google Docs. And for any HTML design and yes, for IE6 too :-) How? All we need is to compute letter absolute position. How? Replace textNode with inline element without layout, that's important, and then use Element.getClientRects I remember I also needed wrap just letter and compute its position via fast and reliable https://developer.mozilla.org/en-US/docs/Web/API/Element.getBoundingClientRect
The trick how to detect lines and wraps for home and end keys was based on some vertical heuristic letter position change. Something like if base line is different, than stop caret walking. It was pretty fast and with any markup and without any caching. Holy grail :)
The only not resolvable problem was justified text, because letters were distributed randomly and spaces between them was not computable.
That project is dead http://webeena.com now. Bad management killed it (and me almost too).

javascript string clicked position

If I have a string in a div
<div id="article">
lots of text goes here
<strong> it's really interesting</strong>
and it's free to read
</div>
if a user double click or single click on a particular word, is there a way to determine the position/index of the character clicked?
If that cannot be done, how about determining how many space character there are before the clicked position. So if I click on the word 'goes' in the above example, it returns 3 because there are three spaces before the word goes which is clicked.
Thank You very much for your time.
The ugly hack solution to such a problem involves programming converting the characters into event capturing objects.
For instance
<div id="article">
<span>lots </span><span>of </span><span>text</span>...
</div>
Now this can be done programmatically.
You can grab whatever the current content of a div and convert it to something like the formatting above. Then attach event handlers to the contained spans which count the number of spans preceding it in the same container.
a way to determine the position/index of the character clicked
Well, first I need to use a fixed width size font like Courier.
Then, I'd create two span with only a char each. With those two, I should be able to know the width of a char (beware padding and margin) and its height.
Then, you need to know your upper left coordinates for your div whose id is 'article' and capture click event with event.x and event.y.
After that,
var line = (event.x - divLeft) / charHeight;
var column = (event.y - divTop) / charWidth;

Categories