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

contenteditable div实现多行选中Tab/Shift+Tab缩进功能

contenteditable 多行Tab缩进/Shift+Tab反缩进实现方案

原有代码仅支持单光标位置插入缩进,无法覆盖多行选中批量操作、反向缩进的需求,核心要解决的是选中范围对应行的提取问题,以下是可直接运行的完整实现:

实现逻辑

  • 拦截Tab键默认行为,避免焦点从编辑区跳走
  • 通过Selection API拿到选中范围的起止节点,向上遍历找到对应所属的行节点
  • 兼容从下往上反向选中的场景,按顺序收集所有覆盖到的行
  • 区分Tab/Shift+Tab操作,逐行在头部添加缩进、或删除行首对应长度的缩进
  • 操作完成后恢复选中状态,支持连续按Tab/Shift+Tab调整缩进层级

完整代码

JS逻辑

// 配置:单个缩进对应的不间断空格数量
const TAB_LENGTH = 1;
const TAB_STR = '\u00a0';

$('#wrap').on('keydown', function(e) {
    if (e.keyCode !== 9) return;
    e.preventDefault();
    const editor = this;
    const sel = window.getSelection();
    if (!sel.rangeCount) return;
    const range = sel.getRangeAt(0);

    // 工具:获取节点所属的编辑区直接子行
    const getLine = (node) => {
        while (node && node.parentNode !== editor) node = node.parentNode;
        return node;
    };

    let startLine = getLine(range.startContainer);
    let endLine = getLine(range.endContainer);

    // 单行场景兼容原有逻辑
    if (startLine === endLine) {
        if (!e.shiftKey) {
            const tabNode = document.createTextNode(TAB_STR.repeat(TAB_LENGTH));
            range.insertNode(tabNode);
            range.setStartAfter(tabNode);
            range.collapse(true);
            sel.removeAllRanges();
            sel.addRange(range);
        } else {
            // 单行反缩进:删除行首缩进
            const firstText = startLine.firstChild;
            if (firstText?.nodeType === 3 && firstText.textContent.startsWith(TAB_STR)) {
                firstText.textContent = firstText.textContent.slice(TAB_LENGTH);
            }
        }
        return;
    }

    // 处理反向选中(从下往上拖选)的顺序问题
    if (startLine.compareDocumentPosition(endLine) & Node.DOCUMENT_POSITION_PRECEDING) {
        [startLine, endLine] = [endLine, startLine];
    }

    // 收集所有选中行
    const lines = [];
    let cur = startLine;
    while (cur) {
        lines.push(cur);
        if (cur === endLine) break;
        cur = cur.nextSibling;
    }

    // 逐行处理缩进
    lines.forEach(line => {
        if (!e.shiftKey) {
            // 加缩进:行首插入缩进字符
            line.insertBefore(document.createTextNode(TAB_STR.repeat(TAB_LENGTH)), line.firstChild);
        } else {
            // 减缩进:仅删除行首的缩进字符
            const firstChild = line.firstChild;
            if (firstChild?.nodeType === 3) {
                const text = firstChild.textContent;
                let delCount = 0;
                while (delCount < Math.min(TAB_LENGTH, text.length) && text[delCount] === TAB_STR) {
                    delCount++;
                }
                if (delCount) firstChild.textContent = text.slice(delCount);
            }
        }
    });

    // 重新选中处理过的行,保留选中状态方便连续操作
    const newRange = document.createRange();
    newRange.setStartBefore(lines[0]);
    newRange.setEndAfter(lines.at(-1));
    sel.removeAllRanges();
    sel.addRange(newRange);
});

CSS样式

.wrap{
    padding:0 14px 9px 14px;
    white-space:pre-wrap;
    background:orange;
    outline:none;
    min-height: 120px;
}

HTML结构

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class='wrap' id='wrap' contenteditable>
lorem ipsum
dolor sit
amet
</div>

适配说明

  • 如果你的编辑区行内容被<p>、<div>等标签嵌套,只需要修改getLine函数的遍历终止条件,匹配你实际的行节点层级即可
  • 如果需要用制表符\t作为缩进字符,直接把TAB_STR的值改成\t即可
  • 反缩进逻辑只会删除行首的缩进字符,不会误删行中间的正常文本内容

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 05:21:47