Search code examples
javascripthtmlsyntax-highlighting

Saving location of cursor in contenteditable div


I have a contenteditable div. I am trying to implement "on-the-fly syntax highlighting". Here's what I have so far:

const codeElement = document.getElementById('code');

// func to apply highlighting
function highlight() {
  const controlRegex = /\b(function|const|let)\b/g;
  const stringRegex = /["'].*["']/g;
  const functionCallRegex = /[a-zA-Z_\d]*\([^)]*\)/g;
  const constantRegex = /.*[a-zA-Z_\d]+\./g;

  codeElement.innerHTML = codeElement.textContent.replace(stringRegex,
    '<span style="color: #0d0; font-weight: normal;">$&</span>')
    .replace(controlRegex, '<span style="color: pink;">$&</span>')
    .replace(functionCallRegex, '<span style="color: #0bb; font-weight: bold;">$&</span>')
    .replace(constantRegex, '<span style="color: yellow;">$&</span>');
}


codeElement.addEventListener('input', highlight);
highlight();

It works well, except for one problem. Whenever I press a key, it moves the cursor to the start of the div again.

I tried using the combination of window.getSelection and document.createRange to move the cursor to the end of the div whenever a key is pressed, but that raises issues when editing something that's not the last character.


Solution

  • I fixed it by keeping a div that had the highlighting function, and overlayed on top of it a textarea with color: transparent and caret-color: white used for input.

    const inputElement = document.getElementById("input");
    const codeElement = document.getElementById("code");
    
    
    function onInput(evt) {
      const cInput = evt.target.value;
    
      // func to apply highlighting
      const controlRegex = /\b(function|const|let)\b/g;
      const stringRegex = /["'].*["']/g;
      const functionCallRegex = /[a-zA-Z_\d]*\([^)]*\)/g;
      const constantRegex = /.*[a-zA-Z_\d]+\./g;
    
      codeElement.innerHTML = cInput.replace(stringRegex,
          '<span style="color: #0d0; font-weight: normal;">$&</span>')
        .replace(controlRegex, '<span style="color: pink;">$&</span>')
        .replace(functionCallRegex, '<span style="color: #0bb; font-weight: bold;">$&</span>')
        .replace(constantRegex, '<span style="color: yellow;">$&</span>');
    }
    
    
    inputElement.addEventListener('input', onInput);
    inputElement.focus();
    pre#code,
    #input {
      overflow-wrap: break-word;
      text-wrap: wrap;
      font-size: 1rem;
      font-family: monospace;
      font-weight: lighter;
      position: fixed;
      top: -50px;
      left: -10px;
      right: -10px;
      bottom: -50px;
      ;
      color: white;
      background-color: rgb(20, 20, 20);
      padding: 60px 20px;
    }
    
    #input {
      color: transparent;
      caret-color: white;
      background: transparent;
    }
    
    pre#code {
      padding-top: calc(60px - 1em);
    }
    <pre id="code"></pre>
    <textarea id="input" spellcheck="false"></textarea>