Obsidian | 适用于Excalidraw的自定义对话框模块

Obsidian原生对话框很大、且不能拖动,所以写了两个自定义对话框,并导出为模块,可被其他脚本加载使用。

1、展示Excalidraw自定义对话框的效果:
ff_hq
  
2、文件存放位置:将模块文件DialogUtils.md,存放在文件夹“Excalidraw/Module”中,如下图所示。
biaoge9

  


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
};