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

聊天应用@提及用户后光标位置丢失问题及解决方案咨询

问题:contenteditable文本框插入@提及后光标丢失

开发聊天应用时,实现了在contenteditable输入框输入@后加载匹配用户列表的功能,但点击列表项替换文本后,光标位置丢失。

解决思路

光标丢失的核心原因是替换innerHTML后,原有的选区(光标)被重置。需要在替换文本前记录光标相关的位置,替换后重新定位光标到提及内容的末尾。

具体处理步骤:

  • 替换文本前,计算好替换后光标应该停留的位置(新增提及内容的末尾+空格位置)
  • 替换完成后,使用Range和Selection API手动恢复光标位置
  • 处理contenteditable中的HTML元素结构,避免因元素嵌套导致定位错误

修改后的JavaScript代码

let adUsername = null;

$(document).off('keyup', '#w-input-text');  
$(document).on('keyup', '#w-input-text', function(event) {
  if ($.inArray(event.keyCode, [9, 16, 17, 18]) !== -1) return;
  let inputValue = $(this).text();
  if (inputValue.length == 0) return;
  inputValue = getActiveWord();
  if (!inputValue.includes("@")) return;
  inputValue = inputValue.replace('@', '');
  if (inputValue.length > 0) {
    if (adUsername == inputValue) return;
    adUsername = inputValue;
    $.ajax({
      url: BASE_URL + 'user-list/' + adUsername,
      method: 'GET',
      dataType: 'json',
      success: function(response) {
        // Clear the user list
        $('#userListOnAd').empty();
        // Populate the user list with the retrieved data
        for (let i = 0; i < response.data.length; i++) {
          let name = response.data[i].name;
          let user_id = response.data[i].user_id;
          let listItem = $('<li>').text(name);
          listItem.on('click', function() {
            let selectedUsername = $(this).text();
            $('#userListOnAd').hide();
            // 替换文本并恢复光标
            replaceAndRestoreCursor(selectedUsername);
          });
          $('#userListOnAd').append(listItem);
        }
        // Show the user list
        $('#userListOnAd').show();
      },
      error: function() {
        // Handle the error case
      }
    });
  } else {
    // Hide the user list if the input does not match the pattern
    $('#userListOnAd').hide();
  }
});

function getActiveWord() {
  let text = $('#w-input-text').text();
  let sel = window.getSelection();
  let range = sel.getRangeAt(0);
  let start_index = range.startOffset;
  let end_index = range.endOffset;
  let previous_space_index = text.lastIndexOf(" ", start_index - 1);
  let next_space_index = text.indexOf(" ", end_index);
  let begin = previous_space_index < 0 ? 0 : previous_space_index + 1;
  let end = next_space_index < 0 ? text.length : next_space_index;
  let between_spaces = text.substring(begin, end);
  return between_spaces;
}

// 替换文本并恢复光标
function replaceAndRestoreCursor(selectedUsername) {
  const inputEl = $('#w-input-text')[0];
  let text = inputEl.textContent;
  let sel = window.getSelection();
  let range = sel.getRangeAt(0);
  let start_index = range.startOffset;
  let end_index = range.endOffset;
  let previous_space_index = text.lastIndexOf(" ", start_index - 1);
  let next_space_index = text.indexOf(" ", end_index);
  let begin = previous_space_index < 0 ? 0 : previous_space_index + 1;
  let end = next_space_index < 0 ? text.length : next_space_index;
  let wordToReplace = text.substring(begin, end);

  // 计算替换后光标目标位置
  const replacementContent = `<mention data-id="101">@${selectedUsername}</mention> `;
  const cursorPosition = begin + replacementContent.length;

  // 构造新HTML内容
  let substringBefore = text.substring(0, begin);
  let substringAfter = text.substring(end);
  let updatedHtml = substringBefore + replacementContent + substringAfter;
  
  // 更新输入框内容
  inputEl.innerHTML = updatedHtml;

  // 恢复光标位置
  setCursorPosition(inputEl, cursorPosition);
}

// 工具函数:设置contenteditable的光标位置
function setCursorPosition(element, position) {
  const selection = window.getSelection();
  const range = document.createRange();
  let currentNode = element.firstChild;
  let currentLength = 0;

  // 遍历节点找到对应位置
  while (currentNode) {
    if (currentNode.nodeType === Node.TEXT_NODE) {
      if (currentLength + currentNode.length >= position) {
        range.setStart(currentNode, position - currentLength);
        range.setEnd(currentNode, position - currentLength);
        break;
      } else {
        currentLength += currentNode.length;
      }
    }
    currentNode = currentNode.nextSibling;
  }

  // 未找到对应文本节点时,光标定位到末尾
  if (!currentNode) {
    range.selectNodeContents(element);
    range.collapse(false);
  }

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

HTML代码

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul id="userListOnAd">
  <li>Durvash Nimje</li>
  <li>ABC Nimje</li>
  <li>XYZ Nimje</li>
  <li>PQR Nimje</li>
</ul>
<div id="w-input-text" contenteditable></div>

关键修改点

  • 新增replaceAndRestoreCursor函数,整合文本替换与光标恢复逻辑
  • 添加setCursorPosition工具函数,精确计算并定位光标位置
  • 替换文本前预先计算光标目标位置,避免替换后选区丢失

内容的提问来源于stack exchange,提问作者Durvash Nimje

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 12:55:56