脚本分享:定制悬浮命令按钮(带吸附功能)

Echors 启发,我做了一个js脚本,可以根据自己需求定制一个悬浮命令按钮。你需要修改command id 以及 lucide图标名
如下图所示,左键为默认按钮,右键为展开命令按钮,按钮可随意拖动(移动端:短按为默认按钮,长按不动为展开命令按钮,长按拖动可随意拖动)


最后的效果如下:
屏幕录制-2025-07-14-16-02-54

V2版本增加了边缘吸附功能(同样支持移动端)
脚本:

V1
// == Obsidian 可拖动自定义按钮脚本(增强版)==
(function() {
    'use strict';
    
    console.log('🚀 初始化自定义浮动按钮脚本(增强版)');
    
    // 配置项 - 可修改
    // 左键配置
    const CONFIG = {
        commandId: 'form-flow:@CFORM_00配置/components/form/标签切换.cform', 
        commandName: '标签面板',        
        defaultPosition: { right: 50, bottom: 50 },
        
        // 右键配置
        statusBarCommands: [
            {id: 'command-palette:open', icon: 'command', name: '命令'},
            {id: 'app:open-settings', icon: 'settings', name: '设置'},
            {id: 'remotely-save:start-sync', icon: 'folder-sync', name: '同步'}            
        ],
        menuAnimationDuration: 200,
        longPressDuration: 500,
        longPressMoveThreshold: 15 // 长按移动阈值(像素)
    };
    
    let button = null;
    let menu = null;
    let isDragging = false;
    let startX = 0;
    let startY = 0;
    let startRight = 0;
    let startBottom = 0;
    let dragDistance = 0;
    const dragThreshold = 5;
    let longPressTimer = null;
    let touchStartPoint = null;
    let menuTriggered = false; // 新增:标记菜单是否已触发
    
    // 创建按钮元素
    function createButton() {
        // 如果按钮已存在,则先移除
        const existingButton = document.getElementById('obsidian-custom-floating-button');
        if (existingButton) existingButton.remove();
        
        // 创建按钮元素
        button = document.createElement('div');
        button.id = 'obsidian-custom-floating-button';
        button.className = 'custom-floating-button';
        
        // 添加SVG图标
        const svgIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svgIcon.setAttribute('width', '24');
        svgIcon.setAttribute('height', '24');
        svgIcon.setAttribute('viewBox', '0 0 24 24');
        svgIcon.setAttribute('fill', 'none');
        svgIcon.setAttribute('stroke', 'currentColor');
        svgIcon.setAttribute('stroke-width', '2');
        svgIcon.setAttribute('stroke-linecap', 'round');
        svgIcon.setAttribute('stroke-linejoin', 'round');
        
        const rect1 = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
        rect1.setAttribute('x', '3');
        rect1.setAttribute('y', '3');
        rect1.setAttribute('width', '18');
        rect1.setAttribute('height', '18');
        rect1.setAttribute('rx', '2');
        rect1.setAttribute('ry', '2');
        
        const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        path1.setAttribute('d', 'M9 3v18');
        
        const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        path2.setAttribute('d', 'M15 3v18');
        
        svgIcon.appendChild(rect1);
        svgIcon.appendChild(path1);
        svgIcon.appendChild(path2);
        
        button.appendChild(svgIcon);
        
        // 添加提示文本
        const tooltip = document.createElement('div');
        tooltip.className = 'command-tooltip';
        tooltip.textContent = `${CONFIG.commandName}`;
        button.appendChild(tooltip);
        
        // 设置初始样式
        button.style.position = 'fixed';
        button.style.zIndex = '9999';
        button.style.width = '50px';
        button.style.height = '50px';
        button.style.borderRadius = '50%';
        button.style.backgroundColor = 'var(--interactive-accent)';
        button.style.color = 'white';
        button.style.display = 'flex';
        button.style.alignItems = 'center';
        button.style.justifyContent = 'center';
        button.style.cursor = 'move';
        button.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.3)';
        button.style.transition = 'transform 0.2s, box-shadow 0.2s, background-color 0.2s';
        button.style.userSelect = 'none';
        button.style.fontSize = '24px';
        button.style.fontWeight = 'bold';
        button.style.touchAction = 'none';
        
        // 图标样式
        svgIcon.style.width = '24px';
        svgIcon.style.height = '24px';
        svgIcon.style.pointerEvents = 'none';
        
        // 工具提示样式
        tooltip.style.position = 'absolute';
        tooltip.style.top = '-30px';
        tooltip.style.background = 'var(--background-secondary)';
        tooltip.style.color = 'var(--text-normal)';
        tooltip.style.padding = '4px 8px';
        tooltip.style.borderRadius = '4px';
        tooltip.style.fontSize = '12px';
        tooltip.style.whiteSpace = 'nowrap';
        tooltip.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.3)';
        tooltip.style.opacity = '0';
        tooltip.style.transition = 'opacity 0.3s';
        tooltip.style.pointerEvents = 'none';
        tooltip.style.transform = 'translateX(-50%)';
        tooltip.style.left = '50%';
        
        // 添加到文档
        document.body.appendChild(button);
        
        // 创建命令菜单
        createCommandMenu();
        
        // 绑定事件
        bindEvents();
    }
    
    // 创建命令菜单
    function createCommandMenu() {
        // 移除已存在的菜单
        const existingMenu = document.getElementById('obsidian-command-menu');
        if (existingMenu) existingMenu.remove();
        
        menu = document.createElement('div');
        menu.id = 'obsidian-command-menu';
        menu.className = 'command-menu';
        menu.style.display = 'none';
        menu.style.position = 'fixed';
        menu.style.zIndex = '10000';
        menu.style.backgroundColor = 'var(--background-primary)';
        menu.style.borderRadius = '8px';
        menu.style.boxShadow = '0 6px 16px rgba(0, 0, 0, 0.3)';
        menu.style.padding = '8px';
        menu.style.transition = `opacity ${CONFIG.menuAnimationDuration}ms, transform ${CONFIG.menuAnimationDuration}ms`;
        menu.style.opacity = '0';
        menu.style.transform = 'scale(0.8)';
        menu.style.transformOrigin = 'bottom center';
        menu.style.userSelect = 'none';
        menu.style.pointerEvents = 'none';
        
        // 创建菜单项
        CONFIG.statusBarCommands.forEach(command => {
            const menuItem = document.createElement('div');
            menuItem.className = 'menu-item';
            menuItem.dataset.commandId = command.id;
            
            // 添加图标
            const iconSvg = createIconSvg(command.icon);
            menuItem.appendChild(iconSvg);
            
            // 添加标签
            const label = document.createElement('span');
            label.className = 'menu-label';
            label.textContent = command.name;
            menuItem.appendChild(label);
            
            // 样式设置
            menuItem.style.display = 'flex';
            menuItem.style.alignItems = 'center';
            menuItem.style.padding = '8px 12px';
            menuItem.style.borderRadius = '4px';
            menuItem.style.cursor = 'pointer';
            menuItem.style.marginBottom = '4px';
            menuItem.style.transition = 'background-color 0.2s';
            
            // 悬停效果
            menuItem.addEventListener('mouseenter', () => {
                menuItem.style.backgroundColor = 'var(--background-secondary)';
            });
            menuItem.addEventListener('mouseleave', () => {
                menuItem.style.backgroundColor = '';
            });
            
            // 点击执行命令
            menuItem.addEventListener('click', (e) => {
                e.stopPropagation();
                executeCommandById(command.id);
                hideMenu();
            });
            
            menu.appendChild(menuItem);
        });
        
        document.body.appendChild(menu);
    }
    
    // 创建图标SVG
    function createIconSvg(iconName) {
        const svgNS = 'http://www.w3.org/2000/svg';
        const svg = document.createElementNS(svgNS, 'svg');
        svg.setAttribute('width', '20');
        svg.setAttribute('height', '20');
        svg.setAttribute('viewBox', '0 0 24 24');
        svg.setAttribute('fill', 'none');
        svg.setAttribute('stroke', 'currentColor');
        svg.setAttribute('stroke-width', '2');
        svg.setAttribute('stroke-linecap', 'round');
        svg.setAttribute('stroke-linejoin', 'round');
        
        const path = document.createElementNS(svgNS, 'path');
        
        switch(iconName) {
            case 'folder-sync':
                path.setAttribute('d', 'M22 12c0-5.52-4.48-10-10-10S2 6.48 2 12s4.48 10 10 10');
                break;
            case 'settings':
                path.setAttribute('d', 'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z');
                break;
            case 'command':
                path.setAttribute('d', 'M18 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 3 3 0 0 0-3-3z');
                break;
            default:
                path.setAttribute('d', 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z');
        }
        
        svg.appendChild(path);
        return svg;
    }
    
    // 显示命令菜单
    function showMenu() {
        if (!menu || !button) return;
        
        // 计算菜单位置
        const buttonRect = button.getBoundingClientRect();
        const menuWidth = 180;
        const menuItemHeight = 36;
        const menuHeight = CONFIG.statusBarCommands.length * menuItemHeight + 16;
        
        // 计算上方空间是否足够
        const spaceAbove = buttonRect.top;
        const spaceBelow = window.innerHeight - buttonRect.bottom;
        
        let menuTop, menuLeft;
        
        // 优先向上展开
        if (spaceAbove >= menuHeight || spaceAbove >= spaceBelow) {
            menuTop = buttonRect.top - menuHeight - 10;
            menu.style.transformOrigin = 'bottom center';
        } 
        // 向上空间不足时向下展开
        else {
            menuTop = buttonRect.bottom + 10;
            menu.style.transformOrigin = 'top center';
        }
        
        // 水平居中
        menuLeft = buttonRect.left + (buttonRect.width - menuWidth) / 2;
        
        // 边界检查
        if (menuLeft < 10) menuLeft = 10;
        if (menuLeft + menuWidth > window.innerWidth - 10) {
            menuLeft = window.innerWidth - menuWidth - 10;
        }
        
        // 应用位置
        menu.style.left = `${menuLeft}px`;
        menu.style.top = `${menuTop}px`;
        menu.style.display = 'block';
        menu.style.pointerEvents = 'auto';
        
        // 触发动画
        setTimeout(() => {
            menu.style.opacity = '1';
            menu.style.transform = 'scale(1)';
        }, 10);
        
        // 添加全局点击关闭监听
        setTimeout(() => {
            document.addEventListener('click', globalClickHandler);
        }, 50);
    }
    
    // 隐藏命令菜单
    function hideMenu() {
        if (!menu) return;
        
        menu.style.opacity = '0';
        menu.style.transform = 'scale(0.8)';
        
        // 动画结束后隐藏
        setTimeout(() => {
            menu.style.display = 'none';
            menu.style.pointerEvents = 'none';
            document.removeEventListener('click', globalClickHandler);
        }, CONFIG.menuAnimationDuration);
    }
    
    // 全局点击处理(关闭菜单)
    function globalClickHandler(e) {
        if (menu && !menu.contains(e.target) && e.target !== button) {
            hideMenu();
        }
    }
    
    // 绑定事件处理
    function bindEvents() {
        // 桌面端:拖动开始
        button.addEventListener('mousedown', function(e) {
            if (e.button === 0) {
                if (e.target === button || e.target.className === 'command-tooltip') {
                    e.preventDefault();
                    startDrag(e.clientX, e.clientY);
                }
            }
        });
        
        // 桌面端:右键菜单
        button.addEventListener('contextmenu', function(e) {
            e.preventDefault();
            showMenu();
        });
        
        // 移动端:触摸开始
        button.addEventListener('touchstart', function(e) {
            if (e.target === button || e.target.className === 'command-tooltip') {
                e.preventDefault();
                const touch = e.touches[0];
                
                // 重置菜单触发标记
                menuTriggered = false;
                
                // 记录起始点位置
                touchStartPoint = {
                    x: touch.clientX,
                    y: touch.clientY
                };
                
                // 设置长按计时器
                longPressTimer = setTimeout(() => {
                    // 检查是否仍在按钮上
                    if (longPressTimer) {
                        showMenu();
                        menuTriggered = true; // 标记菜单已触发
                        
                        // 取消拖动状态
                        isDragging = false;
                        button.style.cursor = 'move';
                        button.style.opacity = '';
                        button.style.transition = '';
                        longPressTimer = null;
                    }
                }, CONFIG.longPressDuration);
                
                startDrag(touch.clientX, touch.clientY);
            }
        }, { passive: false });
        
        // 移动端:触摸移动
        button.addEventListener('touchmove', function(e) {
            if (!touchStartPoint) return;
            
            const touch = e.touches[0];
            const dx = Math.abs(touch.clientX - touchStartPoint.x);
            const dy = Math.abs(touch.clientY - touchStartPoint.y);
            const distance = Math.sqrt(dx * dx + dy * dy);
            
            // 如果移动距离超过阈值,取消长按菜单
            if (distance > CONFIG.longPressMoveThreshold && longPressTimer) {
                clearTimeout(longPressTimer);
                longPressTimer = null;
            }
        });
        
        // 移动端:触摸结束
        button.addEventListener('touchend', function(e) {
            // 清除长按计时器
            if (longPressTimer) {
                clearTimeout(longPressTimer);
                longPressTimer = null;
            }
            
            // 如果菜单已触发,重置标记并返回
            if (menuTriggered) {
                menuTriggered = false;
                return;
            }
            
            touchStartPoint = null;
            
            // 执行默认命令(仅在未触发菜单且拖动距离小于阈值时)
            if (dragDistance < dragThreshold) {
                console.log(`📱 执行命令: ${CONFIG.commandId}`);
                executeCommand();
            }
        });
        
        // 点击执行命令(桌面端)
        button.addEventListener('click', function(e) {
            if (dragDistance < dragThreshold && e.button === 0) {
                console.log(`🔍 执行命令: ${CONFIG.commandId}`);
                executeCommand();
            }
        });
        
        // 鼠标悬停效果(仅桌面端)
        button.addEventListener('mouseenter', function() {
            const tooltip = button.querySelector('.command-tooltip');
            tooltip.style.opacity = '1';
            button.style.backgroundColor = 'var(--interactive-accent-hover)';
        });
        
        button.addEventListener('mouseleave', function() {
            const tooltip = button.querySelector('.command-tooltip');
            tooltip.style.opacity = '0';
            button.style.backgroundColor = 'var(--interactive-accent)';
        });
    }
    
    // 开始拖动
    function startDrag(clientX, clientY) {
        isDragging = true;
        dragDistance = 0;
        startX = clientX;
        startY = clientY;
        
        // 获取当前位置
        startRight = parseInt(button.style.right) || CONFIG.defaultPosition.right;
        startBottom = parseInt(button.style.bottom) || CONFIG.defaultPosition.bottom;
        
        // 缓存初始位置和尺寸
        button._dragCache = {
            width: button.offsetWidth,
            height: button.offsetHeight
        };
        
        button.style.cursor = 'grabbing';
        button.style.opacity = '0.9';
        button.style.transition = 'none';
        
        console.log('🎯 开始拖动按钮');
    }
    
    // 执行Obsidian命令
    function executeCommand() {
        executeCommandById(CONFIG.commandId);
    }
    
    // 通过ID执行命令
    function executeCommandById(commandId) {
        // 等待app对象可用
        const waitForApp = setInterval(() => {
            if (window.app) {
                clearInterval(waitForApp);
                
                try {
                    // 执行命令
                    window.app.commands.executeCommandById(commandId);
                    console.log(`✅ 成功执行命令: ${commandId}`);
                    
                    // 添加视觉反馈
                    button.style.transform = 'scale(0.95)';
                    button.style.backgroundColor = 'var(--interactive-accent-active)';
                    setTimeout(() => {
                        button.style.transform = '';
                        button.style.backgroundColor = 'var(--interactive-accent)';
                    }, 200);
                } catch (error) {
                    console.error(`❌ 执行命令失败: ${error}`);
                    button.style.backgroundColor = '#ff6b6b';
                    setTimeout(() => {
                        button.style.backgroundColor = 'var(--interactive-accent)';
                    }, 1000);
                }
            }
        }, 100);
    }
    
    // 加载保存的位置
    function loadPosition() {
        const savedPosition = localStorage.getItem('obsidianFloatingButtonPosition');
        if (savedPosition) {
            try {
                const position = JSON.parse(savedPosition);
                
                // 直接应用新位置
                if (position.right !== undefined && position.bottom !== undefined) {
                    button.style.right = `${position.right}px`;
                    button.style.bottom = `${position.bottom}px`;
                    console.log('📌 加载保存的按钮位置');
                    return;
                }
            } catch (e) {
                console.error('❌ 位置数据解析失败', e);
            }
        }
        setDefaultPosition();
    }
    
    // 设置默认位置
    function setDefaultPosition() {
        button.style.right = `${CONFIG.defaultPosition.right}px`;
        button.style.bottom = `${CONFIG.defaultPosition.bottom}px`;
        console.log('⚙️ 使用默认按钮位置');
    }
    
    // 保存位置到本地存储
    function savePosition() {
        const position = {
            right: parseInt(button.style.right) || CONFIG.defaultPosition.right,
            bottom: parseInt(button.style.bottom) || CONFIG.defaultPosition.bottom
        };
        
        localStorage.setItem('obsidianFloatingButtonPosition', JSON.stringify(position));
        console.log('💾 保存按钮位置');
    }
    
    // 处理拖动移动
    function handleDragMove(clientX, clientY) {
        if (!isDragging || !button) return;
        
        // 计算移动距离
        const deltaX = startX - clientX;
        const deltaY = startY - clientY;
        
        // 更新拖动距离
        dragDistance += Math.abs(deltaX) + Math.abs(deltaY);
        
        // 更新位置
        const newRight = startRight + deltaX;
        const newBottom = startBottom + deltaY;
        
        // 边界检查
        const minRight = 10;
        const minBottom = 10;
        const maxRight = window.innerWidth - button._dragCache.width - 10;
        const maxBottom = window.innerHeight - button._dragCache.height - 10;
        
        button.style.right = `${Math.max(minRight, Math.min(newRight, maxRight))}px`;
        button.style.bottom = `${Math.max(minBottom, Math.min(newBottom, maxBottom))}px`;
    }
    
    // 使用requestAnimationFrame优化桌面端拖动
    function setupDesktopDrag() {
        let lastMoveTime = 0;
        
        document.addEventListener('mousemove', function(e) {
            if (!isDragging) return;
            
            // 使用节流优化性能
            const now = Date.now();
            if (now - lastMoveTime < 16) return;
            lastMoveTime = now;
            
            handleDragMove(e.clientX, e.clientY);
        });
    }
    
    // 移动端拖动处理
    function setupMobileDrag() {
        document.addEventListener('touchmove', function(e) {
            if (!isDragging || !button) return;
            
            const touch = e.touches[0];
            handleDragMove(touch.clientX, touch.clientY);
            
            // 防止页面滚动
            if (dragDistance > 10) {
                e.preventDefault();
            }
        }, { passive: false });
    }
    
    // 结束拖动
    function endDrag() {
        if (isDragging) {
            isDragging = false;
            button.style.cursor = 'move';
            button.style.opacity = '';
            button.style.transition = 'transform 0.2s, box-shadow 0.2s, background-color 0.2s';
            delete button._dragCache;
            savePosition();
            console.log('✅ 拖动结束');
        }
    }
    
    // 窗口大小变化处理
    function handleWindowResize() {
        if (!button) return;
        
        // 获取当前位置
        const currentRight = parseInt(button.style.right) || CONFIG.defaultPosition.right;
        const currentBottom = parseInt(button.style.bottom) || CONFIG.defaultPosition.bottom;
        
        // 检查按钮是否在可视区域外
        const maxRight = window.innerWidth - button.offsetWidth - 10;
        const maxBottom = window.innerHeight - button.offsetHeight - 10;
        
        if (currentRight > maxRight || currentBottom > maxBottom) {
            // 自动调整到安全位置
            button.style.right = `${Math.min(currentRight, maxRight)}px`;
            button.style.bottom = `${Math.min(currentBottom, maxBottom)}px`;
            savePosition();
            console.log('🔄 窗口大小变化,自动调整按钮位置');
        }
        
        // 如果菜单显示,重新定位
        if (menu && menu.style.display === 'block') {
            showMenu();
        }
    }
    
    // 添加重置按钮位置的功能
    window.resetFloatingButton = function() {
        localStorage.removeItem('obsidianFloatingButtonPosition');
        setDefaultPosition();
        console.log('🔄 按钮位置已重置');
    };
    
    // 初始化
    function init() {
        createButton();
        loadPosition();
        
        // 设置事件监听
        setupDesktopDrag();
        setupMobileDrag();
        
        // 添加窗口大小变化监听
        let resizeTimeout;
        window.addEventListener('resize', function() {
            clearTimeout(resizeTimeout);
            resizeTimeout = setTimeout(handleWindowResize, 200);
        });
        
        // 桌面端释放
        document.addEventListener('mouseup', endDrag);
        
        // 移动端释放
        document.addEventListener('touchend', endDrag);
        
        console.log('✅ 自定义浮动按钮已创建');
    }
    
    // 在DOM加载完成后初始化
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        setTimeout(init, 100);
    } else {
        document.addEventListener('DOMContentLoaded', init);
    }
    
    console.log('🎉 自定义浮动按钮脚本加载完成(增强版)');
})();
V2

下载链接

食用方法:
目前找到两种方式触发脚本
方式一:安装插件 obsidian-form-flow,直接把js脚本放在form-flow的脚本文件夹下即可加载脚本
方式二:安装插件 obsidian-js-engine-plugin ,加载此脚本

cmenu差不多

cmenu不能折叠,不能拖动

note toolbar 差不多

1 个赞

note toolbar 也无法拖动,而且不知道什么原因,我移动端note toolbar会有bug。