The default textarea behavior wraps with omitted spaces at the end and beginning of rows. Note: lines in this are for multi-lined code block format with line break characters. This should also display that correctly; as well as long “single-line” wrapping phrases
<div class="layout-container" >
<div phx-update="ignore" id="timer-box">
<div id="js-timer">READY</div>
</div>
<div class="js-content" style="position: relative;">
<!-- JavaScript Managed Area -->
<div id="phrase-data" data-phrase-text={@phrase.text}></div>
<div phx-update="ignore" id="js-text-area" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
<div class="js-typing-area" style="position: relative;">
<pre style="position: absolute; top 0; left: 0; width: 100%; height: 100%; margin: 0; padding: 0; box-sizing: border-box; font-family: monospace; font-size: 10px; line-height: 120%; width: 100%; height: 100%;">
<code style="position: absolute; top 0; left: 0; width: 100%; height: 100% white-space: pre-wrap;" id="js-typing-area"></code></pre>
</div>
</div>
<form class="typing-area" phx-change="input" phx-submit="submit" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 2;">
<textarea class="user-input" id="code-editor" name="user_input" phx-hook = "AutoFocus" phx-debounce="1000"
style="background-color: transparent; color: transparent; border: none; width: 100%; height: 100%; font-family: monospace; font-size: 10px; line-height: 120%; box-sizing: border-box;"></textarea>
</form>
</div>
<div class="elixir-content">
<div id="elixir-content">
<pre ><code class="elixir-content" ><%= render_typing_area(@phrase, @user_input) %></code></pre>
</div>
</div>
</div>
and my current JS rendering function goes line by line, and char by char within, and creates for non-breaking overflow word wrap. But each space is now a separate “word”, but this keeps spaces after wrap, breaking the overlay.
Also, now this isn’t doing newlines on multi-line code blocks anymore q.q
function updateJSTypingArea(phrase, userInput) {
const codeElement = document.querySelector('#js-typing-area');
if (!codeElement) return;
const phraseLines = phrase.split('n');
const userInputLines = userInput.split('n');
let htmlContent = '';
phraseLines.forEach((phraseLine, lineIndex) => {
const userInputLine = userInputLines[lineIndex] || '';
let lineHtmlContent = '';
let wordHtmlContent = '';
let extraSpacesHandled = false;
let isLineBlank = true; // Assume the line is blank until proven otherwise
let isLineStart = true; // Flag to track the start of a linea
for (let i = 0; i < phraseLine.length; i++) {
const phraseChar = phraseLine[i];
const userInputChar = userInputLine[i] || ' ';
let classList = ['ghost-text'];
let displayChar = phraseChar === ' ' ? ' ' : phraseChar;
if (userInputChar !== undefined) {
if (phraseChar === userInputChar || (phraseChar === ' ' && userInputChar === ' ')) {
classList = ['correct-input'];
isLineBlank = false; // There's content in this line
} else if (userInputChar !== ' ') {
classList = ['error'];
isLineBlank = false; // There's content in this line
displayChar = userInputChar === ' ' ? '▄' : userInputChar; // Use ▄ for error spaces
}
}
// Handle the zero-width space for the first character in a line or after a newline
// Append the character to the word HTML, handling spaces as their own "word"
if (phraseChar === ' ') {
if (isLineStart) {
displayChar = '​'; // Use zero-width space if the actual space is the first character
isLineStart = false; // Reset flag after handling the first character'
} else{
isLineStart = false; // Any non-space character means we're no longer at the start
}
// Close the previous word and start a new span for the space
lineHtmlContent += `<span class="word">${wordHtmlContent}</span>`;
wordHtmlContent = ''; // Reset word HTML content
// Add the space as its own word span
lineHtmlContent += `<span class="word"><span class="${classList.join(' ')}">${displayChar}</span></span>`;
} else {
wordHtmlContent += `<span class="${classList.join(' ')}">${displayChar}</span>`;
}
// Ensure the last word is added if it's not followed by a space
if (i === phraseLine.length - 1 && wordHtmlContent !== ' ') {
lineHtmlContent += `<span class="word">${wordHtmlContent}</span>`;
}
}
// wordHtmlContent += `<span class="${classList.join(' ')}">${displayChar}</span>`;//changed from space to blank
// if (phraseChar === ' ' || i === phraseLine.length - 1) {
// lineHtmlContent += `<span class="word">${wordHtmlContent}</span>`;
// wordHtmlContent = '';
// }
// }
// Handle trailing spaces in user input as correct, if they exist beyond the phrase length
if (userInputLine.length > phraseLine.length) {
const extraChars = userInputLine.slice(phraseLine.length);
if (/^s*$/.test(extraChars)) { // Check if all extra characters are spaces
extraChars.split('').forEach(() => {
lineHtmlContent += `<span class="correct"> </span>`;
});
extraSpacesHandled = true;
}
}
// If there were no extra spaces or other characters that were handled as correct,
// handle any remaining extra characters as errors.
if (!extraSpacesHandled && userInputLine.length > phraseLine.length) {
const extraChars = userInputLine.slice(phraseLine.length);
extraChars.split('').forEach(char => {
const displayChar = char === ' ' ? ' ' : char;
lineHtmlContent += `<span class="error">${displayChar}</span>`;
});
}
if (isLineBlank) {
// If the line is still considered blank, insert a zero-width space
lineHtmlContent += `<div class="line">​</div>`;
}
htmlContent += `<div class="line">${lineHtmlContent}</div>`;
});
codeElement.innerHTML = htmlContent;
}
.typing-overlay-container {
/* Adjust dimensions as needed */
width: 100%;
height: auto;
min-height: 100px; /* Ensure it's tall enough for your content */
}
/* Ensure the textarea is transparent and overlays the feedback area */
pre code {
white-space: pre-wrap; /* Allow long lines to wrap */
word-wrap: break-word; /* Ensure words can break and wrap to the next line */
overflow-wrap: break-word; /* Similar to word-wrap, for better compatibility */
}
.layout-container {
position: relative;
width: 100%; /* or specific width as needed */
}
.js-content {
position: absolute;
top: 0;
left: 0;
width: 100%; /* or specific width as needed */
height: 400px; /* Adjust based on content */
z-index: 1;
}
.js-typing-area, .user-input {
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
.user-input {
position: absolute;
top: 0;
left: 0;
width: 100%; /* Ensure it covers the container width */
min-height: 100%;
z-index: 0;
color: transparent;
border: none;
outline: none;
font-family: monospace;
font-size: 10px;
line-height: 120%;
box-sizing: border-box; /* Ensure padding and border are included in width/height */
padding: 0; /* Remove padding */
margin: 0; /* Remove margin */
caret-color: rgb(1, 68, 12);
letter-spacing: normal; /* Ensure letter spacing is consistent */
word-spacing: normal; /* Ensure word spacing is consistent */
}
.typing-area{
position: absolute;
top: 0;
left: 0;
width: 100%; /* Ensure it covers the container width */
min-height: 100%; /* Adjust if you have a specific height in mind or use 100% for full container height */
z-index: 2; /* Ensure the textarea is above the content it overlays */
background-color: transparent;
/* To keep the caret visible */
border: none;
outline: none;
font-family: monospace;
font-size: 10px;
line-height: 120%;
letter-spacing: normal; /* Ensure letter spacing is consistent */
word-spacing: normal; /* Ensure word spacing is consistent */
margin: 0;
padding: 0; /* Adjust as necessary, but keep them the same */
box-sizing: border-box;
}
.typing-area2{
position: absolute;
top: 0;
left: 0;
width: 100%; /* Ensure it covers the container width */
min-height: 100%; /* Adjust if you have a specific height in mind or use 100% for full container height */
z-index: 2; /* Ensure the textarea is above the content it overlays */
border: none;
outline: none;
font-family: monospace;
font-size: 10px;
line-height: 120%;
letter-spacing: normal; /* Ensure letter spacing is consistent */
word-spacing: normal; /* Ensure word spacing is consistent */
margin: 0;
padding: 0; /* Adjust as necessary, but keep them the same */
box-sizing: border-box;
}
.elixir-content {
position: relative;
width: 100%;
}
.line {
/* display: block;Ensures each .line starts on a new line */
white-space: pre-wrap; /* Maintains whitespace but allows normal wrapping within the .line */
/* margin-bottom: 1em;Adds space between lines, adjust as needed */
}
.line, .word, span {
display: inline; /* .word {display: inline-block;} Treat words as inline blocks to manage spacing and wrapping */
margin: 0;
padding: 0;
font-family: monospace;
font-size: 10px; /* Adjust based on your setup */
line-height: normal; /* Adjust based on your setup */
}
.word {
white-space: nowrap; /* Prevent words from wrapping */
display: inline-block;
}
span.ghost-text, span.error {
display: inline-block; /* Treat characters individually but maintain flow */
width: 1ch; /* Use the 'ch' unit for width based on character size */
}
.js-typing-area, .user-input, pre, code {
position: absolute;
top: 0;
left: 0;
font-family: monospace;
font-size: 10px;
line-height: 120%;
margin: 0;
padding: 0;
box-sizing: border-box;
}