Last active
March 3, 2026 17:53
-
-
Save andesco/ac6e1e3a5708814cca61c14e0aa54c05 to your computer and use it in GitHub Desktop.
Wealthsimple Tax: fix paste userscript
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 Wealthsimple Tax: fix paste | |
| // @namespace https://andrewe.ca | |
| // @version 0.2.1 | |
| // @description Fix paste bug on Wealthsimple Tax, by automatically typing ".00" after pasting value in numeric inputs. | |
| // @author @andrewe.ca | |
| // @match https://my.wealthsimple.com/tax/* | |
| // @downloadURL https://gist.githubusercontent.com/andesco/ac6e1e3a5708814cca61c14e0aa54c05/raw/Wealthsimple%20Tax%3A%20fix%20paste.user.js | |
| // @updateURL https://gist.githubusercontent.com/andesco/ac6e1e3a5708814cca61c14e0aa54c05/raw/Wealthsimple%20Tax%3A%20fix%20paste.user.js | |
| // @run-at document-start | |
| // @grant none | |
| // @license MIT | |
| // ==/UserScript== | |
| // Wealthsimple Tax clears pasted values on blur (click, tap, tab out) for numeric fields unless the field also receives a typed edit or change. When this script detects a paste action in a numeric field (`data-son-field='{"type":"Numeric"}'`), it immediately types `.00`, which Wealthsimple Tax drops only if one or more decimal places already exist in the field, thereby preserving the numeric value. | |
| (() => { | |
| "use strict"; | |
| const queuedPasteTimes = new WeakMap(); | |
| const INSERTION_TEXT = ".00"; | |
| function getEventInput(event) { | |
| const path = typeof event.composedPath === "function" ? event.composedPath() : []; | |
| for (const node of path) { | |
| if (node instanceof HTMLInputElement) { | |
| return node; | |
| } | |
| } | |
| return event.target instanceof HTMLInputElement ? event.target : null; | |
| } | |
| function parseSonField(input) { | |
| const rawValue = input.getAttribute("data-son-field"); | |
| if (!rawValue) { | |
| return null; | |
| } | |
| try { | |
| return JSON.parse(rawValue); | |
| } catch { | |
| const typeMatch = rawValue.match(/"type"\s*:\s*"([^"]+)"/i); | |
| if (!typeMatch) { | |
| return null; | |
| } | |
| return { type: typeMatch[1] }; | |
| } | |
| } | |
| function isNumericInput(input) { | |
| if (!(input instanceof HTMLInputElement) || input.disabled || input.readOnly) { | |
| return false; | |
| } | |
| const sonField = parseSonField(input); | |
| return sonField?.type === "Numeric"; | |
| } | |
| function getKeyMeta(character) { | |
| if (character === ".") { | |
| return { code: "Period", keyCode: 190, charCode: 46 }; | |
| } | |
| return { | |
| code: `Digit${character}`, | |
| keyCode: 48 + Number(character), | |
| charCode: 48 + Number(character), | |
| }; | |
| } | |
| function dispatchKeyboardEvent(input, type, character) { | |
| const { code, keyCode, charCode } = getKeyMeta(character); | |
| input.dispatchEvent( | |
| new KeyboardEvent(type, { | |
| key: character, | |
| code, | |
| bubbles: true, | |
| cancelable: true, | |
| composed: true, | |
| keyCode, | |
| which: keyCode, | |
| charCode, | |
| }), | |
| ); | |
| } | |
| function insertCharacter(input, character) { | |
| dispatchKeyboardEvent(input, "keydown", character); | |
| dispatchKeyboardEvent(input, "keypress", character); | |
| const beforeInput = new InputEvent("beforeinput", { | |
| bubbles: true, | |
| cancelable: true, | |
| composed: true, | |
| inputType: "insertText", | |
| data: character, | |
| }); | |
| const beforeInputAllowed = input.dispatchEvent(beforeInput); | |
| let inserted = false; | |
| if (beforeInputAllowed) { | |
| input.setSelectionRange(input.value.length, input.value.length); | |
| try { | |
| inserted = document.execCommand("insertText", false, character); | |
| } catch { | |
| inserted = false; | |
| } | |
| if (!inserted && typeof input.setRangeText === "function") { | |
| const end = input.selectionEnd ?? input.value.length; | |
| input.setRangeText(character, end, end, "end"); | |
| inserted = true; | |
| } | |
| } | |
| if (inserted) { | |
| input.dispatchEvent( | |
| new InputEvent("input", { | |
| bubbles: true, | |
| composed: true, | |
| inputType: "insertText", | |
| data: character, | |
| }), | |
| ); | |
| } | |
| dispatchKeyboardEvent(input, "keyup", character); | |
| } | |
| function typeInsertionText(input) { | |
| input.focus({ preventScroll: true }); | |
| input.setSelectionRange(input.value.length, input.value.length); | |
| for (const character of INSERTION_TEXT) { | |
| insertCharacter(input, character); | |
| } | |
| } | |
| function applyPasteFix(input) { | |
| if (!input.isConnected || !isNumericInput(input)) { | |
| return; | |
| } | |
| if (!input.value) { | |
| return; | |
| } | |
| typeInsertionText(input); | |
| } | |
| function queuePasteFix(input) { | |
| if (!isNumericInput(input)) { | |
| return; | |
| } | |
| const now = Date.now(); | |
| const lastQueuedAt = queuedPasteTimes.get(input) || 0; | |
| if (now - lastQueuedAt < 50) { | |
| return; | |
| } | |
| queuedPasteTimes.set(input, now); | |
| window.setTimeout(() => applyPasteFix(input), 30); | |
| } | |
| document.addEventListener( | |
| "paste", | |
| (event) => { | |
| const input = getEventInput(event); | |
| if (input) { | |
| queuePasteFix(input); | |
| } | |
| }, | |
| true, | |
| ); | |
| document.addEventListener( | |
| "beforeinput", | |
| (event) => { | |
| if (event.inputType !== "insertFromPaste" && event.inputType !== "insertFromPasteAsQuotation") { | |
| return; | |
| } | |
| const input = getEventInput(event); | |
| if (input) { | |
| queuePasteFix(input); | |
| } | |
| }, | |
| true, | |
| ); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment