Real time text annotation

Allow user to edit text, which gets analyzed and styled on the fly without disturbing the cursor position

by larsolesimonsen

HTML

<p>
  Edit the text. Text between forward slashes gets grouped together so that eg vue components can be attached and styling can be done
</p>
<div contenteditable="true" id="editor"><span class="pretty">First</span>/<span class="pretty">Second</span></div>

CSS

#editor {
  width: 90%;
  height: 50px;
  border: 1px solid black;
  padding: 10px;
  white-space: nowrap;
  overflow:hidden;
}

.pretty {
  border: 1px solid black;
  border-radius: 5px;
  padding: 3px;
  margin: 3px;
}

JavaScript

var editor = document.querySelector('#editor');
var btn1 = document.querySelector('#button-1');
var btn2 = document.querySelector('#button-2');

function charsInElm(elm) {
  return elm.textContent.length;
}

// Recursive visitor/walker
function walk(node, func) {
  var stop = func(node); // Return true to stop exit out of the recursion
  var children = node.childNodes;
  for (var i = 0; !stop && i < children.length; i++) // Children are siblings to each other
    if (walk(children[i], func)) return true;
  return stop;
}

function saveSelection() { // Counts the number of characters left of the cursor
  var r = window.getSelection().getRangeAt(0);
  if (r == null) return 0;
  var count = 0;
  walk(editor, (elm) => {
    if (elm.nodeType === Node.TEXT_NODE) {
      if (elm !== r.endContainer) {
        count += elm.textContent.length;
      } else {
        count += r.endOffset;
        return true;
      }
    }
    return false;
  }, 0)
  return count;
}

function findChildWithCharIndex(charIndex) {
  var child = editor.firstChild,
    charCount = 0;
  walk(editor, (elm) => {
    if (elm.nodeType === Node.TEXT_NODE) {
      if (charCount + elm.textContent.length < charIndex) {
        charCount += elm.textContent.length;
      } else {
        child = elm;
        return true;
      }
    }
    return false;
  });
  return {
    child,
    offsetInChild: charIndex - charCount
  }
}

function restoreSelection(savedRange) {
  if (!savedRange) return;
  editor.focus();
  var s = window.getSelection();
  if (s.rangeCount > 0)
    s.removeAllRanges();
  var {
    child,
    offsetInChild
  } = findChildWithCharIndex(savedRange);
  if (child) {
    var r = document.createRange();
    r.setStart(child, offsetInChild);
    r.setEnd(child, offsetInChild);
    s.addRange(r);
  }
}

var keyUpHandler = () => {
  var contents = editor.textContent;
  var tokens = contents.split('/');
  var range = saveSelection();
  while (editor.firstChild) {
   ...