---
name: SmartArt 实时生成器
description: 悬浮面板:选中模板容器或文本,输入缩进大纲,画布元素将【实时发生形变与替换】,所见即所得。
author: ymjr
version: 1.0.0
license: MIT
usage: 选中画板上带有层级编号(如 1.1, 1.2)的文本节点,或包裹这些文本的图形容器。运行脚本将唤起“SmartArt 构建”浮动面板。在文本框内通过回车缩进键入大纲层级文本,画布中的图形和文字将根据大纲实时动态更新。
features:
- 独立的纯净单栏 React 级重绘浮动面板
- 支持穿透容器提取文字并绑定 Node ID
- 随着输入内容实时刷新 `refreshTextDimensions` 以确保容器根据新文字内容自动撑开宽高
dependencies:
- 无前置依赖
autorun: false
---
/*
```javascript
*/
var locales = {
zh: {
notice_select: "⚠️ 请先选中包含层级编号 (如 1.1, 1.2) 的文本,或包裹着它们的图形容器!",
notice_cancelled: "↩️ 已撤销 SmartArt 更改",
notice_done: "✅ SmartArt 渲染完成!",
ui_title: "✨ SmartArt 实时构建",
ui_tip: "📝 在此输入大纲,画布将实时更新",
ui_placeholder: "一级主题\n 子节点 A\n 子节点 B\n 孙节点 B1\n另一个主题",
ui_undo: "撤销",
ui_apply: "应用并关闭",
node_missing: "[{id}] 空缺"
},
en: {
notice_select: "⚠️ Please select text with hierarchy numbers (e.g., 1.1, 1.2) or their containers first!",
notice_cancelled: "↩️ SmartArt changes reverted",
notice_done: "✅ SmartArt rendering complete!",
ui_title: "✨ SmartArt Live Builder",
ui_tip: "📝 Enter outline here, canvas updates in real-time",
ui_placeholder: "Main Topic\n Child A\n Child B\n Grandchild B1\nAnother Topic",
ui_undo: "Undo",
ui_apply: "Apply & Close",
node_missing: "[{id}] Missing"
}
};
const { Notice } = ea.obsidian;
const api = ea.getExcalidrawAPI();
const selectedEls = ea.getViewSelectedElements();
const elementsMap = api.App.scene.nonDeletedElementsMap;
// ==========================================
// 1. 穿透获取目标文本元素 (支持容器与独立文本)
// ==========================================
const targetEls = [];
let hasNewNodeIds = false;
selectedEls.forEach(el => {
let textEls = [];
// 情况A:直接选中了文本
if (el.type === "text") textEls.push(el);
// 情况B:选中了带文本的容器 (如圆角矩形、菱形等)
if (el.boundElements) {
el.boundElements.forEach(b => {
if (b.type === "text") {
const boundText = elementsMap.get(b.id);
if (boundText) textEls.push(boundText);
}
});
}
// 提取并固化 Node ID
textEls.forEach(textEl => {
let nodeId = textEl.customData?.smartartNodeId;
// 如果没有绑定过,且文本长得像层级编号 (1, 1.1, 1.2.1)
if (!nodeId && /^(\d+\.)*\d+$/.test(textEl.text.trim())) {
nodeId = textEl.text.trim();
hasNewNodeIds = true;
}
if (nodeId) {
// 防重录入
if (!targetEls.find(t => t.id === textEl.id)) {
targetEls.push({
id: textEl.id,
nodeId: nodeId,
originalText: textEl.originalText || textEl.text // 记录初始状态,用于取消时恢复
});
}
}
});
});
if (targetEls.length === 0) {
new Notice(t("notice_select"), 4000);
return;
}
// 首次固化 ID 到画布 (触发一次更新以保存 customData)
if (hasNewNodeIds) {
const initElements = api.getSceneElements().map(el => {
const target = targetEls.find(t => t.id === el.id);
if (target && !el.customData?.smartartNodeId) {
return {
...el,
customData: { ...(el.customData || {}), smartartNodeId: target.nodeId },
version: el.version + 1
};
}
return el;
});
api.updateScene({ elements: initElements });
}
// 全局缓存命名空间
if (!ExcalidrawAutomate.plugin._ymjr_smartart) ExcalidrawAutomate.plugin._ymjr_smartart = {};
const ctx = ExcalidrawAutomate.plugin._ymjr_smartart;
// ==========================================
// 2. 核心大纲解析器
// ==========================================
function parseIndentedText(rawText) {
const lines = rawText.split('\n');
const map = {};
let currentLevels = [];
lines.forEach(line => {
if (line.trim() === '') return;
const match = line.match(/^(\s*)/);
const indent = match ? match[1].replace(/\t/g, ' ').length : 0;
const cleanText = line.replace(/^\s*([-*+]|\d+\.)\s+/, '').trim();
let levelIndex = 0;
while (levelIndex < currentLevels.length && currentLevels[levelIndex].indent < indent) {
levelIndex++;
}
if (levelIndex === currentLevels.length) {
currentLevels.push({ indent, count: 1 });
} else {
currentLevels = currentLevels.slice(0, levelIndex + 1);
currentLevels[levelIndex].indent = indent;
currentLevels[levelIndex].count++;
}
const numbering = currentLevels.map(l => l.count).join('.');
map[numbering] = cleanText;
});
return map;
}
// ==========================================
// 3. 画布实时连动逻辑
// ==========================================
const applyToCanvas = (currentMap, restoreOriginal = false) => {
let updatedCount = 0;
const elements = api.getSceneElements().map(el => {
const target = targetEls.find(t => t.id === el.id);
if (target) {
// 确定目标文本
let finalText = target.originalText;
if (!restoreOriginal) {
// 如果用户输入了内容但未匹配到当前节点,给一个视觉缺省提示
const isMapEmpty = Object.keys(currentMap).length === 0;
finalText = isMapEmpty ? target.originalText : (currentMap[target.nodeId] || t("node_missing", { id: target.nodeId }));
}
if (el.text !== finalText) {
let updatedEl = { ...el, text: finalText, originalText: finalText, version: el.version + 1 };
// 实时重算物理宽高,确保与图形容器的排版完美契合
try {
if (typeof ExcalidrawLib !== 'undefined') {
const dims = ExcalidrawLib.refreshTextDimensions(updatedEl, null, elementsMap, finalText);
updatedEl.width = dims.width;
updatedEl.height = dims.height;
updatedEl.baseline = dims.baseline;
}
} catch(e) {}
updatedCount++;
return updatedEl;
}
}
return el;
});
if (updatedCount > 0) {
api.updateScene({ elements });
}
};
// ==========================================
// 4. 纯净单栏 UI 构建 (悬浮面板)
// ==========================================
function createLiveSmartArtPanel() {
const existingPanel = document.getElementById("ea-smartart-live-panel");
if (existingPanel) existingPanel.remove();
const panel = document.createElement('div');
panel.id = "ea-smartart-live-panel";
panel.style.cssText = `position:fixed; top:15%; left:10%; width:380px; background:var(--background-primary); border:1px solid var(--background-modifier-border); box-shadow:0 10px 30px rgba(0,0,0,0.3); border-radius:8px; z-index:9999; display:flex; flex-direction:column;`;
// 头部拖拽区
const header = document.createElement('div');
header.style.cssText = `padding:12px 15px; background:var(--background-secondary); cursor:move; border-bottom:1px solid var(--background-modifier-border); display:flex; justify-content:space-between; align-items:center; user-select:none; border-radius:8px 8px 0 0;`;
header.innerHTML = `${t("ui_title")}`;
panel.appendChild(header);
// 内容区:只需一个文本框
const content = document.createElement('div');
content.style.cssText = `padding:15px; display:flex; flex-direction:column; gap:8px; height: 350px;`;
content.innerHTML = ``;
const textarea = document.createElement('textarea');
textarea.placeholder = t("ui_placeholder");
textarea.style.cssText = "flex: 1; resize: none; padding: 12px; border-radius: 6px; border: 1px solid var(--background-modifier-border); background: var(--background-secondary-alt); color: var(--text-normal); font-family: var(--font-monospace); font-size: 13px; white-space: pre; outline: none;";
textarea.value = ctx.lastInputText || "";
content.appendChild(textarea);
panel.appendChild(content);
// 底部操作区
const footer = document.createElement('div');
footer.style.cssText = `padding:12px 15px; border-top:1px solid var(--background-modifier-border); display:flex; justify-content:flex-end; gap:10px; background:var(--background-secondary); border-radius: 0 0 8px 8px;`;
const btnCancel = document.createElement('button');
btnCancel.innerText = t("ui_undo");
btnCancel.style.cssText = "padding: 6px 16px; cursor:pointer;";
const btnDone = document.createElement('button');
btnDone.innerText = t("ui_apply");
btnDone.className = "mod-cta";
btnDone.style.cssText = "padding: 6px 16px; cursor:pointer; font-weight:bold;";
footer.appendChild(btnCancel);
footer.appendChild(btnDone);
panel.appendChild(footer);
document.body.appendChild(panel);
// ==========================================
// 5. 交互事件挂载
// ==========================================
// 监听打字,实时刷新画板
textarea.oninput = () => {
ctx.lastInputText = textarea.value;
const currentMap = parseIndentedText(textarea.value);
applyToCanvas(currentMap, false);
};
// 关闭/撤销逻辑:恢复原状
const handleCancel = () => {
applyToCanvas({}, true); // 强制恢复初始文本
panel.remove();
new Notice(t("notice_cancelled"));
};
header.querySelector('#close-btn').onclick = handleCancel;
btnCancel.onclick = handleCancel;
// 完成逻辑:保存并关闭
btnDone.onclick = () => {
panel.remove();
// 触发一次视图重绘以确保底层彻底落盘
api.updateScene({ appState: { ...api.getAppState() } });
new Notice(t("notice_done"));
};
// 拖拽逻辑防跳跃
let isDragging = false, startX, startY, initLeft, initTop;
header.onmousedown = (e) => {
if(e.target.tagName === 'BUTTON') return;
isDragging = true;
startX = e.clientX; startY = e.clientY;
const rect = panel.getBoundingClientRect();
panel.style.transform = 'none';
panel.style.left = rect.left + 'px';
panel.style.top = rect.top + 'px';
initLeft = rect.left; initTop = rect.top;
e.preventDefault();
document.onmousemove = (e) => {
if(!isDragging) return;
panel.style.left = (initLeft + e.clientX - startX) + 'px';
panel.style.top = (initTop + e.clientY - startY) + 'px';
};
document.onmouseup = () => {
isDragging = false;
document.onmousemove = null; document.onmouseup = null;
};
};
// 如果一开始就有缓存文字,立刻执行一次实时渲染
if (textarea.value) {
applyToCanvas(parseIndentedText(textarea.value), false);
}
setTimeout(() => { textarea.focus(); }, 100);
}
// 唤起实时发生器
createLiveSmartArtPanel();