From c465ce130fb89c217b057c89a87a9c9335a16417 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 01:19:44 +0000 Subject: [PATCH 1/2] Fix bugs and modernize event handling in script.js - Fix null regex matching bug in getKeystrokes: passing null to String.match() matched the literal string "null"; extracted a countMatches() helper that guards against null patterns - Replace deprecated e.keyCode with e.key ('F5', ' ') - Replace direct event property assignments (onkeydown, oninput, etc.) with addEventListener to avoid clobbering existing handlers - Replace confusing null/undefined dual-state for startTime with an explicit boolean `running` flag - Replace var with let/const throughout; rename `inter` to `intervalId` - Bump version to 4.15 https://claude.ai/code/session_01N9V91DXkQC4uZhvJPiu63P --- script.js | 69 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/script.js b/script.js index ae62158..733c47b 100644 --- a/script.js +++ b/script.js @@ -1,7 +1,7 @@ // ==UserScript== // @name 10FF Live WPM // @namespace https://github.com/wRadion/10FFLiveWPMScript -// @version 4.14 +// @version 4.15 // @description Live WPM for 10FF tests // @author wRadion // @match *://10fastfingers.com/typing-test/* @@ -52,16 +52,16 @@ const smallStyle = '
Words
|
' + '
Score
WPM
' + ''; - let infoBar = document.createElement('div'); + const infoBar = document.createElement('div'); infoBar.innerHTML = html; document.querySelector('#words').before(infoBar); /* VARIABLES */ - var language, - inter, + let intervalId, timer, durationRatio, startTime, + running, keystrokesCorrect, keystrokesWrong, wordsCorrect, @@ -70,7 +70,7 @@ const smallStyle = /* SETUP */ const languageId = parseInt(document.querySelector('#speedtest-id').attributes.value.value); - language = [ + const language = [ null, 'english', 'german', 'french', 'portuguese', 'spanish', 'indonesian', 'turkish', 'vietnamese', 'polish', 'romanian', 'malaysian', 'norwegian', 'persian', 'hungarian', 'chinese_traditional', 'chinese_simplified', 'danish', 'dutch', 'swedish', 'italian', 'finnish', 'serbian', 'catalan', 'filipino', 'croatian', 'russian', 'arabic', 'bulgarian', 'japanese', 'albanian', 'korean', 'greek', 'czech', 'estonian', 'latvian', 'hebrew', 'urdu', 'galician', 'lithuanian', 'georgian', 'armenian', 'kurdish', 'azerbaijani', 'hindi', 'slovak', 'slovenian', null, 'icelandic', null, 'thai', 'pashto', 'esperanto', 'ukrainian', 'macedonian', 'malagasy', 'bengali' @@ -90,12 +90,17 @@ const smallStyle = function updateWs(wc, ww) { document.getElementById("live-wc").innerText = wc; document.getElementById("live-ww").innerText = ww; } function updateRaw(raw) { document.getElementById("live-raw").innerText = raw; } + function countMatches(word, pattern) { + if (!pattern) return 0; + return (word.match(pattern) || []).length; + } + function getKeystrokes(word) { - var oneKeystroke = null; - var twoKeystrokes = null; - var threeKeystrokes = null; - var fourKeystrokes = null; - var fiveKeystrokes = null; + let oneKeystroke = null; + let twoKeystrokes = null; + let threeKeystrokes = null; + let fourKeystrokes = null; + let fiveKeystrokes = null; switch (language) { /****************************** @@ -270,12 +275,12 @@ const smallStyle = oneKeystroke = /[a-z]/g; twoKeystrokes = /[A-Zăâîșț]/g; // Should be one break; - + case 'russian': oneKeystroke = /[явертыуиопшщэючасдфгхйклзьцжбнм\-]/g; twoKeystrokes = /[ЮёЁъЪЧЯВЕРТЫУИОПШЩЭАСДФГХЙКЛЗЬЦЖБНМ]/g; // phonetic layout break; - + case 'serbian': oneKeystroke = /[a-zćčđšž]/g; twoKeystrokes = /[A-ZĆČĐŠŽ]/g; @@ -337,11 +342,11 @@ const smallStyle = /******************************/ } - const one = (word.match(oneKeystroke) || []).length; - const two = (word.match(twoKeystrokes) || []).length * 2; - const three = (word.match(threeKeystrokes) || []).length * 3; - const four = (word.match(fourKeystrokes) || []).length * 4; - const five = (word.match(fiveKeystrokes) || []).length * 5; + const one = countMatches(word, oneKeystroke); + const two = countMatches(word, twoKeystrokes) * 2; + const three = countMatches(word, threeKeystrokes) * 3; + const four = countMatches(word, fourKeystrokes) * 4; + const five = countMatches(word, fiveKeystrokes) * 5; let extra = 0; if (language === "arabic") { @@ -359,11 +364,12 @@ const smallStyle = } function reset() { - if (inter) clearInterval(inter); - inter = null; + if (intervalId) clearInterval(intervalId); + intervalId = null; timer = getDuration(); durationRatio = 60 / timer; startTime = null; + running = false; keystrokesCorrect = 0; keystrokesWrong = 0; wordsCorrect = 0; @@ -377,44 +383,45 @@ const smallStyle = } function stop() { - if (inter) clearInterval(inter); - startTime = undefined; + if (intervalId) clearInterval(intervalId); + running = false; } function start() { startTime = Date.now(); + running = true; updateWpm(0); updateKs(0, 0); updateWs(0, 0); updateRaw(0); - inter = setInterval(() => { if (--timer === 0) stop(); }, 1000); + intervalId = setInterval(() => { if (--timer === 0) stop(); }, 1000); } /* EVENT HANDLERS */ // F5 - document.onkeydown = function(e) { if (e.keyCode === 116) { reset(); } }; + document.addEventListener('keydown', function(e) { if (e.key === 'F5') { reset(); } }); // Reload Button - document.getElementById("reload-btn").onclick = function(e) { reset(); }; + document.getElementById("reload-btn").addEventListener('click', function(e) { reset(); }); // Apply Settings Button (Custom Test) const applySettingsBtn = document.getElementById("apply-settings"); if (applySettingsBtn) { - applySettingsBtn.onclick = function(e) { reset(); }; + applySettingsBtn.addEventListener('click', function(e) { reset(); }); } - document.getElementById("inputfield").oninput = function(e) { + document.getElementById("inputfield").addEventListener('input', function(e) { if (startTime === null) start(); - }; + }); - document.getElementById("inputfield").onkeyup = function(e) { - if (startTime === undefined) return; + document.getElementById("inputfield").addEventListener('keyup', function(e) { + if (!running) return; if (document.getElementById('words').style.display === 'none') { stop(); return; } - if (e.keyCode === 32) { + if (e.key === ' ') { const word = document.querySelectorAll(".correct[wordnr]")[index++]; if (word) { @@ -433,7 +440,7 @@ const smallStyle = updateWs(wordsCorrect, wordsWrong); updateRaw((tmp * durationRatio).toFixed(2)); } - }; + }); /* CODE */ reset(); From f09785d34e4380f25ad48e5a3d6edcc4a302b27e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 01:36:27 +0000 Subject: [PATCH 2/2] Add live settings panel via gear icon (v4.16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add a ⚙ gear button pinned to the right edge of the stats bar - Clicking it opens a floating panel with: - Alignment selector (Left / Center / Right) with active state highlight - Visibility checkboxes for Speed, Keystrokes, Words, Score - Changes apply instantly to the UI (no page reload needed) - Settings persist across sessions via localStorage - Hardcoded const defaults are still respected as fallback values - Panel closes when clicking outside; uses stopPropagation internally https://claude.ai/code/session_01N9V91DXkQC4uZhvJPiu63P --- script.js | 242 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 210 insertions(+), 32 deletions(-) diff --git a/script.js b/script.js index 733c47b..20277f8 100644 --- a/script.js +++ b/script.js @@ -1,7 +1,7 @@ // ==UserScript== // @name 10FF Live WPM // @namespace https://github.com/wRadion/10FFLiveWPMScript -// @version 4.15 +// @version 4.16 // @description Live WPM for 10FF tests // @author wRadion // @match *://10fastfingers.com/typing-test/* @@ -13,49 +13,227 @@ /******************* ** CUSTOMISATION ** + ** These are now defaults. Use the ⚙ gear icon on the page to + ** change settings live — no code editing required! *******************/ -const alignment = 'center'; // left | center | right -const speedVisible = true; // true | false -const keystrokesVisible = true; // true | false -const wordsVisible = true; // true | false -const scoreVisible = true; // true | false +const DEFAULT_ALIGNMENT = 'center'; // left | center | right +const DEFAULT_SPEED_VISIBLE = true; // true | false +const DEFAULT_KEYSTROKES_VISIBLE = true; // true | false +const DEFAULT_WORDS_VISIBLE = true; // true | false +const DEFAULT_SCORE_VISIBLE = true; // true | false /*******************/ -const style = window.getComputedStyle(document.getElementById('words'), null); +(function() { + 'use strict'; + + /* SETTINGS */ + const SETTING_KEY = '10ff-livewpm-settings'; + + function loadSettings() { + try { + const saved = JSON.parse(localStorage.getItem(SETTING_KEY)); + if (saved && typeof saved === 'object') { + return { + alignment: ['left', 'center', 'right'].includes(saved.alignment) + ? saved.alignment : DEFAULT_ALIGNMENT, + speedVisible: typeof saved.speedVisible === 'boolean' + ? saved.speedVisible : DEFAULT_SPEED_VISIBLE, + keystrokesVisible: typeof saved.keystrokesVisible === 'boolean' + ? saved.keystrokesVisible : DEFAULT_KEYSTROKES_VISIBLE, + wordsVisible: typeof saved.wordsVisible === 'boolean' + ? saved.wordsVisible : DEFAULT_WORDS_VISIBLE, + scoreVisible: typeof saved.scoreVisible === 'boolean' + ? saved.scoreVisible : DEFAULT_SCORE_VISIBLE, + }; + } + } catch (e) {} + return { + alignment: DEFAULT_ALIGNMENT, + speedVisible: DEFAULT_SPEED_VISIBLE, + keystrokesVisible: DEFAULT_KEYSTROKES_VISIBLE, + wordsVisible: DEFAULT_WORDS_VISIBLE, + scoreVisible: DEFAULT_SCORE_VISIBLE, + }; + } -const divStyle = - 'font-size: 22px;' + - 'line-height: 18px;' + - 'margin-bottom: -1px;'; + let cfg = loadSettings(); -const commonStyle = - 'background: ' + style.getPropertyValue('background-color') + ';' + - 'border-radius: 4px 4px 0 0;' + - 'border: ' + style.getPropertyValue('border') + ';' + - 'margin: 0 4px;' + - 'padding: 8px 12px 12px 12px;'; + function saveSettings() { + localStorage.setItem(SETTING_KEY, JSON.stringify(cfg)); + } -const smallStyle = - 'text-align: center;' + - 'font-size: 10px;' + - 'margin-bottom: 6px;'; + /* PAGE STYLES */ + const pageStyle = window.getComputedStyle(document.getElementById('words'), null); + const bgColor = pageStyle.getPropertyValue('background-color'); + const borderVal = pageStyle.getPropertyValue('border'); + + const commonStyle = + 'background:' + bgColor + ';' + + 'border-radius:4px 4px 0 0;' + + 'border:' + borderVal + ';' + + 'margin:0 4px;' + + 'padding:8px 12px 12px 12px;'; + + const smallStyle = + 'text-align:center;' + + 'font-size:10px;' + + 'margin-bottom:6px;'; + + /* BUILD INFO BAR HTML */ + const d = (visible) => visible ? 'inline-block' : 'none'; + + const barHTML = + '
' + + + // Stats boxes (alignment-controlled) + '
' + + '
' + + '
Speed
' + + ' WPM' + + '
' + + '
' + + '
Keystrokes
' + + ' | ' + + '
' + + '
' + + '
Words
' + + ' | ' + + '
' + + '
' + + '
Score
' + + ' WPM' + + '
' + + '
' + + + // Gear icon — always pinned to the right edge + '
' + + + '
' + + + // Settings panel (drops down from gear button) + '' + // end settings panel + '
' + // end gear wrap + + '
'; // end outer -(function() { - 'use strict'; - - const html = - '
' + - '
Speed
WPM
' + - '
Keystrokes
|
' + - '
Words
|
' + - '
Score
WPM
' + - '
'; const infoBar = document.createElement('div'); - infoBar.innerHTML = html; + infoBar.innerHTML = barHTML; document.querySelector('#words').before(infoBar); + /* SETTINGS PANEL LOGIC */ + + function syncPanel() { + document.getElementById('live-chk-speed').checked = cfg.speedVisible; + document.getElementById('live-chk-ks').checked = cfg.keystrokesVisible; + document.getElementById('live-chk-words').checked = cfg.wordsVisible; + document.getElementById('live-chk-score').checked = cfg.scoreVisible; + document.querySelectorAll('#live-align-btns button').forEach(btn => { + const active = btn.dataset.align === cfg.alignment; + btn.style.background = active ? 'rgba(0,0,0,0.12)' : 'transparent'; + btn.style.fontWeight = active ? 'bold' : 'normal'; + }); + } + + function applySettings() { + document.getElementById('live-wpm-bar').style.textAlign = cfg.alignment; + document.getElementById('live-speed-box').style.display = cfg.speedVisible ? 'inline-block' : 'none'; + document.getElementById('live-ks-box').style.display = cfg.keystrokesVisible ? 'inline-block' : 'none'; + document.getElementById('live-words-box').style.display = cfg.wordsVisible ? 'inline-block' : 'none'; + document.getElementById('live-score-box').style.display = cfg.scoreVisible ? 'inline-block' : 'none'; + saveSettings(); + syncPanel(); + } + + syncPanel(); + + // Toggle panel + document.getElementById('live-gear-btn').addEventListener('click', function(e) { + e.stopPropagation(); + const panel = document.getElementById('live-settings-panel'); + const willOpen = panel.style.display === 'none'; + panel.style.display = willOpen ? 'block' : 'none'; + if (willOpen) syncPanel(); + }); + + // Close panel on outside click + document.addEventListener('click', function() { + const panel = document.getElementById('live-settings-panel'); + if (panel) panel.style.display = 'none'; + }); + document.getElementById('live-settings-panel').addEventListener('click', e => e.stopPropagation()); + + // Alignment buttons + document.querySelectorAll('#live-align-btns button').forEach(btn => { + btn.addEventListener('click', function() { + cfg.alignment = this.dataset.align; + applySettings(); + }); + }); + + // Visibility checkboxes + [ + ['live-chk-speed', 'speedVisible'], + ['live-chk-ks', 'keystrokesVisible'], + ['live-chk-words', 'wordsVisible'], + ['live-chk-score', 'scoreVisible'], + ].forEach(([id, key]) => { + document.getElementById(id).addEventListener('change', function() { + cfg[key] = this.checked; + applySettings(); + }); + }); + /* VARIABLES */ let intervalId, timer,