diff --git a/script.js b/script.js
index ae62158..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.14
+// @version 4.16
// @description Live WPM for 10FF tests
// @author wRadion
// @match *://10fastfingers.com/typing-test/*
@@ -13,55 +13,233 @@
/*******************
** 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);
-
-const divStyle =
- 'font-size: 22px;' +
- 'line-height: 18px;' +
- 'margin-bottom: -1px;';
+(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 commonStyle =
- 'background: ' + style.getPropertyValue('background-color') + ';' +
- 'border-radius: 4px 4px 0 0;' +
- 'border: ' + style.getPropertyValue('border') + ';' +
- 'margin: 0 4px;' +
- 'padding: 8px 12px 12px 12px;';
+ let cfg = loadSettings();
-const smallStyle =
- 'text-align: center;' +
- 'font-size: 10px;' +
- 'margin-bottom: 6px;';
+ function saveSettings() {
+ localStorage.setItem(SETTING_KEY, JSON.stringify(cfg));
+ }
-(function() {
- 'use strict';
-
- const html =
- '
' +
- '
' +
- '
' +
- '
' +
- '
' +
- '
';
- let infoBar = document.createElement('div');
- infoBar.innerHTML = html;
+ /* 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)
+ '
' +
+
+ // Header
+ '
⚙ Settings
' +
+
+ // Alignment
+ '
' +
+ '
Alignment
' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '
' +
+
+ // Visibility toggles
+ '
' +
+
+ '
' + // end settings panel
+ '
' + // end gear wrap
+
+ '
'; // end outer
+
+ const infoBar = document.createElement('div');
+ 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 */
- var language,
- inter,
+ let intervalId,
timer,
durationRatio,
startTime,
+ running,
keystrokesCorrect,
keystrokesWrong,
wordsCorrect,
@@ -70,7 +248,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 +268,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 +453,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 +520,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 +542,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 +561,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 +618,7 @@ const smallStyle =
updateWs(wordsCorrect, wordsWrong);
updateRaw((tmp * durationRatio).toFixed(2));
}
- };
+ });
/* CODE */
reset();