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();
}
}
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;
const range = currentRange;
const sel = window.getSelection();
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();
}
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);
}