Possible strategies in creating a web based editor

An article, posted 11 days ago filed in textarea, web, editor, wysiwyg, trix, online, contenteditable & canvas.

This may not be the complete list of possible strategies, but this is my own documentation of a short exploration.

textarea

One of the simplest form of “editors” is the plain textarea. Sometimes enriched by Javascript, adding snippets of text to assist more complicated markup styles (e.g. select text, and make it bold by surrounding it with a double asterisk (in case of markdown)).

Advantages:

Disadvantages:

By offering a preview of the markup, the disadvantage can be mitigated to some extend.

More information: MDN on <textarea>

It is possible to position autocomplete helpers when the entry font is of a fixed type; as you can find the position of the caret within the text (using selectionStart (on MDN)) and using the value to calculate the currently active line. But it is a risky approach. It is sad that the onkey-events don’t expose the coordinates of the caret.

contenteditable=true

Create a <div>, set attribute contenteditable to true and you have a HTML editor; which value can be read by Javascript and submitted accordingly.

Advantages:

Disadvantages:

More information: MDN on contenteditable

Mirror

If you want to create a plain text entry, but want to add e.g. code formatting or autocomplete you can use textarea with a mirrored representation of what is in the textarea, but replicated with markup. The ‘mirror’ can display auto complete hints, or syntax highlighting. All code is duplicated to another section that shares almost all markup with the textarea (font, line-height, width, line breaking, etc.) but then contains some markup (e.g. boldens or colours some fonts).

Advantage:

Risk:

Some code exploration

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Editor test</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      line-height: 1.6;
      margin: 0;
      padding: 0;
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
      background-color: #f4f4f9;
      color: #333;
    }
    main {
      max-width: 600px;
      padding: 20px;
      background: #fff;
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
      border-radius: 8px;
      text-align: center;
    }
    h1 {
      margin-bottom: 1rem;
      color: #0078d7;
    }
    p {
      margin-bottom: 1rem;
    }
    a {
      color: #0078d7;
      text-decoration: none;
    }
    a:hover {
      text-decoration: underline;
    }

    textarea, #mirrorEditor {
      width: 40em;
      height: 10em;
      border: 1px solid #ccc;
    }

    #debug {
      text-align: left;
      max-height: 20em;
      overflow: scroll;
    }
    #pointer {
      position: absolute;
      left: 10px;
      top: 10px;
      background: rgba(255,0,0,0.5);
      width: 5px;
      height: 5px;
    }
    #mirrorEditor .caret::before {
      display: inline-block;
      background: #f00;
      width: 3px;
      height: 1em;
      content: " "
    }
  </style>

</head>
<body>
  <main>
    <textarea>test</textarea>
    <div id="mirrorEditor"><span class="before"></span><span class="caret"></span><span class="after"></span></div>
    <div id="debug"></div>
    <div id="pointer"></div>

  </main>
  <script>
    function log(something) {
      let debugTag = document.getElementById("debug")
      console.log(something)
      debugTag.innerHTML = debugTag.innerHTML + something + "<br/>"
    }
    window.onkeyup = function(e) {
      const textarea = document.querySelector("textarea");
      const mirror = document.getElementById("mirrorEditor");

      const rect = textarea.getBoundingClientRect();
      const selectionStart = textarea.selectionStart;
      const textBeforeCaret = textarea.value.substring(0, selectionStart);
      const textAfterCaret = textarea.value.substring(selectionStart, textarea.value.length -1);

      const beforeContainer = mirror.getElementsByClassName("before")[0]
      const afterContainer = mirror.getElementsByClassName("after")[0]

      const textareaStyles = window.getComputedStyle(textarea);
      [
        'border',
        'boxSizing',
        'fontFamily',
        'fontSize',
        'fontWeight',
        'letterSpacing',
        'lineHeight',
        'padding',
        'textDecoration',
        'textIndent',
        'textTransform',
        'whiteSpace',
        'wordSpacing',
        'wordWrap',
        'textAlign'
      ].forEach((property) => {
        mirror.style[property] = textareaStyles[property];
      });

      beforeContainer.textContent = textBeforeCaret;
      afterContainer.textContent = textAfterCaret;


      document.body.appendChild(tempDiv);
      const caretRect = tempDiv.getBoundingClientRect();
      document.body.removeChild(tempDiv);

      pointer.style.left = rect.left + caretRect.width + "px";
      pointer.style.top = rect.top + caretRect.height - textarea.scrollTop + "px";
    }
  </script>
</body>
</html>

Internal content model

Catch the events, process the intent, and write the HTML to both an input for submission and content-editable. This is an approach that modern plugin text editors use nowadays.

Advantage:

Disadvantage:

Canvas

Editors like Google Docs and Collabora and others render their internal content model to a canvas-element. This requires reimplementing a lot of things that are already available when using the DOM directly. They no longer rely on the markup engine of the browser, but instead have a fully controlled environment where text can be rendered dynamically. Instead of driving these canvasses with Javascript, one can even consider driving it using Rust.

Advantages:

Disadvantages:

More information: MDN on the canvas API

Op de hoogte blijven?

Maandelijks maak ik een selectie artikelen en zorg ik voor wat extra context bij de meer technische stukken. Schrijf je hieronder in:

Mailfrequentie = 1x per maand. Je privacy wordt serieus genomen: de mailinglijst bestaat alleen op onze servers.