@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.
Files changed (134) hide show
  1. package/.eslintrc.json +46 -0
  2. package/jest.config.ts +21 -0
  3. package/ng-package.json +15 -0
  4. package/package.json +7 -22
  5. package/project.json +36 -0
  6. package/src/lib/components/tgo-typing-replay-input/tgo-typing-replay-input.component.html +30 -0
  7. package/src/lib/components/tgo-typing-replay-input/tgo-typing-replay-input.component.spec.ts +250 -0
  8. package/src/lib/components/tgo-typing-replay-input/tgo-typing-replay-input.component.ts +47 -0
  9. package/src/lib/components/tgo-typing-test/tgo-typing-test.component.html +72 -0
  10. package/src/lib/components/tgo-typing-test/tgo-typing-test.component.spec.ts +699 -0
  11. package/src/lib/components/tgo-typing-test/tgo-typing-test.component.ts +287 -0
  12. package/src/lib/helpers/config.ts +28 -0
  13. package/src/lib/helpers/constants/default-config.ts +103 -0
  14. package/src/lib/helpers/controllers/input-controller.ts +710 -0
  15. package/src/lib/helpers/controllers/quotes-controller.ts +183 -0
  16. package/src/lib/helpers/observables/banner-event.ts +18 -0
  17. package/src/lib/helpers/observables/config-event.ts +31 -0
  18. package/src/lib/helpers/observables/timer-event.ts +18 -0
  19. package/src/lib/helpers/states/active-page.ts +9 -0
  20. package/src/lib/helpers/states/composition.ts +29 -0
  21. package/src/lib/helpers/states/page-transition.ts +9 -0
  22. package/src/lib/helpers/states/slow-timer.ts +16 -0
  23. package/src/lib/helpers/states/test-active.ts +9 -0
  24. package/src/lib/helpers/states/time.ts +13 -0
  25. package/src/lib/helpers/test/caps-warning.ts +50 -0
  26. package/src/lib/helpers/test/caret.ts +92 -0
  27. package/src/lib/helpers/test/custom-text.ts +73 -0
  28. package/src/lib/helpers/test/english-punctuation.ts +38 -0
  29. package/src/lib/helpers/test/focus.ts +39 -0
  30. package/src/lib/helpers/test/manual-restart-tracker.ts +13 -0
  31. package/src/lib/helpers/test/out-of-focus.ts +19 -0
  32. package/src/lib/helpers/test/replay.ts +265 -0
  33. package/src/lib/helpers/test/test-input.ts +320 -0
  34. package/src/lib/helpers/test/test-logic.ts +1039 -0
  35. package/src/lib/helpers/test/test-state.ts +17 -0
  36. package/src/lib/helpers/test/test-stats.ts +442 -0
  37. package/src/lib/helpers/test/test-timer.ts +209 -0
  38. package/src/lib/helpers/test/test-ui.ts +370 -0
  39. package/src/lib/helpers/test/test-words.ts +72 -0
  40. package/src/lib/helpers/test/timer-progress.ts +16 -0
  41. package/src/lib/helpers/test/tts.ts +42 -0
  42. package/src/lib/helpers/test/weak-spot.ts +74 -0
  43. package/src/lib/helpers/test/wordset.ts +109 -0
  44. package/src/lib/styles/animations.scss +101 -0
  45. package/src/lib/styles/caret.scss +108 -0
  46. package/src/lib/styles/core.scss +498 -0
  47. package/src/lib/styles/index.scss +19 -0
  48. package/src/lib/styles/inputs.scss +290 -0
  49. package/src/lib/styles/popups.scss +1311 -0
  50. package/src/lib/styles/test.scss +1008 -0
  51. package/src/lib/styles/z_media-queries.scss +848 -0
  52. package/src/lib/types/types.d.ts +731 -0
  53. package/src/lib/utils/misc.ts +776 -0
  54. package/src/test-setup.ts +20 -0
  55. package/tsconfig.json +16 -0
  56. package/tsconfig.lib.json +14 -0
  57. package/tsconfig.lib.prod.json +9 -0
  58. package/tsconfig.spec.json +11 -0
  59. package/esm2022/index.mjs +0 -3
  60. package/esm2022/lib/components/tgo-typing-replay-input/tgo-typing-replay-input.component.mjs +0 -45
  61. package/esm2022/lib/components/tgo-typing-test/tgo-typing-test.component.mjs +0 -299
  62. package/esm2022/lib/helpers/config.mjs +0 -24
  63. package/esm2022/lib/helpers/constants/default-config.mjs +0 -103
  64. package/esm2022/lib/helpers/controllers/input-controller.mjs +0 -586
  65. package/esm2022/lib/helpers/controllers/quotes-controller.mjs +0 -118
  66. package/esm2022/lib/helpers/observables/config-event.mjs +0 -16
  67. package/esm2022/lib/helpers/observables/timer-event.mjs +0 -16
  68. package/esm2022/lib/helpers/states/active-page.mjs +0 -8
  69. package/esm2022/lib/helpers/states/composition.mjs +0 -20
  70. package/esm2022/lib/helpers/states/page-transition.mjs +0 -8
  71. package/esm2022/lib/helpers/states/slow-timer.mjs +0 -15
  72. package/esm2022/lib/helpers/states/test-active.mjs +0 -8
  73. package/esm2022/lib/helpers/states/time.mjs +0 -11
  74. package/esm2022/lib/helpers/test/caps-warning.mjs +0 -50
  75. package/esm2022/lib/helpers/test/caret.mjs +0 -80
  76. package/esm2022/lib/helpers/test/custom-text.mjs +0 -59
  77. package/esm2022/lib/helpers/test/english-punctuation.mjs +0 -29
  78. package/esm2022/lib/helpers/test/focus.mjs +0 -35
  79. package/esm2022/lib/helpers/test/manual-restart-tracker.mjs +0 -11
  80. package/esm2022/lib/helpers/test/out-of-focus.mjs +0 -14
  81. package/esm2022/lib/helpers/test/replay.mjs +0 -217
  82. package/esm2022/lib/helpers/test/test-input.mjs +0 -264
  83. package/esm2022/lib/helpers/test/test-logic.mjs +0 -927
  84. package/esm2022/lib/helpers/test/test-state.mjs +0 -13
  85. package/esm2022/lib/helpers/test/test-stats.mjs +0 -342
  86. package/esm2022/lib/helpers/test/test-timer.mjs +0 -207
  87. package/esm2022/lib/helpers/test/test-ui.mjs +0 -341
  88. package/esm2022/lib/helpers/test/test-words.mjs +0 -69
  89. package/esm2022/lib/helpers/test/timer-progress.mjs +0 -15
  90. package/esm2022/lib/helpers/test/weak-spot.mjs +0 -65
  91. package/esm2022/lib/helpers/test/wordset.mjs +0 -100
  92. package/esm2022/lib/utils/misc.mjs +0 -673
  93. package/esm2022/testgorilla-tgo-typing-test.mjs +0 -5
  94. package/fesm2022/testgorilla-tgo-typing-test.mjs +0 -4707
  95. package/fesm2022/testgorilla-tgo-typing-test.mjs.map +0 -1
  96. package/lib/components/tgo-typing-replay-input/tgo-typing-replay-input.component.d.ts +0 -14
  97. package/lib/components/tgo-typing-test/tgo-typing-test.component.d.ts +0 -54
  98. package/lib/helpers/config.d.ts +0 -98
  99. package/lib/helpers/constants/default-config.d.ts +0 -3
  100. package/lib/helpers/controllers/input-controller.d.ts +0 -16
  101. package/lib/helpers/controllers/quotes-controller.d.ts +0 -20
  102. package/lib/helpers/observables/config-event.d.ts +0 -5
  103. package/lib/helpers/observables/timer-event.d.ts +0 -4
  104. package/lib/helpers/states/active-page.d.ts +0 -2
  105. package/lib/helpers/states/composition.d.ts +0 -10
  106. package/lib/helpers/states/page-transition.d.ts +0 -2
  107. package/lib/helpers/states/slow-timer.d.ts +0 -3
  108. package/lib/helpers/states/test-active.d.ts +0 -2
  109. package/lib/helpers/states/time.d.ts +0 -3
  110. package/lib/helpers/test/caps-warning.d.ts +0 -5
  111. package/lib/helpers/test/caret.d.ts +0 -11
  112. package/lib/helpers/test/custom-text.d.ts +0 -16
  113. package/lib/helpers/test/english-punctuation.d.ts +0 -3
  114. package/lib/helpers/test/focus.d.ts +0 -7
  115. package/lib/helpers/test/manual-restart-tracker.d.ts +0 -3
  116. package/lib/helpers/test/out-of-focus.d.ts +0 -4
  117. package/lib/helpers/test/replay.d.ts +0 -20
  118. package/lib/helpers/test/test-input.d.ts +0 -86
  119. package/lib/helpers/test/test-logic.d.ts +0 -25
  120. package/lib/helpers/test/test-state.d.ts +0 -7
  121. package/lib/helpers/test/test-stats.d.ts +0 -92
  122. package/lib/helpers/test/test-timer.d.ts +0 -6
  123. package/lib/helpers/test/test-ui.d.ts +0 -27
  124. package/lib/helpers/test/test-words.d.ts +0 -23
  125. package/lib/helpers/test/timer-progress.d.ts +0 -3
  126. package/lib/helpers/test/weak-spot.d.ts +0 -3
  127. package/lib/helpers/test/wordset.d.ts +0 -7
  128. package/lib/utils/misc.d.ts +0 -81
  129. /package/{assets → src/assets}/typing-test-languages/english.json +0 -0
  130. /package/{assets → src/assets}/typing-test-languages/english_punctuation.json +0 -0
  131. /package/{assets → src/assets}/typing-test-languages/quotes/english_version_1.json +0 -0
  132. /package/{assets → src/assets}/typing-test-languages/quotes/english_version_2.json +0 -0
  133. /package/{assets → src/assets}/typing-test-languages/quotes/filtered_sources.json +0 -0
  134. /package/{index.d.ts → src/index.ts} +0 -0
@@ -0,0 +1,710 @@
1
+ import * as TestLogic from '../test/test-logic';
2
+ import * as TestUI from '../test/test-ui';
3
+ import * as TestStats from '../test/test-stats';
4
+ import Config, * as UpdateConfig from '../config';
5
+ import * as Misc from '../../utils/misc';
6
+ import * as Caret from '../test/caret';
7
+ import * as CustomText from '../test/custom-text';
8
+ import * as TimerProgress from '../test/timer-progress';
9
+ import * as Focus from '../test/focus';
10
+ import * as Replay from '../test/replay';
11
+ import * as WeakSpot from '../test/weak-spot';
12
+ import * as TestActive from '../states/test-active';
13
+ import * as CompositionState from '../states/composition';
14
+ import * as TestInput from '../test/test-input';
15
+ import * as TestWords from '../test/test-words';
16
+
17
+ let dontInsertSpace = false;
18
+ const correctShiftUsed = true;
19
+
20
+ let wordsInput: HTMLInputElement;
21
+
22
+ function setWordsInput(value: string): void {
23
+ // Only change #wordsInput if it's not already the wanted value
24
+ // Avoids Safari triggering unneeded events, causing issues with
25
+ // dead keys.
26
+ if (value !== wordsInput.value) {
27
+ wordsInput.value = value;
28
+ }
29
+ }
30
+
31
+ function updateUI(): void {
32
+ const acc = Misc.roundTo2(TestStats.calculateAccuracy());
33
+ }
34
+
35
+ function backspaceToPrevious(): void {
36
+ if (!TestActive.get()) return;
37
+
38
+ if (TestInput.input.history.length == 0 || TestUI.currentWordElementIndex == 0) {
39
+ return;
40
+ }
41
+
42
+ if (
43
+ (TestInput.input.history[TestWords.words.currentIndex - 1] ==
44
+ TestWords.words.get(TestWords.words.currentIndex - 1) &&
45
+ !Config.freedomMode) ||
46
+ document
47
+ .querySelectorAll('.word')
48
+ [TestWords.words.currentIndex - 1]?.classList.contains('hidden')
49
+ ) {
50
+ return;
51
+ }
52
+
53
+ if (Config.confidenceMode === 'on' || Config.confidenceMode === 'max') {
54
+ return;
55
+ }
56
+
57
+ TestInput.input.current = TestInput.input.popHistory();
58
+ TestInput.corrected.popHistory();
59
+ if (Config.funbox === 'nospace' || Config.funbox === 'arrows') {
60
+ TestInput.input.current = TestInput.input.current.slice(0, -1);
61
+ }
62
+ TestWords.words.decreaseCurrentIndex();
63
+ TestUI.setCurrentWordElementIndex(TestUI.currentWordElementIndex - 1);
64
+ TestUI.updateActiveElement(true);
65
+ TestUI.updateWordElement();
66
+
67
+ Caret.updatePosition();
68
+ Replay.addReplayEvent('backWord');
69
+ }
70
+
71
+ function handleSpace(): void {
72
+ if (!TestActive.get()) return;
73
+
74
+ if (TestInput.input.current === '') return;
75
+
76
+ const currentWord = TestWords.words.getCurrent();
77
+ if (Config.funbox === 'layoutfluid' && Config.mode !== 'time') {
78
+ // here I need to check if Config.customLayoutFluid exists because of my scuffed solution of returning whenever value is undefined in the setCustomLayoutfluid function
79
+ const layouts = Config.customLayoutfluid
80
+ ? Config.customLayoutfluid.split('#')
81
+ : ['qwerty', 'dvorak', 'colemak'];
82
+ let index = 0;
83
+ const outof = TestWords.words.length;
84
+ index = Math.floor((TestInput.input.history.length + 1) / (outof / layouts.length));
85
+ }
86
+ dontInsertSpace = true;
87
+
88
+ const burst = TestStats.calculateBurst();
89
+ TestInput.pushBurstToHistory(burst);
90
+
91
+ //correct word or in zen mode
92
+ const isWordCorrect = currentWord == TestInput.input.current || Config.mode == 'zen';
93
+ TestInput.incrementAccuracy(isWordCorrect);
94
+ if (isWordCorrect) {
95
+ TestInput.input.pushHistory();
96
+ TestWords.words.increaseCurrentIndex();
97
+ TestUI.setCurrentWordElementIndex(TestUI.currentWordElementIndex + 1);
98
+ TestUI.updateActiveElement();
99
+ Caret.updatePosition();
100
+ TestInput.incrementKeypressCount();
101
+ TestInput.pushKeypressWord(TestWords.words.currentIndex);
102
+ Replay.addReplayEvent('submitCorrectWord');
103
+ } else {
104
+ TestInput.pushMissedWord(TestWords.words.getCurrent());
105
+ TestInput.incrementKeypressErrors();
106
+ const cil = TestInput.input.current.length;
107
+ if (cil <= TestWords.words.getCurrent().length) {
108
+ if (cil >= TestInput.corrected.current.length) {
109
+ TestInput.corrected.current += '_';
110
+ } else {
111
+ TestInput.corrected.current =
112
+ TestInput.corrected.current.substring(0, cil) +
113
+ '_' +
114
+ TestInput.corrected.current.substring(cil + 1);
115
+ }
116
+ }
117
+ if (Config.stopOnError != 'off') {
118
+ if (Config.difficulty == 'expert' || Config.difficulty == 'master') {
119
+ //failed due to diff when pressing space
120
+ TestLogic.fail('difficulty');
121
+ return;
122
+ }
123
+ if (Config.stopOnError == 'word') {
124
+ dontInsertSpace = false;
125
+ Replay.addReplayEvent('incorrectLetter', '_');
126
+ TestUI.updateWordElement(true);
127
+ Caret.updatePosition();
128
+ }
129
+ return;
130
+ }
131
+ TestInput.input.pushHistory();
132
+ TestUI.highlightBadWord(TestUI.currentWordElementIndex, !Config.blindMode);
133
+ TestWords.words.increaseCurrentIndex();
134
+ TestUI.setCurrentWordElementIndex(TestUI.currentWordElementIndex + 1);
135
+ TestUI.updateActiveElement();
136
+ Caret.updatePosition();
137
+ TestInput.incrementKeypressCount();
138
+ TestInput.pushKeypressWord(TestWords.words.currentIndex);
139
+ TestInput.updateLastKeypress();
140
+ if (Config.difficulty == 'expert' || Config.difficulty == 'master') {
141
+ TestLogic.fail('difficulty');
142
+ return;
143
+ } else if (TestWords.words.currentIndex == TestWords.words.length) {
144
+ //submitted last word that is incorrect
145
+ TestLogic.finish();
146
+ return;
147
+ }
148
+ Replay.addReplayEvent('submitErrorWord');
149
+ }
150
+
151
+ let wordLength;
152
+ if (Config.mode === 'zen') {
153
+ wordLength = TestInput.input.current.length;
154
+ } else {
155
+ wordLength = TestWords.words.getCurrent().length;
156
+ }
157
+
158
+ const flex = Misc.whorf(Config.minBurstCustomSpeed, wordLength);
159
+
160
+ TestInput.corrected.pushHistory();
161
+
162
+ if (
163
+ !Config.showAllLines ||
164
+ Config.mode == 'time' ||
165
+ (CustomText.isWordRandom && CustomText.word == 0) ||
166
+ CustomText.isTimeRandom
167
+ ) {
168
+ const currentTop = Math.floor(
169
+ document.querySelectorAll<HTMLElement>('#words .word')[TestUI.currentWordElementIndex - 1]
170
+ .offsetTop
171
+ );
172
+ let nextTop;
173
+ try {
174
+ nextTop = Math.floor(
175
+ document.querySelectorAll<HTMLElement>('#words .word')[TestUI.currentWordElementIndex]
176
+ .offsetTop
177
+ );
178
+ } catch (e) {
179
+ nextTop = 0;
180
+ }
181
+
182
+ if (nextTop > currentTop && !TestUI.lineTransition) {
183
+ TestUI.lineJump(currentTop);
184
+ }
185
+ } //end of line wrap
186
+
187
+ if (
188
+ Config.mode === 'words' ||
189
+ Config.mode === 'custom' ||
190
+ Config.mode === 'quote' ||
191
+ Config.mode === 'zen'
192
+ ) {
193
+ TimerProgress.update();
194
+ }
195
+ if (
196
+ Config.mode == 'time' ||
197
+ Config.mode == 'words' ||
198
+ Config.mode == 'custom' ||
199
+ Config.mode == 'quote'
200
+ ) {
201
+ TestLogic.addWord();
202
+ }
203
+ }
204
+
205
+ function isCharCorrect(char: string, charIndex: number): boolean {
206
+ if (!correctShiftUsed) return false;
207
+
208
+ if (Config.mode == 'zen') {
209
+ return true;
210
+ }
211
+
212
+ const originalChar = TestWords.words.getCurrent()[charIndex];
213
+
214
+ if (originalChar == char) {
215
+ return true;
216
+ }
217
+
218
+ if (Config.language.split('_')[0] == 'russian') {
219
+ if ((char === 'е' || char === 'e') && originalChar == 'ё') {
220
+ return true;
221
+ }
222
+ if (char === 'ё' && (originalChar == 'е' || originalChar === 'e')) {
223
+ return true;
224
+ }
225
+ }
226
+
227
+ if (Config.funbox === 'arrows') {
228
+ if ((char === 'w' || char === 'ArrowUp') && originalChar == '↑') {
229
+ return true;
230
+ }
231
+ if ((char === 's' || char === 'ArrowDown') && originalChar == '↓') {
232
+ return true;
233
+ }
234
+ if ((char === 'a' || char === 'ArrowLeft') && originalChar == '←') {
235
+ return true;
236
+ }
237
+ if ((char === 'd' || char === 'ArrowRight') && originalChar == '→') {
238
+ return true;
239
+ }
240
+ }
241
+
242
+ if (
243
+ (char === `’` || char === '‘' || char === "'") &&
244
+ (originalChar == `’` || originalChar === '‘' || originalChar === "'")
245
+ ) {
246
+ return true;
247
+ }
248
+
249
+ if (
250
+ (char === `"` || char === '”' || char == '“' || char === '„') &&
251
+ (originalChar == `"` || originalChar === '”' || originalChar === '“' || originalChar === '„')
252
+ ) {
253
+ return true;
254
+ }
255
+
256
+ if (
257
+ (char === '–' || char === '—' || char == '-') &&
258
+ (originalChar == '-' || originalChar === '–' || originalChar === '—')
259
+ ) {
260
+ return true;
261
+ }
262
+
263
+ return false;
264
+ }
265
+
266
+ function handleChar(char: string, charIndex: number): void {
267
+ if (TestUI.resultCalculating || TestUI.resultVisible) {
268
+ return;
269
+ }
270
+
271
+ if (char === '…') {
272
+ for (let i = 0; i < 3; i++) {
273
+ handleChar('.', charIndex + i);
274
+ }
275
+
276
+ return;
277
+ }
278
+
279
+ if (char === '\n' && (Config.funbox === '58008' || Config.funbox === 'tenKeyMode')) {
280
+ char = ' ';
281
+ }
282
+
283
+ if (char !== '\n' && char !== '\t' && /\s/.test(char)) {
284
+ if (Config.funbox === 'nospace' || Config.funbox === 'arrows') return;
285
+ handleSpace();
286
+
287
+ //insert space for expert and master or strict space,
288
+ //or for stop on error set to word,
289
+ //otherwise dont do anything
290
+ if (
291
+ Config.difficulty !== 'normal' ||
292
+ (Config.strictSpace && Config.mode !== 'zen') ||
293
+ Config.stopOnError === 'word'
294
+ ) {
295
+ if (dontInsertSpace) {
296
+ dontInsertSpace = false;
297
+ return;
298
+ }
299
+ } else {
300
+ return;
301
+ }
302
+ }
303
+
304
+ if (Config.mode !== 'zen' && TestWords.words.getCurrent()[charIndex] !== '\n' && char === '\n') {
305
+ return;
306
+ }
307
+
308
+ //start the test
309
+ if (!TestActive.get() && !TestLogic.startTest()) {
310
+ return;
311
+ }
312
+
313
+ Focus.set(true);
314
+ Caret.stopAnimation();
315
+
316
+ const thisCharCorrect = isCharCorrect(char, charIndex);
317
+
318
+ if (thisCharCorrect && Config.mode !== 'zen') {
319
+ char = TestWords.words.getCurrent().charAt(charIndex);
320
+ }
321
+
322
+ if (!thisCharCorrect && char === '\n') {
323
+ if (TestInput.input.current === '') return;
324
+ char = ' ';
325
+ }
326
+
327
+ if (TestInput.input.current === '') {
328
+ TestInput.setBurstStart(performance.now());
329
+ }
330
+
331
+ const resultingWord =
332
+ TestInput.input.current.substring(0, charIndex) +
333
+ char +
334
+ TestInput.input.current.substring(charIndex + 1);
335
+
336
+ // If a trailing composed char is used, ignore it when counting accuracy
337
+ if (
338
+ !thisCharCorrect &&
339
+ Misc.trailingComposeChars.test(resultingWord) &&
340
+ CompositionState.getComposing()
341
+ ) {
342
+ TestInput.input.current = resultingWord;
343
+ TestUI.updateWordElement();
344
+ Caret.updatePosition();
345
+ return;
346
+ }
347
+
348
+ // MonkeyPower.addPower(thisCharCorrect);
349
+ TestInput.incrementAccuracy(thisCharCorrect);
350
+
351
+ if (!thisCharCorrect) {
352
+ TestInput.incrementKeypressErrors();
353
+ TestInput.pushMissedWord(TestWords.words.getCurrent());
354
+ }
355
+
356
+ WeakSpot.updateScore(
357
+ Config.mode === 'zen' ? char : TestWords.words.getCurrent()[charIndex],
358
+ thisCharCorrect
359
+ );
360
+
361
+ if (!correctShiftUsed && Config.difficulty != 'master') return;
362
+
363
+ //update current corrected version. if its empty then add the current char. if its not then replace the last character with the currently pressed one / add it
364
+ if (TestInput.corrected.current === '') {
365
+ TestInput.corrected.current += resultingWord;
366
+ } else {
367
+ if (charIndex >= TestInput.corrected.current.length) {
368
+ TestInput.corrected.current += char;
369
+ } else if (!thisCharCorrect) {
370
+ TestInput.corrected.current =
371
+ TestInput.corrected.current.substring(0, charIndex) +
372
+ char +
373
+ TestInput.corrected.current.substring(charIndex + 1);
374
+ }
375
+ }
376
+
377
+ TestInput.incrementKeypressCount();
378
+ TestInput.updateLastKeypress();
379
+ TestInput.pushKeypressWord(TestWords.words.currentIndex);
380
+
381
+ if (Config.difficulty !== 'master' && Config.stopOnError == 'letter' && !thisCharCorrect) {
382
+ return;
383
+ }
384
+
385
+ Replay.addReplayEvent(thisCharCorrect ? 'correctLetter' : 'incorrectLetter', char);
386
+
387
+ //update the active word top, but only once
388
+ if (TestInput.input.current.length === 1 && TestWords.words.currentIndex === 0) {
389
+ TestUI.setActiveWordTop((<HTMLElement>document.querySelector('#words .active'))?.offsetTop);
390
+ }
391
+
392
+ //max length of the input is 20 unless in zen mode then its 30
393
+ if (
394
+ (Config.mode === 'zen' && charIndex < 30) ||
395
+ (Config.mode !== 'zen' && charIndex < TestWords.words.getCurrent().length + 20)
396
+ ) {
397
+ TestInput.input.current = resultingWord;
398
+ }
399
+
400
+ if (!thisCharCorrect && Config.difficulty == 'master') {
401
+ TestInput.input.pushHistory();
402
+ TestInput.corrected.pushHistory();
403
+ TestLogic.fail('difficulty');
404
+ return;
405
+ }
406
+
407
+ if (Config.mode != 'zen') {
408
+ //not applicable to zen mode
409
+ //auto stop the test if the last word is correct
410
+ const currentWord = TestWords.words.getCurrent();
411
+ const lastindex = TestWords.words.currentIndex;
412
+ if (
413
+ (currentWord == TestInput.input.current ||
414
+ (Config.quickEnd &&
415
+ currentWord.length == TestInput.input.current.length &&
416
+ Config.stopOnError == 'off')) &&
417
+ lastindex == TestWords.words.length - 1
418
+ ) {
419
+ TestInput.input.pushHistory();
420
+ TestInput.corrected.pushHistory();
421
+ TestLogic.finish();
422
+ return;
423
+ }
424
+ }
425
+
426
+ const activeWordTopBeforeJump = document.querySelector<HTMLElement>('#words .word.active')
427
+ ?.offsetTop as number;
428
+ TestUI.updateWordElement();
429
+
430
+ if (!Config.hideExtraLetters) {
431
+ const newActiveTop = document.querySelector<HTMLElement>('#words .word.active')
432
+ ?.offsetTop as number;
433
+ //stop the word jump by slicing off the last character, update word again
434
+ if (
435
+ activeWordTopBeforeJump < newActiveTop &&
436
+ !TestUI.lineTransition &&
437
+ TestInput.input.current.length > 1
438
+ ) {
439
+ if (Config.mode == 'zen') {
440
+ const currentTop = Math.floor(
441
+ document.querySelectorAll<HTMLElement>('#words .word')[TestUI.currentWordElementIndex - 1]
442
+ ?.offsetTop
443
+ ) as number;
444
+ if (!Config.showAllLines) TestUI.lineJump(currentTop);
445
+ } else {
446
+ TestInput.input.current = TestInput.input.current.slice(0, -1);
447
+ TestUI.updateWordElement();
448
+ }
449
+ }
450
+ }
451
+
452
+ //simulate space press in nospace funbox
453
+ if (
454
+ ((Config.funbox === 'nospace' || Config.funbox === 'arrows') &&
455
+ TestInput.input.current.length === TestWords.words.getCurrent().length) ||
456
+ (char === '\n' && thisCharCorrect)
457
+ ) {
458
+ handleSpace();
459
+ }
460
+
461
+ if (char !== '\n') {
462
+ Caret.updatePosition();
463
+ }
464
+ }
465
+
466
+ function handleTab(
467
+ event: { preventDefault: () => void; shiftKey: any },
468
+ popupVisible: boolean
469
+ ): void {
470
+ if (TestUI.resultCalculating) {
471
+ event.preventDefault();
472
+ return;
473
+ }
474
+
475
+ let shouldInsertTabCharacter = false;
476
+
477
+ if ((Config.mode == 'zen' && !event.shiftKey) || (TestWords.hasTab && !event.shiftKey)) {
478
+ shouldInsertTabCharacter = true;
479
+ }
480
+ }
481
+
482
+ export function setWordsInputElement(wordsElement: HTMLInputElement) {
483
+ wordsInput = wordsElement;
484
+ }
485
+
486
+ export async function inputKeydown(event: any, wordsFocused: boolean) {
487
+ const allowTyping = !TestUI.resultVisible && (wordsFocused || event.key !== 'Enter');
488
+
489
+ //no space for tenKeyMode
490
+ if (Config.funbox === 'tenKeyMode' && event.keyCode === 32) {
491
+ event.preventDefault();
492
+ }
493
+
494
+ if (allowTyping && !wordsFocused && event.key !== 'Enter') {
495
+ TestUI.focusWords();
496
+ if (Config.showOutOfFocusWarning) {
497
+ event.preventDefault();
498
+ }
499
+ }
500
+
501
+ //tab
502
+ if (event.key == 'Tab') {
503
+ handleTab(event, false);
504
+ }
505
+
506
+ if (!allowTyping) return;
507
+
508
+ if (!event.isTrusted || TestUI.testRestarting) {
509
+ event.preventDefault();
510
+ return;
511
+ }
512
+
513
+ if (TestInput.spacingDebug) {
514
+ console.log(
515
+ 'spacing debug',
516
+ 'keypress',
517
+ event.key,
518
+ 'length',
519
+ TestInput.keypressTimings.spacing.array.length
520
+ );
521
+ }
522
+ TestInput.recordKeypressSpacing();
523
+ TestInput.setKeypressDuration(performance.now());
524
+ TestInput.setKeypressNotAfk();
525
+
526
+ //blocking firefox from going back in history with backspace
527
+ if (event.key === 'Backspace') {
528
+ const t = /INPUT|SELECT|TEXTAREA/i;
529
+ if (!t.test((event.target as unknown as Element).tagName)) {
530
+ event.preventDefault();
531
+ }
532
+
533
+ if (Config.confidenceMode === 'max') {
534
+ event.preventDefault();
535
+ return;
536
+ }
537
+ }
538
+
539
+ if (Config.funbox !== 'arrows' && /Arrow/i.test(event.key)) {
540
+ event.preventDefault();
541
+ return;
542
+ }
543
+
544
+ if (event.key === 'Backspace' && TestInput.input.current.length === 0) {
545
+ backspaceToPrevious();
546
+ if (TestInput.input.current) {
547
+ setWordsInput(' ' + TestInput.input.current + ' ');
548
+ }
549
+ }
550
+
551
+ if (event.key === 'Enter') {
552
+ if (event.shiftKey && Config.mode == 'zen') {
553
+ TestLogic.finish();
554
+ } else if (
555
+ event.shiftKey &&
556
+ ((Config.mode == 'time' && Config.time === 0) ||
557
+ (Config.mode == 'words' && Config.words === 0))
558
+ ) {
559
+ TestInput.setBailout(true);
560
+ TestLogic.finish();
561
+ } else {
562
+ handleChar('\n', TestInput.input.current.length);
563
+ setWordsInput(' ' + TestInput.input.current);
564
+ }
565
+ }
566
+
567
+ //show dead keys
568
+ if (event.key === 'Dead' && !CompositionState.getComposing()) {
569
+ const word = document.querySelector<HTMLElement>('#words .word.active');
570
+ const len = TestInput.input.current.length; // have to do this because prettier wraps the line and causes an error
571
+
572
+ // Check to see if the letter actually exists to toggle it as dead
573
+ const deadLetter = word?.querySelectorAll('letter')[len];
574
+ if (deadLetter) {
575
+ deadLetter.classList.toggle('dead');
576
+ }
577
+ }
578
+
579
+ if (Config.funbox === 'arrows') {
580
+ let char = event.key;
581
+ if (['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown'].includes(char)) {
582
+ if (char === 'ArrowLeft') char = 'a';
583
+ if (char === 'ArrowRight') char = 'd';
584
+ if (char === 'ArrowDown') char = 's';
585
+ if (char === 'ArrowUp') char = 'w';
586
+ event.preventDefault();
587
+ handleChar(char, TestInput.input.current.length);
588
+ updateUI();
589
+ setWordsInput(' ' + TestInput.input.current);
590
+ }
591
+ }
592
+ }
593
+
594
+ export function wordsInputKeyup(event: any) {
595
+ if (!event.isTrusted || TestUI.testRestarting) {
596
+ event.preventDefault();
597
+ return;
598
+ }
599
+
600
+ if (TestUI.resultVisible) return;
601
+ const now = performance.now();
602
+ if (TestInput.keypressTimings.duration.current !== -1) {
603
+ const diff = Math.abs(TestInput.keypressTimings.duration.current - now);
604
+ TestInput.pushKeypressDuration(diff);
605
+ }
606
+ TestInput.setKeypressDuration(now);
607
+ }
608
+
609
+ export function wordsInputBeforeinput(event: any) {
610
+ if (!event.isTrusted) return;
611
+ if ((event.target as HTMLInputElement).value === '') {
612
+ (event.target as HTMLInputElement).value = ' ';
613
+ }
614
+ }
615
+
616
+ export function wordsInputInput(event: any) {
617
+ if (!event.isTrusted || TestUI.testRestarting) {
618
+ (event.target as HTMLInputElement).value = ' ';
619
+ return;
620
+ }
621
+
622
+ TestInput.setKeypressNotAfk();
623
+
624
+ const realInputValue = (event.target as HTMLInputElement).value.normalize();
625
+ const inputValue = realInputValue.slice(1);
626
+
627
+ // input will be modified even with the preventDefault() in
628
+ // beforeinput/keydown if it's part of a compose sequence. this undoes
629
+ // the effects of that and takes the input out of compose mode.
630
+ if (Config.layout !== 'default' && inputValue.length >= TestInput.input.current.length) {
631
+ setWordsInput(' ' + TestInput.input.current);
632
+ return;
633
+ }
634
+
635
+ if (realInputValue.length === 0 && TestInput.input.current.length === 0) {
636
+ // fallback for when no Backspace keydown event (mobile)
637
+ backspaceToPrevious();
638
+ } else if (inputValue.length < TestInput.input.current.length) {
639
+ TestInput.input.current = inputValue;
640
+ TestUI.updateWordElement();
641
+ Caret.updatePosition();
642
+ if (!CompositionState.getComposing()) {
643
+ Replay.addReplayEvent('setLetterIndex', TestInput.input.current.length);
644
+ }
645
+ } else if (inputValue !== TestInput.input.current) {
646
+ let diffStart = 0;
647
+ while (inputValue[diffStart] === TestInput.input.current[diffStart]) {
648
+ diffStart++;
649
+ }
650
+
651
+ for (let i = diffStart; i < inputValue.length; i++) {
652
+ handleChar(inputValue[i], i);
653
+ }
654
+ }
655
+
656
+ setWordsInput(' ' + TestInput.input.current);
657
+ updateUI();
658
+
659
+ const statebefore = CompositionState.getComposing();
660
+ setTimeout(() => {
661
+ // checking composition state during the input event and on the next loop
662
+ // this is done because some browsers (e.g. Chrome) will fire the input
663
+ // event before the compositionend event.
664
+ // this ensures the UI is correct
665
+
666
+ const stateafter = CompositionState.getComposing();
667
+ if (statebefore !== stateafter) {
668
+ TestUI.updateWordElement();
669
+ }
670
+
671
+ // force caret at end of input
672
+ // doing it on next cycle because Chromium on Android won't let me edit
673
+ // the selection inside the input event
674
+ if (
675
+ (event.target as HTMLInputElement).selectionStart !==
676
+ (event.target as HTMLInputElement).value.length &&
677
+ (!Misc.trailingComposeChars.test((event.target as HTMLInputElement).value) ||
678
+ ((event.target as HTMLInputElement).selectionStart ?? 0) <
679
+ (event.target as HTMLInputElement).value.search(Misc.trailingComposeChars))
680
+ ) {
681
+ (event.target as HTMLInputElement).selectionStart = (
682
+ event.target as HTMLInputElement
683
+ ).selectionEnd = (event.target as HTMLInputElement).value.length;
684
+ }
685
+ }, 0);
686
+ }
687
+
688
+ export function wordsInputFocus(event: { target: HTMLInputElement }) {
689
+ (event.target as HTMLInputElement).selectionStart = (
690
+ event.target as HTMLInputElement
691
+ ).selectionEnd = (event.target as HTMLInputElement).value.length;
692
+ }
693
+
694
+ export function wordsInputCopy(event: { preventDefault: () => void }) {
695
+ event.preventDefault();
696
+ }
697
+
698
+ export function wordsInputPaste(event: { preventDefault: () => void }) {
699
+ event.preventDefault();
700
+ }
701
+
702
+ // Composing events
703
+ export function wordsInputCompositionstart() {
704
+ CompositionState.setComposing(true);
705
+ CompositionState.setStartPos(TestInput.input.current.length);
706
+ }
707
+
708
+ export function wordsInputCompositionend() {
709
+ CompositionState.setComposing(false);
710
+ }