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

基于JS实现类Discord键盘文本高亮编辑器的问题求助

解决方案

针对你遇到的嵌套span光标失效和换行问题,核心思路是基于原始标记文本的光标偏移量来保存/恢复位置,同时单独处理换行逻辑,避免默认行为破坏结构。

1. 光标位置的正确保存与恢复(支持嵌套span)

不要直接依赖DOM节点记录光标位置——嵌套span会改变DOM结构,导致原有节点引用失效。我们改为记录光标在用户实际输入的标记文本(比如**粗体/y黄色/y**)中的偏移量,再将偏移量映射到转换后的HTML结构中。

代码实现:

// 获取光标在原始标记文本中的偏移量
function getCursorOffset(element) {
  const range = window.getSelection().getRangeAt(0);
  const preRange = document.createRange();
  preRange.selectNodeContents(element);
  preRange.setEnd(range.startContainer, range.startOffset);
  return preRange.toString().length;
}

// 根据偏移量在HTML结构中定位光标(支持嵌套节点)
function setCursorOffset(element, offset) {
  const selection = window.getSelection();
  let currentOffset = 0;
  
  const traverseNodes = (node) => {
    if (node.nodeType === Node.TEXT_NODE) {
      if (currentOffset + node.textContent.length >= offset) {
        const range = document.createRange();
        range.setStart(node, offset - currentOffset);
        range.setEnd(node, offset - currentOffset);
        selection.removeAllRanges();
        selection.addRange(range);
        return true;
      }
      currentOffset += node.textContent.length;
    } else {
      for (const child of node.childNodes) {
        if (traverseNodes(child)) return true;
      }
    }
    return false;
  };
  
  traverseNodes(element);
}

2. 标记与HTML的双向转换

需要把用户输入的标记(**/#//y)转换成对应的span,同时能把HTML还原回原始标记文本,确保光标偏移量的一致性:

// 把原始标记文本转换成HTML
function markupToHtml(text) {
  // 先处理优先级低的嵌套标记(比如/y黄色/y),避免被高优先级标记覆盖
  text = text.replace(/\/y([^\/]+)\/y/g, '<span class="yellow">$1</span>');
  text = text.replace(/#([^#]+)#/g, '<span class="bold">$1</span>');
  text = text.replace(/\*\*([^*]+)\*\*/g, '<span class="bold">$1</span>');
  // 把换行符转换成<br>
  text = text.replace(/\n/g, '<br>');
  return text;
}

// 把HTML还原成原始标记文本
function htmlToMarkup(element) {
  let markup = '';
  
  const traverseHtml = (node) => {
    if (node.nodeType === Node.TEXT_NODE) {
      markup += node.textContent;
    } else if (node.tagName === 'BR') {
      markup += '\n';
    } else if (node.tagName === 'SPAN') {
      const prefix = node.className.includes('yellow') ? '/y' : '**';
      const suffix = prefix;
      markup += prefix;
      for (const child of node.childNodes) {
        traverseHtml(child);
      }
      markup += suffix;
    } else {
      for (const child of node.childNodes) {
        traverseHtml(child);
      }
    }
  };
  
  traverseHtml(element);
  return markup;
}

3. 核心编辑逻辑与换行处理

监听input事件处理实时标记转换,同时单独拦截Enter键,手动处理换行避免默认行为破坏光标逻辑:

const editor = document.getElementById('editor');

// 实时处理标记转换与光标恢复
editor.addEventListener('input', () => {
  // 1. 获取原始标记文本和当前光标偏移量
  const originalMarkup = htmlToMarkup(editor);
  const cursorPos = getCursorOffset(editor);
  
  // 2. 转换标记为HTML
  const newHtml = markupToHtml(originalMarkup);
  
  // 3. 更新编辑器内容
  editor.innerHTML = newHtml;
  
  // 4. 恢复光标位置
  setCursorOffset(editor, cursorPos);
});

// 处理Enter键换行
editor.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    e.preventDefault(); // 阻止默认插入div/p的行为
    
    const originalMarkup = htmlToMarkup(editor);
    const cursorPos = getCursorOffset(editor);
    
    // 在原始标记中插入换行符
    const newMarkup = originalMarkup.slice(0, cursorPos) + '\n' + originalMarkup.slice(cursorPos);
    const newHtml = markupToHtml(newMarkup);
    
    editor.innerHTML = newHtml;
    // 光标定位到换行后的位置
    setCursorOffset(editor, cursorPos + 1);
  }
});

4. 配套CSS样式

#editor {
  border: 1px solid #ddd;
  padding: 10px;
  min-height: 120px;
  font-family: sans-serif;
}
.bold { font-weight: 700; }
.yellow { color: #ffd700; }

为什么之前的方案失效?

  • 直接替换innerHTML后基于DOM节点恢复光标:嵌套span会改变原有DOM结构,原来的文本节点可能被拆分或包裹,导致光标定位失效。
  • 换行问题:contenteditable默认按下Enter会插入<div>或<p>标签,破坏了我们基于纯文本偏移量的光标逻辑;而innerText会忽略<br>,导致换行丢失。

内容的提问来源于stack exchange,提问作者Penzboti

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 23:45:42