Files
omnivore/packages/web/lib/hooks/useReadingProgressAnchor.tsx

154 lines
4.8 KiB
TypeScript

import { useEffect } from 'react'
const ANCHOR_ELEMENTS_BLOCKED_ATTRIBUTES = [
'omnivore-highlight-id',
'data-twitter-tweet-id',
'data-instagram-id',
]
// We search in reverse so we can find the last element
// that is visible on the page
export const getTopOmnivoreAnchorElement = (
articleContentElement: HTMLDivElement
): string | undefined => {
let lastVisibleAnchor: Element | undefined = undefined
const anchors = Array.from(
articleContentElement.querySelectorAll(`[data-omnivore-anchor-idx]`)
).reverse()
for (const anchor of anchors) {
const rect = anchor.getBoundingClientRect()
if (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= articleContentElement.clientHeight
) {
lastVisibleAnchor = anchor
} else if (lastVisibleAnchor) {
break
}
}
console.log('last', lastVisibleAnchor)
return (
lastVisibleAnchor?.getAttribute(`data-omnivore-anchor-idx`) ?? undefined
)
}
export const useReadingProgressAnchor = (
articleContentRef: React.MutableRefObject<HTMLDivElement | null>,
setReadingAnchorIndex: React.Dispatch<React.SetStateAction<number>>
): void => {
useEffect(() => {
const visitedNodeList = parseDomTree(articleContentRef.current)
const observerOptions = {
root: null,
rootMargin: '0px',
// we only track elements on becoming completely visible.
threshold: [1],
}
function intersectionCallback(entries: IntersectionObserverEntry[]): void {
let topIntersectingElemId = 0
let minTopElem = 100000
entries.forEach(function (entry: IntersectionObserverEntry) {
const elem = entry.target
const elemId = elem.getAttribute('data-omnivore-anchor-idx') || '0'
if (entry.isIntersecting && entry.intersectionRatio === 1) {
// Among all intersecting elements, find the topmost element.
if (entry.boundingClientRect.top < minTopElem) {
minTopElem = entry.boundingClientRect.top
topIntersectingElemId = parseInt(elemId)
}
}
})
if (topIntersectingElemId > 0) {
/*
* Intersection observer is great in finding us the last element on the
* page that becomes visible on scroll. But for better user experience
* we are interested in the topmost element visible on the page that we
* can scroll to at the top on the next article page reader view. We
* iterate in reverse over anchor elements here us to find the topmost
* visible element.
*/
let topVisibleElemId = topIntersectingElemId
while (topVisibleElemId - 1 > 0) {
const elem = document.querySelector(
`[data-omnivore-anchor-idx='${(topVisibleElemId - 1).toString()}']`
)
if (elem) {
const rect = elem.getBoundingClientRect()
if (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <=
(window.innerWidth || document.documentElement.clientWidth)
) {
/* Is Visible */
topVisibleElemId = topVisibleElemId - 1
} else {
break
}
} else {
// Prevents the Event loop from the eternal blocking
throw new Error('Unable to find previous intersection element!')
}
}
setReadingAnchorIndex(topVisibleElemId)
}
}
const nodeObserver = new IntersectionObserver(
intersectionCallback,
observerOptions
)
visitedNodeList?.forEach((elem) => {
nodeObserver.observe(elem)
})
return () => {
nodeObserver.disconnect()
}
}, [articleContentRef, setReadingAnchorIndex])
}
function parseDomTree(pageNode: HTMLDivElement | null): HTMLDivElement[] {
if (!pageNode || pageNode.childNodes.length == 0) {
return []
}
const nodesToVisitStack: [HTMLDivElement] = [pageNode]
const visitedNodeList = []
while (nodesToVisitStack.length > 0) {
const currentNode = nodesToVisitStack.pop()
if (
currentNode?.nodeType !== Node.ELEMENT_NODE ||
// Avoiding dynamic elements from being counted as anchor-allowed elements
ANCHOR_ELEMENTS_BLOCKED_ATTRIBUTES.some((attrib) =>
currentNode.hasAttribute(attrib)
)
) {
continue
}
visitedNodeList.push(currentNode)
;[].slice
.call(currentNode.childNodes)
.reverse()
.forEach(function (node) {
nodesToVisitStack.push(node)
})
}
visitedNodeList.shift()
visitedNodeList.forEach((node, index) => {
// start from index 1, index 0 reserved for anchor unknown.
node.setAttribute('data-omnivore-anchor-idx', (index + 1).toString())
})
return visitedNodeList
}