Possible strategies in creating a web based editor
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:
- robust
- simple
Disadvantages:
- No advanced markup (and/or relies on e.g. markdown, which is nice, but not for everyone)
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:
- Almost free WYSIWYG editing
Disadvantages:
- Submitting depends on javascript
- Harder to limit text input
- HTML output between browsers might not be similar
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:
- Logical upgrade path from plain
textarea
-usage - Progressive enhancement
Risk:
- May turn into a dead end with ever complicating requirements
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:
- Much control over the editor
- Reliable HTML output
- All styling of the web possible
Disadvantage:
- Relies on JS; hard to make progressively enhanced
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:
- Full control:
- can go beyond web standards
- predictable output
Disadvantages:
- Requires building a lot of the rendering of text.
More information: MDN on the canvas API