JSFiddle - React, Tailwind, and code Playground

by Jason Caldwell

HTML

<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/styles/github.css">
<script src="https://unpkg.com/[email protected]/lodash.min.js"></script>
<script src="https://unpkg.com/[email protected]/highlight.pack.min.js"></script>
<div class="wrap">
  <div class="w3c-selector container">
    <h1>W3C Annotation Selectors</h1>
    <pre><code>JSON appears here: please make a selection below</code></pre>
    <p>
      <small><strong>Tips:</strong> 1. Visiting links in the text below requires right-click (open in new tab) because these are contentEditable divs used as a part of the demo. 2. There's more info in browser console.log.</small>
    </p>
  </div>

  <hr />

  <div class="gutenberg-mock-layout container">
    <div class="editor-layout__editor">
      <div class="editor-visual-editor">
        <div>
          <div class="editor-post-title">
            <div>
              <textarea class="editor-post-title__input" rows="1">Hello World</textarea>
            </div>
          </div>
          <div>
            <div class="editor-block-list__sibling-inserter"></div>

            <div class="editor-block-list__block is-selected" data-type="core/paragraph">
              <div class="editor-block-list__block-edit">
                <div>
                  <div>
                    <div class="components-autocomplete">
                      <div class="blocks-editable">
                        <p class="wp-block-paragraph blocks-editable__tinymce mce-content-body" contenteditable="true" id="c9xi4jhi" spellcheck="false"><img class="float-right ml-3" src="https://cdn3.iconfinder.com/data/icons/tango-icon-library/48/edit-select-all-64.png" />This demo builds a new <a href="https://www.w3.org/TR/annotation-model/#selectors" target="_blank"><span>W3C Annotation Selector</span></a> using <a href="https://developer.mozilla.org/en-US/docs/Web/API/Selection"...

SCSS

.wrap {
  margin: 2rem 0;
  font-size: 14px;
}

.gutenberg-mock-layout {
  .editor-post-title {
    display: none;
  }
  
  .editor-block-list__block-edit {
    position: relative;

    > .annotation-markers {
      > .annotation-marker {
        > * {
          z-index: -1;
          position: absolute;
          background: #fdf451;
        }
      }
    }
  }

  .blocks-editable__tinymce {
    padding: 2em;
    position: relative;
    outline: 1px solid #e2e4e7;

    em,
    em * {
      font-style: italic;
    }
    
    img {
      cursor: pointer;
    }
  }
}

.fade-in {
  animation-duration: .5s;
  animation-name: fade-in-animation;
}

@keyframes fade-in-animation {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

JavaScript

const onSelectionChange = (e) => {
  const selection = window.getSelection();
  const range = selection.rangeCount ? selection.getRangeAt(0) : null;
  const editable = range ? getClosestEditable(range.startContainer) : null;

  if (!range || !editable) {
    return; // Not an editable selection.
  }
  if (range.startContainer === range.endContainer) {
    if (range.startOffset === range.endOffset) {
      return; // Selection is empty.
    }
  }
  const w3cSelector = getW3CSelector(editable, range);
  demoOutput(w3cSelector, range);
  markW3CSelector(w3cSelector);
};

const markW3CSelector = (w3cSelector) => {
	const range = getW3CXPathSelectorRange(w3cSelector);
	const editable = getClosestEditable(range.startContainer);

  const editor = getClosestEditor(editable);
  const editorRect = editor.getBoundingClientRect();

  let markers = editor.querySelector('.annotation-markers');
	let marker = document.createElement('div');

  if (markers) {
  	markers.innerHTML = '';
  } else {
		markers = document.createElement('div');
    markers.classList.add('annotation-markers');
    editor.appendChild(markers);
	}
  marker.classList.add('annotation-marker');
  markers.appendChild(marker);

	_.forEach(range.getClientRects(), (rect) => {
    const rectDiv = document.createElement('div');

		rectDiv.style.width = rect.width + 'px';
		rectDiv.style.height = rect.height + 'px';

		rectDiv.style.top = (rect.top - editorRect.top) + 'px';
		rectDiv.style.left = (rect.left - editorRect.left) + 'px';

	  marker.appendChild(rectDiv);
  });
};

/* ---------------------------------- */

const getW3CSelector = (editable, range) => {
  let xPathStartSelector = getXPathSelector(editable, range.startContainer);
  xPathStartSelector = prefixXPathSelector(editable, xPathStartSelector);

  let xPathEndSelector = getXPathSelector(editable, range.endContainer);
  xPathEndSelector = prefixXPathSelector(editable, xPathEndSelector);

  return {
    type: 'RangeSelector',
    startSelector: {
     ...