JSFiddle - React, Tailwind, and code Playground

by Jason Caldwell

HTML

<link rel="stylesheet" href="https://unpkg.com/bootstrap@4.0.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://unpkg.com/highlightjs@9.10.0/styles/github.css">
<script src="https://unpkg.com/lodash@4.17.4/lodash.min.js"></script>
<script src="https://unpkg.com/highlightjs@9.10.0/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 {
  font-size: 14px;
  margin: 2rem 0;
}

.gutenberg-mock-layout {
  .editor-post-title {
    display: none;
  }
  .blocks-editable__tinymce {
    padding: 2em;
    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.
    }
  }
  demoOutput(getW3CSelector(editable, range), range);
};
document.addEventListener('selectionchange', _.debounce(onSelectionChange, 500));

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

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: {
      type: 'XPathSelector',
      value: xPathStartSelector,
      refinedBy: {
        type: 'TextPositionSelector',
        start: range.startOffset,
        end: range.startOffset,
      },
    },
    endSelector: {
      type: 'XPathSelector',
      value: xPathEndSelector,
      refinedBy: {
        type: 'TextPositionSelector',
        start: range.endOffset,
        end: range.endOffset,
      },
    },
  };
};

const getTagName = (element) => {
  return element.tagName.toLowerCase();
};

const prefixXPathSelector = (editable, xPathSelector) => {
  xPathSelector = xPathSelector ? '/' + xPathSelector : xPathSelector;
  return "//*[@id='" + editable.id + "']" + xPathSelector;
};

const getXPathSelector = (root, node) => {
  const selectors = [];

  while (node.parentNode) {
    if (node === root) {
      break;
    }

    switch (node.nodeType) {
      case Node.ELEMENT_NODE:
        selectors.unshift(getTagName(node) + '[' +...