@testgorilla/tgo-typing-test 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +46 -0
- package/jest.config.ts +21 -0
- package/ng-package.json +15 -0
- package/package.json +7 -22
- package/project.json +36 -0
- package/src/lib/components/tgo-typing-replay-input/tgo-typing-replay-input.component.html +30 -0
- package/src/lib/components/tgo-typing-replay-input/tgo-typing-replay-input.component.spec.ts +250 -0
- package/src/lib/components/tgo-typing-replay-input/tgo-typing-replay-input.component.ts +47 -0
- package/src/lib/components/tgo-typing-test/tgo-typing-test.component.html +72 -0
- package/src/lib/components/tgo-typing-test/tgo-typing-test.component.spec.ts +699 -0
- package/src/lib/components/tgo-typing-test/tgo-typing-test.component.ts +287 -0
- package/src/lib/helpers/config.ts +28 -0
- package/src/lib/helpers/constants/default-config.ts +103 -0
- package/src/lib/helpers/controllers/input-controller.ts +710 -0
- package/src/lib/helpers/controllers/quotes-controller.ts +183 -0
- package/src/lib/helpers/observables/banner-event.ts +18 -0
- package/src/lib/helpers/observables/config-event.ts +31 -0
- package/src/lib/helpers/observables/timer-event.ts +18 -0
- package/src/lib/helpers/states/active-page.ts +9 -0
- package/src/lib/helpers/states/composition.ts +29 -0
- package/src/lib/helpers/states/page-transition.ts +9 -0
- package/src/lib/helpers/states/slow-timer.ts +16 -0
- package/src/lib/helpers/states/test-active.ts +9 -0
- package/src/lib/helpers/states/time.ts +13 -0
- package/src/lib/helpers/test/caps-warning.ts +50 -0
- package/src/lib/helpers/test/caret.ts +92 -0
- package/src/lib/helpers/test/custom-text.ts +73 -0
- package/src/lib/helpers/test/english-punctuation.ts +38 -0
- package/src/lib/helpers/test/focus.ts +39 -0
- package/src/lib/helpers/test/manual-restart-tracker.ts +13 -0
- package/src/lib/helpers/test/out-of-focus.ts +19 -0
- package/src/lib/helpers/test/replay.ts +265 -0
- package/src/lib/helpers/test/test-input.ts +320 -0
- package/src/lib/helpers/test/test-logic.ts +1039 -0
- package/src/lib/helpers/test/test-state.ts +17 -0
- package/src/lib/helpers/test/test-stats.ts +442 -0
- package/src/lib/helpers/test/test-timer.ts +209 -0
- package/src/lib/helpers/test/test-ui.ts +370 -0
- package/src/lib/helpers/test/test-words.ts +72 -0
- package/src/lib/helpers/test/timer-progress.ts +16 -0
- package/src/lib/helpers/test/tts.ts +42 -0
- package/src/lib/helpers/test/weak-spot.ts +74 -0
- package/src/lib/helpers/test/wordset.ts +109 -0
- package/src/lib/styles/animations.scss +101 -0
- package/src/lib/styles/caret.scss +108 -0
- package/src/lib/styles/core.scss +498 -0
- package/src/lib/styles/index.scss +19 -0
- package/src/lib/styles/inputs.scss +290 -0
- package/src/lib/styles/popups.scss +1311 -0
- package/src/lib/styles/test.scss +1008 -0
- package/src/lib/styles/z_media-queries.scss +848 -0
- package/src/lib/types/types.d.ts +731 -0
- package/src/lib/utils/misc.ts +776 -0
- package/src/test-setup.ts +20 -0
- package/tsconfig.json +16 -0
- package/tsconfig.lib.json +14 -0
- package/tsconfig.lib.prod.json +9 -0
- package/tsconfig.spec.json +11 -0
- package/esm2022/index.mjs +0 -3
- package/esm2022/lib/components/tgo-typing-replay-input/tgo-typing-replay-input.component.mjs +0 -45
- package/esm2022/lib/components/tgo-typing-test/tgo-typing-test.component.mjs +0 -299
- package/esm2022/lib/helpers/config.mjs +0 -24
- package/esm2022/lib/helpers/constants/default-config.mjs +0 -103
- package/esm2022/lib/helpers/controllers/input-controller.mjs +0 -586
- package/esm2022/lib/helpers/controllers/quotes-controller.mjs +0 -118
- package/esm2022/lib/helpers/observables/config-event.mjs +0 -16
- package/esm2022/lib/helpers/observables/timer-event.mjs +0 -16
- package/esm2022/lib/helpers/states/active-page.mjs +0 -8
- package/esm2022/lib/helpers/states/composition.mjs +0 -20
- package/esm2022/lib/helpers/states/page-transition.mjs +0 -8
- package/esm2022/lib/helpers/states/slow-timer.mjs +0 -15
- package/esm2022/lib/helpers/states/test-active.mjs +0 -8
- package/esm2022/lib/helpers/states/time.mjs +0 -11
- package/esm2022/lib/helpers/test/caps-warning.mjs +0 -50
- package/esm2022/lib/helpers/test/caret.mjs +0 -80
- package/esm2022/lib/helpers/test/custom-text.mjs +0 -59
- package/esm2022/lib/helpers/test/english-punctuation.mjs +0 -29
- package/esm2022/lib/helpers/test/focus.mjs +0 -35
- package/esm2022/lib/helpers/test/manual-restart-tracker.mjs +0 -11
- package/esm2022/lib/helpers/test/out-of-focus.mjs +0 -14
- package/esm2022/lib/helpers/test/replay.mjs +0 -217
- package/esm2022/lib/helpers/test/test-input.mjs +0 -264
- package/esm2022/lib/helpers/test/test-logic.mjs +0 -927
- package/esm2022/lib/helpers/test/test-state.mjs +0 -13
- package/esm2022/lib/helpers/test/test-stats.mjs +0 -342
- package/esm2022/lib/helpers/test/test-timer.mjs +0 -207
- package/esm2022/lib/helpers/test/test-ui.mjs +0 -341
- package/esm2022/lib/helpers/test/test-words.mjs +0 -69
- package/esm2022/lib/helpers/test/timer-progress.mjs +0 -15
- package/esm2022/lib/helpers/test/weak-spot.mjs +0 -65
- package/esm2022/lib/helpers/test/wordset.mjs +0 -100
- package/esm2022/lib/utils/misc.mjs +0 -673
- package/esm2022/testgorilla-tgo-typing-test.mjs +0 -5
- package/fesm2022/testgorilla-tgo-typing-test.mjs +0 -4707
- package/fesm2022/testgorilla-tgo-typing-test.mjs.map +0 -1
- package/lib/components/tgo-typing-replay-input/tgo-typing-replay-input.component.d.ts +0 -14
- package/lib/components/tgo-typing-test/tgo-typing-test.component.d.ts +0 -54
- package/lib/helpers/config.d.ts +0 -98
- package/lib/helpers/constants/default-config.d.ts +0 -3
- package/lib/helpers/controllers/input-controller.d.ts +0 -16
- package/lib/helpers/controllers/quotes-controller.d.ts +0 -20
- package/lib/helpers/observables/config-event.d.ts +0 -5
- package/lib/helpers/observables/timer-event.d.ts +0 -4
- package/lib/helpers/states/active-page.d.ts +0 -2
- package/lib/helpers/states/composition.d.ts +0 -10
- package/lib/helpers/states/page-transition.d.ts +0 -2
- package/lib/helpers/states/slow-timer.d.ts +0 -3
- package/lib/helpers/states/test-active.d.ts +0 -2
- package/lib/helpers/states/time.d.ts +0 -3
- package/lib/helpers/test/caps-warning.d.ts +0 -5
- package/lib/helpers/test/caret.d.ts +0 -11
- package/lib/helpers/test/custom-text.d.ts +0 -16
- package/lib/helpers/test/english-punctuation.d.ts +0 -3
- package/lib/helpers/test/focus.d.ts +0 -7
- package/lib/helpers/test/manual-restart-tracker.d.ts +0 -3
- package/lib/helpers/test/out-of-focus.d.ts +0 -4
- package/lib/helpers/test/replay.d.ts +0 -20
- package/lib/helpers/test/test-input.d.ts +0 -86
- package/lib/helpers/test/test-logic.d.ts +0 -25
- package/lib/helpers/test/test-state.d.ts +0 -7
- package/lib/helpers/test/test-stats.d.ts +0 -92
- package/lib/helpers/test/test-timer.d.ts +0 -6
- package/lib/helpers/test/test-ui.d.ts +0 -27
- package/lib/helpers/test/test-words.d.ts +0 -23
- package/lib/helpers/test/timer-progress.d.ts +0 -3
- package/lib/helpers/test/weak-spot.d.ts +0 -3
- package/lib/helpers/test/wordset.d.ts +0 -7
- package/lib/utils/misc.d.ts +0 -81
- /package/{assets → src/assets}/typing-test-languages/english.json +0 -0
- /package/{assets → src/assets}/typing-test-languages/english_punctuation.json +0 -0
- /package/{assets → src/assets}/typing-test-languages/quotes/english_version_1.json +0 -0
- /package/{assets → src/assets}/typing-test-languages/quotes/english_version_2.json +0 -0
- /package/{assets → src/assets}/typing-test-languages/quotes/filtered_sources.json +0 -0
- /package/{index.d.ts → src/index.ts} +0 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import Config from '../../helpers/config';
|
|
2
|
+
import * as TestWords from './test-words';
|
|
3
|
+
import * as TestInput from './test-input';
|
|
4
|
+
import * as Caret from './caret';
|
|
5
|
+
import * as OutOfFocus from './out-of-focus';
|
|
6
|
+
import * as Misc from '../../utils/misc';
|
|
7
|
+
import * as CompositionState from '../states/composition';
|
|
8
|
+
|
|
9
|
+
let wordsWrapper: HTMLElement;
|
|
10
|
+
let wordsInput: HTMLInputElement;
|
|
11
|
+
let words: HTMLElement;
|
|
12
|
+
|
|
13
|
+
export let currentWordElementIndex = 0;
|
|
14
|
+
export let resultVisible = false;
|
|
15
|
+
export let activeWordTop = 0;
|
|
16
|
+
export let testRestarting = false;
|
|
17
|
+
export let testRestartingPromise: Promise<unknown>;
|
|
18
|
+
export const lineTransition = false;
|
|
19
|
+
export let currentTestLine = 0;
|
|
20
|
+
export let resultCalculating = false;
|
|
21
|
+
|
|
22
|
+
export function setWordsWrapperElement(wordsWrapperElem: HTMLElement) {
|
|
23
|
+
wordsWrapper = wordsWrapperElem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function setWordsInputElementTestUI(wordsElement: HTMLInputElement) {
|
|
27
|
+
wordsInput = wordsElement;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function setWordsElement(wordsElem: HTMLElement) {
|
|
31
|
+
words = wordsElem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function setResultVisible(val: boolean): void {
|
|
35
|
+
resultVisible = val;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function setCurrentWordElementIndex(val: number): void {
|
|
39
|
+
currentWordElementIndex = val;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function setActiveWordTop(val: number): void {
|
|
43
|
+
activeWordTop = val;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let restartingResolve: null | ((value?: unknown) => void);
|
|
47
|
+
export function setTestRestarting(val: boolean): void {
|
|
48
|
+
testRestarting = val;
|
|
49
|
+
if (val === true) {
|
|
50
|
+
testRestartingPromise = new Promise(resolve => {
|
|
51
|
+
restartingResolve = resolve;
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
if (restartingResolve) restartingResolve();
|
|
55
|
+
restartingResolve = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function setResultCalculating(val: boolean): void {
|
|
60
|
+
resultCalculating = val;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function reset(): void {
|
|
64
|
+
currentTestLine = 0;
|
|
65
|
+
currentWordElementIndex = 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function focusWords(): void {
|
|
69
|
+
if (!wordsWrapper.classList.contains('hidden')) {
|
|
70
|
+
wordsInput.focus();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function updateActiveElement(backspace?: boolean): void {
|
|
75
|
+
const active = document.querySelector('#words .active');
|
|
76
|
+
if (Config.mode == 'zen' && backspace) {
|
|
77
|
+
active?.remove();
|
|
78
|
+
} else if (active !== null) {
|
|
79
|
+
if (Config.highlightMode == 'word') {
|
|
80
|
+
active.querySelectorAll('letter').forEach(e => {
|
|
81
|
+
e.classList.remove('correct');
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
active.classList.remove('active');
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const activeWord = document.querySelectorAll('#words .word')[currentWordElementIndex];
|
|
88
|
+
activeWord.classList.add('active');
|
|
89
|
+
activeWord.classList.remove('error');
|
|
90
|
+
activeWordTop = (<HTMLElement>document.querySelector('#words .active')).offsetTop;
|
|
91
|
+
if (Config.highlightMode == 'word') {
|
|
92
|
+
activeWord.querySelectorAll('letter').forEach(e => {
|
|
93
|
+
e.classList.add('correct');
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.error(e);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getWordHTML(word: string): string {
|
|
102
|
+
let newlineafter = false;
|
|
103
|
+
let retval = `<div class='word'>`;
|
|
104
|
+
for (let c = 0; c < word.length; c++) {
|
|
105
|
+
if (Config.funbox === 'tenKeyMode') {
|
|
106
|
+
newlineafter = true;
|
|
107
|
+
}
|
|
108
|
+
if (Config.funbox === 'arrows') {
|
|
109
|
+
if (word.charAt(c) === '↑') {
|
|
110
|
+
retval += `<letter><i class="fas fa-arrow-up"></i></letter>`;
|
|
111
|
+
}
|
|
112
|
+
if (word.charAt(c) === '↓') {
|
|
113
|
+
retval += `<letter><i class="fas fa-arrow-down"></i></letter>`;
|
|
114
|
+
}
|
|
115
|
+
if (word.charAt(c) === '←') {
|
|
116
|
+
retval += `<letter><i class="fas fa-arrow-left"></i></letter>`;
|
|
117
|
+
}
|
|
118
|
+
if (word.charAt(c) === '→') {
|
|
119
|
+
retval += `<letter><i class="fas fa-arrow-right"></i></letter>`;
|
|
120
|
+
}
|
|
121
|
+
} else if (word.charAt(c) === '\t') {
|
|
122
|
+
retval += `<letter class='tabChar'><i class="fas fa-long-arrow-alt-right"></i></letter>`;
|
|
123
|
+
} else if (word.charAt(c) === '\n') {
|
|
124
|
+
newlineafter = true;
|
|
125
|
+
retval += `<letter class='nlChar'><i class="fas fa-angle-down"></i></letter>`;
|
|
126
|
+
} else {
|
|
127
|
+
retval += '<letter>' + word.charAt(c) + '</letter>';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
retval += '</div>';
|
|
131
|
+
if (newlineafter) retval += "<div class='newline'></div>";
|
|
132
|
+
return retval;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function showWords(): void {
|
|
136
|
+
if (Config.indicateTypos === 'below') {
|
|
137
|
+
words.classList.add('indicateTyposBelow');
|
|
138
|
+
wordsWrapper.classList.add('indicateTyposBelow');
|
|
139
|
+
} else {
|
|
140
|
+
words.classList.remove('indicateTyposBelow');
|
|
141
|
+
wordsWrapper.classList.remove('indicateTyposBelow');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let wordsHTML = '';
|
|
145
|
+
for (let i = 0; i < TestWords.words.length; i++) {
|
|
146
|
+
wordsHTML += getWordHTML(<string>TestWords.words.get(i));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
words.innerHTML = wordsHTML;
|
|
150
|
+
|
|
151
|
+
wordsWrapper.classList.remove('hidden');
|
|
152
|
+
const currentWord = <Element>document.querySelector('.word');
|
|
153
|
+
const currentWordStyle = window.getComputedStyle(currentWord);
|
|
154
|
+
const wordHeight =
|
|
155
|
+
parseFloat(currentWordStyle.marginTop) +
|
|
156
|
+
parseFloat(currentWordStyle.marginBottom) +
|
|
157
|
+
currentWord.getBoundingClientRect().height;
|
|
158
|
+
|
|
159
|
+
words.style.height = wordHeight * (Config.funbox === 'tenKeyMode' ? 5 : 4) + 'px';
|
|
160
|
+
words.style.overflow = 'hidden';
|
|
161
|
+
words.style.width = '100%';
|
|
162
|
+
words.style.marginLeft = 'unset';
|
|
163
|
+
wordsWrapper.style.height = wordHeight * (Config.funbox === 'tenKeyMode' ? 5 : 3) + 'px';
|
|
164
|
+
wordsWrapper.style.overflow = 'hidden';
|
|
165
|
+
|
|
166
|
+
updateActiveElement();
|
|
167
|
+
Caret.updatePosition();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function addWord(word: string): void {
|
|
171
|
+
words.insertAdjacentHTML('beforeend', getWordHTML(word));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function updateWordElement(showError = !Config.blindMode): void {
|
|
175
|
+
const input = TestInput.input.current;
|
|
176
|
+
const wordAtIndex = <Element>document.querySelector('#words .word.active');
|
|
177
|
+
const currentWord = TestWords.words.getCurrent();
|
|
178
|
+
if (!currentWord && Config.mode !== 'zen') return;
|
|
179
|
+
|
|
180
|
+
let ret = '';
|
|
181
|
+
|
|
182
|
+
let newlineafter = false;
|
|
183
|
+
|
|
184
|
+
if (Config.mode === 'zen') {
|
|
185
|
+
for (let i = 0; i < TestInput.input.current.length; i++) {
|
|
186
|
+
if (TestInput.input.current[i] === '\t') {
|
|
187
|
+
ret += `<letter class='tabChar correct' style="opacity: 0"><i class="fas fa-long-arrow-alt-right"></i></letter>`;
|
|
188
|
+
} else if (TestInput.input.current[i] === '\n') {
|
|
189
|
+
newlineafter = true;
|
|
190
|
+
ret += `<letter class='nlChar correct' style="opacity: 0"><i class="fas fa-angle-down"></i></letter>`;
|
|
191
|
+
} else {
|
|
192
|
+
ret += `<letter class="correct">${TestInput.input.current[i]}</letter>`;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
let correctSoFar = false;
|
|
197
|
+
|
|
198
|
+
// slice earlier if input has trailing compose characters
|
|
199
|
+
const inputWithoutComposeLength = Misc.trailingComposeChars.test(input)
|
|
200
|
+
? input.search(Misc.trailingComposeChars)
|
|
201
|
+
: input.length;
|
|
202
|
+
if (
|
|
203
|
+
input.search(Misc.trailingComposeChars) < currentWord.length &&
|
|
204
|
+
currentWord.slice(0, inputWithoutComposeLength) === input.slice(0, inputWithoutComposeLength)
|
|
205
|
+
) {
|
|
206
|
+
correctSoFar = true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let wordHighlightClassString = correctSoFar ? 'correct' : 'incorrect';
|
|
210
|
+
if (Config.blindMode) {
|
|
211
|
+
wordHighlightClassString = 'correct';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (let i = 0; i < input.length; i++) {
|
|
215
|
+
const charCorrect = currentWord[i] == input[i];
|
|
216
|
+
|
|
217
|
+
let correctClass = 'correct';
|
|
218
|
+
if (Config.highlightMode == 'off') {
|
|
219
|
+
correctClass = '';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let currentLetter = currentWord[i];
|
|
223
|
+
let tabChar = '';
|
|
224
|
+
let nlChar = '';
|
|
225
|
+
if (Config.funbox === 'arrows') {
|
|
226
|
+
if (currentLetter === '↑') {
|
|
227
|
+
currentLetter = `<i class="fas fa-arrow-up"></i>`;
|
|
228
|
+
}
|
|
229
|
+
if (currentLetter === '↓') {
|
|
230
|
+
currentLetter = `<i class="fas fa-arrow-down"></i>`;
|
|
231
|
+
}
|
|
232
|
+
if (currentLetter === '←') {
|
|
233
|
+
currentLetter = `<i class="fas fa-arrow-left"></i>`;
|
|
234
|
+
}
|
|
235
|
+
if (currentLetter === '→') {
|
|
236
|
+
currentLetter = `<i class="fas fa-arrow-right"></i>`;
|
|
237
|
+
}
|
|
238
|
+
} else if (currentLetter === '\t') {
|
|
239
|
+
tabChar = 'tabChar';
|
|
240
|
+
currentLetter = `<i class="fas fa-long-arrow-alt-right"></i>`;
|
|
241
|
+
} else if (currentLetter === '\n') {
|
|
242
|
+
nlChar = 'nlChar';
|
|
243
|
+
currentLetter = `<i class="fas fa-angle-down"></i>`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (charCorrect) {
|
|
247
|
+
ret += `<letter class="${
|
|
248
|
+
Config.highlightMode == 'word' ? wordHighlightClassString : correctClass
|
|
249
|
+
} ${tabChar}${nlChar}">${currentLetter}</letter>`;
|
|
250
|
+
} else if (
|
|
251
|
+
currentLetter !== undefined &&
|
|
252
|
+
CompositionState.getComposing() &&
|
|
253
|
+
i >= CompositionState.getStartPos()
|
|
254
|
+
) {
|
|
255
|
+
ret += `<letter class="${
|
|
256
|
+
Config.highlightMode == 'word' ? wordHighlightClassString : ''
|
|
257
|
+
} dead">${currentLetter}</letter>`;
|
|
258
|
+
} else if (!showError) {
|
|
259
|
+
if (currentLetter !== undefined) {
|
|
260
|
+
ret += `<letter class="${
|
|
261
|
+
Config.highlightMode == 'word' ? wordHighlightClassString : correctClass
|
|
262
|
+
} ${tabChar}${nlChar}">${currentLetter}</letter>`;
|
|
263
|
+
}
|
|
264
|
+
} else if (currentLetter === undefined) {
|
|
265
|
+
if (!Config.hideExtraLetters) {
|
|
266
|
+
let letter = input[i];
|
|
267
|
+
if (letter == ' ' || letter == '\t' || letter == '\n') {
|
|
268
|
+
letter = '_';
|
|
269
|
+
}
|
|
270
|
+
ret += `<letter class="${
|
|
271
|
+
Config.highlightMode == 'word' ? wordHighlightClassString : 'incorrect'
|
|
272
|
+
} extra ${tabChar}${nlChar}">${letter}</letter>`;
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
ret +=
|
|
276
|
+
`<letter class="${
|
|
277
|
+
Config.highlightMode == 'word' ? wordHighlightClassString : 'incorrect'
|
|
278
|
+
} ${tabChar}${nlChar}">` +
|
|
279
|
+
(Config.indicateTypos === 'replace'
|
|
280
|
+
? input[i] == ' '
|
|
281
|
+
? '_'
|
|
282
|
+
: input[i]
|
|
283
|
+
: currentLetter) +
|
|
284
|
+
(Config.indicateTypos === 'below' ? `<hint>${input[i]}</hint>` : '') +
|
|
285
|
+
'</letter>';
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (let i = input.length; i < currentWord.length; i++) {
|
|
290
|
+
if (Config.funbox === 'arrows') {
|
|
291
|
+
if (currentWord[i] === '↑') {
|
|
292
|
+
ret += `<letter><i class="fas fa-arrow-up"></i></letter>`;
|
|
293
|
+
}
|
|
294
|
+
if (currentWord[i] === '↓') {
|
|
295
|
+
ret += `<letter><i class="fas fa-arrow-down"></i></letter>`;
|
|
296
|
+
}
|
|
297
|
+
if (currentWord[i] === '←') {
|
|
298
|
+
ret += `<letter><i class="fas fa-arrow-left"></i></letter>`;
|
|
299
|
+
}
|
|
300
|
+
if (currentWord[i] === '→') {
|
|
301
|
+
ret += `<letter><i class="fas fa-arrow-right"></i></letter>`;
|
|
302
|
+
}
|
|
303
|
+
} else if (currentWord[i] === '\t') {
|
|
304
|
+
ret += `<letter class='tabChar'><i class="fas fa-long-arrow-alt-right"></i></letter>`;
|
|
305
|
+
} else if (currentWord[i] === '\n') {
|
|
306
|
+
ret += `<letter class='nlChar'><i class="fas fa-angle-down"></i></letter>`;
|
|
307
|
+
} else {
|
|
308
|
+
ret +=
|
|
309
|
+
`<letter class="${Config.highlightMode == 'word' ? wordHighlightClassString : ''}">` +
|
|
310
|
+
currentWord[i] +
|
|
311
|
+
'</letter>';
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (Config.highlightMode === 'letter' && Config.hideExtraLetters) {
|
|
316
|
+
if (input.length > currentWord.length && !Config.blindMode) {
|
|
317
|
+
wordAtIndex.classList.add('error');
|
|
318
|
+
} else if (input.length == currentWord.length) {
|
|
319
|
+
wordAtIndex.classList.remove('error');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
wordAtIndex.innerHTML = ret;
|
|
324
|
+
if (newlineafter) words.insertAdjacentHTML('beforeend', "<div class='newline'></div>");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function lineJump(currentTop: number): void {
|
|
328
|
+
if (
|
|
329
|
+
(Config.tapeMode === 'off' && currentTestLine > 0) ||
|
|
330
|
+
(Config.tapeMode !== 'off' && currentTestLine >= 0)
|
|
331
|
+
) {
|
|
332
|
+
const hideBound = currentTop;
|
|
333
|
+
|
|
334
|
+
const toHide: any[] = [];
|
|
335
|
+
const wordElements = document.querySelectorAll('#words .word') as NodeListOf<HTMLElement>;
|
|
336
|
+
for (let i = 0; i < currentWordElementIndex; i++) {
|
|
337
|
+
if (wordElements[i].classList.contains('hidden')) continue;
|
|
338
|
+
const forWordTop = Math.floor(wordElements[i].offsetTop);
|
|
339
|
+
if (forWordTop < (Config.tapeMode === 'off' ? hideBound - 10 : hideBound + 10)) {
|
|
340
|
+
toHide.push(document.querySelectorAll('#words .word')[i]);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
toHide.forEach(el => el.remove());
|
|
344
|
+
currentWordElementIndex -= toHide.length;
|
|
345
|
+
}
|
|
346
|
+
currentTestLine++;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function highlightBadWord(index: number, showError: boolean): void {
|
|
350
|
+
if (!showError) return;
|
|
351
|
+
document.querySelectorAll('#words .word')[index].classList.add('error');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function wordsInputFocusTestUI() {
|
|
355
|
+
if (!resultVisible && Config.showOutOfFocusWarning) {
|
|
356
|
+
OutOfFocus.hide();
|
|
357
|
+
}
|
|
358
|
+
Caret.show();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function wordsInputFocusOut() {
|
|
362
|
+
if (!resultVisible && Config.showOutOfFocusWarning) {
|
|
363
|
+
OutOfFocus.show();
|
|
364
|
+
}
|
|
365
|
+
Caret.hide();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function wordsWrapperClick() {
|
|
369
|
+
focusWords();
|
|
370
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { MonkeyTypes } from '../../types/types';
|
|
2
|
+
class Words {
|
|
3
|
+
public list: string[];
|
|
4
|
+
public length: number;
|
|
5
|
+
public currentIndex: number;
|
|
6
|
+
constructor() {
|
|
7
|
+
this.list = [];
|
|
8
|
+
this.length = 0;
|
|
9
|
+
this.currentIndex = 0;
|
|
10
|
+
}
|
|
11
|
+
get(i?: undefined, raw?: boolean): string[];
|
|
12
|
+
get(i: number, raw?: boolean): string;
|
|
13
|
+
get(i?: number | undefined, raw = false): string | string[] {
|
|
14
|
+
if (i === undefined) {
|
|
15
|
+
return this.list;
|
|
16
|
+
} else {
|
|
17
|
+
if (raw) {
|
|
18
|
+
return this.list[i]?.replace(/[.?!":\-,]/g, "")?.toLowerCase();
|
|
19
|
+
} else {
|
|
20
|
+
return this.list[i];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
getCurrent(): string {
|
|
25
|
+
return this.list[this.currentIndex];
|
|
26
|
+
}
|
|
27
|
+
getLast(): string {
|
|
28
|
+
return this.list[this.list.length - 1];
|
|
29
|
+
}
|
|
30
|
+
push(word: string): void {
|
|
31
|
+
this.list.push(word);
|
|
32
|
+
this.length = this.list.length;
|
|
33
|
+
}
|
|
34
|
+
reset(): void {
|
|
35
|
+
this.list = [];
|
|
36
|
+
this.currentIndex = 0;
|
|
37
|
+
this.length = this.list.length;
|
|
38
|
+
}
|
|
39
|
+
resetCurrentIndex(): void {
|
|
40
|
+
this.currentIndex = 0;
|
|
41
|
+
}
|
|
42
|
+
decreaseCurrentIndex(): void {
|
|
43
|
+
this.currentIndex--;
|
|
44
|
+
}
|
|
45
|
+
increaseCurrentIndex(): void {
|
|
46
|
+
this.currentIndex++;
|
|
47
|
+
}
|
|
48
|
+
clean(): void {
|
|
49
|
+
for (const s of this.list) {
|
|
50
|
+
if (/ +/.test(s)) {
|
|
51
|
+
const id = this.list.indexOf(s);
|
|
52
|
+
const tempList = s.split(" ");
|
|
53
|
+
this.list.splice(id, 1);
|
|
54
|
+
for (let i = 0; i < tempList.length; i++) {
|
|
55
|
+
this.list.splice(id + i, 0, tempList[i]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const words = new Words();
|
|
63
|
+
export let hasTab = false;
|
|
64
|
+
export let randomQuote = null as unknown as MonkeyTypes.Quote;
|
|
65
|
+
|
|
66
|
+
export function setRandomQuote(rq: MonkeyTypes.Quote): void {
|
|
67
|
+
randomQuote = rq;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function setHasTab(tf: boolean): void {
|
|
71
|
+
hasTab = tf;
|
|
72
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import Config from '../../helpers/config';
|
|
2
|
+
import * as Misc from '../../utils/misc';
|
|
3
|
+
import * as Time from '../states/time';
|
|
4
|
+
import { BehaviorSubject } from 'rxjs';
|
|
5
|
+
|
|
6
|
+
export const timeLeft = new BehaviorSubject<string>('1:00');
|
|
7
|
+
|
|
8
|
+
export function update(): void {
|
|
9
|
+
const time = Time.get();
|
|
10
|
+
const maxtime = Config.time;
|
|
11
|
+
let displayTime = Misc.secondsToString(maxtime - time);
|
|
12
|
+
if (maxtime === 0) {
|
|
13
|
+
displayTime = Misc.secondsToString(time);
|
|
14
|
+
}
|
|
15
|
+
timeLeft.next(displayTime);
|
|
16
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import Config from '../../helpers/config';
|
|
2
|
+
import * as Misc from '../../utils/misc';
|
|
3
|
+
import * as ConfigEvent from "../observables/config-event";
|
|
4
|
+
|
|
5
|
+
let voice: SpeechSynthesisUtterance | undefined;
|
|
6
|
+
|
|
7
|
+
export async function setLanguage(lang = Config.language): Promise<void> {
|
|
8
|
+
if (!voice) return;
|
|
9
|
+
const language = await Misc.getLanguage(lang);
|
|
10
|
+
const bcp = language.bcp47 ? language.bcp47 : "en-US";
|
|
11
|
+
voice.lang = bcp;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function init(): Promise<void> {
|
|
15
|
+
voice = new SpeechSynthesisUtterance();
|
|
16
|
+
setLanguage();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function clear(): void {
|
|
20
|
+
voice = undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function speak(text: string): void {
|
|
24
|
+
window.speechSynthesis.cancel();
|
|
25
|
+
if (voice === undefined) init();
|
|
26
|
+
|
|
27
|
+
if (voice !== undefined) {
|
|
28
|
+
voice.text = text;
|
|
29
|
+
window.speechSynthesis.speak(voice);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
ConfigEvent.subscribe((eventKey, eventValue) => {
|
|
34
|
+
if (eventKey === "funbox") {
|
|
35
|
+
if (eventValue === "none") {
|
|
36
|
+
clear();
|
|
37
|
+
} else if (eventValue === "tts") {
|
|
38
|
+
init();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (eventKey === "language" && Config.funbox === "tts") setLanguage();
|
|
42
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as TestInput from './test-input';
|
|
2
|
+
import { Wordset } from './wordset';
|
|
3
|
+
|
|
4
|
+
// Changes how quickly it 'learns' scores - very roughly the score for a char
|
|
5
|
+
// is based on last perCharCount occurrences. Make it smaller to adjust faster.
|
|
6
|
+
const perCharCount = 50;
|
|
7
|
+
|
|
8
|
+
// Choose the highest scoring word from this many random words. Higher values
|
|
9
|
+
// will choose words with more weak letters on average.
|
|
10
|
+
const wordSamples = 20;
|
|
11
|
+
|
|
12
|
+
// Score penatly (in milliseconds) for getting a letter wrong.
|
|
13
|
+
const incorrectPenalty = 5000;
|
|
14
|
+
|
|
15
|
+
const scores: { [char: string]: Score } = {};
|
|
16
|
+
|
|
17
|
+
class Score {
|
|
18
|
+
public average: number;
|
|
19
|
+
public count: number;
|
|
20
|
+
constructor() {
|
|
21
|
+
this.average = 0.0;
|
|
22
|
+
this.count = 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
update(score: number): void {
|
|
26
|
+
if (this.count < perCharCount) {
|
|
27
|
+
this.count++;
|
|
28
|
+
}
|
|
29
|
+
const adjustRate = 1.0 / this.count;
|
|
30
|
+
// Keep an exponential moving average of the score over time.
|
|
31
|
+
this.average = score * adjustRate + this.average * (1 - adjustRate);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function updateScore(char: string, isCorrect: boolean): void {
|
|
36
|
+
const timings = TestInput.keypressTimings.spacing.array;
|
|
37
|
+
if (timings.length === 0 || typeof timings === 'string') {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let score = timings[timings.length - 1];
|
|
41
|
+
if (!isCorrect) {
|
|
42
|
+
score += incorrectPenalty;
|
|
43
|
+
}
|
|
44
|
+
if (!(char in scores)) {
|
|
45
|
+
scores[char] = new Score();
|
|
46
|
+
}
|
|
47
|
+
scores[char].update(score);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function score(word: string): number {
|
|
51
|
+
let total = 0.0;
|
|
52
|
+
let numChars = 0;
|
|
53
|
+
for (const c of word) {
|
|
54
|
+
if (c in scores) {
|
|
55
|
+
total += scores[c].average;
|
|
56
|
+
numChars++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return numChars == 0 ? 0.0 : total / numChars;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getWord(wordset: Wordset): string {
|
|
63
|
+
let highScore;
|
|
64
|
+
let randomWord = '';
|
|
65
|
+
for (let i = 0; i < wordSamples; i++) {
|
|
66
|
+
const newWord = wordset.randomWord();
|
|
67
|
+
const newScore = score(newWord);
|
|
68
|
+
if (i == 0 || highScore === undefined || newScore > highScore) {
|
|
69
|
+
randomWord = newWord;
|
|
70
|
+
highScore = newScore;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return randomWord;
|
|
74
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { randomElementFromArray, randomIntFromRange } from '../../utils/misc';
|
|
2
|
+
|
|
3
|
+
let currentWordset: Wordset | null = null;
|
|
4
|
+
let currentWordGenerator: WordGenerator | null = null;
|
|
5
|
+
|
|
6
|
+
export class Wordset {
|
|
7
|
+
public words: string[];
|
|
8
|
+
public length: number;
|
|
9
|
+
constructor(words: string[]) {
|
|
10
|
+
this.words = words;
|
|
11
|
+
this.length = this.words.length;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public randomWord(): string {
|
|
15
|
+
return randomElementFromArray(this.words);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const prefixSize = 2;
|
|
20
|
+
|
|
21
|
+
class CharDistribution {
|
|
22
|
+
public chars: { [char: string]: number };
|
|
23
|
+
public count: number;
|
|
24
|
+
constructor() {
|
|
25
|
+
this.chars = {};
|
|
26
|
+
this.count = 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public addChar(char: string): void {
|
|
30
|
+
this.count++;
|
|
31
|
+
if (char in this.chars) {
|
|
32
|
+
this.chars[char]++;
|
|
33
|
+
} else {
|
|
34
|
+
this.chars[char] = 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public randomChar(): string {
|
|
39
|
+
const randomIndex = randomIntFromRange(0, this.count - 1);
|
|
40
|
+
let runningCount = 0;
|
|
41
|
+
for (const [char, charCount] of Object.entries(this.chars)) {
|
|
42
|
+
runningCount += charCount;
|
|
43
|
+
if (runningCount > randomIndex) {
|
|
44
|
+
return char;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return Object.keys(this.chars)[0];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class WordGenerator extends Wordset {
|
|
53
|
+
public ngrams: { [prefix: string]: CharDistribution } = {};
|
|
54
|
+
constructor(words: string[]) {
|
|
55
|
+
super(words);
|
|
56
|
+
// Can generate an unbounded number of words in theory.
|
|
57
|
+
this.length = Infinity;
|
|
58
|
+
|
|
59
|
+
for (let word of words) {
|
|
60
|
+
// Mark the end of each word with a space.
|
|
61
|
+
word += " ";
|
|
62
|
+
let prefix = "";
|
|
63
|
+
for (const c of word) {
|
|
64
|
+
// Add `c` to the distribution of chars that can come after `prefix`.
|
|
65
|
+
if (!(prefix in this.ngrams)) {
|
|
66
|
+
this.ngrams[prefix] = new CharDistribution();
|
|
67
|
+
}
|
|
68
|
+
this.ngrams[prefix].addChar(c);
|
|
69
|
+
prefix = (prefix + c).substr(-prefixSize);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public override randomWord(): string {
|
|
75
|
+
let word = "";
|
|
76
|
+
for (;;) {
|
|
77
|
+
const prefix = word.substr(-prefixSize);
|
|
78
|
+
const charDistribution = this.ngrams[prefix];
|
|
79
|
+
if (!charDistribution) {
|
|
80
|
+
// This shouldn't happen if this.ngrams is complete. If it does
|
|
81
|
+
// somehow, start generating a new word.
|
|
82
|
+
word = "";
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// Pick a random char from the distribution that comes after `prefix`.
|
|
86
|
+
const nextChar = charDistribution.randomChar();
|
|
87
|
+
if (nextChar == " ") {
|
|
88
|
+
// A space marks the end of the word, so stop generating and return.
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
word += nextChar;
|
|
92
|
+
}
|
|
93
|
+
return word;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function withWords(words: string[], funbox: string): Wordset {
|
|
98
|
+
if (funbox == "pseudolang") {
|
|
99
|
+
if (currentWordGenerator == null || words !== currentWordGenerator.words) {
|
|
100
|
+
currentWordGenerator = new WordGenerator(words);
|
|
101
|
+
}
|
|
102
|
+
return currentWordGenerator;
|
|
103
|
+
} else {
|
|
104
|
+
if (currentWordset == null || words !== currentWordset.words) {
|
|
105
|
+
currentWordset = new Wordset(words);
|
|
106
|
+
}
|
|
107
|
+
return currentWordset;
|
|
108
|
+
}
|
|
109
|
+
}
|