click it for you

在符合正則表達式的網址上自動點擊指定的元素。

As of 2025-06-14. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         click it for you
// @name:zh-TW   為你自動點擊
// @name:ja      あなたのためにクリック
// @name:en      click it for you
// @name:de      Für dich klicken
// @name:es      Clic automático para ti
// @description  在符合正則表達式的網址上自動點擊指定的元素。
// @description:zh-TW 在符合正則表達式的網址上自動點擊指定的元素。
// @description:ja 正規表現に一致するURLで指定された要素を自動的にクリックします。
// @description:en Automatically clicks specified elements on URLs matching a regular expression.
// @description:de Klickt automatisch auf angegebene Elemente auf URLs, die mit einem regulären Ausdruck übereinstimmen.
// @description:es Hace clic automáticamente en elementos especificados en URLs que coinciden con una expresión regular.

// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_info
// @version      1.0.5

// @author       Max
// @namespace    https://github.com/Max46656
// @license      MPL2.0
// ==/UserScript==

class RuleManager {
    clickRules;

    constructor() {
        this.clickRules = 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();
    }

    updateRules() {
        GM_setValue('clickRules', this.clickRules);
    }
}

class WebElementHandler {
    ruleManager;
    clickTaskManager;
    i18n = {
        'zh-TW': {
            title: '自動點擊設定',
            matchingRules: '符合的規則',
            noMatchingRules: '當前網址無符合的規則。',
            addRuleSection: '新增規則',
            ruleName: '規則名稱:',
            urlPattern: '網址正則表達式:',
            selectorType: '選擇器類型:',
            selector: '選擇器:',
            nthElement: '第幾個元素(從 1 開始):',
            clickDelay: '點擊延遲(毫秒):',
            addRule: '新增規則',
            save: '儲存',
            delete: '刪除',
            ruleNamePlaceholder: '例如:我的規則',
            urlPatternPlaceholder: '例如:https://example\\.com/.*',
            selectorPlaceholder: '例如:button.submit 或 //button[@class="submit"]',
            invalidRegex: '無效的正則表達式',
            invalidSelector: '無效的選擇器'
        },
        'en': {
            title: 'Auto Click Configuration',
            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 (1-based):',
            clickDelay: 'Click Delay (ms):',
            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"]',
            invalidRegex: 'Invalid regular expression',
            invalidSelector: 'Invalid selector'
        },
        'ja': {
            title: '自動クリック設定',
            matchingRules: '一致するルール',
            noMatchingRules: '現在のURLに一致するルールはありません。',
            addRuleSection: '新しいルールを追加',
            ruleName: 'ルール名:',
            urlPattern: 'URLパターン(正規表現):',
            selectorType: 'セレクタタイプ:',
            selector: 'セレクタ:',
            nthElement: '何番目の要素(1から):',
            clickDelay: 'クリック遅延(ミリ秒):',
            addRule: 'ルールを追加',
            save: '儲存',
            delete: '削除',
            ruleNamePlaceholder: '例:マイルール',
            urlPatternPlaceholder: '例:https://example\\.com/.*',
            selectorPlaceholder: '例:button.submit または //button[@class="submit"]',
            invalidRegex: '無効な正規表現',
            invalidSelector: '無効なセレクター'
        },
        'de': {
            title: 'Automatische Klick-Einstellungen',
            matchingRules: 'Passende Regeln',
            noMatchingRules: 'Keine Regeln passen zur aktuellen URL.',
            addRuleSection: 'Neue Regel hinzufügen',
            ruleName: 'Regelname:',
            urlPattern: 'URL-Muster (Regulärer Ausdruck):',
            selectorType: 'Selektortyp:',
            selector: 'Selektor:',
            nthElement: 'N-tes Element (ab 1):',
            clickDelay: 'Klickverzögerung (ms):',
            addRule: 'Regel hinzufügen',
            save: 'Speichern',
            delete: 'Löschen',
            ruleNamePlaceholder: 'Beispiel: Meine Regel',
            urlPatternPlaceholder: 'Beispiel: https://example\\.com/.*',
            selectorPlaceholder: 'Beispiel: button.submit oder //button[@class="submit"]',
            invalidRegex: 'Ungültiger regulärer Ausdruck',
            invalidSelector: 'Ungültiger Selektor'
        },
        'es': {
            title: 'Configuración de Clic Automático',
            matchingRules: 'Reglas Coincidentes',
            noMatchingRules: 'No hay reglas que coincidan con la URL actual.',
            addRuleSection: 'Agregar Nueva Regla',
            ruleName: 'Nombre de la Regla:',
            urlPattern: 'Patrón de URL (Regex):',
            selectorType: 'Tipo de Selector:',
            selector: 'Selector:',
            nthElement: 'N-ésimo Elemento (desde 1):',
            clickDelay: 'Retraso de Clic (ms):',
            addRule: 'Agregar Regla',
            save: 'Guardar',
            delete: 'Eliminar',
            ruleNamePlaceholder: 'Ejemplo: Mi Regla',
            urlPatternPlaceholder: 'Ejemplo: https://example\\.com/.*',
            selectorPlaceholder: 'Ejemplo: button.submit o //button[@class="submit"]',
            invalidRegex: 'Expresión regular inválida',
            invalidSelector: 'Selector inválido'
        }
    };

    constructor(ruleManager, clickTaskManager) {
        this.ruleManager = ruleManager;
        this.clickTaskManager = clickTaskManager;
        this.setupUrlChangeListener();
    }

    // 獲取選單標題(用於 registerMenu)
    getMenuTitle() {
        return this.i18n[this.getLanguage()].title;
    }

    // 獲取當前語言
    getLanguage() {
        const lang = navigator.language || navigator.userLanguage;
        if (lang.startsWith('zh')) return 'zh-TW';
        if (lang.startsWith('ja')) return 'ja';
        if (lang.startsWith('de')) return 'de';
        if (lang.startsWith('es')) return 'es';
        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;
        }
        return true;
    }

    // 創建單個規則的 HTML 結構
    createRuleElement(rule, ruleIndex) {
        const i18n = this.i18n[this.getLanguage()];
        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;">
                    <label>${i18n.ruleName}</label>
                    <input type="text" id="updateRuleName${ruleIndex}" value="${rule.ruleName || ''}">
                    <label>${i18n.urlPattern}</label>
                    <input type="text" id="updateUrlPattern${ruleIndex}" value="${rule.urlPattern}">
                    <label>${i18n.selectorType}</label>
                    <select id="updateSelectorType${ruleIndex}">
                        <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}">
                    <label>${i18n.nthElement}</label>
                    <input type="number" id="updateNthElement${ruleIndex}" min="1" value="${rule.nthElement}">
                    <label>${i18n.clickDelay}</label>
                    <input type="number" id="updateClickDelay${ruleIndex}" min="100" value="${rule.clickDelay || 200}">
                    <button id="updateRule${ruleIndex}">${i18n.save}</button>
                    <button id="deleteRule${ruleIndex}">${i18n.delete}</button>
                </div>
            `;
            return ruleDiv;
        }

        // 創建組態選單
        createMenuElement() {
            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>
                    #autoClickMenu {
                        overflow-y: auto;
                        max-height: 80vh;
                    }
                    #autoClickMenu input, #autoClickMenu select, #autoClickMenu 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;
                    }
                    #autoClickMenu button {
                        cursor: pointer;
                    }
                    #autoClickMenu button:hover {
                        background: rgb(70, 70, 70);
                    }
                    #autoClickMenu label {
                        margin-top: 5px;
                        display: block;
                    }
                    #autoClickMenu .ruleHeader {
                        cursor: pointer;
                        background: rgb(50, 50, 50);
                        padding: 5px;
                        margin: 5px 0;
                        border-radius: 3px;
                    }
                    #autoClickMenu .readRule {
                        padding: 5px;
                        border: 1px solid rgb(80, 80, 80);
                        border-radius: 3px;
                        margin-bottom: 5px;
                    }
                    #autoClickMenu .headerContainer {
                        display: flex;
                        justify-content: space-between;
                        align-items: center;
                        margin-bottom: 10px;
                    }
                    #autoClickMenu .closeButton {
                        width: auto;
                        padding: 5px 10px;
                        margin: 0;
                    }
                </style>
                <div id="autoClickMenu">
                    <div class="headerContainer">
                        <h3>${i18n.title}</h3>
                        <button id="closeMenu" class="closeButton">✕</button>
                    </div>
                    <div id="rulesList"></div>
                    <h4>${i18n.addRuleSection}</h4>
                    <label>${i18n.ruleName}</label>
                    <input type="text" id="ruleName" placeholder="${i18n.ruleNamePlaceholder}">
                    <label>${i18n.urlPattern}</label>
                    <input type="text" id="urlPattern" placeholder="${i18n.urlPatternPlaceholder}">
                    <label>${i18n.selectorType}</label>
                    <select id="selectorType">
                        <option value="css">CSS</option>
                        <option value="xpath">XPath</option>
                    </select>
                    <label>${i18n.selector}</label>
                    <input type="text" id="selector" placeholder="${i18n.selectorPlaceholder}">
                    <label>${i18n.nthElement}</label>
                    <input type="number" id="nthElement" min="1" value="1">
                    <label>${i18n.clickDelay}</label>
                    <input type="number" id="clickDelay" min="50" value="10000">
                    <button id="addRule" style="margin-top: 10px;">${i18n.addRule}</button>
                </div>
            `;
            document.body.appendChild(menu);

            this.updateRulesElement();

            document.getElementById('addRule').addEventListener('click', () => {
                const newRule = {
                    ruleName: document.getElementById('ruleName').value || `規則 ${this.ruleManager.clickRules.rules.length + 1}`,
                    urlPattern: document.getElementById('urlPattern').value,
                    selectorType: document.getElementById('selectorType').value,
                    selector: document.getElementById('selector').value,
                    nthElement: parseInt(document.getElementById('nthElement').value) || 1,
                    clickDelay: parseInt(document.getElementById('clickDelay').value) || 200
                };
                if (!this.validateRule(newRule)) return;
                this.ruleManager.addRule(newRule);
                this.updateRulesElement();
                this.clickTaskManager.clearAutoClicks();
                this.clickTaskManager.runAutoClicks();
                document.getElementById('ruleName').value = '';
                document.getElementById('urlPattern').value = '';
                document.getElementById('selector').value = '';
                document.getElementById('nthElement').value = '1';
                document.getElementById('clickDelay').value = '200';
            });

            document.getElementById('closeMenu').addEventListener('click', () => {
                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) => {
                const ruleIndex = this.ruleManager.clickRules.rules.indexOf(rule);
                const ruleDiv = this.createRuleElement(rule, ruleIndex);
                rulesList.appendChild(ruleDiv);

                document.getElementById(`ruleHeader${ruleIndex}`).addEventListener('click', () => {
                    const details = document.getElementById(`readRule${ruleIndex}`);
                    details.style.display = details.style.display === 'none' ? 'block' : 'none';
                });

                document.getElementById(`updateRule${ruleIndex}`).addEventListener('click', () => {
                    const updatedRule = {
                        ruleName: document.getElementById(`updateRuleName${ruleIndex}`).value || `規則 ${ruleIndex + 1}`,
                        urlPattern: document.getElementById(`updateUrlPattern${ruleIndex}`).value,
                        selectorType: document.getElementById(`updateSelectorType${ruleIndex}`).value,
                        selector: document.getElementById(`updateSelector${ruleIndex}`).value,
                        nthElement: parseInt(document.getElementById(`updateNthElement${ruleIndex}`).value) || 1,
                        clickDelay: parseInt(document.getElementById(`updateClickDelay${ruleIndex}`).value) || 200
                    };
                    if (!this.validateRule(updatedRule)) return;
                    this.ruleManager.updateRule(ruleIndex, updatedRule);
                    this.updateRulesElement();
                    this.clickTaskManager.clearAutoClicks();
                    this.clickTaskManager.runAutoClicks();
                });

                document.getElementById(`deleteRule${ruleIndex}`).addEventListener('click', () => {
                    this.ruleManager.deleteRule(ruleIndex);
                    this.updateRulesElement();
                    this.clickTaskManager.clearAutoClicks();
                    this.clickTaskManager.runAutoClicks();
                });
            });
        }

        // 設置 URL 變更監聽器
        setupUrlChangeListener() {
            const oldPushState = history.pushState;
            history.pushState = function pushState() {
                const ret = oldPushState.apply(this, arguments);
                window.dispatchEvent(new Event('pushstate'));
                window.dispatchEvent(new Event('locationchange'));
                return ret;
            };

            const oldReplaceState = history.replaceState;
            history.replaceState = function replaceState() {
                const ret = oldReplaceState.apply(this, arguments);
                window.dispatchEvent(new Event('replacestate'));
                window.dispatchEvent(new Event('locationchange'));
                return ret;
            };

            window.addEventListener('popstate', () => {
                window.dispatchEvent(new Event('locationchange'));
            });

            window.addEventListener('locationchange', () => {
                this.clickTaskManager.clearAutoClicks();
                this.clickTaskManager.runAutoClicks();
            });
        }
    }

class ClickTaskManager {
    ruleManager;
    intervalIds = {};

    constructor(ruleManager) {
        this.ruleManager = ruleManager;
        this.runAutoClicks();
    }

    // 清除所有自動點擊任務
    clearAutoClicks() {
        Object.keys(this.intervalIds).forEach(index => {
            clearInterval(this.intervalIds[index]);
            delete this.intervalIds[index];
        });
    }

    // 執行所有符合規則的自動點擊
    runAutoClicks() {
        this.ruleManager.clickRules.rules.forEach((rule, index) => {
            if (rule.urlPattern && rule.selector && !this.intervalIds[index]) {
                const intervalId = setInterval(() => {
                    const clicked = this.autoClick(rule, index);
                    if (clicked) {
                        clearInterval(this.intervalIds[index]);
                        delete this.intervalIds[index];
                    }
                }, rule.clickDelay || 200);
                this.intervalIds[index] = intervalId;
            } else if (!rule.urlPattern || !rule.selector) {
                console.warn(`${GM_info.script.name}: 規則 "${rule.ruleName}" 無效(索引 ${index}):缺少 urlPattern 或 selector`);
            }
        });
    }

    // 執行單條規則的自動點擊,並返回是否成功
    autoClick(rule, ruleIndex) {
        try {
            const urlRegex = new RegExp(rule.urlPattern);
            if (!urlRegex.test(window.location.href)) {
                return false;
            }

            const elements = this.getElements(rule.selectorType, rule.selector);
            if (elements.length === 0) {
                console.warn(`${GM_info.script.name}: 規則 "${rule.ruleName}" 未找到符合元素:`, rule.selector);
                return false;
            }

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

            const targetElement = elements[rule.nthElement - 1];
            if (targetElement) {
                console.log(`${GM_info.script.name}: 規則 "${rule.ruleName}" 成功點擊元素:`, targetElement);
                targetElement.click();
                if (targetElement.tagName === "A" && targetElement.href) {
                    window.location.href = targetElement.href;
                }
                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;
        }
    }

    // 根據選擇器類型獲取元素
    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 [];
        }
    }
}

const Shirisaku = new RuleManager();
const Yubisaku = new ClickTaskManager(Shirisaku);
const Mika = new WebElementHandler(Shirisaku, Yubisaku);
GM_registerMenuCommand(Mika.getMenuTitle(), () => Mika.createMenuElement());