JSFiddle - React, Tailwind, and code Playground

by sirfizx

HTML

<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.4/underscore-min.js"></script>
<button data-action="B"><b>b</b>
</button>
<button data-action="I"><i>i</i>
</button>
<button data-action="U"><u>u</u>
</button>
<p contenteditable>The quick brown fox jumps over the lazy dog.</p>

CSS

body {
          background: white;
          color: black;
          font-family: sans-serif;
      }
      p {
          outline: 1px solid #ddd;
          padding: 1em;
      }

JavaScript

var selectionWrapper = 'X-SELECTION';

  function getTextData(element) {
      function getTextNodesIn(root) {
          var textNodes = [];
          var parents = [];

          function getTextNodes(node) {
              if (node.nodeType === 3) {
                  var text = node.textContent;
                  textNodes.push({
                      text: text,
                      parents: parents.slice(0)
                  });
              } else {
                  if (node !== root) {
                      parents.push(node.tagName);
                  }
                  for (var i = 0, len = node.childNodes.length; i < len; ++i) {
                      getTextNodes(node.childNodes[i]);
                  }
                  parents.pop();
              }
          }

          getTextNodes(element);

          return textNodes;
      }

      return getTextNodesIn(element);
  }

  function handleSelection(container, action) {
      var textData = getTextData(container);
      container.innerHTML = '';

      //if every textNode in the selection has action as a parent, we want
      //to remove it from all of them.
      var selection = _(textData).filter(function (data) {
          return _(data.parents).contains(selectionWrapper);
      });
      var remove = _(selection).every(function (data) {
          return _(data.parents).contains(action) || data.text.trim() === '';
      });
      _(selection).each(function (data) {
          if (remove) {
              data.parents = _(data.parents).without(action);
          } else {
              data.parents.push(action);
          }
      });

      //rebuild each text node
      _(textData).each(function (data) {
          //no need to add empty text nodes
          if (data.text === '') {
              return;
          }
          //remove duplicates of the same parent tag and remove the selection wrapper
          var parents = _(data.parents).chain().uniq().without(selectionWrapper).value();
         ...