Last active
April 9, 2026 15:48
-
-
Save scottj/2b54464c7d5cbd6df7f2e38d4a04156e to your computer and use it in GitHub Desktop.
Keyboard refresh for Fidelity portfolio
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 Keyboard refresh for Fidelity portfolio | |
| // @namespace https://scottj.info/ | |
| // @version 1.2 | |
| // @description Press "u" or "r" to trigger a click action on configured pages. | |
| // @author Scott Johnson | |
| // @match https://digital.fidelity.com/* | |
| // @run-at document-idle | |
| // @grant none | |
| // ==/UserScript== | |
| (function () { | |
| "use strict"; | |
| /*** ------------------ CONFIG ------------------ ***/ | |
| // Map URL paths to the CSS selector of the element to click. | |
| // All paths are on digital.fidelity.com. | |
| const PATH_SELECTORS = { | |
| "/ftgw/digital/portfolio/activity": ".ao-refresh button", | |
| "/ftgw/digital/portfolio/positions": "div[data-testid=refresh-section] button", | |
| // Add more paths here: | |
| // "/ftgw/digital/portfolio/activity": "button.refresh", | |
| }; | |
| // Whether to click the FIRST matching button, or ALL | |
| const CLICK_FIRST_ONLY = true; | |
| // Keys that should trigger the action (lowercase single chars) | |
| const TRIGGER_KEYS = new Set(["u", "r"]); | |
| // Optional: add a small debounce to avoid rapid repeats | |
| const DEBOUNCE_MS = 200; | |
| /*** -------------------------------------------- ***/ | |
| let lastRun = 0; | |
| // Utility: don’t trigger while the user is typing in inputs/textareas | |
| function isTypingTarget(el) { | |
| if (!el) return false; | |
| const tag = el.tagName?.toLowerCase(); | |
| const editable = el.isContentEditable; | |
| return ( | |
| editable || | |
| tag === "input" || | |
| tag === "textarea" || | |
| tag === "select" | |
| ); | |
| } | |
| // Resolve the CSS selector for the current page path | |
| function getSelectorForPath() { | |
| const path = window.location.pathname; | |
| for (const [prefix, selector] of Object.entries(PATH_SELECTORS)) { | |
| if (path === prefix || path.startsWith(prefix + "/") || path.startsWith(prefix + "?")) { | |
| return selector; | |
| } | |
| } | |
| return null; | |
| } | |
| // Find the buttons for the current page | |
| function getButtons() { | |
| const selector = getSelectorForPath(); | |
| if (!selector) return []; | |
| return Array.from(document.querySelectorAll(selector)); | |
| } | |
| // The action to run when hotkey is pressed | |
| function runAction(triggerKey) { | |
| const now = Date.now(); | |
| if (now - lastRun < DEBOUNCE_MS) return; | |
| lastRun = now; | |
| const buttons = getButtons(); | |
| if (buttons.length === 0) { | |
| const selector = getSelectorForPath(); | |
| console.debug(`[Userscript] No buttons found for path "${window.location.pathname}"${selector ? ` with selector "${selector}"` : " (no selector configured)"}`); | |
| return; | |
| } | |
| if (CLICK_FIRST_ONLY) { | |
| const btn = buttons[0]; | |
| safeClick(btn, triggerKey); | |
| } else { | |
| buttons.forEach((btn) => safeClick(btn, triggerKey)); | |
| } | |
| } | |
| // Safely click: bring into view, focus, then click | |
| function safeClick(btn, triggerKey) { | |
| try { | |
| //btn.scrollIntoView({ behavior: "smooth", block: "center" }); | |
| btn.focus({ preventScroll: true }); | |
| // If the site uses data attributes or special handlers, add custom behavior here: | |
| // Example: btn.dispatchEvent(new Event('click', { bubbles: true })); | |
| btn.click(); | |
| console.debug(`[Userscript] Triggered "${triggerKey}" -> clicked`, btn); | |
| } catch (e) { | |
| console.warn("[Userscript] Failed to click button:", e); | |
| } | |
| } | |
| // Key listener | |
| window.addEventListener( | |
| "keydown", | |
| (e) => { | |
| // Respect IME and modifiers; only plain key presses | |
| if (e.isComposing || e.ctrlKey || e.metaKey || e.altKey) return; | |
| const key = e.key?.toLowerCase(); | |
| if (!TRIGGER_KEYS.has(key)) return; | |
| // Avoid firing while typing | |
| if (isTypingTarget(e.target)) return; | |
| // Optional: prevent default if the page has its own hotkeys for the same keys | |
| // e.preventDefault(); | |
| runAction(key); | |
| }, | |
| { passive: true } | |
| ); | |
| // Optional: watch for dynamic DOM changes (SPAs) | |
| //const mo = new MutationObserver(() => { | |
| // You can hook logic here if you need to ensure content is present | |
| // e.g., pre-cache button references if performance matters | |
| //}); | |
| //mo.observe(document.documentElement, { subtree: true, childList: true }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment