|
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) |
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.
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