脚本主文件 Excalidraw Outline.md 的代码:
/*
Excalidraw Outline 脚本
功能: 大纲管理与可视化
```javascript
*/
async function runOutlineScript() {
const ea = window.ea || ExcalidrawAutomate;
if (!window.ea) window.ea = ea;
ea.reset();
if (!ea.targetView) {
ea.setView("active");
}
const currentFilePath = ea.targetView?.file?.path;
if (!currentFilePath) {
new Notice("无法获取当前文件路径", 2000);
return;
}
const loadModule = async (filePath) => {
try {
const fileContent = await app.vault.adapter.read(filePath);
let jsCode = null;
const codeBlockRegex = /```(?:javascript|js)([\s\S]*?)```/;
const match = fileContent.match(codeBlockRegex);
if (match && match[1]) {
jsCode = match[1].trim();
} else {
const altRegex = /```(?:javascript|js)\n([\s\S]*?)\n```/;
const altMatch = fileContent.match(altRegex);
if (altMatch && altMatch[1]) {
jsCode = altMatch[1].trim();
}
}
if (!jsCode) {
if (fileContent.includes("function") || fileContent.includes("const") || fileContent.includes("return")) {
jsCode = fileContent;
} else {
throw new Error(`Module ${filePath} does not contain valid JavaScript code`);
}
}
const sandbox = {};
const moduleExports = {};
const functionWrapper = new Function(
'exports',
`
try {
${jsCode}
return exports;
} catch (e) {
console.error("Module execution error:", e);
throw e;
}
`
);
return functionWrapper.call(sandbox, moduleExports);
} catch (e) {
console.error(`Error loading module ${filePath}:`, e);
throw new Error(`Failed to load module: ${e.message}`);
}
};
const OutlineDataManager = {
getOutlineData: () => {
const settings = ea.getScriptSettings();
if (!settings.fileOutlines) {
settings.fileOutlines = {};
}
if (!settings.fileOutlines[currentFilePath]) {
settings.fileOutlines[currentFilePath] = { elements: [] };
}
return settings.fileOutlines[currentFilePath];
},
saveOutlineData: (data) => {
const settings = ea.getScriptSettings();
settings.fileOutlines = settings.fileOutlines || {};
settings.fileOutlines[currentFilePath] = data;
ea.setScriptSettings(settings);
},
addOrUpdateElement: (elementId, level, text, position) => {
const data = OutlineDataManager.getOutlineData();
const existingIndex = data.elements.findIndex(el => el.id === elementId);
if (existingIndex >= 0) {
data.elements[existingIndex] = {
...data.elements[existingIndex],
level,
text,
position
};
} else {
data.elements.push({
id: elementId,
text,
level,
position
});
}
OutlineDataManager.saveOutlineData(data);
return data;
},
removeElement: (elementId) => {
const data = OutlineDataManager.getOutlineData();
data.elements = data.elements.filter(el => el.id !== elementId);
OutlineDataManager.saveOutlineData(data);
return data;
},
reorderElements: (newOrder) => {
const data = OutlineDataManager.getOutlineData();
const elementMap = new Map();
data.elements.forEach(el => elementMap.set(el.id, el));
const orderedElements = [];
for (const id of newOrder) {
if (elementMap.has(id)) {
orderedElements.push(elementMap.get(id));
elementMap.delete(id);
}
}
for (const [id, el] of elementMap) {
orderedElements.push(el);
}
data.elements = orderedElements;
OutlineDataManager.saveOutlineData(data);
return data;
},
refreshElementPositions: () => {
const data = OutlineDataManager.getOutlineData();
const allElements = ea.getViewElements();
data.elements.forEach(element => {
const actualElement = allElements.find(el => el.id === element.id);
if (actualElement) {
element.position = {
x: actualElement.x,
y: actualElement.y
};
}
});
OutlineDataManager.saveOutlineData(data);
return data;
},
refreshOutlineData: () => {
const data = OutlineDataManager.getOutlineData();
const allElements = ea.getViewElements();
const updatedElements = [];
let removedCount = 0;
let updatedCount = 0;
data.elements.forEach(element => {
// 查找对应的Excalidraw元素
const actualElement = allElements.find(el => el.id === element.id);
if (!actualElement) {
removedCount++;
return;
}
if (actualElement.type !== "text") {
removedCount++;
return;
}
if (actualElement.text !== element.text) {
element.text = actualElement.text;
updatedCount++;
}
updatedElements.push(element);
});
data.elements = updatedElements;
OutlineDataManager.saveOutlineData(data);
return { removedCount, updatedCount };
}
};
let outlinePanel = null;
let activeLeafChangeListener = null;
function getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll(".outline-item:not(.dragging)")];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
const OutlinePanel = {
exists: () => {
return document.getElementById("ex-outline-panel") !== null;
},
createContent: (elements) => {
// 创建内容容器
const content = document.createElement("div");
content.style.padding = "8px 0";
content.style.flexGrow = "1";
content.style.overflowY = "auto"; // 启用滚动条
content.style.maxHeight = "calc(100% - 8px)"; // 为手柄留出空间
content.style.scrollbarWidth = "thin"; // 细滚动条
content.style.scrollbarColor = "var(--text-muted) var(--background-primary)"; // 滚动条颜色
content.id = "outline-content-container";
// 定义分级颜色(1-6级)
const levelColors = [
"#FF6B6B", // 1级 - 红色
"#4ECDC4", // 2级 - 青绿色
"#FFD166", // 3级 - 黄色
"#06D6A0", // 4级 - 绿色
"#118AB2", // 5级 - 蓝色
"#9B5DE5" // 6级 - 紫色
];
// 添加大纲项目
elements.forEach((element) => {
const item = document.createElement("div");
item.className = "outline-item";
item.dataset.elementId = element.id;
item.style.padding = "8px 16px";
item.style.margin = "4px 0";
item.style.cursor = "pointer";
item.style.borderRadius = "4px";
item.style.transition = "all 0.2s ease";
item.style.display = "flex";
item.style.alignItems = "center";
item.style.position = "relative";
// 添加嵌套竖线(高级别包含低级别)
for (let l = 1; l <= element.level; l++) {
const verticalLine = document.createElement("div");
verticalLine.style.position = "absolute";
verticalLine.style.left = `${(l - 1) * 12 + 24}px`;
verticalLine.style.width = "1.5px";
verticalLine.style.height = "100%";
verticalLine.style.top = "0";
verticalLine.style.backgroundColor = levelColors[l - 1] || "#CCCCCC";
verticalLine.style.borderRadius = "2px";
verticalLine.style.zIndex = "1";
item.appendChild(verticalLine);
}
// 添加缩进
const indent = document.createElement("div");
indent.style.width = `${element.level * 12}px`;
indent.style.minWidth = `${element.level * 12}px`;
indent.style.position = "relative";
indent.style.zIndex = "2";
item.appendChild(indent);
// 添加文本容器
const textContainer = document.createElement("div");
textContainer.style.display = "flex";
textContainer.style.alignItems = "center";
textContainer.style.flexGrow = "1";
textContainer.style.zIndex = "2";
// 添加文本
const text = document.createElement("div");
text.textContent = element.text;
text.style.whiteSpace = "nowrap";
text.style.overflow = "hidden";
text.style.textOverflow = "ellipsis";
text.style.flexGrow = "1";
text.style.fontSize = "16px";
textContainer.appendChild(text);
// 添加层级标记
const levelBadge = document.createElement("div");
levelBadge.textContent = `L${element.level}`;
levelBadge.style.fontSize = "12px";
levelBadge.style.color = "var(--text-muted)";
levelBadge.style.marginLeft = "8px";
levelBadge.style.padding = "2px 6px";
levelBadge.style.backgroundColor = "var(--background-secondary)";
levelBadge.style.borderRadius = "10px";
textContainer.appendChild(levelBadge);
item.appendChild(textContainer);
// 悬停效果
item.addEventListener("mouseenter", () => {
item.style.backgroundColor = "var(--background-modifier-hover)";
});
item.addEventListener("mouseleave", () => {
item.style.backgroundColor = "";
});
// 点击聚焦元素
item.addEventListener("click", () => {
try {
// 确保设置到活动视图
ea.setView("active");
// 获取所有视图元素
const allElements = ea.getViewElements();
// 查找对应的文本元素
const targetElement = allElements.find(el => el.id === element.id);
if (targetElement) {
// 选中元素
ea.selectElementsInView([targetElement]);
// 聚焦元素
ea.viewZoomToElements(true, [targetElement]);
} else {
new Notice("未找到对应的文本元素", 2000);
}
} catch (e) {
console.error("聚焦元素时出错:", e);
new Notice("聚焦元素时出错,请重试", 2000);
}
});
// 拖拽排序功能
item.draggable = true;
item.addEventListener("dragstart", (e) => {
e.dataTransfer.setData("text/plain", element.id);
item.style.opacity = "0.5";
item.style.backgroundColor = "var(--interactive-accent)";
item.classList.add("dragging");
});
item.addEventListener("dragend", () => {
item.style.opacity = "1";
item.style.backgroundColor = "";
item.classList.remove("dragging");
});
content.appendChild(item);
});
// 设置拖拽放置区域
content.addEventListener("dragover", (e) => {
e.preventDefault();
const draggedItem = content.querySelector(".outline-item.dragging");
if (!draggedItem) return;
const afterElement = getDragAfterElement(content, e.clientY);
// 移动被拖拽的元素到新位置
if (afterElement) {
content.insertBefore(draggedItem, afterElement);
} else {
content.appendChild(draggedItem);
}
});
content.addEventListener("drop", (e) => {
e.preventDefault();
const elementId = e.dataTransfer.getData("text/plain");
// 获取当前面板中的所有项目(按新的DOM顺序)
const items = content.querySelectorAll(".outline-item");
const newOrder = Array.from(items).map(item => item.dataset.elementId);
try {
// 更新数据顺序(确保使用新顺序)
OutlineDataManager.reorderElements(newOrder);
// 刷新元素位置(确保位置信息最新)
OutlineDataManager.refreshElementPositions();
// 显示成功提示
new Notice("大纲顺序已更新", 1500);
} catch (error) {
console.error("排序更新失败:", error);
new Notice("大纲顺序更新失败", 2000);
}
});
return content;
},
// 创建面板(带高度调整功能)
create: (elements) => {
// 如果面板已存在,只更新内容
if (OutlinePanel.exists()) {
OutlinePanel.update();
return;
}
// 创建新面板
outlinePanel = document.createElement("div");
outlinePanel.id = "ex-outline-panel";
outlinePanel.style.position = "fixed";
outlinePanel.style.zIndex = "10000";
outlinePanel.style.backgroundColor = "var(--background-primary)";
outlinePanel.style.border = "1px solid var(--background-modifier-border)";
outlinePanel.style.borderRadius = "8px";
outlinePanel.style.boxShadow = "0 4px 20px rgba(0,0,0,0.3)";
outlinePanel.style.minWidth = "280px";
outlinePanel.style.maxWidth = "400px";
outlinePanel.style.display = "flex";
outlinePanel.style.flexDirection = "column";
// 从本地存储加载位置和高度
const savedPosition = JSON.parse(localStorage.getItem("outline-panel-position")) || {
x: window.innerWidth - 320,
y: 100,
height: 400 // 默认高度
};
outlinePanel.style.left = `${savedPosition.x}px`;
outlinePanel.style.top = `${savedPosition.y}px`;
outlinePanel.style.height = `${savedPosition.height}px`;
// 添加标题栏
const header = document.createElement("div");
header.textContent = "Excalidraw大纲";
header.style.padding = "12px 16px";
header.style.fontWeight = "600";
header.style.fontSize = "18px";
header.style.borderBottom = "1px solid var(--background-modifier-border)";
header.style.cursor = "move";
header.style.userSelect = "none";
header.style.display = "flex";
header.style.justifyContent = "space-between";
header.style.alignItems = "center";
// 添加按钮容器
const buttonContainer = document.createElement("div");
buttonContainer.style.display = "flex";
buttonContainer.style.gap = "8px";
// 添加更新按钮
const refreshButton = document.createElement("div");
refreshButton.innerHTML = "🔄";
refreshButton.title = "更新大纲";
refreshButton.style.cursor = "pointer";
refreshButton.style.fontSize = "18px";
refreshButton.style.width = "24px";
refreshButton.style.height = "24px";
refreshButton.style.display = "flex";
refreshButton.style.justifyContent = "center";
refreshButton.style.alignItems = "center";
refreshButton.style.borderRadius = "50%";
refreshButton.style.color = "var(--text-muted)";
refreshButton.style.transition = "all 0.2s ease";
refreshButton.addEventListener("mouseenter", () => {
refreshButton.style.backgroundColor = "var(--background-modifier-hover)";
refreshButton.style.color = "var(--text-normal)";
});
refreshButton.addEventListener("mouseleave", () => {
refreshButton.style.backgroundColor = "transparent";
refreshButton.style.color = "var(--text-muted)";
});
refreshButton.addEventListener("click", () => {
// 更新大纲数据
const result = OutlineDataManager.refreshOutlineData();
// 更新面板内容
OutlinePanel.update();
// 显示更新结果
if (result.removedCount > 0 || result.updatedCount > 0) {
new Notice(`已更新: ${result.updatedCount}项, 已移除: ${result.removedCount}项`, 2000);
} else {
new Notice("大纲数据已是最新", 2000);
}
});
// 添加关闭按钮
const closeButton = document.createElement("div");
closeButton.innerHTML = "×";
closeButton.title = "关闭面板";
closeButton.style.cursor = "pointer";
closeButton.style.fontSize = "20px";
closeButton.style.width = "24px";
closeButton.style.height = "24px";
closeButton.style.display = "flex";
closeButton.style.justifyContent = "center";
closeButton.style.alignItems = "center";
closeButton.style.borderRadius = "50%";
closeButton.style.color = "var(--text-muted)";
closeButton.style.transition = "all 0.2s ease";
closeButton.addEventListener("mouseenter", () => {
closeButton.style.backgroundColor = "var(--background-modifier-hover)";
closeButton.style.color = "var(--text-normal)";
});
closeButton.addEventListener("mouseleave", () => {
closeButton.style.backgroundColor = "transparent";
closeButton.style.color = "var(--text-muted)";
});
closeButton.addEventListener("click", () => {
OutlinePanel.hide();
});
// 将按钮添加到容器
buttonContainer.appendChild(refreshButton);
buttonContainer.appendChild(closeButton);
// 将按钮容器添加到标题栏
header.appendChild(buttonContainer);
outlinePanel.appendChild(header);
// 添加内容容器(使用当前存储的顺序)
const content = OutlinePanel.createContent(elements);
outlinePanel.appendChild(content);
// 添加高度调整手柄
const resizeHandle = document.createElement("div");
resizeHandle.style.position = "absolute";
resizeHandle.style.bottom = "0";
resizeHandle.style.left = "0";
resizeHandle.style.right = "0";
resizeHandle.style.height = "8px";
resizeHandle.style.cursor = "ns-resize";
resizeHandle.style.backgroundColor = "transparent";
resizeHandle.style.borderTop = "1px solid var(--background-modifier-border)";
resizeHandle.style.zIndex = "100";
// 添加手柄图标
const handleIcon = document.createElement("div");
handleIcon.style.position = "absolute";
handleIcon.style.top = "50%";
handleIcon.style.left = "50%";
handleIcon.style.transform = "translate(-50%, -50%)";
handleIcon.style.width = "32px";
handleIcon.style.height = "3px";
handleIcon.style.backgroundColor = "var(--text-muted)";
handleIcon.style.borderRadius = "2px";
resizeHandle.appendChild(handleIcon);
outlinePanel.appendChild(resizeHandle);
document.body.appendChild(outlinePanel);
// 高度调整功能
let isResizing = false;
let startY, startHeight;
resizeHandle.addEventListener("mousedown", (e) => {
isResizing = true;
startY = e.clientY;
startHeight = parseInt(document.defaultView.getComputedStyle(outlinePanel).height, 10);
outlinePanel.style.userSelect = "none";
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});
function onMouseMove(e) {
if (!isResizing) return;
const newHeight = startHeight + (e.clientY - startY);
const minHeight = 200; // 最小高度
const maxHeight = window.innerHeight - 100; // 最大高度
outlinePanel.style.height = `${Math.max(minHeight, Math.min(maxHeight, newHeight))}px`;
}
function onMouseUp() {
isResizing = false;
outlinePanel.style.userSelect = "";
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
// 保存高度
const savedPosition = JSON.parse(localStorage.getItem("outline-panel-position")) || {};
savedPosition.height = parseInt(outlinePanel.style.height);
localStorage.setItem("outline-panel-position", JSON.stringify(savedPosition));
}
// 拖拽移动功能
let isDragging = false;
let offsetX, offsetY;
header.addEventListener("mousedown", (e) => {
// 只有当点击在按钮之外时才触发拖拽
if (!refreshButton.contains(e.target) && !closeButton.contains(e.target)) {
isDragging = true;
offsetX = e.clientX - outlinePanel.getBoundingClientRect().left;
offsetY = e.clientY - outlinePanel.getBoundingClientRect().top;
outlinePanel.style.opacity = "0.9";
}
});
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
const newX = e.clientX - offsetX;
const newY = e.clientY - offsetY;
// 限制在视窗范围内
const maxX = window.innerWidth - outlinePanel.offsetWidth;
const maxY = window.innerHeight - outlinePanel.offsetHeight;
outlinePanel.style.left = `${Math.max(0, Math.min(newX, maxX))}px`;
outlinePanel.style.top = `${Math.max(0, Math.min(newY, maxY))}px`;
});
document.addEventListener("mouseup", () => {
if (isDragging) {
isDragging = false;
outlinePanel.style.opacity = "1";
// 保存位置
const savedPosition = JSON.parse(localStorage.getItem("outline-panel-position")) || {};
savedPosition.x = parseInt(outlinePanel.style.left);
savedPosition.y = parseInt(outlinePanel.style.top);
localStorage.setItem("outline-panel-position", JSON.stringify(savedPosition));
}
});
// 添加活动文件切换监听器
if (!activeLeafChangeListener) {
activeLeafChangeListener = () => {
OutlinePanel.hide();
};
app.workspace.on('active-leaf-change', activeLeafChangeListener);
}
return outlinePanel;
},
// 更新面板
update: () => {
const panel = document.getElementById("ex-outline-panel");
if (!panel) return;
// 刷新元素位置(确保位置信息最新)
OutlineDataManager.refreshElementPositions();
const data = OutlineDataManager.getOutlineData();
// 移除旧内容
const contentContainer = panel.querySelector("#outline-content-container");
if (contentContainer) {
contentContainer.remove();
}
// 添加新内容(使用当前存储的顺序)
const newContent = OutlinePanel.createContent(data.elements);
panel.appendChild(newContent);
// 应用保存的高度
const savedPosition = JSON.parse(localStorage.getItem("outline-panel-position")) || {};
if (savedPosition.height) {
panel.style.height = `${savedPosition.height}px`;
}
},
// 隐藏面板
hide: () => {
const panel = document.getElementById("ex-outline-panel");
if (panel) {
panel.remove();
}
outlinePanel = null;
// 移除活动文件切换监听器
if (activeLeafChangeListener) {
app.workspace.off('active-leaf-change', activeLeafChangeListener);
activeLeafChangeListener = null;
}
},
// 切换面板显示状态
toggle: () => {
if (OutlinePanel.exists()) {
// 面板已存在,更新内容
OutlinePanel.update();
} else {
// 刷新元素位置(确保位置信息最新)
OutlineDataManager.refreshElementPositions();
const data = OutlineDataManager.getOutlineData();
if (data.elements.length > 0) {
OutlinePanel.create(data.elements);
} else {
new Notice("没有可显示的大纲数据", 2000);
}
}
}
};
// 主功能函数
async function addOutlineLevel() {
try {
const DialogUtils = await loadModule("Excalidraw/Module/DialogUtils.md");
const selectedElement = ea.getViewSelectedElement();
if (!selectedElement || selectedElement.type !== "text") {
new Notice("请先选中一个文本元素", 2000);
return;
}
const choice = await DialogUtils.createCustomDialog(
"Outline功能选择",
"请选择要执行的操作:",
["取消", "添加大纲层级", "Excalidraw大纲"]
);
if (choice === 1) { // 添加大纲层级
const level = await DialogUtils.createMiniInputDialog(
"输入标题级别",
"请输入标题的级别(1-6):",
"1"
);
if (level && /^[1-6]$/.test(level)) {
OutlineDataManager.addOrUpdateElement(
selectedElement.id,
parseInt(level),
selectedElement.text,
{ x: selectedElement.x, y: selectedElement.y }
);
new Notice(`已设置大纲层级: L${level}`, 2000);
// 如果面板已打开,更新内容
if (OutlinePanel.exists()) {
OutlinePanel.update();
}
} else {
new Notice("请输入1-6之间的数字", 2000);
}
} else if (choice === 2) { // Excalidraw大纲
OutlinePanel.toggle();
}
} catch (e) {
console.error("Outline脚本错误:", e);
new Notice(`Outline脚本错误: ${e.message}`, 4000);
}
}
async function excalidrawOutline() {
try {
const DialogUtils = await loadModule("Excalidraw/Module/DialogUtils.md");
const choice = await DialogUtils.createCustomDialog(
"Outline功能选择",
"请选择要执行的操作:",
["取消", "添加大纲层级", "Excalidraw大纲"]
);
if (choice === 1) { // 添加大纲层级
const selectedElement = ea.getViewSelectedElement();
if (selectedElement && selectedElement.type === "text") {
const level = await DialogUtils.createMiniInputDialog(
"输入标题级别",
"请输入标题的级别(1-6):",
"1"
);
if (level && /^[1-6]$/.test(level)) {
OutlineDataManager.addOrUpdateElement(
selectedElement.id,
parseInt(level),
selectedElement.text,
{ x: selectedElement.x, y: selectedElement.y }
);
new Notice(`已设置大纲层级: L${level}`, 2000);
// 如果面板已打开,更新内容
if (OutlinePanel.exists()) {
OutlinePanel.update();
}
} else {
new Notice("请输入1-6之间的数字", 2000);
}
} else {
new Notice("请先选中一个文本元素", 2000);
}
} else if (choice === 2) { // Excalidraw大纲
OutlinePanel.toggle();
}
} catch (e) {
console.error("Outline脚本错误:", e);
new Notice(`Outline脚本错误: ${e.message}`, 4000);
}
}
// 根据当前选择自动执行适当的功能
if (!ea.activeScript) {
ea.activeScript = "Outline";
}
const selection = ea.getViewSelectedElements();
if (selection.length === 1 && selection[0].type === "text") {
await addOutlineLevel();
} else {
await excalidrawOutline();
}
}
// 执行主函数
runOutlineScript();
// 导出功能
return {
runOutlineScript
};