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: {
...