实现固定宽度、替换模式的contenteditable div文本编辑器
优化固定宽度contenteditable文本编辑器的实现
你的目标是打造每行规格不同的固定宽度文本编辑器,核心需求是固定宽度、替换输入模式、字符数量限制。针对你当前的实现,这里提供更可靠的优化方案:
核心优化点
- 直接存储字符限制:不再从样式宽度解析最大字符数,而是给每个输入框绑定
data-max-length属性,数据与样式解耦,更可靠。 - 用
beforeinput实现精准控制:在输入动作触发前拦截处理,避免事后修改DOM的冗余操作,同时完美实现替换模式。 - 更鲁棒的光标处理:替换字符后自动维护光标位置,确保编辑体验流畅。
优化后的完整代码
<html> <head> <style type="text/css"> .col { display: inline-block; border: 1px solid; padding: 4px; font-family: monospace; overflow: hidden; text-overflow: clip; white-space: nowrap; } .row { white-space: nowrap; } </style> <script type="text/javascript"> function getCaretPosition(node) { const selection = document.getSelection(); if (!selection || !node) return 0; const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null; if (!range || !range.startContainer.isEqualNode(node.firstChild)) return 0; return range.startOffset; } function setCaretPosition(node, pos) { node.focus(); const textNode = node.firstChild || node.appendChild(document.createTextNode('')); pos = Math.max(0, Math.min(pos, textNode.length)); range.setStart(textNode, pos); range.setEnd(textNode, pos); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); } function setupEventListeners() { document.addEventListener("beforeinput", function(e) { const target = e.target; if (!target.hasAttribute('contenteditable') || target.getAttribute('contenteditable') !== 'true') return; const maxLength = parseInt(target.dataset.maxLength, 10); if (isNaN(maxLength)) return; const currentText = target.innerText || ''; const caretPos = getCaretPosition(target); // 允许删除、剪切等非输入字符的操作 const allowedActions = ['deleteContentBackward', 'deleteContentForward', 'cut']; if (allowedActions.includes(e.inputType)) { return; } // 只保留输入的第一个字符,实现替换模式 let inputText = e.data || ''; inputText = inputText.charAt(0); if (!inputText) { e.preventDefault(); return; } let newText = currentText; let newCaretPos = caretPos + 1; // 处理字符替换逻辑 if (currentText.length >= maxLength) { if (caretPos >= maxLength) { // 光标在末尾时替换最后一个字符 newText = currentText.substring(0, maxLength - 1) + inputText; newCaretPos = maxLength; } else { // 替换当前光标位置的字符 newText = currentText.substring(0, caretPos) + inputText + currentText.substring(caretPos + 1); } } else { if (caretPos < currentText.length) { // 未达上限时替换光标位置字符 newText = currentText.substring(0, caretPos) + inputText + currentText.substring(caretPos + 1); } else { // 光标在末尾时追加字符 newText = currentText + inputText; } } // 阻止默认输入,手动更新文本和光标 e.preventDefault(); target.innerText = newText; setCaretPosition(target, newCaretPos); }, false); } function setup(cfg){ let root = document.getElementById(cfg.root); for (const row of cfg.specs){ const rdiv = document.createElement('div'); rdiv.className = 'row'; root.appendChild(rdiv); for (const col of row){ const cdiv = document.createElement('div'); cdiv.className = 'col'; cdiv.setAttribute('contenteditable', true); cdiv.style.width = `${col}ch`; cdiv.style.height = '1em'; // 绑定最大字符数到data属性 cdiv.dataset.maxLength = col; rdiv.appendChild(cdiv); } } } const cfg = { root : 'container', specs : [ [2,4,5], [3,6,2] ] }; window.addEventListener('load', function() { setup(cfg); setupEventListeners(); }, false); </script> </head> <body> <div id="container"></div> </body> </html>
关键代码解释
data-max-length属性:在setup函数中给每个输入框绑定该属性,直接对应宽度的ch值(等宽字体下1ch=1字符),字符限制与宽度完全匹配,且不受样式解析误差影响。beforeinput事件处理:- 先校验目标元素是否为可编辑框,获取预设的最大长度。
- 放行删除、剪切等基础编辑操作,保证功能完整性。
- 只保留输入的第一个字符,严格实现替换模式(每次输入仅替换一个字符)。
- 根据文本长度和光标位置自动处理替换逻辑,同时调整光标到正确位置。
光标位置处理:
- 优化
getCaretPosition,确保仅获取当前输入框内的光标偏移量。 setCaretPosition自动处理边界情况,避免光标超出文本范围。
- 优化
这个方案相比原有实现更可靠、性能更优,编辑体验也更流畅,完全满足你的三个核心需求。
内容的提问来源于stack exchange,提问作者sten
相关产品推荐
相关产品推荐

