Typing Test POC

Typing Test POC

by Csaba Hellinger

HTML

<div id="display"></div>
<textarea id="input">Hello, World!</textarea>


<p>

</p>

CSS

html, body, #input {
  background: #333;
  color: white;
}
#input {
  margin-top: 1rem;
  opacity: 0.2;
}

#display {
  display: flex;
  flex-wrap: wrap;
  background: #222;
  font: 42px monospace;
  padding: 5px;
  border-radius: 5px;
  gap: 1px;
}

.word {
  display: flex;
  gap: 1px;
}

.char {
  position: relative;
  white-space: pre;
  background: #333;
  border-radius: 5px;
  padding: 2px;
  --border-left: 2px inset red;
}
.char.selected {
  background: #357;
}
.char.cursor:before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 4px;
  height: 100%;  
  background: #08f;
  animation: blink 800ms infinite;
}

.char.end-cursor {
  background: #08f;
  animation: blink 800ms infinite;
}

@keyframes blink {
  0% { opacity: 1; }
  60% { opacity: 1; }
  80% { opacity: 0; }
}

JavaScript

const input = document.getElementById('input');
const display = document.getElementById('display');

const updateDisplay = (text) => {
  const { selectionStart, selectionEnd, selectionDirection } = input;
  let charIndex = 0;
  display.innerHTML = '';
  const words = text.split(/\b/);
  words.forEach(word => {
    const wordSpan = document.createElement('span');
    wordSpan.classList.add('word')
    const chars = word.split('');
    chars.forEach(char => {
      const charSpan = document.createElement('span');
      const isSelected = selectionStart <= charIndex && charIndex < selectionEnd;
      const cursorIndex = selectionDirection === 'backward' ? selectionStart : selectionEnd;
      const isCursor = cursorIndex === charIndex;
      console.log({char, selectionStart, selectionEnd, selectionDirection });
      charSpan.classList.add('char', isSelected && 'selected', isCursor && 'cursor');
      charSpan.innerText = char;
      wordSpan.appendChild(charSpan);
      charIndex += 1;
    });
    display.appendChild(wordSpan);
  });  
  if (selectionEnd === text.length) {
    const cursorSpan = document.createElement('span');
    cursorSpan.classList.add('char', 'end-cursor');
    cursorSpan.innerText = ' ';
    display.appendChild(cursorSpan);
  }  
};

const focus = () => {
  input.focus();
  input.selectionStart = input.value.length;
  input.selectionEnd = input.value.length;
};

// TODO: debounce
input.oninput = (event) => updateDisplay(event.target.value);
input.onkeyup = (event) => updateDisplay(event.target.value);
display.onclick = focus;

focus();
updateDisplay(input.value);