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