SOURCE

console 命令行工具 X clear

                    
>
console
  const editor = document.getElementById('editor');
  const menu = document.getElementById('menu');
  const suggestions = ['Alice', 'Bob', 'Charlie'];
  let currentRange = null;
  let menuItems = [];
  let activeIndex = -1;
  let suppressMenu = false;


  // 检测输入
  editor.addEventListener('input', () => {
    if (suppressMenu) {
        suppressMenu = false;
        return; // 不弹菜单
    }
    const sel = window.getSelection();
    const range = sel.getRangeAt(0);
    const textBeforeCursor = getTextBeforeCursor(range);

    const atMatch = textBeforeCursor.match(/@([\w]*)$/);
    if (atMatch) {
      const keyword = atMatch[1];
      const filtered = suggestions.filter(name =>
        name.toLowerCase().includes(keyword.toLowerCase())
      );
      showMenu(filtered, range);
    } else {
      hideMenu();
    }
  });

editor.addEventListener('keydown', (e) => {
  const sel = window.getSelection();

  if (e.key === 'Backspace') {
    const node = sel.anchorNode;
    const offset = sel.anchorOffset;
    suppressMenu = true;


    const range = sel.getRangeAt(0);
    const textBefore = getTextBeforeCursor(range);
    if (textBefore.endsWith('@')) {
        hideMenu();
    }

    // 情况一:当前光标处于 mention 块“后”
    // if (node.nodeType === 1) {
    //   const before = node.childNodes[offset - 1];
    //   if (before && before.dataset?.mention) {
    //     e.preventDefault();
    //     before.remove();
    //     return;
    //   }
    // }

    // // 情况二:当前光标在文本节点中,位置在 mention 块“后”
    // if (node.nodeType === 3 && offset === 0) {
    //   const prev = node.previousSibling;
    //   if (prev && prev.dataset?.mention) {
    //     e.preventDefault();
    //     prev.remove();
    //     return;
    //   }
    // }

    // // 情况三:当前光标在 mention 块内(非常规情况)
    // if (node.parentElement?.dataset?.mention) {
    //   e.preventDefault();
    //   node.parentElement.remove();
    //   return;
    // }
  }

  // 菜单操作处理
  if (menu.style.display === 'block') {
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      activeIndex = (activeIndex + 1) % menuItems.length;
      updateActiveItem();
    }
    if (e.key === 'ArrowUp') {
      e.preventDefault();
      activeIndex = (activeIndex - 1 + menuItems.length) % menuItems.length;
      updateActiveItem();
    }
    if (e.key === 'Enter') {
      e.preventDefault();
      if (menuItems[activeIndex]) {
        insertMention(menuItems[activeIndex].textContent);
      }
    }
  }
});


  // 获取光标前的文字
  function getTextBeforeCursor(range) {
    const clonedRange = range.cloneRange();
    clonedRange.collapse(true);
    clonedRange.setStart(editor, 0);
    const fragment = clonedRange.cloneContents();
    const div = document.createElement('div');
    div.appendChild(fragment);
    return div.innerText;
  }

  function showMenu(items, range) {
    currentRange = range.cloneRange();
    const rect = range.getBoundingClientRect();
    menu.style.top = rect.bottom + window.scrollY + 'px';
    menu.style.left = rect.left + window.scrollX + 'px';
    menu.innerHTML = '';

    menuItems = items.map(item => {
      const li = document.createElement('li');
      li.textContent = item;
      li.addEventListener('mousedown', (e) => {
        e.preventDefault();
        insertMention(item);
      });
      menu.appendChild(li);
      return li;
    });

    activeIndex = 0;
    updateActiveItem();
    menu.style.display = items.length ? 'block' : 'none';
  }

  function updateActiveItem() {
    menuItems.forEach((li, index) => {
      li.classList.toggle('active', index === activeIndex);
    });
  }

  function hideMenu() {
    menu.style.display = 'none';
    menuItems = [];
    activeIndex = -1;
  }

function insertMention(name) {
  if (!currentRange) return;

  // 删除当前 Range 前的 @关键字
  const range = currentRange;
  const sel = window.getSelection();

  // 移动 range 向前,直到找到 @ 开头
  const fullText = getTextBeforeCursor(range);
  const match = fullText.match(/@[\w]*$/);
  if (match) {
    const matchedText = match[0];
    range.setStart(range.endContainer, range.endOffset - matchedText.length);
    range.deleteContents();
  }

  // 创建 mention 块
  const mention = document.createElement('span');
  mention.textContent = '@' + name;
  mention.contentEditable = 'false';
  mention.setAttribute('data-mention', name);
  mention.style.backgroundColor = '#d0f0ff';
  mention.style.padding = '2px 4px';
  mention.style.borderRadius = '4px';

  const space = document.createTextNode(' ');

  range.insertNode(space);
  range.insertNode(mention);

  // 移动光标到后面
  const newRange = document.createRange();
  newRange.setStartAfter(space);
  newRange.collapse(true);
  sel.removeAllRanges();
  sel.addRange(newRange);

  hideMenu();
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>@ Mention Demo</title>
  <style>
    #editor {
      border: 1px solid #ccc;
      padding: 10px;
      min-height: 100px;
      width: 500px;
      white-space: pre-wrap;
    }
    [data-mention] {
      background: #d0f0ff;
      padding: 2px 4px;
      border-radius: 4px;
      margin: 0 2px;
    }
    #menu {
      position: absolute;
      background: white;
      border: 1px solid #ccc;
      display: none;
      list-style: none;
      padding: 0;
      margin: 0;
      z-index: 1000;
    }
    #menu li {
      padding: 4px 8px;
      cursor: pointer;
    }
    #menu li.active {
      background: #007bff;
      color: white;
    }
    #menu li:hover {
      background: #eee;
    }
  </style>
</head>
<body>

<div id="editor" contenteditable="true" data-placeholder="请输入对话""></div>
<ul id="menu"></ul>



</body>
</html>
#editor {
    border: 1px solid #ccc;
    padding: 10px;
    min-height: 100px;
    width: 500px;
    position: relative;
  }

  #editor:before {
    content: attr(data-placeholder);
    color: #aaa;
    position: absolute;
    left: 10px;
    top: 10px;
    opacity: 0.6;
    pointer-events: none;
  }

  #editor:not(:empty):before {
    content: '';
  }

 #editor:empty:before {
    content: attr(data-placeholder);
  }