You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

contenteditable div光标位置插入span包裹的自动补全内容异常求助

修复contenteditable自动补全插入位置错乱问题

问题描述

现有代码实现了点击「Add」按钮添加可编辑div,输入「<>」时展示其他同类型div内容的建议列表,但点击下拉选项时,无法将选中内容(需用span包裹以便后续着色)插入到正确光标位置,插入位置出现错乱。

修复后的完整代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Textarea Suggestions</title>
  <link rel="stylesheet" href="style.css">
  <style>
    /* 可选:测试span着色效果 */
    .suggestion-span {
      color: blue;
      font-weight: bold;
    }
  </style>
</head>
<body>
  <div class="container">
    <button id="addButton">Add</button>
    <div class="textarea-container">
    </div>
  </div>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <script>
    let count = 0;
    let firstBackspacePressed = false;
    let suggestion_selected = "";
    let cposition = 0;
    // 保存当前活跃编辑框和光标位置
    let activeEditor = null;
    let lastCaretPosition = 0;

    $(document).ready(function () {
      let textareaCount = 0;

      const addButton = $("#addButton");
      addButton.click(function () {
        textareaCount++;
        const textareaId = textareaCount;
        const textareaHtml = `<div class="textarea-item"><div class="edit" role="textbox" contenteditable="true" id="${textareaId}"></div><ul class="autocomplete-list"></ul></div>`;
        $(".textarea-container").append(textareaHtml);
      });

      let textareaValues = [];
    
      $(".textarea-container").on("input", ".edit", function (e) {
        const div = $(this);
        activeEditor = div[0];
        const selection = window.getSelection();
        if (selection.rangeCount > 0) {
          lastCaretPosition = selection.getRangeAt(0).startOffset;
        }
        
        const newValue = div.text();
        if(e.keyCode === 8){
          const updatedValue = newValue.substring(newValue.indexOf("<>"), newValue.lastIndexOf("'<>"));
          return div.textContent = updatedValue;
        }
        
        const suggestionsList = div.next(".autocomplete-list");
        const value = div.text();
        const lastBracketIndex = value.lastIndexOf("<>", lastCaretPosition);
        count = lastBracketIndex;

        const index = $(".textarea-container .edit").index(div);
        textareaValues[index] = value;

        if (lastBracketIndex !== -1) {
          const filterText = value.substring(lastBracketIndex + 2, lastCaretPosition);

          const suggestionArr = textareaValues
            .filter((item, idx) => idx !== index && item.includes(filterText))
            .map(item => item);

          const suggestionItems = suggestionArr.map(suggestion => `<li>${suggestion}</li>`).join("");
          suggestionsList.html(suggestionItems);
          suggestionsList.show();
        } else {
          suggestionsList.empty();
          suggestionsList.hide();
        }
      });
  
      // 修复后的建议点击事件
      $(".textarea-container").on("click", ".autocomplete-list li", function () {
        const selectedSuggestion = $(this).text();
        if (!activeEditor) return;

        // 创建带span包裹的内容
        const span = document.createElement("span");
        span.className = "suggestion-span";
        span.textContent = selectedSuggestion;

        // 恢复光标位置并插入内容
        const selection = window.getSelection();
        selection.removeAllRanges();
        const range = document.createRange();
        range.setStart(activeEditor.firstChild || activeEditor, lastCaretPosition);
        range.collapse(true);
        range.insertNode(span);
        
        // 将光标移到插入内容后方
        range.setStartAfter(span);
        selection.addRange(range);

        // 更新内容记录
        const index = $(".textarea-container .edit").index($(activeEditor));
        textareaValues[index] = $(activeEditor).text();

        // 隐藏建议列表
        $(this).closest(".autocomplete-list").empty().hide();
      });

      $(".textarea-container").on("keydown", ".edit", function (e) {
        const div = $(this);
        let id = $(this).attr('id');
        const value = div.text();
        const selection = window.getSelection();
        const caretPosition = selection.rangeCount > 0 ? selection.getRangeAt(0).startOffset : 0;
        const lastBracketIndex = value.lastIndexOf("<>", caretPosition);  
        var pos = (0 + caretPosition - (suggestion_selected.length + 2));
        const matchingSubstring = textareaValues.slice(0,-1)
          .find(item => item.includes("<>"+value) );

        if(value.includes('<>')){
          if (e.key === 'Backspace' || e.key === 'Delete') {
            if (firstBackspacePressed) {
              firstBackspacePressed = false;
              $(this).unbind('keydown');
            } else {
              e.preventDefault();
              if(value.substring(lastBracketIndex, caretPosition).indexOf(" ") === -1){
                // selectText(lastBracketIndex, caretPosition, id);
              } 
              firstBackspacePressed = true;
            }
          }
        }
      });
    }); 

    function selectText(start, end, id) {
      const range = document.createRange();
      range.setStart(document.getElementById(id).firstChild, start);
      range.setEnd(document.getElementById(id).firstChild, end);
      const selection = window.getSelection();
      selection.removeAllRanges();
      selection.addRange(range);
    }

    function deleteSelectedText() { 
      const selection = window.getSelection();
      if (!selection.isCollapsed) {
        selection.deleteFromDocument();
      }
    }

    function insertTextAtCursor(text) { 
      var sel, range; 
      sel = window.getSelection();
      if (sel.rangeCount > 0) {
        range = sel.getRangeAt(0); 
        range.deleteContents(); 
        var textNode = document.createTextNode(text);
        range.insertNode(textNode);
        range.setStartAfter(textNode);
        sel.removeAllRanges();
        sel.addRange(range);        
      }
    }

    function keyHandle(evt) {
      key = evt.keyCode;
      switch(key) {
        case 9: //Tab
          insertTextAtCursor('\t');
          evt.preventDefault();
          break
        case 13: //Enter
          insertTextAtCursor('\n');
          evt.preventDefault();
          break
      }
    }
  </script>
</body>
</html>

关键改动说明

  • 保存编辑上下文:在input事件中记录当前活跃的编辑框和光标位置,避免点击建议时选区丢失导致位置错误
  • DOM API插入内容:直接创建span元素并通过Range API插入到正确位置,替代字符串拼接方式,确保插入位置准确且内容被span包裹
  • 优化光标定位:插入内容后自动将光标移到span后方,保证编辑体验连贯
  • 移除重复依赖:删除重复引入的jQuery版本,避免冲突

内容的提问来源于stack exchange,提问作者Anirudh S

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 21:14:52