Skip to content

Instantly share code, notes, and snippets.

@Gozala
Last active January 12, 2021 13:52
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Gozala/80cf4d2c9f000548b7a11b110b1d7711 to your computer and use it in GitHub Desktop.
Save Gozala/80cf4d2c9f000548b7a11b110b1d7711 to your computer and use it in GitHub Desktop.
Range hilighting code by wrapping text nodes of the range & replacing images.

Highlight Selection Ranges

Code here takes a DOM Selection Range instance and starts traversing a DOM starting from startContainer up to endContainer and wraps Text elements with elements that have semi-transparent background & swaps img elements with clones that are styled to have semi-transparent yellow overlay.

Issues

  • Only handles selecting text and images & would not cover divs with backgrounds for instance.
var markText = (text) => {
var section = document.createElement('section')
section.role = 'selection'
section.style.backgroundColor = 'rgba(255,255,0, 0.3)'
section.style.display = 'inline'
text.parentNode.replaceChild(section, text)
section.appendChild(text)
return section
}
var markImage = (image) => {
var selected = image.cloneNode()
selected.role = 'selection'
selected.style.objectPosition = `${image.width}px`
selected.style.backgroundImage = `url(${image.src})`
selected.style.backgroundColor = 'rgba(255, 255, 0, 0.3)'
selected.style.backgroundBlendMode = 'overlay'
// Keep original node so we can remove highlighting by
// swapping back images.
image.appendChild(selected)
image.parentElement.replaceChild(selected, image)
return selected
}
var markNode = node => {
const {Image, Text} = node.ownerDocument.defaultView
if (node instanceof Image) {
return markImage(node)
} else if (node instanceof Text) {
return markText(node)
} else {
return node
}
}
var filter = function* (p, iterator) {
for (let item of iterator) {
if (p(item)) {
yield item
}
}
}
var map = function* (f, iterator) {
for (let item of iterator) {
yield f(item)
}
}
var takeWhile = function* (p, iterator) {
for (let item of iterator) {
if (p(item)) {
yield item
} else {
break
}
}
}
var nextNodes = function* (node) {
let next = node
let isWalkingUp = false
while (next != null) {
if (!isWalkingUp && next.firstChild != null) {
[isWalkingUp, next] = [false, next.firstChild]
yield next
} else if (next.nextSibling != null) {
[isWalkingUp, next] = [false, next.nextSibling]
yield next
} else {
[isWalkingUp, next] = [true, next.parentNode]
}
}
}
var childByOffset = (node, offset, fallback=node) => {
const child = offset < node.childNodes.length
? node.childNodes[offset]
: fallback
return child
}
var resolveContainer = (node, offset) => {
const {Text, Element} = node.ownerDocument.defaultView
const result = node instanceof Text
? [node, offset]
: offset < node.childNodes.length
? [node.childNodes[offset], 0]
: Error('No child matching the offset found')
return result
}
var highlightTextRange = (text, startOffset, endOffset) => {
const prefix = text
const content = text.splitText(startOffset)
const suffix = content.splitText(endOffset - startOffset)
return [prefix, content, suffix]
}
var isHighlightableNode = node =>
( isHighlightableText(node) ||
isHighlightableImage(node)
)
var isHighlightableText = node =>
node instanceof node.ownerDocument.defaultView.Text &&
node.textContent.trim().length > 0
var isHighlightableImage = node =>
node instanceof node.ownerDocument.defaultView.Image
var highlightRange = (range) => {
const {startContainer, endContainer, startOffset, endOffset} = range
const start = resolveContainer(startContainer, startOffset)
const end = resolveContainer(endContainer, endOffset)
if (start instanceof Error) {
return Error(`Invalid start of the range: ${start}`)
} else if (end instanceof Error) {
return Error(`Invalid end of the range: ${end}`)
} else {
const {Image, Text} = startContainer.ownerDocument.defaultView
const [startNode, startOffset] = start
const [endNode, endOffset] = end
if (startNode === endNode && startNode instanceof Text) {
const [previous, text, next] = highlightTextRange(startNode, startOffset, endOffset)
markText(text)
range.setStart(text, 0)
range.setEnd(next, 0)
} else {
const contentNodes = takeWhile(node => node !== endNode, nextNodes(startNode))
const highlightableNodes = filter(isHighlightableNode, contentNodes)
;[...highlightableNodes].forEach(markNode)
if (startNode instanceof Text) {
const text = startOffset > 0
? startNode.splitText(startOffset)
: startNode
markText(text)
range.setStart(text, 0)
}
if (endNode instanceof Text) {
const [text, offset] = endOffset < endNode.length
? [endNode.splitText(endOffset).previousSibling, 0]
: [endNode, endOffset]
markText(text)
range.setEnd(text, text.length)
}
}
}
}
var getRanges = function*(selection) {
let index = 0
while (index < selection.rangeCount) {
yield selection.getRangeAt(index)
index ++
}
}
var highlight = (selection) => {
for (let range of getRanges(selection)) {
highlightRange(range)
}
}
selection = document.getSelection()
highlight(selection)
@Andrew3005
Copy link

Andrew3005 commented Dec 25, 2019

great solution, thanks!
also can it be deleted? cause I am doing pdf viewer - and highlighting can be also deleted.
I think about window.onclick and check event target. if event target is section - show actions which can be done with selected text
but section element can be in two differents elements, and now I dont have an idea how to implement it
also have you any solution how to save and restore selection ?
thanks in advance

@Gozala
Copy link
Author

Gozala commented Dec 25, 2019

also can it be deleted?

My immediate thought would be to assign uuid to selections with data-selection-uuid or something & then on custom delete selection event undo changes for the nodes with attribute that have matching uuid, you could even use query selector to get them all for traversal.

also have you any solution how to save and restore selection

You mean something like hypothes.is ? It has being several years since this little gist but if I recall correctly idea was to serialize range into selector format to do that & I think this other gist was doing something along those lines https://gist.github.com/Gozala/58cc14aeae44bf57636108ce9fdd2d31

@Andrew3005
Copy link

yes, something like hypothes.is
I am using Angular 8 and ng2 pdf viewer (which use pdf.js), and have to implement text selection CRUD
problem appear because I have to show hebrew pdf books and pdf.js very bad render it

thanks for response!

@relulus
Copy link

relulus commented Jan 17, 2020

I had errors when bundle/minification after using this solution. It seemed to be an yield related issue. But after replacing it, still got console generic errors. Any idea why?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment