diff --git a/static/css/style.css b/static/css/style.css index 260c600..94e5cb5 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -4,6 +4,38 @@ body { transition: background-color 0.3s, color 0.3s; } +/* Cursor-Styling */ +.cursor-char { + position: relative; +} + +.cursor-char::after { + content: ""; + position: absolute; + left: 0; + bottom: -3px; + width: 100%; + height: 3px; + background-color: #0056b3; + animation: blink 0.7s infinite; +} + +@keyframes blink { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0; + } +} + +/* Dark Theme Anpassungen für Cursor */ +[data-bs-theme="dark"] .cursor-char::after { + background-color: #0cf; + height: 3px; + animation: blink 0.7s infinite; +} + /* Dark Theme Styles */ [data-bs-theme="dark"] { --bs-body-bg: #121212; @@ -64,6 +96,10 @@ body { color: #f87171; /* Helleres Rot für besseren Kontrast */ } +[data-bs-theme="dark"] .incorrect-char-overlay { + color: #ffc107; +} + [data-bs-theme="dark"] .highlight-space, [data-bs-theme="dark"] .highlight-tab, [data-bs-theme="dark"] .highlight-newline { @@ -80,7 +116,7 @@ body { background-color: #2d2d2d; color: #e0e0e0; border-color: #0d6efd; - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + box-shadow: 0 0 0 0.25rem rgb(13 110 253 / 25%); } [data-bs-theme="dark"] .lesson-link:hover { @@ -103,14 +139,16 @@ body { /* Fortschritt und Statistik Link Styling */ .progress-toggle, -.statistics-link { +.statistics-link, +.input-toggle { cursor: pointer; transition: all 0.3s ease; color: inherit; } .progress-toggle:hover, -.statistics-link:hover { +.statistics-link:hover, +.input-toggle:hover { color: #ffc107 !important; text-shadow: 0 0 10px #ffc107, 0 0 20px #ffc107; transform: scale(1.05); @@ -137,14 +175,15 @@ body { line-height: 1.6; letter-spacing: 0.5px; user-select: none; /* Verhindert Textauswahl */ - -webkit-user-select: none; /* Für Safari */ - -moz-user-select: none; /* Für Firefox */ - -ms-user-select: none; /* Für Internet Explorer/Edge */ + user-select: none; /* Für Safari */ + user-select: none; /* Für Firefox */ + user-select: none; /* Für Internet Explorer/Edge */ white-space: pre-wrap; overflow: auto; - word-wrap: break-word; + overflow-wrap: break-word; overflow-wrap: break-word; word-break: normal; + padding-top: 2.5em; /* Vergrößerter Abstand am oberen Rand */ } .text-line { @@ -152,7 +191,7 @@ body { width: 100%; white-space: pre-wrap; font-family: monospace; - word-wrap: break-word; + overflow-wrap: break-word; overflow-wrap: break-word; word-break: normal; hyphens: none; @@ -172,7 +211,7 @@ body { #user-input:focus { border-color: #0d6efd; - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + box-shadow: 0 0 0 0.25rem rgb(13 110 253 / 25%); } .highlight-correct { @@ -184,6 +223,23 @@ body { color: #dc3545; text-decoration: underline; font-weight: bold; + position: relative; +} + +.incorrect-char-container { + position: relative; +} + +.incorrect-char-overlay { + position: absolute; + top: -1em; + left: 0; + font-size: 0.7em; + color: #ffc107; + background-color: transparent; + font-weight: bold; + z-index: 10; + white-space: nowrap; } .badge { @@ -194,7 +250,7 @@ body { .list-group-item { border: none; padding: 0.75rem 1rem; - border-bottom: 1px solid rgba(0,0,0,.125); + border-bottom: 1px solid rgb(0 0 0 / 12.5%); } .list-group-item:last-child { @@ -219,7 +275,7 @@ body { .btn:hover { transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0,0,0,0.1); + box-shadow: 0 4px 8px rgb(0 0 0 / 10%); } .btn:active { @@ -255,6 +311,7 @@ body { font-size: 1rem; } } + /* Theme-Toggle-Button */ #theme-toggle { display: flex; diff --git a/static/js/script.js b/static/js/script.js index 0d02aea..fa3cd15 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -31,7 +31,6 @@ document.addEventListener('DOMContentLoaded', function() { initializeTheme(); document.getElementById('theme-toggle').addEventListener('click', toggleTheme); - const userInputElement = document.getElementById('user-input'); const targetTextElement = document.getElementById('target-text'); const nextBtn = document.getElementById('next-btn'); const endBtn = document.getElementById('end-btn'); @@ -52,6 +51,19 @@ document.addEventListener('DOMContentLoaded', function() { let userInput = ''; // Gesamte Eingabe let keyStrokeCount = 0; // Zähler für alle Tastaturanschläge (inkl. Backspace) let lines = fullText.split('\n'); + let userScrolled = false; // Flag für manuelles Scrollen + + // Setze Fokus auf das Dokument, um Tastatureingaben zu erfassen + document.addEventListener('click', function() { + document.body.focus(); + }); + document.body.tabIndex = -1; + document.body.focus(); + + // Event-Listener für manuelles Scrollen + targetTextElement.addEventListener('scroll', function() { + userScrolled = true; + }); // Berechne die aktuelle Zeile basierend auf currentCharIndex function getCurrentLineIndex() { @@ -88,150 +100,138 @@ document.addEventListener('DOMContentLoaded', function() { return false; }); - // Fokus auf das Eingabefeld setzen - userInputElement.focus(); - - // Verhindere Copy & Paste für das Eingabefeld - userInputElement.addEventListener('paste', function(event) { - event.preventDefault(); - alert('Einfügen (Paste) ist nicht erlaubt! Bitte tippen Sie den Text manuell.'); - }); - - userInputElement.addEventListener('copy', function(event) { - event.preventDefault(); - alert('Kopieren (Copy) ist nicht erlaubt!'); - }); - - userInputElement.addEventListener('cut', function(event) { - event.preventDefault(); - alert('Ausschneiden (Cut) ist nicht erlaubt!'); - }); - - // Verhindere Drag & Drop - userInputElement.addEventListener('drop', function(event) { - event.preventDefault(); - alert('Drag & Drop ist nicht erlaubt! Bitte tippen Sie den Text manuell.'); - }); - - // Verhindere das Einfügen über das Kontextmenü (rechte Maustaste) - userInputElement.addEventListener('contextmenu', function(event) { - event.preventDefault(); - return false; - }); - - // Verhindere das Einfügen über Tastenkombinationen (Strg+V, Cmd+V, etc.) - userInputElement.addEventListener('keydown', function(event) { - // Strg+V oder Cmd+V - if ((event.ctrlKey || event.metaKey) && event.key === 'v') { + // Tastatureingaben direkt erfassen + document.addEventListener('keydown', function(event) { + // Verhindere Standardaktionen für Steuertasten, außer Backspace, Enter und Pfeiltasten + if (event.ctrlKey || event.metaKey) { event.preventDefault(); - alert('Einfügen (Strg+V / Cmd+V) ist nicht erlaubt! Bitte tippen Sie den Text manuell.'); - return false; + return; } - // Strg+C oder Cmd+C - if ((event.ctrlKey || event.metaKey) && event.key === 'c') { + // Behandle Pfeiltasten + if (event.key === 'ArrowLeft') { event.preventDefault(); - alert('Kopieren (Strg+C / Cmd+C) ist nicht erlaubt!'); - return false; - } - - // Strg+X oder Cmd+X - if ((event.ctrlKey || event.metaKey) && event.key === 'x') { - event.preventDefault(); - alert('Ausschneiden (Strg+X / Cmd+X) ist nicht erlaubt!'); - return false; - } - - // Strg+A oder Cmd+A (Selektion verhindern) - if ((event.ctrlKey || event.metaKey) && event.key === 'a') { - event.preventDefault(); - alert('Selektion (Strg+A / Cmd+A) ist nicht erlaubt!'); - return false; - } - - // Rechtsklick-Menü-Tasten (Context Menu key) - if (event.key === 'ContextMenu') { - event.preventDefault(); - return false; - } - }); - - // Starte die Zeitmessung beim ersten Tastendruck und zähle Tastaturanschläge - userInputElement.addEventListener('keydown', function(event) { - const cursorPosition = userInputElement.selectionStart; - const textLength = userInputElement.value.length; - let shouldCountKey = false; - - // Zähle Tastaturanschläge für alle Tasten, die keine Steuertasten sind (inkl. Backspace) - // Beachte: event.key für Backspace ist 'Backspace' - if (event.key.length === 1 || event.key === 'Backspace') { - shouldCountKey = true; - } - // Pfeiltasten: Nur zählen, wenn sich der Cursor tatsächlich bewegt - else if (event.key === 'ArrowLeft') { - if (cursorPosition > 0) { - shouldCountKey = true; - } - } else if (event.key === 'ArrowRight') { - if (cursorPosition < textLength) { - shouldCountKey = true; - } - } - - if (shouldCountKey) { + keyStrokeCount++; if (!startTime) { startTime = new Date(); } - keyStrokeCount++; - // Aktualisiere die Statistik sofort, um die Änderung anzuzeigen + if (currentCharIndex > 0) { + currentCharIndex--; + updateTextDisplay(); + } updateStatistics(); + return; + } + + if (event.key === 'ArrowRight') { + event.preventDefault(); + keyStrokeCount++; + if (!startTime) { + startTime = new Date(); + } + if (currentCharIndex < userInput.length) { + currentCharIndex++; + updateTextDisplay(); + } + updateStatistics(); + return; + } + + // Behandle Backspace + if (event.key === 'Backspace') { + event.preventDefault(); + keyStrokeCount++; + if (!startTime) { + startTime = new Date(); + } + if (userInput.length > 0 && currentCharIndex > 0) { + userInput = userInput.slice(0, currentCharIndex - 1) + userInput.slice(currentCharIndex); + currentCharIndex--; + updateTextDisplay(); + updateStatistics(); + } else { + updateStatistics(); + } + return; + } + + // Behandle Enter (falls im Text vorhanden) + if (event.key === 'Enter') { + event.preventDefault(); + if (currentCharIndex < fullText.length && fullText[currentCharIndex] === '\n') { + userInput = userInput.slice(0, currentCharIndex) + '\n' + userInput.slice(currentCharIndex); + currentCharIndex++; + keyStrokeCount++; + if (!startTime) { + startTime = new Date(); + } + updateTextDisplay(); + updateStatistics(); + } + return; + } + + // Ignoriere andere Steuertasten + if (event.key.length > 1 && event.key !== ' ') { + return; + } + + // Behandle normale Zeichen + event.preventDefault(); + if (currentCharIndex < fullText.length) { + // Wenn der Cursor innerhalb des bereits eingegebenen Textes steht, ersetze das Zeichen + // Andernfalls hänge das Zeichen an + if (currentCharIndex < userInput.length) { + // Ersetze das Zeichen an der aktuellen Position + userInput = userInput.slice(0, currentCharIndex) + event.key + userInput.slice(currentCharIndex + 1); + } else { + // Hänge das Zeichen am Ende an + userInput = userInput.slice(0, currentCharIndex) + event.key + userInput.slice(currentCharIndex); + } + currentCharIndex++; + keyStrokeCount++; + if (!startTime) { + startTime = new Date(); + } + updateTextDisplay(); + updateStatistics(); + + // Wenn die gesamte Lektion abgeschlossen ist + if (currentCharIndex === fullText.length) { + // Speichere die Statistiken für diese Lektion + const lessonIndex = window.currentLessonIndex; + const charsPerMinute = parseInt(speedElement.textContent); + const errorRate = parseFloat(errorRateElement.textContent); + const wpm = parseInt(wpmElement.textContent); + + fetch('/save_lesson_statistics', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + lesson_index: lessonIndex, + chars_per_minute: charsPerMinute, + error_rate: errorRate, + wpm: wpm + }) + }) + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + alert('Lektion beendet! Statistiken wurden gespeichert.'); + } else { + alert('Lektion beendet, aber Statistiken konnten nicht gespeichert werden.'); + } + }) + .catch(error => { + console.error('Fehler beim Speichern der Statistiken:', error); + alert('Lektion beendet! (Fehler beim Speichern der Statistiken)'); + }); + } } }); - // Aktualisiere die Statistik bei jedem Tastendruck (keyup) - userInputElement.addEventListener('keyup', function() { - userInput = this.value; - currentCharIndex = userInput.length; - - // Aktualisiere die Anzeige bei jeder Änderung - updateTextDisplay(); - - // Wenn die gesamte Lektion abgeschlossen ist - if (currentCharIndex === fullText.length) { - // Speichere die Statistiken für diese Lektion - const lessonIndex = window.currentLessonIndex; - const charsPerMinute = parseInt(speedElement.textContent); - const errorRate = parseFloat(errorRateElement.textContent); - const wpm = parseInt(wpmElement.textContent); - - fetch('/save_lesson_statistics', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - lesson_index: lessonIndex, - chars_per_minute: charsPerMinute, - error_rate: errorRate, - wpm: wpm - }) - }) - .then(response => response.json()) - .then(data => { - if (data.status === 'success') { - alert('Lektion beendet! Statistiken wurden gespeichert.'); - } else { - alert('Lektion beendet, aber Statistiken konnten nicht gespeichert werden.'); - } - }) - .catch(error => { - console.error('Fehler beim Speichern der Statistiken:', error); - alert('Lektion beendet! (Fehler beim Speichern der Statistiken)'); - }); - } - - updateStatistics(); - }); // Funktion zum Aktualisieren der Lektionsmarkierung function updateLessonHighlight(lessonIndex) { @@ -278,10 +278,9 @@ document.addEventListener('DOMContentLoaded', function() { startTime = null; keyStrokeCount = 0; // Zurücksetzen des Tastaturanschlagszählers lines = fullText.split('\n'); + userScrolled = false; // Reset Scroll-Flag updateTextDisplay(); document.getElementById('lesson-title').textContent = data.title; - userInputElement.value = ''; - userInputElement.focus(); updateStatistics(); // Aktualisiere die Markierung der aktuellen Lektion updateLessonHighlight(lessonIndex); @@ -314,11 +313,10 @@ document.addEventListener('DOMContentLoaded', function() { startTime = null; keyStrokeCount = 0; // Zurücksetzen des Tastaturanschlagszählers lines = fullText.split('\n'); + userScrolled = false; // Reset Scroll-Flag updateTextDisplay(); // Aktualisiere den Lektionstitel document.getElementById('lesson-title').textContent = data.title; - userInputElement.value = ''; - userInputElement.focus(); // Fokus zurück auf Eingabefeld updateStatistics(); // Aktualisiere die Markierung der aktuellen Lektion updateLessonHighlight(data.index); @@ -383,15 +381,23 @@ document.addEventListener('DOMContentLoaded', function() { } // Hilfsfunktion zum Hervorheben eines Zeichens - function highlightChar(char, type) { + function highlightChar(char, type, isCursor = false, incorrectChar = null) { + let className = `highlight-${type}`; + if (isCursor) { + className += ' cursor-char'; + } if (char === ' ') { - return `·`; + return `·`; } else if (char === '\t') { - return ``; + return ``; } else if (char === '\n') { - return '
'; + return `
`; } else { - return `${char}`; + if (type === 'incorrect' && incorrectChar !== null) { + return `${char}${incorrectChar}`; + } else { + return `${char}`; + } } } @@ -421,14 +427,15 @@ document.addEventListener('DOMContentLoaded', function() { for (let i = 0; i < line.length; i++) { const currentChar = line[i]; const absoluteIndex = lineStart + i; + const isCursor = (absoluteIndex === currentCharIndex); if (absoluteIndex < userInput.length) { if (userInput[absoluteIndex] === currentChar) { - highlightedLine += highlightChar(currentChar, 'correct'); + highlightedLine += highlightChar(currentChar, 'correct', isCursor); } else { - highlightedLine += highlightChar(currentChar, 'incorrect'); + highlightedLine += highlightChar(currentChar, 'incorrect', isCursor, userInput[absoluteIndex]); } } else { - highlightedLine += highlightChar(currentChar, 'neutral'); + highlightedLine += highlightChar(currentChar, 'neutral', isCursor); } } @@ -436,14 +443,15 @@ document.addEventListener('DOMContentLoaded', function() { if (j < lines.length - 1) { const newlinePos = lineStart + line.length; const newlineChar = '\n'; + const isCursor = (newlinePos === currentCharIndex); if (newlinePos < userInput.length) { if (userInput[newlinePos] === newlineChar) { - highlightedLine += highlightChar(newlineChar, 'correct'); + highlightedLine += highlightChar(newlineChar, 'correct', isCursor); } else { - highlightedLine += highlightChar(newlineChar, 'incorrect'); + highlightedLine += highlightChar(newlineChar, 'incorrect', isCursor, userInput[newlinePos]); } } else { - highlightedLine += highlightChar(newlineChar, 'neutral'); + highlightedLine += highlightChar(newlineChar, 'neutral', isCursor); } } @@ -453,10 +461,9 @@ document.addEventListener('DOMContentLoaded', function() { targetTextElement.innerHTML = newHTML; - // Scrolle zur aktuellen Zeile (oben) - const currentLineElement = targetTextElement.querySelector('.current-line'); - if (currentLineElement) { - currentLineElement.scrollIntoView({ block: 'start', behavior: 'smooth' }); + // Behalte Scroll-Position bei, es sei denn der Benutzer hat nicht manuell gescrollt + if (!userScrolled) { + targetTextElement.scrollTop = 0; } } @@ -527,6 +534,7 @@ document.addEventListener('DOMContentLoaded', function() { } } + // Initialisiere Statistik und Textfarben updateTextDisplay(); updateStatistics(); diff --git a/templates/index.html b/templates/index.html index e8cc9d5..e9cfad0 100644 --- a/templates/index.html +++ b/templates/index.html @@ -49,17 +49,6 @@ window.currentCharIndex = 0; - -
-
Ihre Eingabe:
- -