基于JavaScript的textarea@提及系统:高亮与建议列表异常排查
解决textarea中@提及系统的两个问题:文本高亮与建议列表定位
一、@文本未高亮的解决方案
textarea是纯文本输入控件,无法直接对部分文本应用样式。我们需要用叠加层div模拟高亮效果,具体实现:
- 创建与textarea样式完全一致的div,通过绝对定位覆盖在textarea上方,设置背景透明,仅对@开头的文本添加蓝色样式
- 监听textarea的输入/滚动事件,同步内容到叠加层,并将所有@开头的匹配文本用带样式的span包裹
- 同步textarea和叠加层的滚动状态,避免视觉错位
二、建议列表未在光标旁显示的解决方案
要让列表精准定位在光标旁,需计算光标在textarea中的绝对坐标:
- 监听textarea的
input或keydown事件,检测到输入@时,通过SelectionAPI获取光标对应的文本范围 - 用
getBoundingClientRect()计算该范围的屏幕坐标,结合textarea的页面位置,动态设置建议列表的top和left样式 - 调整偏移量确保列表不会遮挡光标
完整实现代码
HTML
<div class="mention-container"> <textarea id="mentionTextarea" placeholder="输入@试试..."></textarea> <div id="highlightOverlay"></div> <ul id="suggestionList" class="suggestion-list"> <li>Alice</li> <li>Bob</li> <li>Charlie</li> </ul> </div>
CSS
.mention-container { position: relative; width: 500px; height: 200px; } #mentionTextarea, #highlightOverlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; padding: 10px; font-size: 16px; font-family: Arial, sans-serif; line-height: 1.5; border: 1px solid #ccc; border-radius: 4px; white-space: pre-wrap; word-wrap: break-word; } #mentionTextarea { background: transparent; z-index: 1; resize: none; } #highlightOverlay { color: transparent; z-index: 0; pointer-events: none; /* 让点击事件穿透到textarea */ } .highlight-mention { color: #1a73e8; /* 蓝色高亮 */ } .suggestion-list { position: absolute; display: none; margin: 0; padding: 0; list-style: none; background: white; border: 1px solid #ccc; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); z-index: 2; width: 150px; } .suggestion-list li { padding: 8px 12px; cursor: pointer; } .suggestion-list li:hover { background: #f1f3f4; }
JavaScript
const textarea = document.getElementById('mentionTextarea'); const highlightOverlay = document.getElementById('highlightOverlay'); const suggestionList = document.getElementById('suggestionList'); // 同步文本内容与高亮样式 function syncHighlight() { let content = textarea.value; // 匹配所有@开头的单词(支持字母、数字、下划线) content = content.replace(/@(\w+)/g, '<span class="highlight-mention">@$1</span>'); highlightOverlay.innerHTML = content; // 同步滚动条位置 highlightOverlay.scrollTop = textarea.scrollTop; } // 获取光标在textarea内的坐标 function getCursorPosition() { const selection = window.getSelection(); if (selection.rangeCount === 0) return null; const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); const containerRect = textarea.getBoundingClientRect(); return { top: rect.top - containerRect.top + textarea.scrollTop + 20, // 偏移20px避免遮挡光标 left: rect.left - containerRect.left + textarea.scrollLeft }; } // 显示建议列表并定位到光标旁 function showSuggestions() { const pos = getCursorPosition(); if (!pos) return; suggestionList.style.display = 'block'; suggestionList.style.top = `${pos.top}px`; suggestionList.style.left = `${pos.left}px`; } // 隐藏建议列表 function hideSuggestions() { suggestionList.style.display = 'none'; } // 监听输入事件 textarea.addEventListener('input', () => { syncHighlight(); const lastChar = textarea.value.slice(-1); if (lastChar === '@') { showSuggestions(); } else { // 若输入内容不在@开头的单词范围内,隐藏列表 const mentionRegex = /@\w*$/; if (!mentionRegex.test(textarea.value)) { hideSuggestions(); } } }); // 监听滚动事件,同步高亮层滚动 textarea.addEventListener('scroll', syncHighlight); // 点击建议项插入提及文本 suggestionList.addEventListener('click', (e) => { if (e.target.tagName === 'LI') { const mentionText = `@${e.target.textContent} `; const cursorPos = textarea.selectionStart; const currentValue = textarea.value; // 替换@为完整提及文本 const newValue = currentValue.slice(0, cursorPos - 1) + mentionText + currentValue.slice(cursorPos); textarea.value = newValue; // 将光标移至提及文本末尾 textarea.selectionStart = textarea.selectionEnd = cursorPos - 1 + mentionText.length; syncHighlight(); hideSuggestions(); } }); // 初始化高亮状态 syncHighlight();
内容的提问来源于stack exchange,提问作者mürtaza güler
相关产品推荐
相关产品推荐

