Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replace word in contenteditable div and properly set caret position

I've been searching through a lot of Range and Selection related questions (mostly answered by @tim-down), but I can't quite get what I need, although I come close.

I want to search the currently focused text node for the word foo. If I find it - replace it with bar and set the caret position at the end of the replaced word. For example:

"Lorem ipsum dolor foo amet, consectetur adipiscing elit."

Turns into:

"Lorem ipsum dolor bar amet, consectetur adipiscing elit."
// -------------------^--- caret position

My attempt

What I currently have works only halfway - it removes the text, but doesn't add anything. I'm not sure it's the best approach, though:

function replacer(search, replace) {
  var sel = window.getSelection();
  if (!sel.focusNode) {
    return;
  }

  var startIndex = sel.focusNode.nodeValue.indexOf(search);
  var endIndex = startIndex + search.length;

  if (startIndex === -1) {
    return;
  }

  var range = document.createRange();
  range.setStart(sel.focusNode, startIndex);
  range.setEnd(sel.focusNode, endIndex);
  range.insertNode(document.createTextNode("bar"));

  sel.removeAllRanges();
  sel.addRange(range);
}

document.addEventListener("keypress", function() {
  replacer("foo", "bar");
});
<div contenteditable="true" style="width: 600px; height: 300px;">Lorem ipsum dolor foo amet, consectetur adipiscing elit.</div>

Note: I only care for compatibility with Chrome.

like image 344
dodov Avatar asked Sep 18 '25 23:09

dodov


2 Answers

Explain in code comment and console log.
See about selection

function replacer(search, replace) {
  var sel = window.getSelection();
  if (!sel.focusNode) {
    return;
  }

  var startIndex = sel.focusNode.nodeValue.indexOf(search);
  var endIndex = startIndex + search.length;
  if (startIndex === -1) {
    return;
  }
  console.log("first focus node: ", sel.focusNode.nodeValue);
  var range = document.createRange();
  //Set the range to contain search text
  range.setStart(sel.focusNode, startIndex);
  range.setEnd(sel.focusNode, endIndex);
  //Delete search text
  range.deleteContents();
  console.log("focus node after delete: ", sel.focusNode.nodeValue);
  //Insert replace text
  range.insertNode(document.createTextNode(replace));
  console.log("focus node after insert: ", sel.focusNode.nodeValue);
  //Move the caret to end of replace text
  sel.collapse(sel.focusNode, 0);
}

document.addEventListener("keypress", function() {
  replacer("foo", "bar");
});
<div contenteditable="true" style="width: 600px; height: 300px;" id='content'>Lorem ipsum dolor foo amet, consectetur adipiscing elit.</div>
like image 163
Duannx Avatar answered Sep 21 '25 13:09

Duannx


Using createNodeIterator to iterate all textNodes with NodeFilter.SHOW_TEXT, it is easy to hunt down all occurrences of the string foo and replace with anything desired, bar textNode in this case.

The second stage, which should be decoupled from the find-and-replace function, is the caret placement request, and is implemented by the function setRangeAtEnd, which receives a single argument, node, which is available from the output of the first function, replaceTextWithNode, which returns an Array of all the nodes replaced in the contentEditable.

const elm = document.querySelector('[contenteditable]');

function replaceTextWithNode( elm, text, replacerNode ){
  var iter = document.createNodeIterator(elm, NodeFilter.SHOW_TEXT),
      textnode, replacedNode, idx, newNode,
      maxIterations = 100,
      addedNodes = [];

  while( textnode = iter.nextNode() ){
      if( !maxIterations-- ) break;
      // get the index of which the text is within the textNode (if at all)
      idx = textnode.nodeValue.indexOf(text)
      if( idx == -1 ) continue
      
      replacedNode = textnode.splitText(idx)
      newNode = replacerNode.cloneNode()

      // clean up the tag's string and put tag element instead
      replacedNode.nodeValue = replacedNode.nodeValue.replace(text, '')
      textnode.parentNode.insertBefore(newNode, replacedNode)
      addedNodes.push(newNode)
  }

  return addedNodes
}


function setRangeAtEnd( node ){
  node = node.firstChild || node;
  const sel = document.getSelection()

  if( sel.rangeCount )
      ['Start', 'End'].forEach(pos =>
          sel.getRangeAt(0)["set" + pos](node, node.length)
      )
}

// Replace all 'foo' with 'bar' textNodes
var addedNodes = replaceTextWithNode(elm, 'foo', document.createTextNode('bar'))

// Place caret at last occurrence of 'bar'
elm.focus()
setRangeAtEnd(addedNodes[addedNodes.length-1])
<div contenteditable>Lorem ipsum foo dolor foo amet foo, consectetur adipiscing elit.</div>
like image 30
vsync Avatar answered Sep 21 '25 14:09

vsync