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

实现三个可编辑Div无缝联动的多段推文文本分割工具(单段最大274字符)

实现三个可编辑Div无缝联动的多段推文文本分割工具(单段最大274字符)

我太懂你现在的糟心感受了——contenteditable元素本身就是前端界出了名的“坑王”,尤其是要做多个框之间的文本联动、光标定位和格式保持的时候,稍不注意就会出现退格乱跳、输入卡顿、粘贴出一堆空白行的问题。咱们先把你现有代码里的核心问题梳理清楚,再给出能顺畅运行的完整修复版本,让三个推文框真正实现无缝联动。


核心问题分析

你遇到的这些异常,本质上是三个原因:

  1. contenteditable的默认样式和行为不符合推文编辑的场景需求
  2. 文本联动时的光标定位逻辑缺失,导致重分配后光标乱跑
  3. 文本分割和合并的逻辑不够严谨,丢失了原始格式或截断了内容

修复后的完整代码

HTML结构(更规整的布局)

<div class="tweet-container">
  <div class="tweet-label">Tweet 1</div>
  <div id='Tweet1' class='Tweet BlueBorder' contenteditable="true"></div>
  
  <div class="tweet-label">Tweet 2</div>
  <div id='Tweet2' class='Tweet BlueBorder' contenteditable="true"></div>
  
  <div class="tweet-label">Tweet 3</div>
  <div id='Tweet3' class='Tweet BlueBorder' contenteditable="true"></div>
</div>

CSS样式(适配推文场景+解决输入异常)

/* 容器样式,让整体布局更规整 */
.tweet-container {
  max-width: 600px;
  margin: 20px auto;
  padding: 0 10px;
}

.tweet-label {
  font-size: 16px;
  font-weight: 600;
  margin: 15px 0 5px 10px;
}

.Tweet {
  height: 120px; /* 固定高度比百分比更可控,贴合推文编辑场景 */
  padding: 12px 15px;
  font-size: 14px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; /* 类Twitter字体更贴合场景 */
  overflow-y: auto;
  word-wrap: break-word;
  white-space: pre-wrap; /* 保留手动换行,同时自动换行 */
  border: 1px solid #e1e8ed;
  border-radius: 8px; /* 圆角更现代 */
  margin: 0 10px 10px;
  background: white;
  outline: none; /* 去掉默认焦点外框,避免干扰 */
  line-height: 1.4; /* 调整行高,避免多余空白 */
}

.Tweet:focus {
  border-color: #1da1f2; /* 焦点时高亮边框,类Twitter风格 */
  box-shadow: 0 0 0 1px #1da1f2;
}

.BlueBorder {
  /* 保留你原有的BlueBorder类,可根据需求扩展样式 */
}

JavaScript逻辑(修复联动+光标+分割问题)

const Tweet1 = document.getElementById("Tweet1");
const Tweet2 = document.getElementById("Tweet2");
const Tweet3 = document.getElementById("Tweet3");
const maxChars = 274;
const urlCharCount = 23; // 遵循Twitter旧规则:URL统一按23字符计数
const tweets = [Tweet1, Tweet2, Tweet3];

// 存储光标位置,用于文本重分配后精准恢复
let lastCaretPosition = {
  divIndex: 0,
  offset: 0
};

// 给所有推文框统一绑定事件
tweets.forEach((div, index) => {
  div.addEventListener("input", () => handleInput(index));
  div.addEventListener("keydown", (e) => handleKeydown(e, index));
  div.addEventListener("paste", handlePaste);
  div.addEventListener("focus", () => saveCaretPosition(index));
});

// 输入事件处理:触发文本重分配
function handleInput(index) {
  saveCaretPosition(index);
  redistributeText();
}

// 键盘事件处理:优化退格/删除键的联动逻辑
function handleKeydown(event, index) {
  const currentDiv = tweets[index];
  const selection = window.getSelection();

  // 处理退格键:支持空框跳转+光标开头时拉取上框内容
  if (event.key === "Backspace") {
    const isEmpty = currentDiv.textContent.trim() === "";
    const isCaretAtStart = selection.anchorOffset === 0 && selection.anchorNode === currentDiv.firstChild;
    
    if (isEmpty && index > 0) {
      event.preventDefault();
      const prevDiv = tweets[index - 1];
      prevDiv.focus();
      moveCaretToEnd(prevDiv);
      redistributeText();
    } else if (isCaretAtStart && index > 0 && currentDiv.textContent.length > 0) {
      event.preventDefault();
      const prevDiv = tweets[index - 1];
      const prevText = prevDiv.textContent;
      if (prevText.length > 0) {
        // 从上个框末尾取一个字符(含换行)
        const charToMove = prevText.slice(-1);
        prevDiv.textContent = prevText.slice(0, -1);
        currentDiv.textContent = charToMove + currentDiv.textContent;
        saveCaretPosition(index, 1);
        redistributeText();
      }
    }
  }

  // 处理删除键:支持空框跳转到下一个框
  if (event.key === "Delete") {
    const isEmpty = currentDiv.textContent.trim() === "";
    const isCaretAtEnd = selection.anchorOffset === currentDiv.textContent.length;
    
    if (isEmpty && index < tweets.length - 1) {
      event.preventDefault();
      const nextDiv = tweets[index + 1];
      nextDiv.focus();
      moveCaretToStart(nextDiv);
      redistributeText();
    }
  }
}

// 粘贴事件处理:只保留纯文本,避免格式混乱
function handlePaste(event) {
  event.preventDefault();
  const text = (event.clipboardData || window.clipboardData).getData("text/plain");
  const targetDiv = event.target;
  const divIndex = tweets.indexOf(targetDiv);

  const selection = window.getSelection();
  if (selection.rangeCount) {
    const range = selection.getRangeAt(0);
    range.deleteContents();
    range.insertNode(document.createTextNode(text));
    saveCaretPosition(divIndex, selection.anchorOffset + text.length);
    redistributeText();
  }
}

// 核心函数:重新分配文本到三个推文框
function redistributeText() {
  // 收集所有文本,保留原始换行(用textContent避免格式丢失)
  let fullText = tweets.map(div => div.textContent).join("\n");
  // 清理多余的连续换行(可选,根据需求调整)
  fullText = fullText.replace(/\n{3,}/g, "\n\n");

  let currentText = "";
  let currentCharCount = 0;
  let tweetIndex = 0;

  // 重置所有推文框
  tweets.forEach(div => div.textContent = "");

  // 按单词+换行分割,确保不截断内容,同时处理URL计数
  const words = fullText.split(/(\s+|\n)/);
  for (let i = 0; i < words.length && tweetIndex < tweets.length; i++) {
    const word = words[i];
    const isUrl = isValidUrl(word.trim());
    const wordLength = isUrl ? urlCharCount : word.length;

    if (currentCharCount + wordLength <= maxChars) {
      currentText += word;
      currentCharCount += wordLength;
    } else {
      // 超过字符限制,切换到下一个框
      tweets[tweetIndex].textContent = currentText.trimEnd();
      tweetIndex++;
      currentText = word;
      currentCharCount = wordLength;
    }
  }

  // 处理最后剩余的文本
  if (tweetIndex < tweets.length && currentText) {
    tweets[tweetIndex].textContent = currentText;
  }

  // 恢复光标位置
  restoreCaretPosition();
}

// 辅助函数:判断是否为有效URL
function isValidUrl(word) {
  if (!word) return false;
  const urlRegex = /^(https?:\/\/)?([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})(\/[^\s]*)?$/;
  return urlRegex.test(word.trim());
}

// 保存当前光标位置
function saveCaretPosition(divIndex, customOffset = null) {
  const selection = window.getSelection();
  if (selection.rangeCount === 0) return;

  const range = selection.getRangeAt(0);
  const offset = customOffset !== null ? customOffset : range.startOffset;
  
  lastCaretPosition = {
    divIndex,
    offset
  };
}

// 恢复光标位置
function restoreCaretPosition() {
  const targetDiv = tweets[lastCaretPosition.divIndex];
  if (!targetDiv) return;

  const selection = window.getSelection();
  const range = document.createRange();
  
  const targetNode = targetDiv.firstChild || targetDiv;
  const safeOffset = Math.min(lastCaretPosition.offset, targetNode.length || 0);
  
  range.setStart(targetNode, safeOffset);
  range.collapse(true);
  
  selection.removeAllRanges();
  selection.addRange(range);
  targetDiv.focus();
}

// 辅助函数:将光标移到元素末尾
function moveCaretToEnd(element) {
  const selection = window.getSelection();
  const range = document.createRange();
  range.selectNodeContents(element);
  range.collapse(false);
  selection.removeAllRanges();
  selection.addRange(range);
  element.focus();
}

// 辅助函数:将光标移到元素开头
function moveCaretToStart(element) {
  const selection = window.getSelection();
  const range = document.createRange();
  range.selectNodeContents(element);
  range.collapse(true);
  selection.removeAllRanges();
  selection.addRange(range);
  element.focus();
}

// 初始化:清空所有推文框
tweets.forEach(div => div.textContent = "");

关键修复点说明

  1. CSS层面解决输入异常

    • 用固定高度替代百分比高度,避免布局波动
    • 优化行高和字体,解决粘贴后出现多余空白行的问题
    • 去掉contenteditable默认焦点外框,提升交互体验
  2. JS层面修复核心逻辑

    • 新增光标位置保存/恢复机制,彻底解决光标跳乱的问题
    • 优化退格/删除键的联动逻辑,更符合用户编辑直觉
    • 用textContent处理文本,避免格式丢失
    • 改进文本分割逻辑,确保不截断单词,同时正确处理URL的特殊计数规则
  3. 代码结构优化

    • 统一事件绑定方式,去掉冗余的oninput属性
    • 简化文本重分配逻辑,提升运行效率

备注:内容来源于stack exchange,提问作者Jake Hembree

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.14 12:45:29