修改的地方在于
- 对看板kanban生效
- 对目录列表生效
/*
Fixed "No Duplicate Leaves" - Solves Focus Issue
*/
var obsidian = require("obsidian");
class NoDuplicatePlugin extends obsidian.Plugin {
async onload() {
console.log("No Duplicate Leaves (Fixed Focus) loaded");
// 1. 拦截 openLinkText (处理 [[双链]] 点击)
this.register(
around(obsidian.Workspace.prototype, {
openLinkText: (next) => {
return function (linktext, sourcePath, newLeaf, openViewState) {
if (newLeaf) {
return next.call(this, linktext, sourcePath, newLeaf, openViewState);
}
const targetFile = app.metadataCache.getFirstLinkpathDest(
obsidian.getLinkpath(linktext),
sourcePath
);
if (targetFile) {
// 链接点击通常不需要延迟,但为了保险起见,也可以复用同样的逻辑
const leafFound = activateLeafByPath(targetFile.path, linktext);
if (leafFound) return;
}
return next.call(this, linktext, sourcePath, newLeaf, openViewState);
};
},
})
);
// 2. 拦截 openFile (处理 文件列表/资源管理器 点击)
this.register(
around(obsidian.WorkspaceLeaf.prototype, {
openFile: (next) => {
return function (file, openState) {
// 尝试查找并跳转,传入 delay=true
const leafFound = activateLeafByPath(file.path, null, this, true);
if (leafFound) {
// 阻止当前标签页加载该文件,直接返回成功
return Promise.resolve();
}
return next.call(this, file, openState);
};
},
})
);
}
onunload() {
console.log("No Duplicate Leaves unloaded");
}
}
// --- 核心辅助函数 ---
/**
* 遍历所有标签页,寻找是否已打开指定路径
* @param {string} path - 文件路径
* @param {string} linktext - 链接文本
* @param {WorkspaceLeaf} ignoreLeaf - 忽略的标签页(通常是当前正在操作的)
* @param {boolean} delay - 是否延迟跳转(解决文件列表点击的焦点抢占问题)
*/
function activateLeafByPath(path, linktext = null, ignoreLeaf = null, delay = false) {
let foundLeaf = null;
app.workspace.iterateAllLeaves((leaf) => {
if (foundLeaf) return;
if (ignoreLeaf && leaf === ignoreLeaf) return;
const viewState = leaf.getViewState();
// 匹配 markdown 或 kanban
const isMatch =
(viewState.type === "markdown" || viewState.type === "kanban") &&
viewState.state &&
viewState.state.file === path;
if (isMatch) {
foundLeaf = leaf;
}
});
if (foundLeaf) {
// --- 核心修复点 ---
// 如果是点击文件列表触发的 (delay=true),我们需要通过 setTimeout
// 让出主线程,等待 Obsidian 原生点击事件处理完焦点后,再把焦点抢过来。
if (delay) {
setTimeout(() => {
app.workspace.setActiveLeaf(foundLeaf, { focus: true });
}, 10); // 10ms 的延迟通常足以骗过原生逻辑
} else {
// 双链点击通常不需要延迟
app.workspace.setActiveLeaf(foundLeaf, { focus: true });
}
// 处理滚动锚点 (只针对 markdown)
if (linktext) {
const viewState = foundLeaf.getViewState();
if (viewState.type === "markdown") {
// 稍微延迟滚动以确保视图已激活
setTimeout(() => scrollToElement(linktext, foundLeaf), 20);
}
}
return true;
}
return false;
}
/**
* 滚动到锚点
*/
function scrollToElement(linktext, leaf) {
const hashMatch = linktext.match(/(#|\^)(.*)$/);
if (!hashMatch) return;
const view = leaf.view;
if (!view || !view.editor) return;
const cache = app.metadataCache.getFileCache(view.file);
if (!cache) return;
const selector = hashMatch[0];
const type = hashMatch[1];
const name = hashMatch[2];
let line = -1;
if (type === "#") {
const heading = cache.headings?.find(h => h.heading === name);
if (heading) line = heading.position.start.line;
} else if (type === "^") {
const block = cache.blocks?.[name];
if (block) line = block.position.start.line;
}
if (line >= 0) {
view.editor.setCursor({ line: line, ch: 0 });
view.editor.scrollIntoView({ from: { line: line, ch: 0 }, to: { line: line, ch: 0 } }, true);
}
}
// --- Monkey Around 工具 ---
function around(obj, factories) {
const removers = Object.keys(factories).map((key) => {
const prev = obj[key];
const next = factories[key](prev);
const original = prev;
obj[key] = next;
return () => {
if (obj[key] === next) {
obj[key] = original;
}
};
});
return {
detach: () => {
removers.forEach((r) => r());
},
};
}
module.exports = NoDuplicatePlugin;