BJSpell与contenteditable div:错误标记定位问题修复咨询
问题描述
使用BJSpell.js开发拼写检查器时,直接修改contenteditable div会导致光标丢失/重置到开头,因此采用叠加透明元素标记错误词,但出现错误标记无法随文本移动的问题:标记的单词位置改变后,错误下划线仍停留在原位置。
现有代码
HTML
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script> <script type="text/javascript" src="https://rawcdn.githack.com/maheshmurag/bjspell/master/BJSpell.js"></script> Test my wordz <div id='my_editing_div' contenteditable='true' spellcheck='false' style='width:500px;height:300px;border:1px solid black;'></div>
CSS
.word-check { color: transparent; } .word-error { border-bottom: 3px solid orange; }
JavaScript
var dictionary = 'https://rawcdn.githack.com/maheshmurag/bjspell/master/dictionary.js/en_US.js'; var lang = BJSpell(dictionary, function() { // }); const containsBadWord = word => { const found = lang.check(word); return found; } const processSpellCheck = text => { const allWords = text.split(/\b/); const newContent = allWords.map((word, index) => { word = word.replace(/[0-9]/g, ''); word = word.replace(/[^\w\s]|_/g, ''); var text = word; if (!containsBadWord(word.toLowerCase())) { text = $("<span />") .addClass("word-error") .text(word); } return text; }); return newContent; } function initalizeSpellcheck(editorRef) { var editorSize = editorRef.getBoundingClientRect(); var spellcheckContainer = $("<div />", {}) .addClass("word-check") .prop("spellcheck", "false"); var spellcheckSpan = $("<div />") .addClass("word-check-text-content") .css({ width: editorSize.width, height: editorSize.height, position: "absolute", zIndex: -1 }); spellcheckContainer.append(spellcheckSpan); spellcheckContainer.insertBefore(editorRef); $(editorRef).on("keyup.spellcheck", function(event) { if (event.keyCode == 32 || event.keyCode == 13) { var newText = $(event.target).text(); var newContent = processSpellCheck(newText); $(".word-check .word-check-text-content").text(""); $(".word-check .word-check-text-content").append(newContent); } }); } $(document).ready(function() { var editor = document.querySelector("#my_editing_div"); initalizeSpellcheck(editor); });
修复方案
核心问题是隐藏层仅同步了纯文本,未同步原编辑层的HTML结构和排版样式,导致标记位置错位。解决方案是让隐藏层与编辑层完全同步HTML结构和样式,再在隐藏层内遍历节点标记错误词:
1. 调整CSS,保证两层样式完全一致
修改.word-check和相关样式,让隐藏层与编辑层重叠且样式同步:
/* 编辑层相对定位,作为隐藏层的参考 */ #my_editing_div { position: relative; z-index: 1; background: white; /* 覆盖隐藏层,保证可编辑 */ } /* 隐藏层绝对定位,完全覆盖编辑层 */ .word-check { position: absolute; top: 0; left: 0; right: 0; bottom: 0; color: transparent; pointer-events: none; /* 禁止点击,不干扰编辑 */ z-index: 0; /* 同步编辑层的字体、行高等样式 */ font-family: inherit; font-size: inherit; line-height: inherit; padding: inherit; } .word-error { border-bottom: 3px solid orange; }
2. 修改JavaScript逻辑,同步HTML并标记错误
不再拆分纯文本,而是同步编辑层的HTML到隐藏层,然后遍历隐藏层的文本节点,逐个检查单词并添加标记:
var dictionary = 'https://rawcdn.githack.com/maheshmurag/bjspell/master/dictionary.js/en_US.js'; var lang = BJSpell(dictionary, function() { // 字典加载完成后可初始化检查 }); // 检查单词是否拼写错误 const isMisspelled = word => { // 过滤空字符和非单词内容 if (!word.trim() || /[^a-zA-Z]/.test(word)) return false; return !lang.check(word.toLowerCase()); } // 遍历文本节点,给错误单词添加标记 function markMisspellings(element) { // 递归处理子节点 $(element).contents().each(function() { if (this.nodeType === Node.TEXT_NODE) { const text = this.textContent; // 按单词拆分文本(保留分隔符) const parts = text.split(/(\b)/); const fragment = document.createDocumentFragment(); parts.forEach(part => { if (isMisspelled(part)) { // 错误单词包裹span const span = $("<span>").addClass("word-error").text(part)[0]; fragment.appendChild(span); } else { // 普通文本直接添加 fragment.appendChild(document.createTextNode(part)); } }); // 替换原文本节点 $(this).replaceWith(fragment); } else if (this.nodeType === Node.ELEMENT_NODE && !$(this).hasClass("word-error")) { // 递归处理非标记元素 markMisspellings(this); } }); } function initializeSpellcheck(editorRef) { const $editor = $(editorRef); // 创建隐藏层 const $spellcheckLayer = $("<div>") .addClass("word-check") .insertAfter($editor); // 同步编辑层样式到隐藏层 const editorStyles = getComputedStyle(editorRef); $spellcheckLayer.css({ font-family: editorStyles.fontFamily, font-size: editorStyles.fontSize, line-height: editorStyles.lineHeight, padding: editorStyles.padding, whiteSpace: editorStyles.whiteSpace, wordBreak: editorStyles.wordBreak }); // 触发拼写检查的函数(防抖优化) const checkSpelling = debounce(() => { // 同步编辑层的HTML到隐藏层 $spellcheckLayer.html($editor.html()); // 在隐藏层标记错误单词 markMisspellings($spellcheckLayer[0]); }, 200); // 200ms防抖,避免频繁触发 // 监听编辑层输入事件 $editor.on("input keyup", checkSpelling); } // 防抖函数,优化性能 function debounce(func, delay) { let timeoutId; return function() { clearTimeout(timeoutId); timeoutId = setTimeout(func, delay); }; } $(document).ready(function() { const editor = document.querySelector("#my_editing_div"); initializeSpellcheck(editor); });
关键改进点
- 隐藏层与编辑层完全重叠,样式1:1同步,保证排版一致
- 同步编辑层的HTML结构而非纯文本,保留换行、空格等排版信息
- 递归遍历文本节点标记错误单词,不破坏原有结构
- 添加防抖函数减少检查频率,提升性能
- 隐藏层设置
pointer-events: none,不干扰编辑操作
内容的提问来源于stack exchange,提问作者David Taylor
相关产品推荐
相关产品推荐

