wrapRangeText

by Seth Van Booven

HTML

<div class="pen" contenteditable="true" spellcheck="false">
<p>One two three four</p>
<p>One two three four</p>
<p>One two three four</p>
</div>
<input type="button" onclick="wrap('strong');" value="Bold">
<input type="button" onclick="wrap('em');" value="Italic">

CSS

body { font:1rem/1.5 sans-serif;  margin:2rem; }
div {
    border:thin solid #ddd;
    padding:1rem;
    margin:1rem 0;
    color:black;
    font-size:1rem;
}
.tag { background:#ffc; }

JavaScript

function wrap(tag) {

  var wrapper = document.createElement(tag);
  wrapper.classList.add('tag');
	
  var editor = document.querySelector('.pen');
  var selection = document.getSelection();
  var range = selection.rangeCount && selection.getRangeAt(0);
  if (!range) range = document.createRange();
  if (!containsNode(editor, range.commonAncestorContainer)) {
    range.selectNodeContents(editor);
    range.collapse(false);
  }
	//console.log(range);
  
  var node = getNode(editor, range, false);
  
  // nodes should be wrapped with `span.fancy-text` elements
  var wrapped = wrapRangeText(wrapper, range);
  
  if (node.nodeName.toLowerCase() === tag) {
    // nodes should be unwrapped
  	wrapped.unwrap();
  }
}

function getNode(root, range, byRoot) {
  var node = range.commonAncestorContainer;
  if (!node || node === root) return null;
  while (node && (node.nodeType !== 1) && (node.parentNode !== root)) node = node.parentNode;
  while (node && byRoot && (node.parentNode !== root)) node = node.parentNode;
  return containsNode(root, node) ? node : null;
}

function containsNode(parent, child) {
  if (parent === child) return true;
  child = child.parentNode;
  while (child) {
    if (child === parent) return true;
    child = child.parentNode;
  }
  return false;
}

// return all text nodes that are contained within `el`
function getTextNodes(el) {
  el = el || document.body

  var doc = el.ownerDocument || document
    , walker = doc.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false)
    , textNodes = []
    , node

  while (node = walker.nextNode()) {
    textNodes.push(node)
  }
  return textNodes
}

// return true if `rangeA` intersects `rangeB`
function rangesIntersect(rangeA, rangeB) {
  return rangeA.compareBoundaryPoints(Range.END_TO_START, rangeB) === -1 &&
    rangeA.compareBoundaryPoints(Range.START_TO_END, rangeB) === 1
}

// create and return a range that selects `node`
function createRangeFromNode(node) {
  var range =...