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
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:
textarea
-usageRisk:
<!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>
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:
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
Enjoyed this? Follow me on Mastodon or add the RSS, euh ATOM feed to your feed reader.
Dit artikel van murblog van Maarten Brouwers (murb) is in licentie gegeven volgens een Creative Commons Naamsvermelding 3.0 Nederland licentie .