Created
December 12, 2025 12:57
-
-
Save xMarch/c79f98c2191b8866c3c8cd1b55d28d6f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name 灰基繁轉簡支援 (huiji-zh-t2s) | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.2 | |
| // @description Convert Traditional Chinese to Simplified Chinese in search inputs on ff14.huijiwiki.com | |
| // @author Gemini | |
| // @match https://ff14.huijiwiki.com/wiki/* | |
| // @match https://ff14.huijiwiki.com/index.php* | |
| // @grant none | |
| // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/t2cn.js | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| // 1. Setup OpenCC | |
| const converter = OpenCC.Converter({ from: 'tw', to: 'cn' }); | |
| // 2. Selectors | |
| const TARGET_SELECTORS = [ | |
| '#skin-root > div.skinroot-searcherwrapper > div > div > div > div.n-base-selection-label > input', | |
| 'input#ooui-php-1' | |
| ]; | |
| // 3. Create the Floating Button | |
| const btn = document.createElement('button'); | |
| btn.textContent = '轉簡(雙擊Ctrl)'; | |
| // Initialize with no binding | |
| btn.dataset.t2cBinding = ''; | |
| btn.style.cssText = ` | |
| z-index: 1000; | |
| display: none; | |
| position: absolute; | |
| right: 2rem; | |
| background: #3b3b3b; | |
| padding: 0 0.25rem; | |
| border-radius: 0.25rem; | |
| border: none; | |
| filter: drop-shadow(0px 0px 2px #cba971); | |
| color: #fff; | |
| cursor: pointer; | |
| font-size: 12px; | |
| line-height: 1.5; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| `; | |
| // 4. State Management | |
| let lastAltTime = 0; | |
| let hideTimeout = null; | |
| let globalInputCounter = 0; // Ensures every found input gets a unique ID | |
| // 5. Core Conversion Logic | |
| function convertText(inputElement) { | |
| if (!inputElement || !inputElement.value) return; | |
| const original = inputElement.value; | |
| const converted = converter(original); | |
| if (original !== converted) { | |
| inputElement.value = converted; | |
| // Dispatch events for frameworks (Vue/React) | |
| inputElement.dispatchEvent(new Event('input', { bubbles: true })); | |
| inputElement.dispatchEvent(new Event('change', { bubbles: true })); | |
| } | |
| } | |
| // 6. Main Logic: Attach to Inputs | |
| function attachListeners() { | |
| const inputs = document.querySelectorAll(TARGET_SELECTORS.join(',')); | |
| inputs.forEach((input) => { | |
| // A. Assign Unique ID (The "Solid Binding" Anchor) | |
| if (!input.dataset.t2sId) { | |
| // Assign an ID like 't2s-input-0', 't2s-input-1' based on discovery order | |
| input.dataset.t2sId = `t2s-input-${globalInputCounter++}`; | |
| } | |
| // Skip if event listeners are already attached | |
| if (input.dataset.t2sAttached) return; | |
| input.dataset.t2sAttached = "true"; | |
| // B. Handle Focus | |
| input.addEventListener('focus', () => { | |
| // Clear any pending hide requests | |
| if (hideTimeout) { | |
| clearTimeout(hideTimeout); | |
| hideTimeout = null; | |
| } | |
| // BINDING STEP: Link button to this specific input ID | |
| btn.dataset.t2cBinding = input.dataset.t2sId; | |
| // Move button to this input's container | |
| if (input.parentNode) { | |
| input.parentNode.style.position = 'relative'; | |
| input.parentNode.insertBefore(btn, input.nextSibling); | |
| btn.style.display = 'block'; | |
| } | |
| }); | |
| // C. Handle Blur | |
| input.addEventListener('blur', () => { | |
| hideTimeout = setTimeout(() => { | |
| const active = document.activeElement; | |
| // If we moved focus to the button itself, or another input, don't hide | |
| if (active !== btn && (!active.tagName || active.tagName !== 'INPUT')) { | |
| btn.style.display = 'none'; | |
| // We do NOT clear the binding here, preserving the link | |
| // in case the user clicks the button immediately after blur. | |
| } | |
| }, 200); | |
| }); | |
| }); | |
| } | |
| // 7. Button Click Handler (Using Data Binding) | |
| btn.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| if (hideTimeout) { | |
| clearTimeout(hideTimeout); | |
| hideTimeout = null; | |
| } | |
| // RETRIEVAL STEP: Find input using the binding ID | |
| const boundId = btn.dataset.t2cBinding; | |
| if (boundId) { | |
| const targetInput = document.querySelector(`input[data-t2s-id="${boundId}"]`); | |
| if (targetInput) { | |
| convertText(targetInput); | |
| targetInput.focus(); | |
| } | |
| } | |
| }); | |
| // 8. Alt Double-Click Handler | |
| document.addEventListener('keyup', (e) => { | |
| if (e.key === 'Control') { | |
| const now = Date.now(); | |
| if (now - lastAltTime < 400) { | |
| // Use document.activeElement as source of truth for hotkey | |
| const active = document.activeElement; | |
| if (active && active.tagName === 'INPUT' && active.dataset.t2sId) { | |
| convertText(active); | |
| } | |
| } | |
| lastAltTime = now; | |
| } | |
| }); | |
| // 9. Observer | |
| const observer = new MutationObserver(() => { | |
| attachListeners(); | |
| }); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| // Initial run | |
| attachListeners(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment