快捷鍵函式庫

根據網址(正規表達式)聆聽按鍵事件點選指定元素的函式庫,提供點選規則與快捷鍵的 CRUD 操作。

当前为 2025-07-25 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/542910/1630011/%E5%BF%AB%E6%8D%B7%E9%8D%B5%E5%87%BD%E5%BC%8F%E5%BA%AB.js

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

/**name => ShortcutLibrary
description => 根據網址(正規表達式)聆聽按鍵事件點選指定元素的函式庫,提供點選規則與快捷鍵的 CRUD 操作。
@version  1.0.0
author => Max
namespace => https://github.com/Max46656
license => MPL2.0
本程式具有以下依賴,須添加在你使用的腳本中
@grant        GM_getValue
@grant        GM_setValue
@grant        GM_info
*/
class ShortcutAPI {
    constructor() {
        // 初始化:設定規則管理器和快捷鍵處理器
        this.ruleManager = new RuleManager();
        this.shortcutHandler = new ShortcutHandler(this.ruleManager);
    }

    // 新增快捷鍵規則
    // 輸入參數: rule (object) - 規則物件,包含 ruleName, urlPattern, selectorType, selector, nthElement, shortcut, ifLinkOpen, isEnabled
    // 返回值: boolean - 是否成功新增規則
    addRule(rule) {
        if (!this.validateRule(rule)) {
            console.warn(`${GM_info.script.name}: 無效的規則物件: ${JSON.stringify(rule)}`);
            return false;
        }
        const conflicts = this.checkConflicts(rule, window.location.href);
        if (conflicts.length > 0) {
            conflicts.forEach(conflict => {
                console.warn(`${GM_info.script.name}: 新規則 "${rule.ruleName}" 檢測到${conflict.type === 'shortcut' ? '相同的快捷鍵組合' : '相同的目標元素'}: 與規則 "${conflict.rule.ruleName}" 衝突 (快捷鍵: ${conflict.rule.shortcut}, 選擇器: ${conflict.rule.selector}, 第幾個元素: ${conflict.rule.nthElement})`);
            });
        }
        this.ruleManager.addRule(rule);
        return true;
    }

    // 更新指定索引的規則
    // 輸入參數: index (number) - 規則索引
    //           rule (object) - 更新後的規則物件
    // 返回值: boolean - 是否成功更新規則
    updateRule(index, rule) {
        if (!this.validateRule(rule)) {
            console.warn(`${GM_info.script.name}: 無效的更新規則物件: ${JSON.stringify(rule)}`);
            return false;
        }
        const conflicts = this.checkConflicts(rule, window.location.href, index);
        if (conflicts.length > 0) {
            conflicts.forEach(conflict => {
                console.warn(`${GM_info.script.name}: 更新規則 "${rule.ruleName}" 檢測到${conflict.type === 'shortcut' ? '相同的快捷鍵組合' : '相同的目標元素'}: 與規則 "${conflict.rule.ruleName}" 衝突 (快捷鍵: ${conflict.rule.shortcut}, 選擇器: ${conflict.rule.selector}, 第幾個元素: ${conflict.rule.nthElement})`);
            });
        }
        this.ruleManager.updateRule(index, rule);
        return true;
    }

    // 刪除指定索引的規則
    // 輸入參數: index (number) - 規則索引
    // 返回值: boolean - 是否成功刪除規則
    deleteRule(index) {
        if (index < 0 || index >= this.ruleManager.clickRules.rules.length) {
            console.warn(`${GM_info.script.name}: 無效的規則索引: ${index}`);
            return false;
        }
        this.ruleManager.deleteRule(index);
        return true;
    }

    // 獲取所有規則
    // 輸入參數: 無
    // 返回值: array - 包含所有規則的陣列
    getRules() {
        return this.ruleManager.clickRules.rules;
    }

    // 檢查規則是否與現有規則衝突
    // 輸入參數: rule (object) - 待檢查的規則物件
    //           url (string) - 檢查衝突的網址
    //           excludeIndex (number, optional) - 排除檢查的規則索引
    // 返回值: array - 包含衝突資訊的陣列
    checkConflicts(rule, url, excludeIndex = -1) {
        return this.ruleManager.checkConflicts(rule, url, excludeIndex);
    }

    // 啟用指定規則
    // 輸入參數: index (number) - 規則索引
    // 返回值: boolean - 是否成功啟用規則
    enableRule(index) {
        if (index < 0 || index >= this.ruleManager.clickRules.rules.length) {
            console.warn(`${GM_info.script.name}: 無效的規則索引: ${index}`);
            return false;
        }
        const rule = this.ruleManager.clickRules.rules[index];
        rule.isEnabled = true;
        this.ruleManager.updateRules();
        return true;
    }

    // 停用指定規則
    // 輸入參數: index (number) - 規則索引
    // 返回值: boolean - 是否成功停用規則
    disableRule(index) {
        if (index < 0 || index >= this.ruleManager.clickRules.rules.length) {
            console.warn(`${GM_info.script.name}: 無效的規則索引: ${index}`);
            return false;
        }
        const rule = this.ruleManager.clickRules.rules[index];
        rule.isEnabled = false;
        this.ruleManager.updateRules();
        return true;
    }

    // 驗證規則是否有效
    // 輸入參數: rule (object) - 規則物件
    // 返回值: boolean - 是否為有效規則
    validateRule(rule) {
        if (!rule || typeof rule !== 'object') return false;
        try {
            new RegExp(rule.urlPattern);
        } catch (e) {
            console.warn(`${GM_info.script.name}: 無效的正則表達式: ${rule.urlPattern}`);
            return false;
        }
        if (!rule.selector || !['css', 'xpath'].includes(rule.selectorType)) {
            console.warn(`${GM_info.script.name}: 無效的選擇器: ${rule.selector}`);
            return false;
        }
        if (!this.shortcutHandler.validateShortcut(rule.shortcut)) {
            console.warn(`${GM_info.script.name}: 無效的快捷鍵: ${rule.shortcut}`);
            return false;
        }
        return true;
    }
}

// 規則管理類,負責儲存、驗證和管理快捷鍵規則
class RuleManager {
    constructor() {
        // 初始化:從 GM_getValue 取得規則,若無則使用預設空規則集
        this.clickRules = this.sanitizeRules(GM_getValue('clickRules', { rules: [] }));
    }

    // 清理並驗證規則,確保規則格式正確
    // 輸入參數: clickRules (object) - 包含規則陣列的物件
    // 返回值: object - 清理後的規則物件
    sanitizeRules(clickRules) {
        const defaultRule = {
            ruleName: '',
            urlPattern: '.*',
            selectorType: 'css',
            selector: '',
            nthElement: 1,
            shortcut: 'Control+A',
            ifLinkOpen: false,
            isEnabled: true
        };
        const validRules = clickRules.rules.filter(rule => {
            return rule && typeof rule === 'object' && rule.shortcut && this.isValidShortcut(rule.shortcut);
        }).map(rule => ({
            ...defaultRule,
            ...rule,
            ruleName: rule.ruleName || `規則 ${clickRules.rules.indexOf(rule) + 1}`,
            isEnabled: rule.isEnabled !== undefined ? rule.isEnabled : true
        }));
        return { rules: validRules };
    }

    // 驗證快捷鍵格式是否有效
    // 輸入參數: shortcut (string) - 快捷鍵字串,例如 "Control+A"
    // 返回值: boolean - 是否為有效快捷鍵
    isValidShortcut(shortcut) {
        const validModifiers = ['Control', 'Alt', 'Shift', 'CapsLock', 'NumLock'];
        if (!shortcut || typeof shortcut !== 'string') return false;
        const parts = shortcut.split('+');
        if (parts.length < 2 || parts.length > 3) return false;
        const mainKey = parts[parts.length - 1];
        const modifiers = parts.slice(0, -1);
        return modifiers.every(mod => validModifiers.includes(mod)) && mainKey.length === 1 && /^[a-zA-Z0-9]$/.test(mainKey);
    }

    // 檢查新規則是否與現有規則衝突
    // 輸入參數: newRule (object) - 新規則物件
    //           currentUrl (string) - 當前網址
    //           excludeIndex (number) - 排除檢查的規則索引(用於更新時)
    // 返回值: array - 包含衝突資訊的陣列
    checkConflicts(newRule, currentUrl, excludeIndex = -1) {
        const conflicts = [];
        this.clickRules.rules.forEach((rule, index) => {
            if (index === excludeIndex) return;
            try {
                if (new RegExp(rule.urlPattern).test(currentUrl)) {
                    if (rule.shortcut.toLowerCase() === newRule.shortcut.toLowerCase()) {
                        conflicts.push({ type: 'shortcut', rule, index });
                    } else if (rule.selector === newRule.selector && rule.nthElement === newRule.nthElement) {
                        conflicts.push({ type: 'element', rule, index });
                    }
                }
            } catch (e) {
                console.warn(`${GM_info.script.name}: 規則 "${rule.ruleName}" 的正則表達式無效: ${rule.urlPattern}`);
            }
        });
        return conflicts;
    }

    // 新增規則到規則集
    // 輸入參數: newRule (object) - 新規則物件
    // 返回值: void
    addRule(newRule) {
        this.clickRules.rules.push(newRule);
        this.updateRules();
    }

    // 更新指定索引的規則
    // 輸入參數: index (number) - 規則索引
    //           updatedRule (object) - 更新後的規則物件
    // 返回值: void
    updateRule(index, updatedRule) {
        this.clickRules.rules[index] = updatedRule;
        this.updateRules();
    }

    // 刪除指定索引的規則
    // 輸入參數: index (number) - 規則索引
    // 返回值: void
    deleteRule(index) {
        this.clickRules.rules.splice(index, 1);
        this.updateRules();
    }

    // 將規則集儲存到 GM_setValue
    // 輸入參數: 無
    // 返回值: void
    updateRules() {
        GM_setValue('clickRules', this.clickRules);
    }
}

// 快捷鍵處理類,負責監聽鍵盤事件並執行點選動作
class ShortcutHandler {
    constructor(ruleManager) {
        // 初始化:設定規則管理器並綁定鍵盤事件監聽器
        this.ruleManager = ruleManager;
        this.keydownHandler = (event) => this.handleKeydown(event);
        window.addEventListener('keydown', this.keydownHandler);
    }

    // 驗證快捷鍵格式是否有效
    // 輸入參數: shortcut (string) - 快捷鍵字串
    // 返回值: boolean - 是否為有效快捷鍵
    validateShortcut(shortcut) {
        const validModifiers = ['Control', 'Alt', 'Shift', 'CapsLock', 'NumLock'];
        if (!shortcut) return false;
        const parts = shortcut.split('+');
        if (parts.length < 2 || parts.length > 3) return false;
        const mainKey = parts[parts.length - 1];
        const modifiers = parts.slice(0, -1);
        return modifiers.every(mod => validModifiers.includes(mod)) && mainKey.length === 1 && /^[a-zA-Z0-9]$/.test(mainKey);
    }

    // 處理鍵盤按下事件,檢查是否符合快捷鍵並執行動作
    // 輸入參數: event (KeyboardEvent) - 鍵盤事件物件
    // 返回值: void
    handleKeydown(event) {
        const currentUrl = window.location.href;
        this.ruleManager.clickRules.rules.forEach((rule, index) => {
            try {
                if (!rule.isEnabled || !new RegExp(rule.urlPattern).test(currentUrl)) return;

                const shortcutParts = rule.shortcut.split('+');
                const mainKey = shortcutParts[shortcutParts.length - 1];
                const modifiers = shortcutParts.slice(0, -1);

                const allModifiersPressed = modifiers.every(mod => event.getModifierState(mod));
                const mainKeyPressed = event.key.toUpperCase() === mainKey.toUpperCase();

                if (allModifiersPressed && mainKeyPressed) {
                    event.preventDefault();
                    this.clickElement(rule, index);
                }
            } catch (e) {
                console.warn(`${GM_info.script.name}: 處理規則 "${rule.ruleName}" 時發生錯誤: ${e}`);
            }
        });
    }

    // 執行點選指定元素的動作
    // 輸入參數: rule (object) - 規則物件
    //           ruleIndex (number) - 規則索引
    // 返回值: boolean - 是否成功點選元素
        clickElement(rule, ruleIndex) {
        try {
            const elements = this.getElements(rule.selectorType, rule.selector);
            if (elements.length === 0) {
                console.warn(`${GM_info.script.name}: 規則 "${rule.ruleName}" 未找到匹配元素: ${rule.selector}`);
                return false;
            }

            let targetIndex;
            if (rule.nthElement > 0) {
                targetIndex = rule.nthElement - 1;
            } else if (rule.nthElement < 0) {
                targetIndex = elements.length + rule.nthElement;
            } else {
                console.warn(`${GM_info.script.name}: 規則 "${rule.ruleName}" 的 nthElement 無效: 0 不允許`);
                return false;
            }

            if (targetIndex < 0 || targetIndex >= elements.length) {
                console.warn(`${GM_info.script.name}: 規則 "${rule.ruleName}" 的 nthElement 無效: ${rule.nthElement}, 找到 ${elements.length} 個元素`);
                return false;
            }

            const targetElement = elements[targetIndex];
            if (targetElement) {
                console.log(`${GM_info.script.name}: 規則 "${rule.ruleName}" 成功點擊元素:`, targetElement);
                if (rule.ifLinkOpen && targetElement.tagName === "A" && targetElement.href) {
                    window.location.href = targetElement.href;
                } else {
                    targetElement.click();
                }
                return true;
            } else {
                console.warn(`${GM_info.script.name}: 規則 "${rule.ruleName}" 的目標元素未找到`);
                return false;
            }
        } catch (e) {
            console.warn(`${GM_info.script.name}: 規則 "${rule.ruleName}" 執行失敗: ${e}`);
            return false;
        }
    }

    // 根據選擇器類型獲取元素
    // 輸入參數: selectorType (string) - 選擇器類型 ('css' 或 'xpath')
    //           selector (string) - 選擇器字串
    // 返回值: array - 符合的元素陣列
    getElements(selectorType, selector) {
        try {
            if (selectorType === 'xpath') {
                const nodes = document.evaluate(selector, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
                const elements = [];
                for (let i = 0; i < nodes.snapshotLength; i++) {
                    elements.push(nodes.snapshotItem(i));
                }
                return elements;
            } else if (selectorType === 'css') {
                return Array.from(document.querySelectorAll(selector));
            }
            return [];
        } catch (e) {
            console.warn(`${GM_info.script.name}: 選擇器 "${selector}" 無效: ${e}`);
            return [];
        }
    }
}

window.ShortcutLibrary = ShortcutAPI;