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) + '[' +...