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

实现固定宽度、替换模式的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>

关键代码解释

  1. data-max-length属性:在setup函数中给每个输入框绑定该属性,直接对应宽度的ch值(等宽字体下1ch=1字符),字符限制与宽度完全匹配,且不受样式解析误差影响。

  2. beforeinput事件处理:

    • 先校验目标元素是否为可编辑框,获取预设的最大长度。
    • 放行删除、剪切等基础编辑操作,保证功能完整性。
    • 只保留输入的第一个字符,严格实现替换模式(每次输入仅替换一个字符)。
    • 根据文本长度和光标位置自动处理替换逻辑,同时调整光标到正确位置。
  3. 光标位置处理:

    • 优化getCaretPosition,确保仅获取当前输入框内的光标偏移量。
    • setCaretPosition自动处理边界情况,避免光标超出文本范围。

这个方案相比原有实现更可靠、性能更优,编辑体验也更流畅,完全满足你的三个核心需求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 14:45:04