Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Highlight syntax in contenteditable

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(/&lt;/gi, "<span style='color:gold;'>&lt;</span>");
var rTags = lTags.replace(/&gt;/gi, "<span style='color:gold'>&gt;</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(/&lt;/gi, "<span style='color:gold;'>&lt;</span>");
   var rTags = lTags.replace(/&gt;/gi, "<span style='color:gold'>&gt;</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!

like image 903
Cannicide Avatar asked Jan 26 '17 23:01

Cannicide


People also ask

How do you highlight text in textarea?

You can't actually highlight text in a <textarea> . Any markup you would add to do so would simply display as plain text within the <textarea> . The good news is, with some carefully crafted CSS and JavaScript, you can fake it.

How do you use Contenteditable in JavaScript?

You can add the contenteditable="true" HTML attribute to the element (a <div> for example) that you want to be editable. If you're anticipating a user to only update a word or two within a paragraph, then you could make a <p> itself editable.

What is Contenteditable div?

contenteditable is an HTML attribute that you can add to any HTML element. If its value is true or an empty string, that means that the user can edit it by clicking the element. For example: <div contenteditable="true"> Change me! </ div>

How do I make my page Contenteditable?

To edit the content in HTML, we will use contenteditable attribute. The contenteditable is used to specify whether the element's content is editable by the user or not. This attribute has two values. true: If the value of the contenteditable attribute is set to true then the element is editable.


1 Answers

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(/(&lt;|&gt;)/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 &lt; &gt; or curly { } brackets
<div class="highLite">
  <div class="highLite_colors">Type &lt;here&gt; {something}</div>
  <div class="highLite_editable" contenteditable>Type &lt;here&gt; {something}</div>
</div>

Advanced example:

(use at your own risk this was my coffee-time playground)

const lang = {
  js: {
    equa: /(\b=\b)/g,
    quot: /((&#39;.*?&#39;)|(&#34;.*?&#34;)|(".*?(?<!\\)")|('.*?(?<!\\)')|`)/g,
    comm: /((\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\/)|(\/\/.*))/g,
    logi: /(%=|%|\-|\+|\*|&amp;{1,2}|\|{1,2}|&lt;=|&gt;=|&lt;|&gt;|!={1,2}|={2,3})/g,
    numb: /(\d+(\.\d+)?(e\d+)?)/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: /(?<=&lt;(?:\/)?)(\w+)(?=\s|\&gt;)/g,
    // Props order matters! Here I rely on "tags"
    // being already applied in the previous iteration
    angl: /(&lt;\/?|&gt;)/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">&lt;h2 class="head"&gt;
TODO: HTML is for &lt;b&gt;homework&lt;/b&gt;
&lt;/h2&gt;</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 &lt; 10) {
  console.log(`${n} is &lt;&#61; 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.
like image 66
Roko C. Buljan Avatar answered Sep 20 '22 19:09

Roko C. Buljan