Place cursor(caret) in specific position in <pre> contenteditable element - javascript

I'm making a web application to test regular expressions. I have an input where I enter the regexp and a contenteditable pre element where I enter the text where the matches are found and highlighted.
Example: asuming the regexp is ab, if the user types abcab in the pre element, both regexp and text are sent to an api I implemented which returns
<span style='background-color: lightgreen'>ab</span>c<span style='background-color: lightgreen'>ab</span>
and this string is set as the innerHTML of the pre element
This operation is made each time the user edites the content of the pre element (keyup event to be exact). The problem I have (and I hope you can solve) is that each time the innterHTML is set, the caret is placed at the beginning, and I want it to be placed right after the last character input by de user. Any suggestions on how to know where the caret is placed and how to place it in a desired position?
Thanks.
UPDATE For better understanding...A clear case:
Regexp is ab and in the contenteditable element we have:
<span style='background-color: lightgreen'>ab</span>c<span style='background-color: lightgreen'>ab</span>
Then I type a c between the first a and the first b, so now we have:
acbc<span style='background-color: lightgreen'>ab</span>
At this moment the caret has returned to the beginning of the contenteditable element, and it should be placed right after the c I typed. That's what I want to achieve, hope now it's more clear.
UPDATE2
function refreshInnerHtml() {
document.getElementById('textInput').innerHTML = "<span style='background-color: lightgreen'>ab</span>c<span style='background-color: lightgreen'>ab</span>";
}
<pre contenteditable onkeyup="refreshInnerHtml()" id="textInput" style="border: 1px solid black;" ></pre>

With some help from these functions from here ->
Add element before/after text selection
I've created something I think your after.
I basically place some temporary tags into the html where the current cursor is. I then render the new HTML, I then replace the tags with the span with a data-cpos attribute. Using this I then re-select the cursor.
var insertHtmlBeforeSelection, insertHtmlAfterSelection;
(function() {
function createInserter(isBefore) {
return function(html) {
var sel, range, node;
if (window.getSelection) {
// IE9 and non-IE
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = window.getSelection().getRangeAt(0);
range.collapse(isBefore);
// Range.createContextualFragment() would be useful here but is
// non-standard and not supported in all browsers (IE9, for one)
var el = document.createElement("div");
el.innerHTML = html;
var frag = document.createDocumentFragment(), node, lastNode;
while ( (node = el.firstChild) ) {
lastNode = frag.appendChild(node);
}
range.insertNode(frag);
}
} else if (document.selection && document.selection.createRange) {
// IE < 9
range = document.selection.createRange();
range.collapse(isBefore);
range.pasteHTML(html);
}
}
}
insertHtmlBeforeSelection = createInserter(true);
insertHtmlAfterSelection = createInserter(false);
})();
function refreshInnerHtml() {
var
tag_start = '⇷', //lets use some unicode chars unlikely to ever use..
tag_end = '⇸',
sel = document.getSelection(),
input = document.getElementById('textInput');
//remove old data-cpos
[].forEach.call(
input.querySelectorAll('[data-cpos]'),
function(e) { e.remove() });
//insert the tags at current cursor position
insertHtmlBeforeSelection(tag_start);
insertHtmlAfterSelection(tag_end);
//now do our replace
let html = input.innerText.replace(/(ab)/g, '<span style="background-color: lightgreen">$1</span>');
input.innerHTML = html.replace(tag_start,'<span data-cpos>').replace(tag_end,'</span>');
//now put cursor back
var e = input.querySelector('[data-cpos]');
if (e) {
var range = document.createRange();
range.setStart(e, 0);
range.setEnd(e, 0);
sel.removeAllRanges();
sel.addRange(range);
}
}
refreshInnerHtml();
Type some text below, with the letters 'ab' somewhere within it. <br>
<pre contenteditable onkeyup="refreshInnerHtml()" id="textInput" style="border: 1px solid black;" >It's about time.. above and beyond</pre>

<html>
<head>
<script>
function test(inp){
document.getElementById(inp).value = document.getElementById(inp).value;
}
</script>
</head>
<body>
<input id="search" type="text" value="mycurrtext" size="30"
onfocus="test(this.id);" onclick="test(this.id);" name="search"/>
</body>
</html>
I quickly made this and it places the cursor at the end of the string in the input box. The onclick is for when the user manually clicks on the input and the onfocus is for when the user tabs on the input.
<input id="search" type="text" value="mycurrtext" size="30"
onfocus="test(this.id);" onclick="test(this.id);" name="search"/>
function test(inp){
document.getElementById(inp).value = document.getElementById(inp).value;
}

Here to make selection easy, I've added another span tag with nothing in it, and given it a data-end attribute to make it easy to select.
I then simply create a range from this and use window.getSelection addRange to apply it.
Update: modified to place caret after the first ab
var e = document.querySelector('[data-end]');
var range = document.createRange();
range.setStart(e, 0);
range.setEnd(e, 0);
document.querySelector('[contenteditable]').focus();
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
<div contenteditable="true">
<span style='background-color: lightgreen'>ab</span><span data-end></span>c<span style='background-color: lightgreen'>ab</span>
</div>

Related

how can I retain the br tags in getSelection().toString in IE11?

Currently the execCommand 'forecolor' command creates a <font> tag with the selected color (which is deprecated). I would like to create a <span> tag instead.
I'm trying to write my own function to do so, this is what I currently do:
var sel = getSelection();
var range = sel.getRangeAt(0);
var selectedText = getSelection().toString();
document.execCommand("delete");
var newNode = document.createElement("span");
newNode.innerHTML = selectedText;
newNode.style.color = currentColor;
range.insertNode(newNode);
range.setStart(newNode, 0)
range.setEnd(newNode, newNode.childNodes.length);
sel.removeAllRanges;
sel.addRange(range);
The issue I'm facing: when there are one or more <br> tags within the selection they are lost. I know that the toString() function strips the tags off, but how can I keep these tags and the text?
UPDATE: unfortunately I realized that altough adding \n at every br works, when you try to move the caret in the div with the keyboard the \n character is considered: so, for example, if I want to delete an empty line I'll have to push backspace twice to send the caret up one line. I consider the issue still unsolved.
you need to convert every newline char to a <br> tags
change this line:
newNode.innerHTML = selectedText;
to this:
newNode.innerHTML = selectedText.replace(/(?:\r\n|\r|\n)/g, '<br />');
this will replace every form of newline to a <br> tag so you'll get the correct structure
Here's a working example:
// fix for IE11
// pasteHtmlAtCaret("<br />\n");
$('button').click(() => {
var sel = window.getSelection();
var range = sel.getRangeAt(0);
var selectedText = getSelection().toString();
document.execCommand("delete");
var newNode = document.createElement("span");
newNode.innerHTML = selectedText.replace(/(?:\r\n|\r|\n)/g, '<br />');
newNode.style.color = 'orange';
range.insertNode(newNode);
range.setStart(newNode, 0)
range.setEnd(newNode, newNode.childNodes.length);
sel.removeAllRanges;
sel.addRange(range);
});
p {
background: #212121;
color: white;
padding: 10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p contenteditable="true">
This content should be edtiable,<br> and when selecting text and clicking the button,<br> the text should get colored and NOT mess up newlines
</p>
<button>
color selection and keep linebreaks
</button>
As #devamat mentions in his answer, IE11 replaces <br> elements with spaces instead of newlines. In order to fix this for IE11, change the caret character using the following function:
pasteHtmlAtCaret("<br />\n");
which should fix the problem for IE11 as well
IE11 won't give the newlines when using getSelection().toString() or range.toString(). The solution for me was to add a \n after every br the user adds to the contentEditable, and then when replacing the new lines making sure to add \n again.
So when the user adds a br:
pasteHtmlAtCaret("<br />\n");
When I replace it:
newNode.innerHTML = selectedText.replace(/(?:\r\n|\r|\n)/g, '<br />\n');

Cross-browser way to insert BR or P tag when hitting Enter on a contentEditable element

When you hit Enter on a contentEditable element every browser is handling the resulting code differently: Firefox inserts a BR tag, Chrome inserts a DIV tag while Internet Explorer inserts a P tag.
I was desperately looking for a solution to at least use a BR or P for all browsers and the most common answer was this:
inserting BR tag :
$("#editableElement").on("keypress", function(e){
if (e.which == 13) {
if (window.getSelection) {
var selection = window.getSelection(),
range = selection.getRangeAt(0),
br = document.createElement("br");
range.deleteContents();
range.insertNode(br);
range.setStartAfter(br);
range.setEndAfter(br);
selection.removeAllRanges();
selection.addRange(range);
return false;
}
}
});
But this doesn't work because it seems that browsers don't know how to set the caret after <br> which means the following is not doing anything useful (especially if you hit enter when the caret is placed at the end of text):
range.setStartAfter(br);
range.setEndAfter(br);
Some people would say: use double <br><br> but this results in two line breaks when you hit enter inside a text node.
Others would say always add an additional <br> at the end of contentEditable, but if you have a <div contenteditable><p>text here</p></div> and you place the cursor at the end of text then hit enter, you will get the wrong behavior.
So I said to myself maybe we can use P instead of BR, and the common answer is:
inserting P tag:
document.execCommand('formatBlock', false, 'p');
But this doesn't work consistently either.
As you can see, all these solutions leave something to be desired. Is there another solution that solves this issue?
One possible solution: append a text node with a zero-width space character after the <br> element. This is a non-printing zero-width character that's specifically designed to:
...indicate word boundaries to text processing systems when using scripts
that do not use explicit spacing, or after characters (such as the
slash) that are not followed by a visible space but after which there
may nevertheless be a line break.
(Wikipedia)
Tested in Chrome 48, Firefox 43, and IE11.
$("#editableElement").on("keypress", function(e) {
//if the last character is a zero-width space, remove it
var contentEditableHTML = $("#editableElement").html();
var lastCharCode = contentEditableHTML.charCodeAt(contentEditableHTML.length - 1);
if (lastCharCode == 8203) {
$("#editableElement").html(contentEditableHTML.slice(0, -1));
}
// handle "Enter" keypress
if (e.which == 13) {
if (window.getSelection) {
var selection = window.getSelection();
var range = selection.getRangeAt(0);
var br = document.createElement("br");
var zwsp = document.createTextNode("\u200B");
var textNodeParent = document.getSelection().anchorNode.parentNode;
var inSpan = textNodeParent.nodeName == "SPAN";
var span = document.createElement("span");
// if the carat is inside a <span>, move it out of the <span> tag
if (inSpan) {
range.setStartAfter(textNodeParent);
range.setEndAfter(textNodeParent);
}
// insert the <br>
range.deleteContents();
range.insertNode(br);
range.setStartAfter(br);
range.setEndAfter(br);
// create a new span on the next line
if (inSpan) {
range.insertNode(span);
range.setStart(span, 0);
range.setEnd(span, 0);
}
// add a zero-width character
range.insertNode(zwsp);
range.setStartBefore(zwsp);
range.setEndBefore(zwsp);
// insert the new range
selection.removeAllRanges();
selection.addRange(range);
return false;
}
}
});
#editableElement {
height: 150px;
width: 500px;
border: 1px solid black;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div contenteditable=true id="editableElement">
<span>sample text</span>
</div>
You can see a full cross browser implementation here. There are so many hacks to make it work. This code from the link would help you to device a solution.
Example of a Geko and IE hack:
doc.createElement( 'br' ).insertAfter( startBlock );
// A text node is required by Gecko only to make the cursor blink.
if ( CKEDITOR.env.gecko )
doc.createText( '' ).insertAfter( startBlock );
// IE has different behaviors regarding position.
range.setStartAt( startBlock.getNext(),
CKEDITOR.env.ie ? CKEDITOR.POSITION_BEFORE_START :
CKEDITOR.POSITION_AFTER_START );

Right cursor position between MathJax formulas

I have some trouble with inserting MathJax formulas. I want that user can insert cursor between each formulas and therefore can insert formula between them.
My code:
function get(elem)
{
currentFormul = elem;
var math = MathJax.Hub.getAllJax(elem)[0];
var input = document.getElementById("MathInput");
input.value = math.originalText;
}
function input()
{
sel = window.getSelection();
var range = sel.getRangeAt(0);
var input = document.getElementById("MathInput");
var span = document.createElement("span");
span.contentEditable="false";
span.addEventListener('click', function() { get(this); }, true);
span.innerHTML = "\\("+input.value+"\\)";
range.insertNode(span);
MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
}
</script>
<body contenteditable="true" spellcheck="false">
<span contenteditable="false">
<button onclick="change()">Change!</button>
<input id="MathInput" size="60" />
<button onclick="input()">INPUT!</button>
<button onclick="someChanges()">FONT!</button>
</span>
<p>S</p>
<span onclick="get(this)" contentEditable="false">\(\color{red}{x=ax^2}\)</span>
<span onclick="get(this)" contentEditable="false">\(x=ax^2\)</span>
<span onclick="get(this)" contentEditable="false">\(x=ax^2\)</span>
</body>
</html>
Now i can't insert cursor between formula for new elements, but can for old.
Can someone tell me how do this right?
There are white spaces between the spans in your html. These white spaces are in fact text node that are child of body, which has contenteditable set to true. So when you click on one of these text nodes, you click in a contenteditable node, hence you get a cursor.
When you add spans dynamically, it adds it without white space. There's a text node created, but it's empty so you can't click it.
One easy way to fix this would be to set the text node before your newly inserted spans to " ". It'll then have the same behavior as your old elements. Like this:
function input()
{
sel = window.getSelection();
var range = sel.getRangeAt(0);
var input = document.getElementById("MathInput");
var span = document.createElement("span");
span.contentEditable="false";
span.addEventListener('click', function() { get(this); }, true);
span.innerHTML = "\\("+input.value+"\\)";
range.insertNode(span);
span.previousSibling.textContent = " ";
MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
}
http://jsfiddle.net/ox20a7oj/

JS - focus to a b tag

I want to set the focus to a b tag (<b>[focus should be here]</b>).
My expected result was that the b tag into the div has the focus and if I would write, that the characters are bold.
Is this impossible? How can I do this?
Idea was from here:
focus an element created on the fly
HTML:
<div id="editor" class="editor" contentEditable="true">Hallo</div>
JS onDomready:
var input = document.createElement("b"); //create it
document.getElementById('editor').appendChild(input); //append it
input.focus(); //focus it
My Solution thanks to A1rPun:
add: 'input.tabIndex = 1;' and listen for the follow keys.
HTML:
<h1>You can start typing</h1>
<div id="editor" class="editor" contentEditable="true">Hallo</div>
JS
window.onload = function() {
var input = document.createElement("b"); //create it
document.getElementById('editor').appendChild(input); //append it
input.tabIndex = 1;
input.focus();
var addKeyEvent = function(e) {
//console.log('add Key');
var key = e.which || e.keyCode;
this.innerHTML += String.fromCharCode(key);
};
var addLeaveEvent = function(e) {
//console.log('blur');
// remove the 'addKeyEvent' handler
e.target.removeEventListener('keydown', addKeyEvent);
// remove this handler
e.target.removeEventListener(e.type, arguments.callee);
};
input.addEventListener('keypress', addKeyEvent);
input.addEventListener('blur', addLeaveEvent);
};
You can add a tabIndex property to allow the element to be focused.
input.tabIndex = 1;
input.focus();//now you can set the focus
jsfiddle
Edit:
I think the best way to solve your problem is to style an input tag with font-weight: bold.
I had to cheat a little by adding an empty space inside the bold area because I couldn't get it to work on the empty element.
This works by moving the selector inside the last element in the contentEditable since the bold element is the last one added.
It can be edited to work on putting the focus on any element.
http://jsfiddle.net/dnzajx21/3/
function appendB(){
var bold = document.createElement("b");
bold.innerHTML = " ";
//create it
document.getElementById('editor').appendChild(bold); //append it
setFocus();
}
function setFocus() {
var el = document.getElementById("editor");
var range = document.createRange();
var sel = window.getSelection();
range.setStartAfter(el.lastChild);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
el.focus();
}
The SetFocus function I took was from this question: How to set caret(cursor) position in contenteditable element (div)?

Wrap certain word with <span> using jquery

I have the following div:
<div id="query" style="width:500px; height:200px;border:1px solid black"
spellcheck="false" contenteditable="true"></div>​
where Clients can write their SQL queries. What I was trying to do is wrap words the client enters right after hitting Space with a span and give this span a certain class according to the word typed:
example
If the client types select i need to wrap this select word like this in the div:
<span class='select'> SELECT </span> <span> emp_name </span>
CSS
.select{color:blue ;text-transform:uppercase;}
It is something very similar to what jsFiddle does. How can i achieve this?
Here is what i have tried so far : jsFiddle
$(function(){
$('div').focus() ;
$('div').keyup(function(e){
//console.log(e.keyCode) ;
if(e.keyCode == 32){
var txt = $('div').text() ;
var x = 'SELECT' ;
$('div:contains("'+x+'")').wrap("<span style='color:blue ;
text-transform:uppercase;'>") ;
if(txt == 'SELECT'){
console.log('found') ; // why This Doesn't do any thing ?
}
}
});
});
I did a proof of concept with some modifications from what you originally had. See below,
DEMO: http://jsfiddle.net/cgy69/
$(function() {
$('div').focus();
var x = ['SELECT', 'WHERE', 'FROM'];
$('div').keyup(function(e) {
//console.log(e.keyCode) ;
if (e.keyCode == 32) {
//using .text() remove prev span inserts
var text = $.trim($(this).text()).split(' ');
$.each(text, function(i, v) {
$.each(x, function(j, xv) {
if (v.toUpperCase() === xv) {
text[i] = '<span style="color: blue; text-transform: uppercase;">' + v + '</span>';
}
});
});
$(this).html(text.join(' ') + ' ');
setEndOfContenteditable(this);
}
});
function setEndOfContenteditable(contentEditableElement) {
var range, selection;
if (document.createRange) //Firefox, Chrome, Opera, Safari, IE 9+
{
range = document.createRange(); //Create a range (a range is a like the selection but invisible)
range.selectNodeContents(contentEditableElement); //Select the entire contents of the element with the range
range.collapse(false); //collapse the range to the end point. false means collapse to end rather than the start
selection = window.getSelection(); //get the selection object (allows you to change selection)
selection.removeAllRanges(); //remove any selections already made
selection.addRange(range); //make the range you have just created the visible selection
}
else if (document.selection) //IE 8 and lower
{
range = document.body.createTextRange(); //Create a range (a range is a like the selection but invisible)
range.moveToElementText(contentEditableElement); //Select the entire contents of the element with the range
range.collapse(false); //collapse the range to the end point. false means collapse to end rather than the start
range.select(); //Select the range (make it the visible selection
}
}
});
You going to extend this further to handle
Backspace
HTML contents from previous inserts
Cursor position Partially done, editing in the middle would still mess up the caret.
and more..
Starting with a contenteditable element we can replace the markup as we need by operating directly on its innerHtml:
$('#query-container').on('keyup', function(e){
var $this = $(this);
//(?!\<\/b\>) negative lookahead is used so that anything already wrapped
//into a markup tag would not get wrapped again
$this.html($this.html().replace(/(SELECT|UPDATE|DELETE)(?!\<\/b\>)/gi, '<b>$1</b>'));
setEndOfContenteditable(this);
});
IMO this is a more readable option. Add the rangeselect method from the previous answer and we have a working fiddle

Categories

Resources