|
11 | 11 | let statsStartOverButton = null; |
12 | 12 | let keyboardContainer = null; |
13 | 13 | let realtimeStatsContainer = null; |
14 | | - let config = { keyboard: true, availableKeys: [], showStats: false, realTimeStats: [] }; |
| 14 | + let config = { keyboard: true, availableKeys: [], showStats: false, realTimeStats: [], keyTips: false }; |
15 | 15 |
|
16 | 16 | // Normalized set of available keys (for fast lookup) |
17 | 17 | let availableKeysSet = new Set(); |
|
28 | 28 | let keyboardEnabled = false; |
29 | 29 | let activeKeyElement = null; |
30 | 30 | let activeKeyTimeout = null; |
| 31 | + let keyTipsEnabled = false; |
| 32 | + let currentTipKeyElement = null; |
| 33 | + let currentTipShiftElement = null; // Track which shift key is highlighted |
31 | 34 |
|
32 | 35 | // Real-time stats update interval |
33 | 36 | let realtimeStatsInterval = null; |
|
136 | 139 | return keyboardContainer.querySelector(`[data-key="${normalizedChar}"]`); |
137 | 140 | } |
138 | 141 |
|
| 142 | + // Map shift symbols to their base keys |
| 143 | + const shiftSymbolMap = { |
| 144 | + '!': '1', '@': '2', '#': '3', '$': '4', '%': '5', |
| 145 | + '^': '6', '&': '7', '*': '8', '(': '9', ')': '0', |
| 146 | + '_': '-', '+': '=', '{': '[', '}': ']', '|': '\\', |
| 147 | + ':': ';', '"': "'", '<': ',', '>': '.', '?': '/', '~': '`' |
| 148 | + }; |
| 149 | + |
| 150 | + // Get the base key for a character (maps shift symbols to their base keys) |
| 151 | + function getBaseKey(char) { |
| 152 | + // If it's a shift symbol, return the base key |
| 153 | + if (shiftSymbolMap[char]) { |
| 154 | + return shiftSymbolMap[char]; |
| 155 | + } |
| 156 | + // For uppercase letters, return lowercase |
| 157 | + if (char >= 'A' && char <= 'Z') { |
| 158 | + return char.toLowerCase(); |
| 159 | + } |
| 160 | + // Otherwise return as-is |
| 161 | + return char; |
| 162 | + } |
| 163 | + |
| 164 | + // Check if a character requires Shift to type |
| 165 | + function requiresShift(char) { |
| 166 | + // Check if it's an uppercase letter |
| 167 | + if (char >= 'A' && char <= 'Z') { |
| 168 | + return true; |
| 169 | + } |
| 170 | + |
| 171 | + // Check if it's a special symbol that requires Shift |
| 172 | + if (shiftSymbolMap[char]) { |
| 173 | + return true; |
| 174 | + } |
| 175 | + |
| 176 | + return false; |
| 177 | + } |
| 178 | + |
| 179 | + // Determine if a key is on the left side of the keyboard |
| 180 | + // Returns true for left side, false for right side |
| 181 | + // Based on standard QWERTY layout: |
| 182 | + // Left: `, 1-5, tab, q-t, caps, a-g, shift, z-b |
| 183 | + // Right: 6-0, -, =, y-p, [, ], \, h-l, ;, ', n-/, shift |
| 184 | + function isKeyOnLeftSide(char) { |
| 185 | + const normalizedChar = char.toLowerCase(); |
| 186 | + |
| 187 | + // Handle special keys |
| 188 | + if (normalizedChar === 'tab' || normalizedChar === 'caps' || normalizedChar === 'shift') { |
| 189 | + // These keys span or are on left side |
| 190 | + return true; |
| 191 | + } |
| 192 | + |
| 193 | + // Number row: left side is `, 1-5 |
| 194 | + if (normalizedChar === '`' || (normalizedChar >= '1' && normalizedChar <= '5')) { |
| 195 | + return true; |
| 196 | + } |
| 197 | + |
| 198 | + // Top letter row: left side is q-t |
| 199 | + if (normalizedChar >= 'q' && normalizedChar <= 't') { |
| 200 | + return true; |
| 201 | + } |
| 202 | + |
| 203 | + // Middle letter row: left side is a-g |
| 204 | + if (normalizedChar >= 'a' && normalizedChar <= 'g') { |
| 205 | + return true; |
| 206 | + } |
| 207 | + |
| 208 | + // Bottom letter row: left side is z-b |
| 209 | + if (normalizedChar >= 'z' && normalizedChar <= 'b') { |
| 210 | + return true; |
| 211 | + } |
| 212 | + |
| 213 | + // Everything else is on the right side |
| 214 | + return false; |
| 215 | + } |
| 216 | + |
| 217 | + // Get shift key element (left or right) |
| 218 | + function getShiftKeyElement(isLeft) { |
| 219 | + if (!keyboardContainer) return null; |
| 220 | + |
| 221 | + // Find shift key by data attribute |
| 222 | + const shiftSide = isLeft ? 'left' : 'right'; |
| 223 | + return keyboardContainer.querySelector(`[data-key="shift"][data-shift-side="${shiftSide}"]`); |
| 224 | + } |
| 225 | + |
| 226 | + // Update the key tip highlight (shows which key to press next) |
| 227 | + function updateKeyTip() { |
| 228 | + // Only update if keyTips mode is enabled and keyboard is visible |
| 229 | + if (!keyTipsEnabled || !keyboardEnabled || !keyboardContainer) { |
| 230 | + return; |
| 231 | + } |
| 232 | + |
| 233 | + // Clear previous tip |
| 234 | + if (currentTipKeyElement) { |
| 235 | + currentTipKeyElement.classList.remove('key-tip'); |
| 236 | + currentTipKeyElement = null; |
| 237 | + } |
| 238 | + |
| 239 | + // Clear previous shift tip |
| 240 | + if (currentTipShiftElement) { |
| 241 | + currentTipShiftElement.classList.remove('key-tip'); |
| 242 | + currentTipShiftElement = null; |
| 243 | + } |
| 244 | + |
| 245 | + // Find the next character to type |
| 246 | + const nextCharIndex = typedText.length; |
| 247 | + if (nextCharIndex >= originalText.length) { |
| 248 | + // All characters typed, no tip needed |
| 249 | + return; |
| 250 | + } |
| 251 | + |
| 252 | + const nextChar = originalText[nextCharIndex]; |
| 253 | + if (!nextChar) { |
| 254 | + return; |
| 255 | + } |
| 256 | + |
| 257 | + // Get the base key for highlighting (maps shift symbols to their base keys) |
| 258 | + const baseKey = getBaseKey(nextChar); |
| 259 | + |
| 260 | + // Get the key element for the base key |
| 261 | + const keyElement = getKeyElement(baseKey); |
| 262 | + if (keyElement) { |
| 263 | + currentTipKeyElement = keyElement; |
| 264 | + keyElement.classList.add('key-tip'); |
| 265 | + } |
| 266 | + |
| 267 | + // Check if shift is needed and highlight appropriate shift key |
| 268 | + if (requiresShift(nextChar)) { |
| 269 | + // Determine which side of keyboard the key is on |
| 270 | + const charForSideCheck = baseKey.toLowerCase(); |
| 271 | + const isLeftSide = isKeyOnLeftSide(charForSideCheck); |
| 272 | + |
| 273 | + // Highlight opposite shift: left side keys use right shift, right side keys use left shift |
| 274 | + const shiftElement = getShiftKeyElement(!isLeftSide); |
| 275 | + if (shiftElement) { |
| 276 | + currentTipShiftElement = shiftElement; |
| 277 | + shiftElement.classList.add('key-tip'); |
| 278 | + } |
| 279 | + } |
| 280 | + } |
| 281 | + |
139 | 282 | // Highlight a key on the keyboard |
140 | 283 | function highlightKey(char, isError = false) { |
| 284 | + // Skip regular highlighting if keyTips mode is enabled |
| 285 | + if (keyTipsEnabled) { |
| 286 | + return; |
| 287 | + } |
| 288 | + |
141 | 289 | // Don't highlight unavailable keys |
142 | 290 | if (!isKeyAvailable(char)) { |
143 | 291 | return; |
|
179 | 327 | const keyboard = document.createElement('div'); |
180 | 328 | keyboard.className = 'keyboard'; |
181 | 329 |
|
| 330 | + let shiftKeyIndex = 0; // Track which shift key we're rendering (0 = left, 1 = right) |
| 331 | + |
182 | 332 | keyboardLayout.forEach(row => { |
183 | 333 | const rowElement = document.createElement('div'); |
184 | 334 | rowElement.className = 'keyboard-row'; |
|
189 | 339 | keyElement.className = 'keyboard-key'; |
190 | 340 | keyElement.setAttribute('data-key', normalizedKey); |
191 | 341 |
|
| 342 | + // Add data attribute to distinguish left vs right shift |
| 343 | + if (key === 'shift') { |
| 344 | + const shiftSide = shiftKeyIndex === 0 ? 'left' : 'right'; |
| 345 | + keyElement.setAttribute('data-shift-side', shiftSide); |
| 346 | + shiftKeyIndex++; |
| 347 | + } |
| 348 | + |
192 | 349 | // Check if this key is available (use isKeyAvailable to ensure space, comma, dot are always available) |
193 | 350 | const isAvailable = isKeyAvailable(key); |
194 | 351 | if (!isAvailable) { |
|
224 | 381 | if (!keyboardContainer) return; |
225 | 382 |
|
226 | 383 | keyboardEnabled = config.keyboard === true; |
| 384 | + keyTipsEnabled = config.keyTips === true; // Defaults to false if undefined or not set |
227 | 385 |
|
228 | 386 | if (keyboardEnabled) { |
229 | 387 | renderKeyboard(); |
230 | 388 | keyboardContainer.classList.add('visible'); |
| 389 | + // Update key tip after keyboard is rendered |
| 390 | + if (keyTipsEnabled) { |
| 391 | + updateKeyTip(); |
| 392 | + } |
231 | 393 | } else { |
232 | 394 | keyboardContainer.classList.remove('visible'); |
233 | 395 | } |
|
330 | 492 | } |
331 | 493 |
|
332 | 494 | textContainer.innerHTML = html; |
| 495 | + |
| 496 | + // Update key tip if enabled |
| 497 | + updateKeyTip(); |
333 | 498 | } |
334 | 499 |
|
335 | 500 | function escapeHtml(text) { |
|
415 | 580 |
|
416 | 581 | renderText(); |
417 | 582 | updateRealtimeStats(); |
| 583 | + // updateKeyTip is called in renderText, so no need to call it here |
418 | 584 | } |
419 | 585 |
|
420 | 586 | function handleKeyDown(e) { |
|
529 | 695 | activeKeyTimeout = null; |
530 | 696 | } |
531 | 697 |
|
| 698 | + // Clear key tip if enabled |
| 699 | + if (currentTipKeyElement) { |
| 700 | + currentTipKeyElement.classList.remove('key-tip'); |
| 701 | + currentTipKeyElement = null; |
| 702 | + } |
| 703 | + if (currentTipShiftElement) { |
| 704 | + currentTipShiftElement.classList.remove('key-tip'); |
| 705 | + currentTipShiftElement = null; |
| 706 | + } |
| 707 | + |
532 | 708 | // Show typing container and hide completion screen and stats dashboard |
533 | 709 | const typingTextContainer = document.querySelector('.typing-text-container'); |
534 | 710 | if (typingTextContainer) { |
@@ -815,6 +991,16 @@ Generated: ${new Date().toLocaleString()} |
815 | 991 | keyboardContainer.classList.remove('visible'); |
816 | 992 | } |
817 | 993 |
|
| 994 | + // Clear key tip when dashboard is shown |
| 995 | + if (currentTipKeyElement) { |
| 996 | + currentTipKeyElement.classList.remove('key-tip'); |
| 997 | + currentTipKeyElement = null; |
| 998 | + } |
| 999 | + if (currentTipShiftElement) { |
| 1000 | + currentTipShiftElement.classList.remove('key-tip'); |
| 1001 | + currentTipShiftElement = null; |
| 1002 | + } |
| 1003 | + |
818 | 1004 | // Hide real-time stats when dashboard is shown |
819 | 1005 | if (realtimeStatsContainer) { |
820 | 1006 | realtimeStatsContainer.style.display = 'none'; |
@@ -909,6 +1095,16 @@ Generated: ${new Date().toLocaleString()} |
909 | 1095 | keyboardContainer.classList.remove('visible'); |
910 | 1096 | } |
911 | 1097 |
|
| 1098 | + // Clear key tip when completion screen is shown |
| 1099 | + if (currentTipKeyElement) { |
| 1100 | + currentTipKeyElement.classList.remove('key-tip'); |
| 1101 | + currentTipKeyElement = null; |
| 1102 | + } |
| 1103 | + if (currentTipShiftElement) { |
| 1104 | + currentTipShiftElement.classList.remove('key-tip'); |
| 1105 | + currentTipShiftElement = null; |
| 1106 | + } |
| 1107 | + |
912 | 1108 | // Hide real-time stats when completion screen is shown |
913 | 1109 | if (realtimeStatsContainer) { |
914 | 1110 | realtimeStatsContainer.style.display = 'none'; |
|
0 commit comments