JSFiddle - React, Tailwind, and code Playground

by mcsf

HTML

<div id="controls">
  <div id="output"></div>
  <input id="input">
  <input id="button" type="button" value="Go">
</div>
<aside>Aside</aside>
<article id="article">
<p>Totam <strong>consectetur</strong> repellendus est maxime. Sint consequuntur dolores sit quis quis ut voluptatem est. Esse quis consectetur repellat iure.</p>
<p>Distinctio rerum qui officiis recusandae repellat aut. Est quia quas aut ut sint provident. Sunt voluptate mollitia qui iusto accusamus. Eaque officiis molestiae voluptatibus ratione fugiat dolore. Exercitationem sint quas enim temporibus porro. Recusandae ratione delectus est et nemo eveniet perferendis.</p>
<p>Atque aperiam illo ducimus id accusamus minima. Velit accusamus nobis pariatur quo. Totam sunt tempore fugit ex qui est. Architecto est iste et minima expedita sunt.</p>
<p>Nisi est ut numquam aut perferendis adipisci. Pariatur aspernatur dolorem voluptas illum nulla. Sunt tenetur aut aut voluptatem dolorum asperiores enim voluptatibus. Earum blanditiis et et. Unde id autem blanditiis.</p>
<p>Vel cum dolores ipsum tempore soluta. Dolor praesentium esse aut est sint odit consequatur. In aperiam voluptas consequatur iusto. Eum totam est et aperiam accusantium.</p>
</article>

CSS

#controls {
  font-family: monospace;
  background-color: #ddd;
  margin-bottom: 1em;
}

aside {
  padding: 1em;
  border: 1px dashed;
}

JavaScript

document.addEventListener('selectionchange', () => {
	const sel = window.getSelection()
  if (! sel.isCollapsed) input.value = serialize(sel) || ''
})

button.addEventListener('click', () => {
	window.getSelection().collapse(article)
	const selData = parse(input.value)
  setTimeout(() => {
  	select(...selData)
	}, 200)
})

function serialize(sel) {
	if (! article.contains(sel.anchorNode)) {
  	console.warn('Anchor node not within article')
    return
  }
  
  if (! article.contains(sel.focusNode)) {
  	console.warn('Focus node not within article')
    return
  }
  
  const anchorIndex = findPath(article, sel.anchorNode)
  const focusIndex = findPath(article, sel.focusNode)
  return JSON.stringify([
  	anchorIndex,
    sel.anchorOffset,
    focusIndex,
    sel.focusOffset
  ])
}

function parse(string) {
  const [
  	anchorIndex, anchorOffset, focusIndex, focusOffset
	] = JSON.parse(string)
	const anchorNode = findNode(article, anchorIndex)
  const focusNode = findNode(article, focusIndex)
  return [anchorNode, anchorOffset, focusNode, focusOffset]
}

function findPath(parent, child, path = []) {
	if (parent === child) {
  	return path
  }
	if (! parent.contains(child)) {
  	console.log('Not a child of the parent')
  	return
  }
	for (let i = 0; i < parent.childNodes.length; i++) {
		if (parent.childNodes[i].contains(child)) {
    	return findPath(parent.childNodes[i], child, [...path, i])
    }
  }
  return
}

function findNode(parent, childPath) {
	if (childPath.length === 0) return parent
  const [i, ...rest] = childPath
	return findNode(
  	parent.childNodes[i],
    rest
	)
}

function select(anchorNode, anchorOffset, focusNode, focusOffset) {
	const sel = window.getSelection()
  sel.setBaseAndExtent(
  	anchorNode, anchorOffset, focusNode, focusOffset
	)
}