如何用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
相关产品推荐
相关产品推荐

