I have a contenteditable div I want users to type in. When users type inside the box with onkeyup, I activate a function that changes the color of certain characters:
var lTags = fixedCode.innerHTML.replace(/</gi, "<span style='color:gold;'><</span>"); var rTags = lTags.replace(/>/gi, "<span style='color:gold'>></span>"); fixedCode.innerHTML = rTags;
What this code does is it takes every < sign and every > sign and turns it into a gold color. However, when I do this, I am no longer able to type words into the contenteditable box since the box refreshes itself every time I press a key.
function checkIt(code) { var fixedCode = document.getElementById(code); var lTags = fixedCode.innerHTML.replace(/</gi, "<span style='color:gold;'><</span>"); var rTags = lTags.replace(/>/gi, "<span style='color:gold'>></span>"); fixedCode.innerHTML = rTags; }
<div id="box" contenteditable="true" onkeyup="checkIt(this.id);">See for yourself</div>
To see for yourself, try typing any HTML tag in the box.
First of all, why does it change the color of the left <
of a tag but not the right part of the tag >
? And how can I actually type inside the box without deleting the color-changing stuff. I’ve seen similar questions, but the answers were Jquery. I do not want to use JQUERY!
Advertisement
Answer
I was too lazy to go hardcore with JavaScript and one idea that popped my mind was to use
- two overlaying DIV elements
- the overlaying contenteditable has transparent text but visible caret!
- the underlaying DIV is the one that shows the colorful syntax highlighted content
PROS
- The pros about this technique is that you always keep (in the contenteditable DIV) the unchanged content in its original state.
CONS
- On every keystroke we parse all over again the same content, and as it gets bigger it might be a performance killer as your replacement list grows
O(n)
For a beautiful read on optimization head to VSCode Optimizations in Syntax Highlighting
Basic example:
const highLite = (el) => { el.previousElementSibling.innerHTML = el.innerHTML .replace(/(<|>)/g, "<span class='hl_angled'>$1</span>") .replace(/({|})/g, "<span class='hl_curly'>$1</span>"); }; document.querySelectorAll("[contenteditable]").forEach(el => { el.addEventListener("input", () => highLite(el)); highLite(el); });
body{margin:0; font:14px/1 sans-serif;} .highLite{ border: 1px solid #888; position: relative; } .highLite_colors, .highLite_editable { padding: 16px; } /* THE UNDERLAYING ONE WITH COLORS */ .highLite_colors { position: absolute; top: 0; right: 0; bottom: 0; left: 0; user-select: none; } /* THE OVERLAYING CONTENTEDITABLE WITH TRANSPARENT TEXT */ .highLite_editable { position: relative; color: transparent; /* Make text invisible */ caret-color: black; /* But keep caret visible */ } .hl_angled{ color: turquoise; } .hl_curly{ color: fuchsia; }
Try to type some angled < > or curly { } brackets <div class="highLite"> <div class="highLite_colors">Type <here> {something}</div> <div class="highLite_editable" contenteditable>Type <here> {something}</div> </div>
Advanced example:
(use at your own risk this was my coffee-time playground)
const lang = { js: { equa: /(b=b)/g, quot: /(('.*?')|(".*?")|(".*?(?<!\)")|('.*?(?<!\)')|`)/g, comm: /((/*([^*]|[rn]|(*+([^*/]|[rn])))**+/)|(//.*))/g, logi: /(%=|%|-|+|*|&{1,2}||{1,2}|<=|>=|<|>|!={1,2}|={2,3})/g, numb: /(d+(.d+)?(ed+)?)/g, func: /(?<=^|s*)(async|await|console|alert|Math|Object|Array|String|class(?!s*=)|function)(?=b)/g, decl: /(?<=^|s*)(var|let|const)/g, pare: /((|))/g, squa: /([|])/g, curl: /({|})/g, }, html: { tags: /(?<=<(?:/)?)(w+)(?=s|>)/g, // Props order matters! Here I rely on "tags" // being already applied in the previous iteration angl: /(</?|>)/g, attr: /((?<=<i class=html_tags>w+</i>)[^<]+)/g, } }; const highLite = el => { const dataLang = el.dataset.lang; // Detect "js", "html", "py", "bash", ... const langObj = lang[dataLang]; // Extract object from lang regexes dictionary let html = el.innerHTML; Object.keys(langObj).forEach(function(key) { html = html.replace(langObj[key], `<i class=${dataLang}_${key}>$1</i>`); }); el.previousElementSibling.innerHTML = html; // Finally, show highlights! }; const editors = document.querySelectorAll(".highLite_editable"); editors.forEach(el => { el.contentEditable = true; el.spellcheck = false; el.autocorrect = "off"; el.autocapitalize = "off"; el.addEventListener("input", () => highLite(el)); highLite(el); // Init! });
* {margin: 0; box-sizing: boder-box;} body { font: 14px/1.4 sans-serif; background: hsl(220, 16%, 16%); color: #fff; padding: 16px; } #editor { display: flex; } h2 { padding: 16px 0; font-weight: 200; font-size: 14px; } .highLite { position: relative; background: hsl(220, 16%, 14%); } .highLite_colors, .highLite_editable { padding: 16px; top: 0; left: 0; right: 0; bottom: 0; white-space: pre-wrap; font-family: monospace; font-size: 13px; } /* THE OVERLAYING CONTENTEDITABLE WITH TRANSPARENT TEXT */ .highLite_editable { position: relative; color: transparent; /* Make text invisible */ caret-color: hsl( 50, 75%, 70%); /* But keep caret visible */ } .highLite_editable:focus { outline: 1px solid hsl(220, 16%, 19%); } .highLite_editable::selection { background: hsla( 0, 0%, 90%, 0.2); } /* THE UNDERLAYING ONE WITH HIGHLIGHT COLORS */ .highLite_colors { position: absolute; user-select: none; } .highLite_colors i { font-style: normal; } /* JS */ i.js_quot { color: hsl( 50, 75%, 70%); } i.js_decl { color: hsl(200, 75%, 70%); } i.js_func { color: hsl(300, 75%, 70%); } i.js_pare { color: hsl(210, 75%, 70%); } i.js_squa { color: hsl(230, 75%, 70%); } i.js_curl { color: hsl(250, 75%, 70%); } i.js_numb { color: hsl(100, 75%, 70%); } i.js_logi { color: hsl(200, 75%, 70%); } i.js_equa { color: hsl(200, 75%, 70%); } i.js_comm { color: hsl(200, 10%, 45%); font-style: italic; } i.js_comm > * { color: inherit; } /* HTML */ i.html_angl { color: hsl(200, 10%, 45%); } i.html_tags { color: hsl( 0, 75%, 70%); } i.html_attr { color: hsl(200, 74%, 70%); }
<h2>HTML</h2> <div class="highLite"> <div class="highLite_colors"></div> <div class="highLite_editable" data-lang="html"><h2 class="head"> TODO: HTML is for <b>homework</b> </h2></div> </div> <h2>JAVASCRIPT</h2> <div class="highLite"> <div class="highLite_colors"></div> <div class="highLite_editable" data-lang="js">// Type some JavaScript here const arr = ["high", 'light']; let n = 2.1 * 3; if (n < 10) { console.log(`${n} is <= than 10`); } function casual(str) { str = str || "non"sense"; alert("Just a casual"+ str +", still many TODOs"); } casual (arr.join('') +" idea!"); /** * The code is a proof of concept and far from * perfect. You should never use regex but create or use a parser. * Meanwhile, play with it and improve it! */</div> </div>
TODO
given this basic idea, some TODOs I left for the reader:
- when the DIV we’re editing receives scrollbars, update accordingly the scroll position for the sibling DIV using JavaScript.
- instead of using Regex, use or create a proper parser to perform lexical analysis (tokenization) or syntactic analysis (parsing) for the specific languages you want to support in your syntax highlighter.