Skip to content

Instantly share code, notes, and snippets.

@Gozala
Last active January 12, 2021 13:52
Show Gist options
  • 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)
@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