---
name: 批量分配文本到容器
description: 将输入的文本按换行/逗号/Tab分割后,批量分配到选中的多个图形容器中。
author: ymjr
version: 1.0.0
license: MIT
usage: 选中多个目标容器(排除已有的纯文本元素),运行脚本,在弹出的优雅面板中输入文本。
features:
- 自定义 HTML UI,支持暗色/亮色模式自适应
- 实时校验输入的文本片段数量与选中容器数量是否匹配
dependencies:
- 独立运行
autorun: false
---
/*
```javascript
*/
var locales = {
zh: {
notice_select: "请先选中至少一个非文本的图形容器!",
ui_title: "批量分配文本",
ui_placeholder: "请在此粘贴或输入文本\n支持通过 换行、逗号、Tab 分割...",
ui_status_waiting: "等待输入...",
ui_status_match: "✅ 数量匹配 ({count}个)",
ui_status_mismatch: "❌ 数量不匹配 (片段: {textCount} / 容器: {elCount})",
ui_btn_cancel: "取消",
ui_btn_apply: "分配"
},
en: {
notice_select: "Please select at least one non-text container element!",
ui_title: "Distribute Text",
ui_placeholder: "Paste or type text here\nSplit by newline, comma, or tab...",
ui_status_waiting: "Waiting for input...",
ui_status_match: "✅ Matched ({count})",
ui_status_mismatch: "❌ Mismatch (Text: {textCount} / Elements: {elCount})",
ui_btn_cancel: "Cancel",
ui_btn_apply: "Apply"
}
};
const api = ea.getExcalidrawAPI();
const state = api.getAppState();
const selectedEls = ea.getViewSelectedElements().filter(el => el.type !== "text");
const elCount = selectedEls.length;
if (elCount === 0) {
new Notice(t("notice_select"));
return;
}
// 清理可能遗留的旧 UI 实例,防止重复创建
if (ExcalidrawAutomate.plugin._ymjr_distribute_ui) {
ExcalidrawAutomate.plugin._ymjr_distribute_ui.remove();
ExcalidrawAutomate.plugin._ymjr_distribute_ui = null;
}
// ------------------------------------------
// 1. 构建现代化 UI (HTML/CSS)
// ------------------------------------------
const overlay = document.createElement("div");
overlay.id = "ymjr-distribute-overlay";
// 判断当前 Obsidian/Excalidraw 是否为暗黑模式
const isDark = document.body.classList.contains("theme-dark");
overlay.style.cssText = `
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(4px);
display: flex; justify-content: center; align-items: center;
z-index: 99999; font-family: var(--font-interface);
`;
const modal = document.createElement("div");
modal.style.cssText = `
width: 400px; padding: 20px; border-radius: 12px;
background: ${isDark ? '#242424' : '#ffffff'};
color: ${isDark ? '#e3e3e3' : '#333333'};
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
display: flex; flex-direction: column; gap: 15px;
border: 1px solid ${isDark ? '#333' : '#e0e0e0'};
`;
modal.innerHTML = `
${t("ui_title")}
${t("ui_status_waiting")}
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
ExcalidrawAutomate.plugin._ymjr_distribute_ui = overlay;
// ------------------------------------------
// 2. 交互与逻辑绑定
// ------------------------------------------
const inputEl = overlay.querySelector("#ymjr-text-input");
const statusEl = overlay.querySelector("#ymjr-status-text");
const btnCancel = overlay.querySelector("#ymjr-btn-cancel");
const btnApply = overlay.querySelector("#ymjr-btn-apply");
let parsedData = [];
// 实时监听输入,校验分段数量
inputEl.addEventListener("input", (e) => {
const rawText = e.target.value.trim();
if (!rawText) {
parsedData = [];
statusEl.textContent = t("ui_status_waiting");
statusEl.style.color = isDark ? '#aaa' : '#666';
btnApply.disabled = true;
btnApply.style.opacity = '0.5';
btnApply.style.cursor = 'not-allowed';
return;
}
parsedData = rawText.split(/,|\t|\n/).filter(element => element && element.trim() !== '');
const textCount = parsedData.length;
if (textCount === elCount) {
statusEl.textContent = t("ui_status_match").replace("{count}", elCount);
statusEl.style.color = "var(--text-success)";
btnApply.disabled = false;
btnApply.style.opacity = '1';
btnApply.style.cursor = 'pointer';
} else {
statusEl.textContent = t("ui_status_mismatch").replace("{textCount}", textCount).replace("{elCount}", elCount);
statusEl.style.color = "var(--text-error)";
btnApply.disabled = true;
btnApply.style.opacity = '0.5';
btnApply.style.cursor = 'not-allowed';
}
});
// 关闭与销毁逻辑
const closeUI = () => {
if (ExcalidrawAutomate.plugin._ymjr_distribute_ui) {
ExcalidrawAutomate.plugin._ymjr_distribute_ui.remove();
ExcalidrawAutomate.plugin._ymjr_distribute_ui = null;
}
};
btnCancel.addEventListener("click", closeUI);
overlay.addEventListener("click", (e) => {
if (e.target === overlay) closeUI(); // 点击遮罩层关闭
});
// 执行分配逻辑
btnApply.addEventListener("click", async () => {
if (parsedData.length !== elCount) return;
ea.copyViewElementsToEAforEditing(selectedEls);
for (let i = 0; i < elCount; i++) {
ea.style.strokeColor = state.currentItemStrokeColor;
ea.style.fontSize = state.currentItemFontSize;
ea.style.fontFamily = state.currentItemFontFamily;
const textId = ea.addText(0, 0, `${parsedData[i].trim()}`, {
textAlign: "center",
textVerticalAlign: "middle"
});
const textEl = ea.getElement(textId);
const containerEl = ea.getElement(selectedEls[i].id);
// 绑定文本到容器并计算绝对中心点
textEl.containerId = containerEl.id;
textEl.x = containerEl.x + containerEl.width / 2 - textEl.width / 2;
textEl.y = containerEl.y + containerEl.height / 2 - textEl.height / 2;
containerEl.boundElements = [...(containerEl.boundElements || []), { type: "text", id: textId }];
}
await ea.addElementsToView(false, false);
closeUI();
});
// 自动聚焦
setTimeout(() => inputEl.focus(), 100);