Easy as 1–2–3

Highlighting text with span tags in JavaScript, from Vanilla JS to React to Redux.

Lucas Eckman
5 min readJul 26, 2018

For my final project at Flatiron School, I am recreating the text highlight / annotation feature implemented by Medium and Genius. The feature allows a user to select text in an article and then save the highlight so it persists for them and in some cases other users. In the case of Genius (and my imitation), users write an annotation associated with the highlight that explains the highlight’s significance, and allows other users to comment on those annotations.

I knew that I would eventually want to implement this feature in a React-Redux web app with Thunk and ConnectedRouter. I briefly entertained going straight there. But after setting up all the plumbing, and brainstorming how my highlighting feature would actually work in this environment, I quickly realized that I should start smaller.

I went to my terminal and touched in an index.html, index.js, and style.css file into a new highlight-testing directory and set to work.

The Basic Idea in Vanilla JavaScript

The first problem in my fresh environment was to consider how I would discover what text on the page a user had selected. window.getSelection() would be a powerful tool (with some limitations and frustrations).

For one string of text inside a div, grabbing the needed information was relatively simple:

The basic idea is that by accessing the starting and ending index of the selection within the body of the statement, I would then be able to go back and somehow insert <span class=’highlight’> tags at those indices. There would be future issues with this approach, but for now I was satisfied to have the data I needed to draw the highlight.

Success! Well, success within the narrow scope of selecting text in a single div with no existing <span> children, getting its starting and ending indices, and then slicing in highlight spans at those indices.

Now came the tricky part. This approach works because I can use the index of each selection of the statement to calculate where to slice in span tags. However, once I sliced in a single span, I ran into two problems:

First, if the user’s selection was after an existing highlight span, the offset index I was getting from window.getSelection() would be from that span, and not from the start of the document.

Second, the innerHTML of the statement now contained these spans, so when I attempted to use an index based on the unaltered statement to slice in more spans, they would be in the wrong position.

Now, I was able to come up with a clever solution for each of these. For the first, I used the id of the neighboring tag to look up its absolute location in the unaltered statement body, and for the second, essentially I kept the span tags and class names a consistent number of characters, and kept a counter of the number of spans I had already inserted, and added that to the index.

This allowed me insert multiple spans while still keeping track of the absolute position of each highlight.

An Array of Children: Moving on to React

In the vanilla JavaScript version of my implementation, I was doing some classic DOM and string manipulation. This was fine and it was working pretty well. I even figured out ways to have overlapping spans with some inspiration from inspecting Medium’s spans as I created tons of highlights on random articles and deciphered the results. (Thanks, Medium!)

But now I needed to adapt my solution to React, where traditional DOM manipulation would be counter to the whole point of using the framework.

There was always the ominously named dangerouslySetInnerHTML… But no, an alternative functionality of JSX would rescue me from danger and actually make my implementation far simpler.

In JSX, you can put a bunch of React components into an array, and then simply reference the array within curly braces in a component, and JSX will just render each of them in order as child components within your parent component!

Now, instead of slicing up the DOM over and over, I could just store the original, unaltered statement as a string, go through my annotation objects one by one in order, and combine the two to create a new array of Fragment and HighlightSpan components.

The first function called in makeStatementArray, processAnnotations, checks the annotations for any overlap of highlights. <span>s themselves cannot overlap. I solved this by splitting two overlapping spans into three.

The “last highlight” extends from index 200 to 400, and “this highlight” extends from 300 to 600. The solution is to split them into three spans, one which extends from 200 to 299 and belongs only to “last highlight”, one which extends from 300–400 and belongs to both, and lastly one from 401–600 that belongs only to “last highlight”.

Within makeStatementArray I track how many characters we’ve rendered in total so I know if there is a trailing text fragment after the last highlight. In the end we get an array of Fragment and HighlightSpan components.

I pass this newStatementArray as props to my Statement component and render simply {props.content}. The result:

Cutting off the Head of the Props Hydra

Now we are highlighting the correct spans in React, which is great! But my next feature is to have a mouse-over effect where the highlight and its corresponding annotation change color.

I solve this by setting up mouse event listeners on the spans that will setState in the parent of both the statement itself and the annotations list and store the id of which highlight and annotation to highlight. This happens to be App.js which means basically that my whole app is updating every time someone hovers over a highlight span. I am also passing a setState function down through four or five child Components and back up again. This feels terrible, but it’s working.

I’d really love a way to avoid tying and twisting up all of my components with state and props like those snake on the Caduceus Staff. What if I wanted to move the annotations to a different component or have them hover over the highlight in the future? Painstaking refactoring and inevitable bugs. So, enter Redux.

With Redux’s mapStateToProps and mapDispatchToProps, we no longer need to pass functions and props down through trees of children and unnecessarily update state or props in components that don’t really need to know about it. Instead, we can target individual components.

Wonderful. Now, I can target my HighlightSpan component with the currentHighlight state no matter where it is in my component tree, and likewise dispatch back the new highlight state. I have the flexibility of reorganizing my components without the headache of refactoring state and the inevitable bugs that result, and just as great is that no other part of my app needs to worry about the currentHighlight state changing. Only the parts that need to know about it are refreshed when that state changes.

And there you have it. Tune in soon for the final product.

--

--