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

如何通过方向键在div元素间移动光标(仿Notion编辑器)

实现方向键在div间移动光标(仿Notion编辑器)

要实现你需要的效果,核心是拦截方向键事件,手动计算光标跳转位置并更新选区。以下是具体实现方案:

第一步:调整HTML结构

首先给每个可编辑的div加上contenteditable="true",让光标能进入并编辑:

<div class="editor-block" contenteditable="true">
    hello
    word
</div>
<div class="editor-block" contenteditable="true">hi</div>
<div class="editor-block" contenteditable="true">notion</div>

第二步:JavaScript逻辑实现

通过监听键盘事件,处理上下方向键的光标跳转逻辑:

const blocks = document.querySelectorAll('.editor-block');

// 全局监听键盘按下事件
document.addEventListener('keydown', (e) => {
  // 只处理上下方向键
  if (!['ArrowUp', 'ArrowDown'].includes(e.key)) return;
  
  e.preventDefault(); // 阻止浏览器默认的光标移动/滚动行为
  
  const selection = window.getSelection();
  if (!selection.rangeCount) return;
  
  const currentRange = selection.getRangeAt(0);
  const currentBlock = currentRange.commonAncestorContainer.closest('.editor-block');
  if (!currentBlock) return;
  
  e.key === 'ArrowDown' ? handleDown(currentRange, currentBlock) : handleUp(currentRange, currentBlock);
});

// 处理向下方向键
function handleDown(range, currentBlock) {
  const text = currentBlock.textContent;
  const currentOffset = range.startOffset;
  const nextLinePos = text.indexOf('\n', currentOffset);
  
  // 同一div内有下一行,跳转到下一行第一个非空白字符
  if (nextLinePos !== -1) {
    let nextOffset = nextLinePos + 1;
    // 跳过换行后的空格/制表符
    while (nextOffset < text.length && /\s/.test(text[nextOffset])) {
      nextOffset++;
    }
    updateCursor(currentBlock.firstChild, nextOffset);
    return;
  }
  
  // 跳转到下一个div的开头
  const currentIndex = Array.from(blocks).indexOf(currentBlock);
  if (currentIndex >= blocks.length - 1) return;
  
  const nextBlock = blocks[currentIndex + 1];
  updateCursor(nextBlock.firstChild, 0);
  nextBlock.focus();
}

// 处理向上方向键
function handleUp(range, currentBlock) {
  const text = currentBlock.textContent;
  const currentOffset = range.startOffset;
  const prevLinePos = text.lastIndexOf('\n', currentOffset - 1);
  
  // 同一div内有上一行,跳转到上一行第一个非空白字符
  if (prevLinePos !== -1) {
    let prevOffset = prevLinePos + 1;
    while (prevOffset < text.length && /\s/.test(text[prevOffset])) {
      prevOffset++;
    }
    updateCursor(currentBlock.firstChild, prevOffset);
    return;
  }
  
  // 跳转到上一个div的末尾
  const currentIndex = Array.from(blocks).indexOf(currentBlock);
  if (currentIndex <= 0) return;
  
  const prevBlock = blocks[currentIndex - 1];
  updateCursor(prevBlock.firstChild, prevBlock.textContent.length);
  prevBlock.focus();
}

// 通用更新光标位置的函数
function updateCursor(textNode, offset) {
  const newRange = document.createRange();
  newRange.setStart(textNode, offset);
  newRange.collapse(true);
  
  const selection = window.getSelection();
  selection.removeAllRanges();
  selection.addRange(newRange);
}

关键逻辑说明

  1. 拦截默认行为:阻止浏览器自带的方向键滚动或光标移动,让我们能完全控制光标位置
  2. 选区获取:通过window.getSelection()获取当前光标所在的Range对象,定位到当前编辑块
  3. 行内跳转:通过文本中的换行符\n判断是否有上下行,跳转时自动跳过行首的空白字符,模拟Notion的排版逻辑
  4. 块间跳转:当光标在块的最后一行/第一行时,按下方向键会跳转到相邻块的开头/末尾

注意事项

  • 如果div内存在多个文本节点(比如插入了其他元素),需要调整currentBlock.firstChild的逻辑,遍历找到对应的文本节点
  • 可以根据需求修改块间跳转的目标位置(比如跳转到上一个块的开头而非末尾)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 17:25:30