先看效果:
光标的历史记录会保存下来(默认 10 条历史),按下 Alt+← 可以跳转到上个标签,按下 Alt+→ 可以跳转到下个标签
请先把思源自带的 Alt←→ 跳转至上一级下一级的快捷键禁用掉
适合做题或者回顾笔记时,想编辑很靠上边的文本,编辑完之后想回到刚才的位置,这样就不用来回用鼠标滚来滚去了
已知 bug:
- 没办法跨文档滚动(似乎无法解决,因为 siyuan 的 dom 就是这样的)
- 光标回复位置成功,但是移出了视线,再按两下左右键就能让屏幕看到那了
// 光标历史记录管理器
class CursorHistory {
constructor(maxLength = 10) {
this.history = []; // 存储光标历史
this.currentIndex = -1; // 当前历史索引
this.maxLength = maxLength; // 最大记录数
this.isNavigating = false; // 标记是否通过快捷键导航
this.init();
}
// 初始化:监听可编辑元素和快捷键
init() {
this.observeEditableElements();
this.listenKeyEvents();
}
// 监听所有可编辑元素(适配含嵌套结构的场景)
observeEditableElements() {
// 选择所有真正可编辑的元素(contenteditable="true")
const editableSelector = '[contenteditable="true"]';
// 监听鼠标点击(记录光标位置)
document.addEventListener('mouseup', (e) => {
const target = e.target.closest(editableSelector);
if (target) {
this.handleCursorChange(target);
}
});
// 监听键盘输入(记录光标移动)
document.addEventListener('keyup', (e) => {
// 排除导航快捷键本身
if (e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) return;
const target = e.target.closest(editableSelector);
if (target) {
this.handleCursorChange(target);
}
});
// 监听焦点离开(记录最终位置)
document.addEventListener('blur', (e) => {
const target = e.target.closest(editableSelector);
if (target) {
this.handleCursorChange(target);
}
}, true); // 捕获阶段监听,确保嵌套元素也能触发
}
// 处理光标位置变化(统一记录逻辑)
handleCursorChange(editableElement) {
// 导航产生的位置不记录
if (this.isNavigating) {
this.isNavigating = false;
return;
}
const selection = window.getSelection();
if (selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
// 验证光标是否在当前可编辑元素内
if (!editableElement.contains(range.commonAncestorContainer)) return;
// 记录详细位置信息(含嵌套节点)
const position = {
parentNodeId: editableElement.closest('[data-node-id]').dataset.nodeId, // 父节点唯一ID
container: range.startContainer, // 光标所在的具体节点(可能是span等)
startOffset: range.startOffset, // 起始偏移量
endOffset: range.endOffset, // 结束偏移量(用于选择范围)
editableElement: editableElement // 可编辑元素本身
};
// 避免连续重复记录
const last = this.history[this.history.length - 1];
if (this.isDuplicate(last, position)) return;
// 截断历史分支(如果不在最新位置)
if (this.currentIndex < this.history.length - 1) {
this.history = this.history.slice(0, this.currentIndex + 1);
}
// 添加新记录并维护长度
this.history.push(position);
this.currentIndex = this.history.length - 1;
if (this.history.length > this.maxLength) {
this.history.shift();
this.currentIndex--;
}
this.updateDebugInfo(); // 可选:更新调试信息
}
// 判断两个位置是否重复
isDuplicate(last, current) {
if (!last) return false;
// 比较父节点ID、容器节点和偏移量
return (last.parentNodeId === current.parentNodeId &&
last.container === current.container &&
last.startOffset === current.startOffset &&
last.endOffset === current.endOffset);
}
// 回退到上一个位置
goBack() {
if (this.currentIndex > 0) {
this.isNavigating = true;
this.currentIndex--;
this.restorePosition();
}
}
// 前进到下一个位置
goForward() {
if (this.currentIndex < this.history.length - 1) {
this.isNavigating = true;
this.currentIndex++;
this.restorePosition();
}
}
// 恢复光标位置(处理嵌套节点)
restorePosition() {
const position = this.history[this.currentIndex];
if (!position) return;
try {
// 聚焦到对应的可编辑元素
position.editableElement.focus();
// 恢复选择范围
const selection = window.getSelection();
const range = document.createRange();
// 处理可能的节点变化(如果原节点已不存在,降级到父元素)
const validContainer = this.isNodeValid(position.container, position.editableElement)
? position.container
: position.editableElement.firstChild || position.editableElement;
range.setStart(validContainer, position.startOffset);
range.setEnd(validContainer, position.endOffset);
selection.removeAllRanges();
selection.addRange(range);
} catch (e) {
console.warn('恢复光标位置失败:', e);
}
this.updateDebugInfo(); // 可选:更新调试信息
}
// 验证节点是否仍有效(避免DOM变动后引用失效)
isNodeValid(container, editableElement) {
return container && editableElement.contains(container);
}
// 监听快捷键(Alt+左/右箭头)
listenKeyEvents() {
document.addEventListener('keydown', (e) => {
if (e.altKey) {
if (e.key === 'ArrowLeft') {
e.preventDefault();
this.goBack();
} else if (e.key === 'ArrowRight') {
e.preventDefault();
this.goForward();
}
}
});
}
// 可选:更新调试信息(可根据需要删除)
updateDebugInfo() {
// 若需要在控制台显示历史记录状态
console.log(`光标历史: ${this.history.length}/${this.maxLength} 条,当前索引: ${this.currentIndex}`);
}
}
// 初始化功能
(() => {
// 可配置最大历史记录数(默认10条)
window.cursorHistory = new CursorHistory(10);
console.log('光标历史功能已启动,使用 Alt+左箭头 回退,Alt+右箭头 前进');
})();

欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于