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

如何用JavaScript实现类似在线文本编辑器的建议弹窗

代码补全弹窗核心问题解决方案

一、弹窗定位:对齐输入单词的最后一个字符

要让弹窗精准定位在输入单词旁,核心是获取光标/当前字符的屏幕坐标,再根据编辑器布局设置弹窗位置。

实现步骤

  • 获取光标所在位置的矩形坐标
  • 根据编辑器是否带滚动条,计算弹窗的相对/绝对定位坐标
  • 监听输入、滚动事件,实时更新弹窗位置

代码示例

// 获取光标位置的矩形信息
function getCaretRect() {
  const selection = window.getSelection();
  if (selection.rangeCount === 0) return null;
  return selection.getRangeAt(0).getBoundingClientRect();
}

// 定位弹窗
function positionPopup(popupEl, editorEl) {
  const caretRect = getCaretRect();
  if (!caretRect) {
    popupEl.style.display = 'none';
    return;
  }

  const editorRect = editorEl.getBoundingClientRect();
  // 区分编辑器是否带滚动:有滚动条用绝对定位,否则用固定定位
  if (editorEl.scrollHeight > editorEl.clientHeight) {
    // 计算相对于编辑器的坐标(抵消滚动偏移)
    const relativeLeft = caretRect.left - editorRect.left + editorEl.scrollLeft;
    const relativeTop = caretRect.bottom - editorRect.top + editorEl.scrollTop;
    popupEl.style.position = 'absolute';
    popupEl.style.left = `${relativeLeft}px`;
    popupEl.style.top = `${relativeTop}px`;
  } else {
    // 相对于视口的固定定位
    popupEl.style.position = 'fixed';
    popupEl.style.left = `${caretRect.left}px`;
    popupEl.style.top = `${caretRect.bottom}px`;
  }
  popupEl.style.display = 'block';
}

// 绑定事件实时更新位置
const editor = document.getElementById('editor');
const popup = document.getElementById('suggestion-popup');
editor.addEventListener('input', () => positionPopup(popup, editor));
editor.addEventListener('scroll', () => positionPopup(popup, editor));

二、替换输入内容:用选中的建议词覆盖当前单词

核心是先识别当前输入单词的范围,再删除原有内容并插入选中的建议词,最后调整光标位置。

实现步骤

  • 找到当前输入单词的起始和结束位置
  • 替换单词并将光标移到新单词末尾
  • 绑定键盘(上下箭头、回车/Tab)和鼠标点击事件

代码示例

// 获取当前输入单词的范围
function getCurrentWordRange(editorEl) {
  const selection = window.getSelection();
  if (selection.rangeCount === 0) return null;

  const range = selection.getRangeAt(0);
  const startContainer = range.startContainer;
  let startOffset = range.startOffset;

  // 向前遍历,直到遇到非单词字符(字母、数字、下划线视为单词部分)
  while (startOffset > 0 && /[a-zA-Z0-9_]/.test(startContainer.textContent[startOffset - 1])) {
    startOffset--;
  }

  const wordRange = document.createRange();
  wordRange.setStart(startContainer, startOffset);
  wordRange.setEnd(range.endContainer, range.endOffset);
  return wordRange;
}

// 替换当前单词为选中的建议词
function replaceWithSuggestion(suggestionText) {
  const editor = document.getElementById('editor');
  const wordRange = getCurrentWordRange(editor);
  if (!wordRange) return;

  // 删除原有单词
  wordRange.deleteContents();
  // 插入新建议词
  const textNode = document.createTextNode(suggestionText);
  wordRange.insertNode(textNode);

  // 将光标移到新单词末尾
  const newRange = document.createRange();
  newRange.setStartAfter(textNode);
  newRange.collapse(true);
  const selection = window.getSelection();
  selection.removeAllRanges();
  selection.addRange(newRange);

  // 关闭弹窗
  document.getElementById('suggestion-popup').style.display = 'none';
}

// 键盘交互逻辑:上下箭头选择,回车/Tab替换
let selectedIndex = -1;
const popup = document.getElementById('suggestion-popup');
const suggestionItems = popup.querySelectorAll('.suggestion-item');

function updateSelectedItem() {
  suggestionItems.forEach((item, idx) => {
    item.classList.toggle('selected', idx === selectedIndex);
  });
}

document.addEventListener('keydown', (e) => {
  if (popup.style.display !== 'block') return;

  switch (e.key) {
    case 'ArrowDown':
      e.preventDefault();
      selectedIndex = (selectedIndex + 1) % suggestionItems.length;
      updateSelectedItem();
      break;
    case 'ArrowUp':
      e.preventDefault();
      selectedIndex = selectedIndex <= 0 ? suggestionItems.length - 1 : selectedIndex - 1;
      updateSelectedItem();
      break;
    case 'Enter':
    case 'Tab':
      e.preventDefault();
      if (selectedIndex >= 0) {
        replaceWithSuggestion(suggestionItems[selectedIndex].textContent);
        selectedIndex = -1;
      }
      break;
  }
});

// 鼠标点击选择建议词
suggestionItems.forEach((item, idx) => {
  item.addEventListener('click', () => {
    replaceWithSuggestion(item.textContent);
    selectedIndex = -1;
  });
});

基础样式参考

#editor {
  position: relative;
  width: 800px;
  height: 400px;
  border: 1px solid #ddd;
  padding: 12px;
  overflow-y: auto;
  font-family: monospace;
  font-size: 14px;
}

#suggestion-popup {
  display: none;
  background: #fff;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  min-width: 180px;
  z-index: 100;
}

.suggestion-item {
  padding: 6px 12px;
  cursor: pointer;
  font-family: monospace;
  font-size: 14px;
}

.suggestion-item.selected {
  background-color: #e8f4ff;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 13:35:35