JSFiddle - React, Tailwind, and code Playground

by sfoster

HTML

<header>
  <h1>Letters From The Earth</h1>
  <h2>by Mark Twain</h2>
  <div class="pubdate">
    <p>originally written in 1909, according to Mark Twain A to
    Z and Mark Twain's Last Days</p>
  </div>
</header>

<section>
  <p>The Creator sat upon the throne, thinking. Behind him
  stretched the illimitable continent of heaven, steeped in a
  glory of light and color; before him rose the black night of
  Space, like a wall. His mighty bulk towered rugged and
  mountain-like into the zenith, and His divine head blazed
  there like a distant sun. At His feet stood three colossal
  figures, diminished to extinction, almost, by contrast --
  archangels -- their heads level with His ankle-bone.</p>

  <p>When the Creator had finished thinking, He said, "I have
  thought. Behold!"</p>

  <p>He lifted His hand, and from it burst a fountain-spray of fire,
  a million stupendous suns, which clove the blackness and
  soared, away and away and away, diminishing in magnitude
  and intensity as they pierced the far frontiers of Space, until
  at last they were but as diamond nailheads sparkling under
  the domed vast roof of the universe.</p>
</section>

<div id="lineHighlight">
  <div id="wordHighlight"></div>
</div>

CSS

#lineHighlight {
        width: 10px;
        height: 10px;
        border: 1px solid blue;
        position: absolute;
      }
      #wordHighlight {
        width: 10px;
        height: 10px;
        border: 1px solid orange;
        background-color: rgba(255,153,0, 0.5);
        position: absolute;
      }
p {
  line-height: 1.5em;
}

JavaScript

let range = document.createRange();

function init() {
  const firstP = document.querySelector("section > p");
  highlightLineAndWord(firstP.firstChild, document.body);

}

function getLineBoxForElement(elem) {
	// returns a DOMRect with bounds for a line of text in this element
  // So that e.g. getting line 3 means box.height * (lineNum -1). 
  const rect = elem.getBoundingClientRect(); 
  const lineHeight = parseFloat(getComputedStyle(elem).lineHeight);
  const blockPadding = parseFloat(getComputedStyle(elem).paddingBlock); 
  const lineCount = rect.height - blockPadding / lineHeight;
  
  Object.assign(rect, {
  	height: lineHeight,
    topOffset: blockPadding,
    bottomOffset: blockPadding,
  });
  console.log("getLineBoxForElement", rect);
  return rect;
}

function highlightLineAndWord(textNode, boundingElem, startOffset, endOffset) {
  // Highlight a word on the first line as an example of using Range
  // The text nodes and offets into them would need to be produced by
  // tokenizing/splitting out into word/word-like sequences
  const startRangeOffset = 4;
  const endRangeOffset = startRangeOffset +  "Creator".length;
  range.setStart(textNode, startRangeOffset);
  range.setEnd(textNode, endRangeOffset);
  
  const containerRect = boundingElem.getBoundingClientRect();
  const currentElemRect = getLineBoxForElement(textNode.parentNode);
  const rangeRect = range.getBoundingClientRect();
  const lineBox = document.getElementById("lineHighlight");
  const wordBox = document.getElementById("wordHighlight");
  lineBox.style.cssText = `
    left: ${window.scrollX + currentElemRect.x}px;
    top: ${window.scrollY + currentElemRect.y}px;
    width: ${containerRect.width}px;
    height: ${lineBox.height}px;
  `;
  console.log("rangeRect:", rangeRect);
  wordBox.style.cssText = `
    left: ${rangeRect.x - currentElemRect.x}px;
    top: 0px;
    width: ${rangeRect.width}px;
    height: ${rangeRect.height}px;
  `;
}
document.addEventListener("DOMContentLoaded",...