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
相关产品推荐
相关产品推荐

