obsidian悬浮命令窗(命令面板),基于quickadd插件

本脚本基于以下帖子提供的代码调整的(基于个人需求)
QuickaddJS实现的悬浮窗指令 - 经验分享 - Obsidian 中文论坛
主要调整方向
1.新增命令按钮时候会提供默认样式
2.新建命令按钮的时候可以自定义按钮名,不输入的话就是命令名作为按钮名
3.提供了按钮包含按钮分类文件夹和不包含按钮分类文件夹两种

包含文件夹

var path = '00-MO/4.脚本/config/folderCommands.json';//保存的配置json路径
var flag = true;//是否点击命令按钮便关闭弹窗
var buttonClass = 'grad_button g_blue'//按钮的样式class
var folderClass = 'grad_button g_yellow'//文件夹的样式class
var folderId = '0'//文件夹id
module.exports = async function (params) {
    const { app, quickAddApi } = params;

    // 添加在脚本开始处
    const existingToolbar = document.querySelector('.float-toolbar');
    if (existingToolbar) {
        document.body.removeChild(existingToolbar);
        return;
    }

    // 读取命令配置
    let { commands, windowConfig, inputBoxConfig = {} } = await loadCommands();
    // console.log('commands', commands);
    // console.log('windowConfig', windowConfig);
    // console.log('inputBoxConfig', inputBoxConfig);
    const useLastPosition = windowConfig.useLastPosition || false;

    // 创建弹窗容器
    const popupContainer = document.createElement('div');
    popupContainer.setAttribute('id', 'quickAddJsFolderCommandsDiv');
    popupContainer.classList.add('float-toolbar');
    Object.assign(popupContainer.style, {
        position: 'fixed',
        width: windowConfig.width || '300px',
        height: windowConfig.height || 'auto',
        left: useLastPosition && windowConfig.left ? windowConfig.left : '50%',
        top: useLastPosition && windowConfig.top ? windowConfig.top : '50%',
        transform: useLastPosition ? 'none' : 'translate(-50%, -50%)',
        backgroundColor: 'var(--background-primary)',
        padding: '20px',
        border: '1px solid var(--background-modifier-border)',
        borderRadius: '10px',
        boxShadow: '0 4px 6px rgba(0, 0, 0, 0.3)',
        zIndex: '9999',
        cursor: 'move',
        userSelect: 'none',
        resize: 'none',
        overflow: 'hidden',
        display: 'block',
    });

    // 创建命令按钮容器
    const commandButtonsContainer = document.createElement('div');
    commandButtonsContainer.classList.add('command-buttons-container');

    // 创建命令按钮
    function createCommandButtons(id) {
        commandButtonsContainer.innerHTML = ''; // 清空现有按钮
        //id是按钮id,folderId是按钮的文件夹id,type是文件夹还是按钮,0是文件夹,1是按钮
        commands.forEach((cmd) => {
            if (cmd.folderId === id) {
                const button = document.createElement('button');
                button.textContent = cmd.text;
                button.className = cmd.class;
                button.addEventListener('click', async (e) => {
                    if (e.button === 0) {
                        // 左键点击
                        try {
                            if (cmd.type === '1') {
                                // console.log('按钮执行===' + cmd.commandId);
                                // 如果是按钮,执行命令
                                await app.commands.executeCommandById(cmd.commandId);
                                if (flag) {
                                    document.body.removeChild(document.getElementById('quickAddJsFolderCommandsDiv'));
                                }
                            } else {
                                // console.log('文件夹执行');
                                //打开folderId为id的文件夹下的内容
                                // removeButtonsByClass('quickAddJsFolderCommandsDiv');
                                folderId = cmd.id;
                                createCommandButtons(folderId);
                            }
                        } catch (error) {
                            console.error(`执行命令 ${cmd.text} 失败:`, error);
                            new Notice(`执行命令 ${cmd.text} 失败,打开控制台查看详情`, 1000);
                        }
                    }
                });

                // 右键点击删除按钮
                button.addEventListener('contextmenu', (e) => {
                    e.preventDefault();
                    // 更新命令列表
                    commands = commands.filter((c) => c.text !== cmd.text); // 🚩 现在可以重新赋值
                    saveCommands(commands);
                    createCommandButtons(folderId);
                });

                commandButtonsContainer.appendChild(button);
            }
        });
    }

    createCommandButtons(folderId);
    // 删除按钮
    function removeButtonsByClass(className) {
        const buttons = document.querySelectorAll(`.${className}`);
        buttons.forEach(button => button.remove());
    }

    // 创建关闭按钮
    const closeButton = document.createElement('button');
    closeButton.textContent = '×';
    Object.assign(closeButton.style, {
        position: 'absolute',
        top: '5px',
        right: '5px',
        width: '20px',
        height: '20px',
        padding: '0',
        border: 'none',
        background: 'transparent',
        color: 'var(--text-muted)',
        fontSize: '20px',
        lineHeight: '20px',
        cursor: 'pointer',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        borderRadius: '50%',
        transition: 'background-color 0.2s, color 0.2s',
    });

    // 添加悬停效果
    closeButton.addEventListener('mouseover', () => {
        closeButton.style.backgroundColor = 'var(--background-modifier-hover)';
        closeButton.style.color = 'var(--text-normal)';
    });

    closeButton.addEventListener('mouseout', () => {
        closeButton.style.backgroundColor = 'transparent';
        closeButton.style.color = 'var(--text-muted)';
    });

    // 定义事件处理函数
    let isDragging = false;
    let offsetX, offsetY;

    const handleMouseMove = (e) => {
        if (isDragging) {
            const x = e.clientX - offsetX;
            const y = e.clientY - offsetY;

            const maxX = window.innerWidth - popupContainer.offsetWidth / 3;
            const maxY = window.innerHeight - popupContainer.offsetHeight / 3;

            popupContainer.style.left = `${Math.max(0, Math.min(x, maxX))}px`;
            popupContainer.style.top = `${Math.max(0, Math.min(y, maxY))}px`;
            popupContainer.style.transform = 'none';
        }
    };
    
    const handleMouseUp = () => {
        if (isDragging) {
            isDragging = false;
            // 保存新位置
            saveWindowConfig({
                left: popupContainer.style.left,
                top: popupContainer.style.top,
                width: popupContainer.style.width,
                height: popupContainer.style.height,
                useLastPosition: true, // 自动启用记忆位置
            });
        }
    };

    // 修改关闭按钮的点击处理程序
    closeButton.addEventListener('click', () => {
        closeToolbar(handleMouseMove, handleMouseUp, popupContainer);
    });

    // 创建新增命令按钮
    const addCommandButton = document.createElement('button');
    addCommandButton.textContent = '新增命令';
    //设置新增命令样式
    // addCommandButton.classList.add('circle_btn','g_lightgreen');
    addCommandButton.style = 'text-align: center; text-transform: uppercase; cursor: pointer; font-size: 20px; letter-spacing: 4px; position: relative; background-color:rgb(37, 152, 234); border: none; color: #fff; padding: 20px; width: 100%; transition-duration: 0.4s; overflow: hidden; box-shadow: 0 5px 15px #193047; border-radius: 4px;';
    
    addCommandButton.addEventListener('click', async () => {
        console.log('按键创建===' + folderId);
        var element = document.getElementById('quickAddJsFolderCommandsDiv');
        //隐藏命令框
        element.style.display = 'none';
        const allCommands = app.commands.listCommands();
        const selectedCommand = await quickAddApi.suggester(
            allCommands.map((cmd) => cmd.name),
            allCommands
        );
        if (!selectedCommand) {
            new Notice('未选择命令');
            element.style.display = 'block';
            return;
        }
        const newCommand = {
            text: selectedCommand.name,
            folderId: folderId,
            type: '1',
            class: buttonClass,
            commandId: selectedCommand.id,
        };
        // const popupInput = document.createElement('select');
        const popupInput = document.createElement('input');
        popupInput.id = 'inputbox';
        let useLastPosition = inputBoxConfig.useLastPosition || false;
        let inputName
        
        Object.assign(popupInput.style, {
            position: 'fixed',
            width: inputBoxConfig.width || '300px',
            height: inputBoxConfig.height || 'auto',
            left: useLastPosition && inputBoxConfig.left ? inputBoxConfig.left : '50%',
            top: useLastPosition && inputBoxConfig.top ? inputBoxConfig.top : '50%',
            transform: useLastPosition ? 'none' : 'translate(-50%, -50%)',
            backgroundColor: 'var(--background-primary)',
            padding: '20px',
            border: '1px solid var(--background-modifier-border)',
            borderRadius: '10px',
            boxShadow: '0 4px 6px rgba(0, 0, 0, 0.3)',
            zIndex: '9999',
            cursor: 'move',
            userSelect: 'none',
            resize: 'none',
            overflow: 'hidden',
        });
        // options.forEach(option => {
        //     const opt = document.createElement('option');
        //     opt.value = option.value;
        //     opt.textContent = option.text;
        //     popupSelect.appendChild(opt);
        // });

        popupInput.addEventListener('keydown', function (event) {
            // 检查是否按下了回车键
            if (event.keyCode === 13) {
                // 获取输入框的值并赋给变量
                inputName = popupInput.value;

                //恢复命令面板
                element.style.display = 'block';

                // 关闭输入框,例如从文档中移除
                document.body.removeChild(popupInput);

                if (/^\s*$/.test(inputName)) {
                    console.log("字符串为空或只包含空白字符");
                } else {
                    newCommand.text = inputName
                }

                commands.push(newCommand);
                saveCommands(commands);
                createCommandButtons(folderId); // 刷新按钮
            }
            document.removeEventListener('mousemove', handleMouseMove);
            document.removeEventListener('mouseup', handleMouseUp);
        });

        // 将弹窗添加到页面中
        document.body.appendChild(popupInput);

        // 定义事件处理函数
        let isDragging = false;
        let offsetX, offsetY;
        // 拖拽功能实现
        popupInput.addEventListener('mousedown', (e) => {
            // if (e.target === resizeHandle) return; // 如果点击的是调整手柄,则不触发拖拽
            isDragging = true;
            offsetX = e.clientX - popupInput.getBoundingClientRect().left;
            offsetY = e.clientY - popupInput.getBoundingClientRect().top;
        });

        const handleMouseMove = (e) => {
            if (isDragging) {
                const x = e.clientX - offsetX;
                const y = e.clientY - offsetY;

                const maxX = window.innerWidth - popupInput.offsetWidth / 3;
                const maxY = window.innerHeight - popupInput.offsetHeight / 3;

                popupInput.style.left = `${Math.max(0, Math.min(x, maxX))}px`;
                popupInput.style.top = `${Math.max(0, Math.min(y, maxY))}px`;
                popupInput.style.transform = 'none';
            }
        };

        const handleMouseUp = () => {
            if (isDragging) {
                isDragging = false;
                // 保存新位置
                saveInputBoxConfig({
                    left: popupInput.style.left,
                    top: popupInput.style.top,
                    width: popupInput.style.width,
                    height: popupInput.style.height,
                    useLastPosition: true, // 自动启用记忆位置
                });
            }
        };
        document.addEventListener('mousemove', handleMouseMove);
        document.addEventListener('mouseup', handleMouseUp);
    });

    // 右键创建文件夹
    addCommandButton.addEventListener('contextmenu', (e) => {
        console.log('文件夹创建===' + folderId);
        var element = document.getElementById('quickAddJsFolderCommandsDiv');
        //隐藏命令框
        element.style.display = 'none';

        const popupInput = document.createElement('input');
        popupInput.id = 'inputbox';
        let useLastPosition = inputBoxConfig.useLastPosition || false;
        let inputName
        const newCommand = {
            text: '',
            folderId: folderId,
            type: '0',
            class: folderClass,
            commandId: '文件夹',
        };
        Object.assign(popupInput.style, {
            position: 'fixed',
            width: inputBoxConfig.width || '300px',
            height: inputBoxConfig.height || 'auto',
            left: useLastPosition && inputBoxConfig.left ? inputBoxConfig.left : '50%',
            top: useLastPosition && inputBoxConfig.top ? inputBoxConfig.top : '50%',
            transform: useLastPosition ? 'none' : 'translate(-50%, -50%)',
            backgroundColor: 'var(--background-primary)',
            padding: '20px',
            border: '1px solid var(--background-modifier-border)',
            borderRadius: '10px',
            boxShadow: '0 4px 6px rgba(0, 0, 0, 0.3)',
            zIndex: '9999',
            cursor: 'move',
            userSelect: 'none',
            resize: 'none',
            overflow: 'hidden',
        });

        popupInput.addEventListener('keydown', function (event) {
            // 检查是否按下了回车键
            if (event.keyCode === 13) {
                // 获取输入框的值并赋给变量
                inputName = popupInput.value;

                // 关闭输入框,例如从文档中移除
                document.body.removeChild(popupInput);

                if (/^\s*$/.test(inputName)) {
                    console.log("字符串为空或只包含空白字符");
                    new Notice('未输入文件夹名');
                    // createCommandButtons();
                } else {
                    newCommand.text = inputName
                    commands.push(newCommand);
                    saveCommands(commands);
                    createCommandButtons(); // 刷新按钮
                }
                // console.log('newCommand', newCommand);
            }
            //恢复命令面板
            element.style.display = 'block';
            document.removeEventListener('mousemove', handleMouseMove);
            document.removeEventListener('mouseup', handleMouseUp);
        });

        document.body.appendChild(popupInput);

        // 定义事件处理函数
        let isDragging = false;
        let offsetX, offsetY;
        // 拖拽功能实现
        popupInput.addEventListener('mousedown', (e) => {
            // if (e.target === resizeHandle) return; // 如果点击的是调整手柄,则不触发拖拽
            isDragging = true;
            offsetX = e.clientX - popupInput.getBoundingClientRect().left;
            offsetY = e.clientY - popupInput.getBoundingClientRect().top;
        });

        const handleMouseMove = (e) => {
            if (isDragging) {
                const x = e.clientX - offsetX;
                const y = e.clientY - offsetY;

                const maxX = window.innerWidth - popupInput.offsetWidth / 3;
                const maxY = window.innerHeight - popupInput.offsetHeight / 3;

                popupInput.style.left = `${Math.max(0, Math.min(x, maxX))}px`;
                popupInput.style.top = `${Math.max(0, Math.min(y, maxY))}px`;
                popupInput.style.transform = 'none';
            }
        };

        const handleMouseUp = () => {
            if (isDragging) {
                isDragging = false;
                // 保存新位置
                saveInputBoxConfig({
                    left: popupInput.style.left,
                    top: popupInput.style.top,
                    width: popupInput.style.width,
                    height: popupInput.style.height,
                    useLastPosition: true, // 自动启用记忆位置
                });
            }
        };
        document.addEventListener('mousemove', handleMouseMove);
        document.addEventListener('mouseup', handleMouseUp);
    });

    // 创建调整大小的手柄
    const resizeHandle = document.createElement('div');
    Object.assign(resizeHandle.style, {
        position: 'absolute',
        bottom: '0',
        right: '0',
        width: '10px',
        height: '10px',
        backgroundColor: 'var(--background-modifier-border)',
        cursor: 'se-resize',
    });

    // 拖拽调整大小的逻辑
    let isResizing = false;
    let startX, startY, startWidth, startHeight;

    resizeHandle.addEventListener('mousedown', (e) => {
        isResizing = true;
        startX = e.clientX;
        startY = e.clientY;
        startWidth = parseInt(document.defaultView.getComputedStyle(popupContainer).width, 10);
        startHeight = parseInt(document.defaultView.getComputedStyle(popupContainer).height, 10);
        e.preventDefault(); // 防止文本选中
    });

    document.addEventListener('mousemove', (e) => {
        if (isResizing) {
            const newWidth = startWidth + (e.clientX - startX);
            const newHeight = startHeight + (e.clientY - startY);
            popupContainer.style.width = `${newWidth}px`;
            popupContainer.style.height = `${newHeight}px`;
        }
    });

    document.addEventListener('mouseup', () => {
        if (isResizing) {
            isResizing = false;
            // 保存调整后的尺寸
            saveWindowConfig({
                width: popupContainer.style.width,
                height: popupContainer.style.height,
                left: popupContainer.style.left,
                top: popupContainer.style.top,
            });
        }
    });

    // 将命令按钮、新增按钮、关闭按钮和调整手柄添加到弹窗容器中
    popupContainer.appendChild(commandButtonsContainer);
    popupContainer.appendChild(addCommandButton);
    popupContainer.appendChild(closeButton);
    popupContainer.appendChild(resizeHandle);

    // 将弹窗添加到页面中
    document.body.appendChild(popupContainer);

    // 拖拽功能实现
    popupContainer.addEventListener('mousedown', (e) => {
        if (e.target === resizeHandle) return; // 如果点击的是调整手柄,则不触发拖拽
        isDragging = true;
        offsetX = e.clientX - popupContainer.getBoundingClientRect().left;
        offsetY = e.clientY - popupContainer.getBoundingClientRect().top;
    });

    // 拖拽过程的具体处理
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
};

function closeToolbar(handleMouseMove, handleMouseUp, popupContainer) {
    document.removeEventListener('mousemove', handleMouseMove);
    document.removeEventListener('mouseup', handleMouseUp);
    // 移除弹窗
    document.body.removeChild(popupContainer);
}

async function loadCommands() {
    try {
        const content = await app.vault.adapter.read(path);
        // 兼容旧格式(纯数组)和新格式(带配置的对象)
        const result = JSON.parse(content);

        return Array.isArray(result)
            ? { commands: result }
            : {
                commands: result.commands || [],
                windowConfig: result.windowConfig || {},
                inputBoxConfig: result.inputBoxConfig || {},
            };
    } catch (error) {
        console.error('读取命令文件失败:', error);
        return { commands: [] };
    }
}

async function saveInputBoxConfig(config) {
    try {
        const existing = await loadCommands();
        const newData = {
            commands: existing.commands,
            windowConfig: existing.windowConfig || {},
            inputBoxConfig: {
                ...existing.inputBoxConfig,
                ...config,
            },
        };
        await app.vault.adapter.write(path, JSON.stringify(newData, null, 2));
    } catch (error) {
        console.error('保存输入框配置失败:', error);
    }
}

async function saveWindowConfig(config) {
    try {
        const existing = await loadCommands();
        const newData = {
            commands: existing.commands,
            inputBoxConfig: existing.inputBoxConfig || {},
            windowConfig: {
                ...existing.windowConfig,
                ...config,
            },
        };
        await app.vault.adapter.write(path, JSON.stringify(newData, null, 2));
    } catch (error) {
        console.error('保存窗口配置失败:', error);
    }
}

async function saveCommands(commands) {
    try {
        const existing = await loadCommands();
        const newData = {
            commands,
            inputBoxConfig: existing.inputBoxConfig || {},
            windowConfig: existing.windowConfig || {},
        };
        await app.vault.adapter.write(path, JSON.stringify(newData, null, 2));
    } catch (error) {
        console.error('保存命令文件失败:', error);
    }
}

配置json

{
  "commands": [
    {
      "text": "常用命令",
      "folderId": "0",
      "type": "0",
      "class": "grad_button g_yellow",
      "commandId": "文件夹"
    }
  ],
  "inputBoxConfig": {
    "left": "534px",
    "top": "431px",
    "width": "300px",
    "height": "auto",
    "useLastPosition": true
  },
  "windowConfig": {
    "left": "973px",
    "top": "59px",
    "width": "289px",
    "height": "353px",
    "useLastPosition": true
  }
}

不包含的我放链接了(不然帖子内容过长)
https://wwbr.lanzouy.com/b00ro15a7g
密码:7x91

图片

操作

左键点击按钮激活命令或者打开按钮分类文件夹,右键删除
左键点击新建命令按钮,会弹出命令选择框,选择后是按钮名输入框,点击回车确认(不输入的话则是以命令名作为按钮名),右键点击新建命令按钮则是会弹出输入框,输入内容并回车会创建一个按钮分类文件夹,并进入该文件夹(这时候创建按钮的话,则是会都在这个按钮分类文件夹中)