You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 16:02:02