您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
根據網址(正規表達式)聆聽按鍵事件點選指定元素的函式庫,提供點選規則與快捷鍵的 CRUD 操作。
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/542910/1632051/%E5%BF%AB%E6%8D%B7%E9%8D%B5%E5%87%BD%E5%BC%8F%E5%BA%AB.js
/** name => ShortcutLibrary description => 根據網址(正規表達式)聆聽按鍵事件點選指定元素的函式庫,提供點選規則與快捷鍵的 CRUD 操作。 version => 1.2.0 author => Max namespace => https://github.com/Max46656 license => MPL2.0 本程式具有以下依賴,須添加在你使用的腳本中 @grant GM_getValue @grant GM_setValue @grant GM_info @grant GM_registerMenuCommand 呼叫方式 shortcutLib = new ShortcutLibrary({ RuleC: false, RuleR: true, RuleU: ['shortcut'] }); */ class RuleManager { constructor() { this.clickRules = this.sanitizeRules(GM_getValue('clickRules', { rules: [] })); } addRule(newRule) { this.clickRules.rules.push(newRule); this.updateRules(); } updateRule(index, updatedRule) { this.clickRules.rules[index] = updatedRule; this.updateRules(); } deleteRule(index) { this.clickRules.rules.splice(index, 1); this.updateRules(); } addRuleFromJSON(jsonString) { try { const rule = JSON.parse(jsonString); if (this.validateRule(rule)) { this.addRule(rule); return true; } return false; } catch (e) { console.error('Error parsing JSON:', e); return false; } } validateRule(rule) { const requiredFields = ['ruleName', 'urlPattern', 'selectorType', 'selector', 'nthElement', 'shortcut', 'ifLinkOpen', 'isEnabled']; return requiredFields.every(field => field in rule) && typeof rule.ruleName === 'string' && typeof rule.urlPattern === 'string' && ['css', 'xpath'].includes(rule.selectorType) && typeof rule.selector === 'string' && Number.isInteger(rule.nthElement) && typeof rule.shortcut === 'string' && typeof rule.ifLinkOpen === 'boolean' && typeof rule.isEnabled === 'boolean' && this.isValidShortcut(rule.shortcut); } updateRules() { GM_setValue('clickRules', this.clickRules); } 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 }; } 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 || /^F[1-9]|F1[0-2]|Esc|Home|End|PageUp|PageDown|Insert|Delete|Tab|Enter|Eliminate|Backspace|ArrowUp|ArrowDown|ArrowLeft|ArrowRight$/.test(mainKey)); } 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; } } class ShortcutHandler { constructor(ruleManager) { this.ruleManager = ruleManager; this.keydownHandler = (event) => this.handleKeydown(event); window.addEventListener('keydown', this.keydownHandler); } handleKeydown(event) { //console.log(event); const currentUrl = window.location.href; [...this.ruleManager.clickRules.rules].reverse().some((rule, index) => { try { if (!rule.isEnabled || !new RegExp(rule.urlPattern).test(currentUrl)) return false; 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(); const originalIndex = this.ruleManager.clickRules.rules.length - 1 - index; this.clickElement(rule, originalIndex); return true; } return false; } catch (e) { console.warn(`${GM_info.script.name}: 處理規則 "${rule.ruleName}" 時發生錯誤: ${e}`); return false; } }); //console.log(this.ruleManager.clickRules.rules); } 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 []; } } 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; } } } class MenuManager { constructor(ruleManager, config) { this.ruleManager = ruleManager; this.config = config; this.validModifierCombos = this.generateModifierCombos(); this.currentExpandedRule = null; this.currentExpandedConflict = null; this.i18n = { 'zh-TW': { titleAdd: '新增快捷鍵規則', titleManage: '管理快捷鍵規則', matchingRules: '符合的規則', noMatchingRules: '當前網頁無符合的規則。', addRuleSection: '新增規則', ruleName: '規則名稱:', urlPattern: '網址正規表達式:', selectorType: '選擇器類型:', selector: '選擇器:', nthElement: '第幾個元素(正數從頭計,負數從尾計):', shortcutModifiers: '快捷鍵修飾鍵組合:', shortcutMainKey: '快捷鍵主鍵:', ifLinkOpen: '若為連結則開啟(否則維持預設):', isEnabled: '啟用規則:', addRule: '新增規則', save: '儲存', delete: '刪除', ruleNamePlaceholder: '例如:我的規則', urlPatternPlaceholder: '例如:https://example.com/.*', selectorPlaceholder: '例如:button.submit 或 //button[@class="submit"]', nthElementPlaceholder: '例如:1 或 -1(最後一個)', mainKeyPlaceholder: '例如:A', invalidRegex: '無效的正規表達式', invalidSelector: '無效的選擇器', invalidMainKey: '無效的主鍵(需為單一字母或數字,例如:A 或 1)', conflictingRules: '衝突的規則', shortcutConflict: '使用相同的快捷鍵組合', elementConflict: '指向相同的目標元素', importJSON: '從 JSON 匯入規則' }, 'en': { titleAdd: 'Add New Shortcut Rule', titleManage: 'Manage Shortcut Rules', matchingRules: 'Matching Rules', noMatchingRules: 'No rules match the current URL.', addRuleSection: 'Add New Rule', ruleName: 'Rule Name:', urlPattern: 'URL Pattern (Regex):', selectorType: 'Selector Type:', selector: 'Selector:', nthElement: 'Nth Element (positive from start, negative from end):', shortcutModifiers: 'Shortcut Modifier Combination:', shortcutMainKey: 'Shortcut Main Key:', ifLinkOpen: 'If it is a link, open it (otherwise keep default):', isEnabled: 'Enable Rule:', addRule: 'Add Rule', save: 'Save', delete: 'Delete', ruleNamePlaceholder: 'e.g., My Rule', urlPatternPlaceholder: 'e.g., https://example\\.com/.*', selectorPlaceholder: 'e.g., button.submit or //button[@class="submit"]', nthElementPlaceholder: 'e.g., 1 or -1 (last element)', mainKeyPlaceholder: 'e.g., A', invalidRegex: 'Invalid regular expression', invalidSelector: 'Invalid selector', invalidMainKey: 'Invalid main key (must be a single character or key, e.g., A or F1)', conflictingRules: 'Conflicting Rules', shortcutConflict: 'Uses the same shortcut key combination', elementConflict: 'Targets the same element', importJSON: 'Import Rule from JSON' } }; } getLanguage() { const lang = navigator.language || navigator.userLanguage; if (lang.startsWith('zh')) return 'zh-TW'; return 'en'; } validateRule(rule) { const i18n = this.i18n[this.getLanguage()]; try { new RegExp(rule.urlPattern); } catch (e) { alert(`${i18n.invalidRegex}: ${rule.urlPattern}`); return false; } if (!rule.selector || !['css', 'xpath'].includes(rule.selectorType)) { alert(`${i18n.invalidSelector}: ${rule.selector}`); return false; } if (!this.validateShortcut(rule.shortcut)) { alert(`${i18n.invalidMainKey}: ${rule.shortcut}`); return false; } return true; } 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 || /^F[1-9]|F1[0-2]|Esc|Home|End|PageUp|PageDown|Insert|Delete|Tab|Enter|Eliminate|Backspace|ArrowUp|ArrowDown|ArrowLeft|ArrowRight$/.test(mainKey)); } generateModifierCombos() { const modifiers = ['CapsLock', 'NumLock', 'Control', 'Alt', 'Shift']; const combos = []; for (let i = 0; i < modifiers.length; i++) { combos.push(modifiers[i]); for (let j = i + 1; j < modifiers.length; j++) { combos.push(`${modifiers[i]}-${modifiers[j]}`); } } return combos; } createRuleElement(rule, ruleIndex) { const i18n = this.i18n[this.getLanguage()]; const modifierCombo = rule.shortcut && this.validateShortcut(rule.shortcut) ? rule.shortcut.split('+').slice(0, -1).join('+') || rule.shortcut.split('+')[0] : 'Control'; const mainKey = rule.shortcut && this.validateShortcut(rule.shortcut) ? rule.shortcut.split('+').pop() : 'A'; const currentUrl = window.location.href; const conflicts = this.ruleManager.checkConflicts(rule, currentUrl, ruleIndex); const conflictHtml = conflicts.length > 0 ? ` <div class="conflictHeader" id="conflictHeader${ruleIndex}"> <strong>${i18n.conflictingRules}</strong> </div> <div class="conflictDetails" id="conflictDetails${ruleIndex}" style="display: none;"> ${conflicts.map(conflict => ` <p>${conflict.type === 'shortcut' ? i18n.shortcutConflict : i18n.elementConflict}: ${conflict.rule.ruleName} (快捷鍵: ${conflict.rule.shortcut}, 選擇器: ${conflict.rule.selector}, 第幾個元素: ${conflict.rule.nthElement})</p> `).join('')} </div> ` : ''; const ruleDiv = document.createElement('div'); ruleDiv.innerHTML = ` <div class="ruleHeader" id="ruleHeader${ruleIndex}"> <strong>${rule.ruleName || `規則 ${ruleIndex + 1}`}</strong> </div> <div class="readRule" id="readRule${ruleIndex}" style="display: none;"> <div class="checkbox-container"> <label>${i18n.isEnabled}</label> <input type="checkbox" id="updateIsEnabled${ruleIndex}" ${rule.isEnabled ? 'checked' : ''} ${this.config.RuleU.includes('isEnabled') ? '' : 'disabled'}> </div> <label>${i18n.ruleName}</label> <input type="text" id="updateRuleName${ruleIndex}" value="${rule.ruleName || ''}" ${this.config.RuleU.includes('ruleName') ? '' : 'readonly'}> <label>${i18n.urlPattern}</label> <input type="text" id="updateUrlPattern${ruleIndex}" value="${rule.urlPattern}" ${this.config.RuleU.includes('urlPattern') ? '' : 'readonly'}> <label>${i18n.selectorType}</label> <select id="updateSelectorType${ruleIndex}" ${this.config.RuleU.includes('selectorType') ? '' : 'disabled'}> <option value="css" ${rule.selectorType === 'css' ? 'selected' : ''}>CSS</option> <option value="xpath" ${rule.selectorType === 'xpath' ? 'selected' : ''}>XPath</option> </select> <label>${i18n.selector}</label> <input type="text" id="updateSelector${ruleIndex}" value="${rule.selector}" ${this.config.RuleU.includes('selector') ? '' : 'readonly'}> <label>${i18n.nthElement}</label> <input type="number" id="updateNthElement${ruleIndex}" value="${rule.nthElement}" ${this.config.RuleU.includes('nthElement') ? '' : 'readonly'} placeholder="${i18n.nthElementPlaceholder}"> <label>${i18n.shortcutModifiers}</label> <select id="updateModifierCombo${ruleIndex}" ${this.config.RuleU.includes('shortcut') ? '' : 'disabled'}> ${this.validModifierCombos.map(combo => ` <option value="${combo}" ${combo === modifierCombo ? 'selected' : ''}>${combo}</option> `).join('')} </select> <label>${i18n.shortcutMainKey}</label> <input type="text" id="updateMainKey${ruleIndex}" value="${mainKey}" ${this.config.RuleU.includes('shortcut') ? '' : 'readonly'} placeholder="${i18n.mainKeyPlaceholder}"> <div class="checkbox-container"> <label>${i18n.ifLinkOpen}</label> <input type="checkbox" id="updateIfLink${ruleIndex}" ${rule.ifLinkOpen ? 'checked' : ''} ${this.config.RuleU.includes('ifLinkOpen') ? '' : 'disabled'}> </div> <button id="updateRule${ruleIndex}">${i18n.save}</button> ${this.config.RuleD ? `<button id="deleteRule${ruleIndex}">${i18n.delete}</button>` : ''} ${conflictHtml} </div> `; return ruleDiv; } initAddRuleMenu() { const i18n = this.i18n[this.getLanguage()]; const menu = document.createElement('div'); menu.style.position = 'fixed'; menu.style.top = '10px'; menu.style.right = '10px'; menu.style.background = 'rgb(36, 36, 36)'; menu.style.color = 'rgb(204, 204, 204)'; menu.style.border = '1px solid rgb(80, 80, 80)'; menu.style.padding = '10px'; menu.style.zIndex = '10000'; menu.style.maxWidth = '400px'; menu.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)'; menu.innerHTML = ` <style> h1 { font-size: 2rem; } h2 { font-size: 1.5rem; } #shortcutMenu { overflow-y: auto; max-height: 80vh; } #shortcutMenu input:not([type="checkbox"]), #shortcutMenu select, #shortcutMenu button, #shortcutMenu textarea { background: rgb(50, 50, 50); color: rgb(204, 204, 204); border: 1px solid rgb(80, 80, 80); margin: 5px 0; padding: 5px; width: 100%; box-sizing: border-box; } #shortcutMenu input[type="checkbox"] { margin: 0 5px 0 0; width: auto; vertical-align: middle; } #shortcutMenu button { cursor: pointer; } #shortcutMenu button:hover { background: rgb(70, 70, 70); } #shortcutMenu label { margin-top: 5px; display: block; } #shortcutMenu .checkbox-container { display: flex; align-items: center; margin-top: 5px; } #shortcutMenu .headerContainer { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } #shortcutMenu .closeButton { width: auto; padding: 5px 10px; margin: 0; } </style> <div id="shortcutMenu"> <div class="headerContainer"> <h1>${GM_info.script.name}</h1> <button id="closeMenu" class="closeButton">✕</button> </div> <h2>${i18n.titleAdd}</h2> <div class="checkbox-container"> <label>${i18n.isEnabled}</label> <input type="checkbox" id="isEnabled" checked ${this.config.RuleU.includes('isEnabled') ? '' : 'disabled'}> </div> <label>${i18n.ruleName}</label> <input type="text" id="ruleName" placeholder="${i18n.ruleNamePlaceholder}" ${this.config.RuleU.includes('ruleName') ? '' : 'readonly'}> <label>${i18n.urlPattern}</label> <input type="text" id="urlPattern" value="${window.location.href}" ${this.config.RuleU.includes('urlPattern') ? '' : 'readonly'}> <label>${i18n.selectorType}</label> <select id="selectorType" ${this.config.RuleU.includes('selectorType') ? '' : 'disabled'}> <option value="css">CSS</option> <option value="xpath">XPath</option> </select> <label>${i18n.selector}</label> <input type="text" id="selector" placeholder="${i18n.selectorPlaceholder}" ${this.config.RuleU.includes('selector') ? '' : 'readonly'}> <label>${i18n.nthElement}</label> <input type="number" id="nthElement" value="1" placeholder="${i18n.nthElementPlaceholder}" ${this.config.RuleU.includes('nthElement') ? '' : 'readonly'}> <label>${i18n.shortcutModifiers}</label> <select id="modifierCombo" ${this.config.RuleU.includes('shortcut') ? '' : 'disabled'}> ${this.validModifierCombos.map(combo => `<option value="${combo}">${combo}</option>`).join('')} </select> <label>${i18n.shortcutMainKey}</label> <input type="text" id="mainKey" placeholder="${i18n.mainKeyPlaceholder}" ${this.config.RuleU.includes('shortcut') ? '' : 'readonly'}> <div class="checkbox-container"> <label>${i18n.ifLinkOpen}</label> <input type="checkbox" id="ifLinkOpen" ${this.config.RuleU.includes('ifLinkOpen') ? '' : 'disabled'}> </div> <button id="addRule" style="margin-top: 10px;">${i18n.addRule}</button> <h3>${i18n.importJSON}</h3> <textarea id="jsonInput" rows="5" placeholder="Paste JSON here (e.g., {\"ruleName\":\"Example\",\"urlPattern\":\".*\",\"selectorType\":\"css\",\"selector\":\".btn\",\"nthElement\":1,\"shortcut\":\"Control+B\",\"ifLinkOpen\":false,\"isEnabled\":true})"></textarea> <button id="importRule">${i18n.importJSON}</button> </div> `; document.body.appendChild(menu); document.getElementById('addRule').addEventListener('click', () => { const modifierCombo = document.getElementById('modifierCombo').value; const mainKey = document.getElementById('mainKey').value; const selector = document.getElementById('selector').value.replace(/"/g, "'"); const newRule = { ruleName: document.getElementById('ruleName').value || `規則 ${this.ruleManager.clickRules.rules.length + 1}`, urlPattern: document.getElementById('urlPattern').value, selectorType: document.getElementById('selectorType').value, selector: selector, nthElement: parseInt(document.getElementById('nthElement').value) || 1, shortcut: `${modifierCombo}+${mainKey}`, ifLinkOpen: document.getElementById('ifLinkOpen').checked, isEnabled: document.getElementById('isEnabled').checked }; if (!this.validateRule(newRule)) return; const conflicts = this.ruleManager.checkConflicts(newRule, window.location.href); conflicts.forEach(conflict => { console.warn(`${GM_info.script.name}: 新規則 "${newRule.ruleName}" 檢測到${conflict.type === 'shortcut' ? '相同的快捷鍵組合' : '相同的目標元素'}: 與規則 "${conflict.rule.ruleName}" 衝突 (快捷鍵: ${conflict.rule.shortcut}, 選擇器: ${conflict.rule.selector}, 第幾個元素: ${conflict.rule.nthElement})`); }); this.ruleManager.addRule(newRule); document.getElementById('ruleName').value = ''; document.getElementById('urlPattern').value = ''; document.getElementById('selector').value = ''; document.getElementById('nthElement').value = '1'; document.getElementById('mainKey').value = ''; document.getElementById('ifLinkOpen').checked = false; document.getElementById('isEnabled').checked = true; menu.remove(); }); document.getElementById('importRule').addEventListener('click', () => { const jsonString = document.getElementById('jsonInput').value; if (this.ruleManager.addRuleFromJSON(jsonString)) { alert('Rule added successfully'); document.getElementById('jsonInput').value = ''; } else { alert('Invalid JSON or rule format'); } }); document.getElementById('closeMenu').addEventListener('click', () => { menu.remove(); }); } initManageRulesMenu() { const i18n = this.i18n[this.getLanguage()]; const menu = document.createElement('div'); menu.style.position = 'fixed'; menu.style.top = '10px'; menu.style.right = '10px'; menu.style.background = 'rgb(36, 36, 36)'; menu.style.color = 'rgb(204, 204, 204)'; menu.style.border = '1px solid rgb(80, 80, 80)'; menu.style.padding = '10px'; menu.style.zIndex = '10000'; menu.style.maxWidth = '400px'; menu.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)'; menu.innerHTML = ` <style> h1 { font-size: 2rem; } h2 { font-size: 1.5rem; } #shortcutMenu { overflow-y: auto; max-height: 80vh; } #shortcutMenu input:not([type="checkbox"]), #shortcutMenu select, #shortcutMenu button { background: rgb(50, 50, 50); color: rgb(204, 204, 204); border: 1px solid rgb(80, 80, 80); margin: 5px 0; padding: 5px; width: 100%; box-sizing: border-box; } #shortcutMenu input[type="checkbox"] { margin: 0 5px 0 0; width: auto; vertical-align: middle; } #shortcutMenu button { cursor: pointer; } #shortcutMenu button:hover { background: rgb(70, 70, 70); } #shortcutMenu label { margin-top: 5px; display: block; } #shortcutMenu .checkbox-container { display: flex; align-items: center; margin-top: 5px; } #shortcutMenu .ruleHeader, #shortcutMenu .conflictHeader { cursor: pointer; background: rgb(50, 50, 50); padding: 5px; margin: 5px 0; border-radius: 3px; } #shortcutMenu .readRule, #shortcutMenu .conflictDetails { padding: 5px; border: 1px solid rgb(80, 80, 80); border-radius: 3px; margin-bottom: 5px; } #shortcutMenu .headerContainer { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } #shortcutMenu .closeButton { width: auto; padding: 5px 10px; margin: 0; } </style> <div id="shortcutMenu"> <div class="headerContainer"> <h1>${GM_info.script.name}</h1> <button id="closeMenu" class="closeButton">✕</button> </div> <h2>${i18n.titleManage}</h2> <div id="rulesList"></div> </div> `; document.body.appendChild(menu); this.updateRulesElement(); document.getElementById('closeMenu').addEventListener('click', () => { this.collapseAllRules(); this.collapseAllConflicts(); menu.remove(); }); } updateRulesElement() { const rulesList = document.getElementById('rulesList'); const i18n = this.i18n[this.getLanguage()]; rulesList.innerHTML = `<h4>${i18n.matchingRules}</h4>`; const currentUrl = window.location.href; const matchingRules = this.ruleManager.clickRules.rules.filter(rule => { try { return new RegExp(rule.urlPattern).test(currentUrl); } catch (e) { console.warn(`${GM_info.script.name}: 規則 "${rule.ruleName}" 的正規表達式無效: ${rule.urlPattern}`); return false; } }); if (matchingRules.length === 0) { rulesList.innerHTML += `<p>${i18n.noMatchingRules}</p>`; return; } matchingRules.forEach((rule, index) => { const ruleIndex = this.ruleManager.clickRules.rules.indexOf(rule); const ruleDiv = this.createRuleElement(rule, ruleIndex); rulesList.appendChild(ruleDiv); document.getElementById(`ruleHeader${ruleIndex}`).addEventListener('click', () => { if (this.currentExpandedRule === ruleIndex) { this.collapseAllRules(); this.collapseAllConflicts(); } else { this.collapseAllRules(); this.collapseAllConflicts(); const details = document.getElementById(`readRule${ruleIndex}`); details.style.display = 'block'; this.currentExpandedRule = ruleIndex; } }); const conflictHeader = document.getElementById(`conflictHeader${ruleIndex}`); if (conflictHeader) { conflictHeader.addEventListener('click', () => { const conflictDetails = document.getElementById(`conflictDetails${ruleIndex}`); if (this.currentExpandedConflict === ruleIndex) { conflictDetails.style.display = 'none'; this.currentExpandedConflict = null; } else { this.collapseAllConflicts(); conflictDetails.style.display = 'block'; this.currentExpandedConflict = ruleIndex; } }); } document.getElementById(`updateRule${ruleIndex}`).addEventListener('click', () => { const modifierCombo = document.getElementById(`updateModifierCombo${ruleIndex}`).value; const mainKey = document.getElementById(`updateMainKey${ruleIndex}`).value; const selector = document.getElementById(`updateSelector${ruleIndex}`).value.replace(/"/g, "'"); const updatedRule = { ruleName: document.getElementById(`updateRuleName${ruleIndex}`).value || `規則 ${ruleIndex + 1}`, urlPattern: document.getElementById(`updateUrlPattern${ruleIndex}`).value, selectorType: document.getElementById(`updateSelectorType${ruleIndex}`).value, selector: selector, nthElement: parseInt(document.getElementById(`updateNthElement${ruleIndex}`).value) || 1, shortcut: `${modifierCombo}+${mainKey}`, ifLinkOpen: document.getElementById(`updateIfLink${ruleIndex}`).checked, isEnabled: document.getElementById(`updateIsEnabled${ruleIndex}`).checked }; if (!this.validateRule(updatedRule)) return; const conflicts = this.ruleManager.checkConflicts(updatedRule, window.location.href, ruleIndex); conflicts.forEach(conflict => { console.warn(`${GM_info.script.name}: 更新規則 "${updatedRule.ruleName}" 檢測到${conflict.type === 'shortcut' ? '相同的快捷鍵組合' : '相同的目標元素'}: 與規則 "${conflict.rule.ruleName}" 衝突 (快捷鍵: ${conflict.rule.shortcut}, 選擇器: ${conflict.rule.selector}, 第幾個元素: ${conflict.rule.nthElement})`); }); this.ruleManager.updateRule(ruleIndex, updatedRule); this.updateRulesElement(); }); const deleteButton = document.getElementById(`deleteRule${ruleIndex}`); if (deleteButton) { deleteButton.addEventListener('click', () => { this.ruleManager.deleteRule(ruleIndex); this.updateRulesElement(); }); } }); } collapseAllRules() { if (this.currentExpandedRule !== null) { const ruleDetails = document.getElementById(`readRule${this.currentExpandedRule}`); if (ruleDetails) ruleDetails.style.display = 'none'; this.currentExpandedRule = null; } } collapseAllConflicts() { if (this.currentExpandedConflict !== null) { const conflictDetails = document.getElementById(`conflictDetails${this.currentExpandedConflict}`); if (conflictDetails) conflictDetails.style.display = 'none'; this.currentExpandedConflict = null; } } } class ShortcutAPI { constructor(config = {}) { this.config = { RuleC: true, RuleR: true, RuleU: ['ruleName', 'urlPattern', 'selectorType', 'selector', 'nthElement', 'shortcut', 'ifLinkOpen', 'isEnabled'], RuleD: true, ...config }; this.ruleManager = new RuleManager(); this.shortcutHandler = new ShortcutHandler(this.ruleManager); this.menuManager = null; if (this.config.RuleC || this.config.RuleR) { this.menuManager = new MenuManager(this.ruleManager, this.config); this.initMenus(); } } initMenus() { const i18n = this.menuManager.i18n[this.menuManager.getLanguage()]; if (this.config.RuleC) { GM_registerMenuCommand(i18n.titleAdd, () => this.menuManager.initAddRuleMenu()); } if (this.config.RuleR) { GM_registerMenuCommand(i18n.titleManage, () => this.menuManager.initManageRulesMenu()); } } addRule(rule) { this.ruleManager.addRule(rule); } getRules() { return this.ruleManager.clickRules.rules; } updateRule(index, updatedRule) { this.ruleManager.updateRule(index, updatedRule); } deleteRule(index) { this.ruleManager.deleteRule(index); } addRuleFromJSON(jsonString) { return this.ruleManager.addRuleFromJSON(jsonString); } initAddRuleMenu() { if (!this.menuManager) { this.menuManager = new MenuManager(this.ruleManager, this.config); } this.menuManager.initAddRuleMenu(); } initManageRulesMenu() { if (!this.menuManager) { this.menuManager = new MenuManager(this.ruleManager, this.config); } this.menuManager.initManageRulesMenu(); } } window.ShortcutLibrary = ShortcutAPI;