Obsidian原生对话框很大、且不能拖动,所以写了两个自定义对话框,并导出为模块,可被其他脚本加载使用。
1、展示Excalidraw自定义对话框的效果:
2、文件存放位置:将模块文件DialogUtils.md,存放在文件夹“Excalidraw/Module”中,如下图所示。
3、模块文件DialogUtils.md完整代码:
/*本文件包含两个模块:
1、自定义输入对话框createMiniInputDialog
2、自定义确认对话框createCustomDialog(3个按钮)
*/
// 存储对话框位置(使用单独的对象,避免干扰插件设置)
let dialogPositions = {};
// 初始化时从localStorage加载位置数据
try {
const savedPositions = localStorage.getItem('excalidraw-dialog-positions');
if (savedPositions) {
dialogPositions = JSON.parse(savedPositions);
}
} catch (e) {
console.error("DialogUtils: Error loading dialog positions:", e);
}
// 保存对话框位置到localStorage
const saveDialogPositions = () => {
try {
localStorage.setItem('excalidraw-dialog-positions', JSON.stringify(dialogPositions));
} catch (e) {
console.error("DialogUtils: Error saving dialog positions:", e);
}
};
// 创建可拖拽对话框(标题栏与拖拽栏合并)
const createDraggableDialog = (dialog, dialogId, defaultPosition, title) => {
let isDragging = false;
let dragStartX, dragStartY;
let dragStartLeft, dragStartTop;
// 创建标题栏(作为拖拽区域)
const header = document.createElement("div");
header.style.cursor = "move";
header.style.position = "relative"; // 改为相对定位
header.style.borderRadius = "8px 8px 0 0";
header.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
header.style.padding = "8px 10px 5px"; //上、左、下、右
header.style.display = "flex";
header.style.flexDirection = "column"; // 改为垂直排列
header.style.alignItems = "flex-start";
header.style.borderBottom = "1px solid var(--background-modifier-border)";
// 添加标题到标题栏(唯一标题位置)
const titleEl = document.createElement("div");
titleEl.textContent = title;
titleEl.style.fontWeight = "600";
titleEl.style.fontSize = "20px"; //标题文本大小
titleEl.style.color = "var(--text-normal)";
titleEl.style.flexGrow = "1";
titleEl.style.pointerEvents = "none";
titleEl.style.width = "100%"; // 宽度100%
titleEl.style.wordWrap = "break-word"; // 允许单词换行
titleEl.style.overflowWrap = "break-word"; // 允许任意位置换行
header.appendChild(titleEl);
// 添加关闭按钮(X图标)
const closeButton = document.createElement("div");
closeButton.innerHTML = "×";
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.style.position = "absolute"; // 绝对定位
closeButton.style.top = "10px"; // 顶部间距
closeButton.style.right = "15px"; // 右侧间距
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", () => {
dialog.dispatchEvent(new Event('dialog-cancel'));
});
header.appendChild(closeButton);
dialog.insertBefore(header, dialog.firstChild);
// 设置初始位置
const savedPosition = dialogPositions[dialogId] || defaultPosition;
dialog.style.position = "absolute";
dialog.style.left = `${savedPosition.x}px`;
dialog.style.top = `${savedPosition.y}px`;
// 拖拽开始
const startDrag = (e) => {
if (e.target !== header && !header.contains(e.target)) return;
isDragging = true;
dragStartX = e.clientX || (e.touches && e.touches[0].clientX);
dragStartY = e.clientY || (e.touches && e.touches[0].clientY);
dragStartLeft = parseInt(dialog.style.left) || 0;
dragStartTop = parseInt(dialog.style.top) || 0;
dialog.style.cursor = "grabbing";
dialog.style.boxShadow = "0 10px 30px rgba(0,0,0,0.3)";
e.preventDefault();
e.stopPropagation();
};
// 拖拽中
const onDrag = (e) => {
if (!isDragging) return;
const currentX = e.clientX || (e.touches && e.touches[0].clientX);
const currentY = e.clientY || (e.touches && e.touches[0].clientY);
if (!currentX || !currentY) return;
const newLeft = dragStartLeft + (currentX - dragStartX);
const newTop = dragStartTop + (currentY - dragStartY);
// 限制在视窗范围内
const maxX = window.innerWidth - dialog.offsetWidth;
const maxY = window.innerHeight - dialog.offsetHeight;
dialog.style.left = `${Math.max(0, Math.min(newLeft, maxX))}px`;
dialog.style.top = `${Math.max(0, Math.min(newTop, maxY))}px`;
e.preventDefault();
e.stopPropagation();
};
// 拖拽结束
const stopDrag = () => {
if (!isDragging) return;
isDragging = false;
dialog.style.cursor = "default";
dialog.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)";
// 保存位置
dialogPositions[dialogId] = {
x: parseInt(dialog.style.left) || 0,
y: parseInt(dialog.style.top) || 0
};
saveDialogPositions();
};
// 添加事件监听器
header.addEventListener("mousedown", startDrag);
document.addEventListener("mousemove", onDrag);
document.addEventListener("mouseup", stopDrag);
// 触摸设备支持
header.addEventListener("touchstart", startDrag);
document.addEventListener("touchmove", onDrag);
document.addEventListener("touchend", stopDrag);
// 返回清理函数
return () => {
header.removeEventListener("mousedown", startDrag);
document.removeEventListener("mousemove", onDrag);
document.removeEventListener("mouseup", stopDrag);
header.removeEventListener("touchstart", startDrag);
document.removeEventListener("touchmove", onDrag);
document.removeEventListener("touchend", stopDrag);
};
};
//▌▌▌▌▌▌▌▌创建自定义输入对话框(使用单行输入框)
const createMiniInputDialog = (title, description = "", defaultValue = "") => {
return new Promise((resolve) => {
try {
// 创建遮罩层(完全透明)
const overlay = document.createElement("div");
overlay.id = "ex-dialog-overlay";
overlay.style.position = "fixed";
overlay.style.top = "0";
overlay.style.left = "0";
overlay.style.width = "100%";
overlay.style.height = "100%";
overlay.style.backgroundColor = "transparent";
overlay.style.zIndex = "9998";
overlay.style.display = "flex";
overlay.style.justifyContent = "flex-end";
overlay.style.alignItems = "flex-end";
overlay.style.padding = "20px 50px 300px 20px";
// 创建对话框容器
const dialog = document.createElement("div");
dialog.id = "ex-dialog-input";
dialog.style.position = "relative";
dialog.style.backgroundColor = "var(--background-primary)";
dialog.style.border = "1px solid var(--background-modifier-border)";
dialog.style.borderRadius = "8px";
dialog.style.zIndex = "9999";
dialog.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)";
dialog.style.minWidth = "300px";
dialog.style.maxWidth = "400px";
dialog.style.fontFamily = "var(--font-interface)";
dialog.style.opacity = "0";
dialog.style.transition = "opacity 0.2s ease-out";
// 添加拖拽功能(标题栏与拖拽栏合并)
const defaultPosition = {
x: window.innerWidth - 400 - 50,
y: window.innerHeight - 250 - 50
};
let cleanupDrag = createDraggableDialog(
dialog,
"input-dialog-position",
defaultPosition,
title // 标题文本(唯一标题)
);
// 创建内容区域(不再包含重复标题)
const content = document.createElement("div");
// 减少内边距(上下减少5px)
content.style.padding = "15px 20px"; // 修改:上右下左内边距调整
content.style.paddingTop = "8px"; // 修改:顶部内边距微调
// 创建描述文本区域
if (description) {
const descriptionEl = document.createElement("div");
descriptionEl.textContent = description;
descriptionEl.style.fontSize = "18px"; //描述文本大小
descriptionEl.style.color = "var(--text-muted)";
descriptionEl.style.marginBottom = "8px"; // 减少底部间距
descriptionEl.style.wordWrap = "break-word";
content.appendChild(descriptionEl);
}
// 创建单行输入框(替换textarea)
const input = document.createElement("input");
input.type = "text";
input.value = defaultValue;
input.style.width = "100%";
input.style.height = "36px"; // 固定高度
input.style.padding = "8px 12px";
input.style.border = "1px solid var(--background-modifier-border)";
input.style.borderRadius = "4px";
input.style.fontSize = "16px";
input.style.backgroundColor = "var(--background-primary)";
input.style.color = "var(--text-normal)";
input.style.marginBottom = "10px"; // 修改:减少底部间距
input.style.fontFamily = "inherit"; // 继承对话框字体
input.style.boxSizing = "border-box"; // 确保宽度包含内边距
content.appendChild(input);
// 按钮容器
const buttonContainer = document.createElement("div");
buttonContainer.style.display = "flex";
buttonContainer.style.justifyContent = "flex-end";
// 减少按钮间距并上移
buttonContainer.style.gap = "8px"; // 修改:按钮间距从10px减少到8px
buttonContainer.style.marginTop = "0px"; // 修改:移除负边距
// 确定按钮(保持原始样式)
const confirmButton = document.createElement("button");
confirmButton.textContent = "确定";
confirmButton.style.padding = "6px 12px";
confirmButton.style.border = "none";
confirmButton.style.borderRadius = "4px";
confirmButton.style.backgroundColor = "#007AFF";
confirmButton.style.color = "var(--text-on-accent)";
confirmButton.style.fontSize = "18px";
confirmButton.style.fontWeight = "500";
confirmButton.style.cursor = "pointer";
confirmButton.style.minWidth = "80px";
// 取消按钮(保持原始样式)
const cancelButton = document.createElement("button");
cancelButton.textContent = "取消";
cancelButton.style.padding = "6px 12px";
cancelButton.style.border = "none";
cancelButton.style.borderRadius = "4px";
cancelButton.style.backgroundColor = "#def1ff";
cancelButton.style.color = "var(--text-normal)";
cancelButton.style.fontSize = "18px";
cancelButton.style.fontWeight = "500";
cancelButton.style.cursor = "pointer";
cancelButton.style.minWidth = "80px";
buttonContainer.appendChild(cancelButton);
buttonContainer.appendChild(confirmButton);
content.appendChild(buttonContainer);
dialog.appendChild(content);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
// 设置输入框焦点
setTimeout(() => {
dialog.style.opacity = "1";
try {
input.focus();
input.select();
} catch (e) {
console.error("DialogUtils: Focus error:", e);
}
}, 10);
// 确定按钮点击事件
confirmButton.onclick = () => {
try {
const value = input.value.trim();
resolve(value || defaultValue);
} catch (e) {
console.error("DialogUtils: Confirm error:", e);
resolve(null);
} finally {
removeDialog();
}
};
// 取消按钮点击事件
cancelButton.onclick = () => {
resolve(null);
removeDialog();
};
// 处理关闭按钮点击
dialog.addEventListener('dialog-cancel', () => {
resolve(null);
removeDialog();
});
// 点击遮罩层关闭
overlay.onclick = (e) => {
if (e.target === overlay) {
resolve(null);
removeDialog();
}
};
// 输入框按键处理
input.onkeydown = (e) => {
try {
if (e.key === "Enter") {
// Enter键提交
const value = input.value.trim();
resolve(value || defaultValue);
removeDialog();
e.preventDefault();
} else if (e.key === "Escape") {
resolve(null);
removeDialog();
e.preventDefault();
}
} catch (e) {
console.error("DialogUtils: Keydown error:", e);
removeDialog();
}
};
// 移除对话框函数
const removeDialog = () => {
try {
dialog.style.opacity = "0";
setTimeout(() => {
if (overlay.parentNode) {
document.body.removeChild(overlay);
}
if (cleanupDrag) {
cleanupDrag();
}
}, 200);
} catch (e) {
console.error("DialogUtils: Removal error:", e);
// 强制移除
if (overlay.parentNode) {
document.body.removeChild(overlay);
}
}
};
} catch (e) {
console.error("DialogUtils: Initialization failed:", e);
resolve(null);
}
});
};
//▌▌▌▌▌▌▌▌创建自定义确认对话框
const createCustomDialog = (title, message, buttons) => {
return new Promise((resolve) => {
try {
// 创建遮罩层
const overlay = document.createElement("div");
overlay.id = "ex-dialog-overlay";
overlay.style.position = "fixed";
overlay.style.top = "0";
overlay.style.left = "0";
overlay.style.width = "100%";
overlay.style.height = "100%";
overlay.style.backgroundColor = "transparent";
overlay.style.zIndex = "9998";
overlay.style.display = "flex";
overlay.style.justifyContent = "center";
overlay.style.alignItems = "center";
// 创建对话框容器
const dialog = document.createElement("div");
dialog.id = "ex-dialog-confirm";
dialog.style.position = "relative";
dialog.style.backgroundColor = "#fcfcfc";
dialog.style.border = "1px solid var(--background-modifier-border)";
dialog.style.borderRadius = "8px";
dialog.style.zIndex = "9999";
dialog.style.boxShadow = "0 4px 20px rgba(0,0,0,0.3)";
dialog.style.minWidth = "320px";
dialog.style.maxWidth = "450px";
dialog.style.fontFamily = "var(--font-interface)";
dialog.style.opacity = "0";
dialog.style.transition = "opacity 0.2s ease-out";
// 添加拖拽功能(标题栏与拖拽栏合并)
const defaultPosition = {
x: (window.innerWidth - 320) / 2,
y: (window.innerHeight - 200) / 2
};
let cleanupDrag = createDraggableDialog(
dialog,
"confirm-dialog-position",
defaultPosition,
title // 标题文本(唯一标题)
);
// 创建内容区域(不再包含重复标题)
const content = document.createElement("div");
content.style.padding = "20px";
content.style.paddingTop = "10px"; // 减少上边距
// 创建消息文本
const messageEl = document.createElement("div");
messageEl.textContent = message;
messageEl.style.marginBottom = "20px";
messageEl.style.fontSize = "20px"; //描述文本大小
messageEl.style.color = "var(--text-normal)";
messageEl.style.lineHeight = "1.4"; // 增加行高
messageEl.style.wordWrap = "break-word"; // 允许自动换行
content.appendChild(messageEl);
// 创建按钮容器
const buttonContainer = document.createElement("div");
buttonContainer.style.display = "flex";
buttonContainer.style.justifyContent = "center";
buttonContainer.style.gap = "10px";
buttonContainer.style.flexWrap = "wrap";
// 创建按钮
buttons.forEach((buttonText, index) => {
const button = document.createElement("button");
button.textContent = buttonText;
button.style.padding = "8px 16px";
button.style.border = "none";
button.style.borderRadius = "4px";
button.style.cursor = "pointer";
button.style.fontSize = "18px";
button.style.fontWeight = "500";
button.style.minWidth = "80px";
// 设置按钮样式
if (index === 0) {
button.style.backgroundColor = "#def1ff";
button.style.color = "var(--text-normal)";
} else if (index === 1) {
button.style.backgroundColor = "#87ffbd";
button.style.color = "var(--text-normal)";
} else if (index === 2) {
button.style.backgroundColor = "#007AFF";
button.style.color = "#f2ff00";
}
button.addEventListener("click", () => {
resolve(index);
removeDialog();
});
buttonContainer.appendChild(button);
});
content.appendChild(buttonContainer);
dialog.appendChild(content);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
// 添加淡入动画
setTimeout(() => {
dialog.style.opacity = "1";
}, 10);
// 处理关闭按钮点击
dialog.addEventListener('dialog-cancel', () => {
resolve(null);
removeDialog();
});
// 点击遮罩层关闭对话框
overlay.addEventListener("click", (e) => {
if (e.target === overlay) {
resolve(null);
removeDialog();
}
});
// 添加ESC键关闭功能
const handleEscape = (e) => {
if (e.key === "Escape") {
resolve(-1);
removeDialog();
}
};
document.addEventListener("keydown", handleEscape);
// 移除对话框函数
const removeDialog = () => {
try {
dialog.style.opacity = "0";
document.removeEventListener("keydown", handleEscape);
setTimeout(() => {
if (overlay.parentNode) {
document.body.removeChild(overlay);
}
if (cleanupDrag) {
cleanupDrag();
}
}, 200);
} catch (e) {
console.error("DialogUtils: Removal error:", e);
// 强制移除
if (overlay.parentNode) {
document.body.removeChild(overlay);
}
}
};
} catch (e) {
console.error("DialogUtils: Initialization failed:", e);
resolve(null);
}
});
};
// 导出模块API
return {
createMiniInputDialog,
createCustomDialog
};