6

Following this SO post, I am able to place the caret inside a span element, which is inside a div contenteditable="true".

I am able to target whichever span I desire, via its id, while also being able to decide which character should the caret be placed after.

But how can I place the caret inside a span that has no text inside?

Simply using the function, as is, gets me this error: TypeError: Range.setStart: Argument 1 is not an object.

Also, for some reason, when the span has content, it works fine, in Firefox. But not in Chrome, where the caret is placed outside the span. Any way to also solve this problem?

I am open to jQuery, if it makes things easier.

Here is my code:

JSFiddle

function setCaret(x, y) {
   var element = document.getElementById(x);
   var range = document.createRange();  
   var node;   
   node = document.getElementById(y);  
   range.setStart(node.childNodes[0], 0);
   var sel = window.getSelection();
   range.collapse(true);
   sel.removeAllRanges();
   sel.addRange(range);
   element.focus();    
}
body {
  font-family: sans-serif;
}

input {
  margin-bottom: 5px;
  padding: 3px;
}

input:last-of-type {
  margin-top: 30px;
}

div {
  width: 300px;
  padding: 5px;
  border: solid 1px #000;
}

span {
  font-weight: bold;
}
<input type="button" value="place caret" onclick="setCaret('editableDiv1', 'span1');">
<div id="editableDiv1" contenteditable="true" spellcheck="false">This one <span id="span1">is</span> working.</div>

<input type="button" value="place caret" onclick="setCaret('editableDiv2', 'span2');">
<div id="editableDiv2" contenteditable="true" spellcheck="false">This one <span id="span2"></span> is not.</div>
MikeMichaels
  • 454
  • 1
  • 6
  • 25
  • On Chrome, when I click the "place caret" button on the one that says it's working and start typing, what I type doesn't go in the `span`, it goes **before** the span (e.g., it's not bold). On Firefox what I type is within the span. So I hate to say it, but the code is already problematic even when the `span` has content. – T.J. Crowder Oct 15 '20 at 11:04
  • Firefox here. Still haven't tested it in other browsers. Here, it works as intended. I click on the button, it moves the caret inside the span, and it stays there as I type. – MikeMichaels Oct 15 '20 at 11:07
  • See also https://stackoverflow.com/questions/53747581/after-range-setstart-new-characters-appear-in-previous-node – John M Oct 19 '20 at 09:16
  • 2
    You can correct the first issue by checking if there's a child node and adding an empty text element if not: `if (node.childNodes.length === 0) { var t = document.createTextNode(''); node.appendChild(t); }` Can't help with the Chrome issue though :-( – John M Oct 19 '20 at 09:18

3 Answers3

7

There's a nice trick with zero-width space that you may consider (look at at the code below) and CSS property white-space: pre that allows spaces to be "visible" when focused.

function makeTextNode() {
    return document.createTextNode('​') // <-- there a zero-width space between quotes
}

function placeCaretInSpan() {
  const range = document.createRange()
  const editable = document.getElementById("editable")
  const span = editable.querySelector("span")
  if (span.childNodes.length === 0) {
    span.appendChild(makeTextNode()) // <-- you have to have something in span in order to place caret inside
  }
  range.setStart(span.childNodes[0], 1) // <-- offset by 1 to be inside SPAN element and not before it
  let selection = window.getSelection()
  range.collapse(true)
  selection.removeAllRanges()
  selection.addRange(range)
  editable.focus()
}
span {
    font-weight: bold;
    background: yellow;
}
#editable:focus {
    white-space: pre;
}
<div contenteditable="true" id="editable">This should be <span></span> editable.</div>
<button onclick="placeCaretInSpan()">place caret</button>
siaznik
  • 496
  • 2
  • 5
  • This works in Chrome, but the zero-width space will mess the navigation through the arrow keys. Is there any other alternative? – MikeMichaels Oct 25 '20 at 20:23
0

try with OR operator, this will return the node if node.childNodes[0] is undefined:

 range.setStart(node.childNodes[0] || node, 0);
lissettdm
  • 12,267
  • 1
  • 18
  • 39
  • i am sorry, my answer is actually the same. I have not seen the rest of the answers. I clicked the fiddle and worked there... Besides that, the case of that question is that, `node.childNodes[0]` is `undefined` –  Oct 24 '20 at 15:17
  • 1
    Hi @PeterDarmis, do not worry ;), the same thing has happened to me many times :). – lissettdm Oct 24 '20 at 15:21
0

The problem occurs in the second case because span2 has no content and node.childNodes[0] becomes undefined. A small workaround is used in the below snippet. The only line changed in your code is this:

range.setStart(node.childNodes[0], 0);

and became

range.setStart((typeof node.childNodes[0] !== 'undefined' ? node.childNodes[0] : node), 0);

You can check the result below.

function setCaret(x, y) {
   let element = document.getElementById(x);   
   let range = document.createRange();  
   let node = document.getElementById(y);  
   range.setStart((typeof node.childNodes[0] !== 'undefined' ? node.childNodes[0] : node), 0);
   let sel = window.getSelection();
   range.collapse(true);
   sel.removeAllRanges();
   sel.addRange(range);
   element.focus();    
}
body {
  padding: 10px;
  font-family: sans-serif;
}

input {
  margin-bottom: 10px;
  padding: 5px;
}

input:last-of-type {
  margin-top: 50px;
}

div {
  width: 300px;
  padding: 5px;
  border: solid 1px #000;
}

span {
  font-weight: bold;
}
<input type="button" value="place caret" onclick="setCaret('editableDiv1', 'span1');">
<div id="editableDiv1" contenteditable="true" spellcheck="false">This one <span id="span1">is</span> working.</div>

<input type="button" value="place caret" onclick="setCaret('editableDiv2', 'span2');">
<div id="editableDiv2" contenteditable="true" spellcheck="false">This one <span id="span2"></span> is working also.</div>