【问题标题】:Replace word in contenteditable div and properly set caret position替换 contenteditable div 中的单词并正确设置插入符号位置
【发布时间】:2017-12-06 14:45:58
【问题描述】:

我一直在搜索很多与范围和选择相关的问题(大部分由 @tim-down 回答),但我无法完全得到我需要的东西,尽管我已经接近了。

我想在当前聚焦的文本节点中搜索单词foo。如果我找到它 - 将其替换为 bar 并将插入符号位置设置在替换单词的末尾。例如:

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

变成:

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

我的尝试

我目前所拥有的只是中途 - 它删除了文本,但没有添加任何内容。不过,我不确定这是不是最好的方法:

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>

注意:我只关心与 Chrome 的兼容性。

【问题讨论】:

  • 如果您的 contenteditable 中有多个 foo 怎么办?应该发生什么,光标应该放在哪里?
  • @vsync 插入符号应该放在最后出现的foo

标签: javascript range selection contenteditable


【解决方案1】:

在代码注释和控制台日志中解释。
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>

【讨论】:

  • 非常感谢!不过,你能解释一下它为什么会起作用吗?插入符号不应该设置在文本节点的第一个字符处,即 I.E.在“Lorem”这个词之前?
  • 我更新了我的答案以添加评论和控制台日志。请检查并随时提出更多问题。
  • @Duannx 当我输入 foo caret go to the first 时,你能看到这个吗?我已经将 textnode 更改为 html 节点。请看下面jsfiddle
  • @sidoshi:我看到了你的小提琴。但问题是什么?
  • @Duannx 你能在最后输入 foo 吗?它将替换为 bar 但插入符号会转到另一个位置。
【解决方案2】:

使用createNodeIteratorNodeFilter.SHOW_TEXT 迭代所有textNodes,很容易找到所有出现的字符串foo 并替换为任何所需的内容,在这种情况下为bar textNode。

第二阶段,应该与查找和替换函数解耦,是插入符号放置请求,由函数setRangeAtEnd实现,该函数接收单个参数node,可从第一个函数replaceTextWithNode 的输出,它返回 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>

【讨论】:

    【解决方案3】:

    sidoshi,这修复了替换后插入符号的位置

    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
      var replacement = document.createTextNode(replace); 
      range.insertNode(replacement);
      range.setStartAfter(replacement);                       
      // Chrome fix
      sel.removeAllRanges();
      sel.addRange(range); 
    }
    
    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>

    这并不完全是要求的,但循环多个替换并像上面一样处理 DIV 和 textareas。

    var replaceValues = {
        'foo' : 'bar',
        'aaa': 'bbbb'
    }
    
    $('.macroenabled').keyup(function(e) {
      // change to keypress if you want it to happen after space
      for (var search in replaceValues) {
        if ($(document.activeElement).is(":input")) {
          var newText = $(e.target).val();
          var temp = new RegExp(search, 'gim');
          newText = newText.replace(temp, replaceValues[search]);
          $(e.target).val(newText);
        } else { // element contenteditable DIV
          var sel = window.getSelection();
          if (!sel.focusNode || ! sel.focusNode.nodeValue) { // Nothing focused
            continue;
          }
          var startIndex = sel.focusNode.nodeValue.indexOf(search);
          var endIndex = startIndex + search.length;
          if (startIndex === -1) { // search not found
            continue;
          }
          var range = document.createRange();
          range.setStart(sel.focusNode, startIndex);
          range.setEnd(sel.focusNode, endIndex);
          range.deleteContents(); //Delete search text
          var replacement = document.createTextNode(replaceValues[search]); 
          range.insertNode(replacement);
          range.setStartAfter(replacement);                       
          sel.removeAllRanges(); // Chrome fix
          sel.addRange(range);
        }
      }
    });
    #div1 {
      border: 1px solid black;
    }
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <div>I am a textarea, try 'aaa' or 'foo'</div>
    <textarea class="macroenabled"></textarea>
    <div>I am a content editable DIV, try 'aaa' or 'foo'</div>
    <div id="div1" contenteditable="true" class="macroenabled"></div>

    【讨论】:

      猜你喜欢
      • 2014-07-29
      • 1970-01-01
      • 1970-01-01
      • 2011-06-17
      • 1970-01-01
      • 2018-05-06
      • 1970-01-01
      • 1970-01-01
      • 2012-01-19
      相关资源
      最近更新 更多