Simplify top positioning

This commit is contained in:
Jackson Harper 2023-03-10 16:47:02 +08:00
parent cc4b07a5ed
commit 8cf0a6ecb2
7 changed files with 20 additions and 81 deletions

View file

@ -1,6 +1,7 @@
import { Box } from '../../elements/LayoutPrimitives'
import {
getTopOmnivoreAnchorElement,
parseDomTree,
useReadingProgressAnchor,
} from '../../../lib/hooks/useReadingProgressAnchor'
import {
@ -18,6 +19,7 @@ export type ArticleProps = {
content: string
initialAnchorIndex: number
initialReadingProgress?: number
initialReadingProgressTop?: number
highlightHref: MutableRefObject<string | null>
articleMutations: ArticleMutations
}
@ -93,6 +95,8 @@ export function Article(props: ArticleProps): JSX.Element {
setShouldScrollToInitialPosition(false)
parseDomTree(articleContentRef.current)
// If we are scrolling to a highlight, dont scroll to read position
if (props.highlightHref.current) {
return

View file

@ -366,6 +366,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
content={props.article.content}
highlightHref={highlightHref}
initialAnchorIndex={props.article.readingProgressAnchorIndex}
initialReadingProgressTop={props.article.readingProgressTopPercent}
articleMutations={props.articleMutations}
/>
<Button

View file

@ -13,7 +13,7 @@ export const getTopOmnivoreAnchorElement = (
): string | undefined => {
let lastVisibleAnchor: Element | undefined = undefined
const anchors = Array.from(
articleContentElement.querySelectorAll(`[data-omnivore-anchor-idx]`)
document.querySelectorAll(`[data-omnivore-anchor-idx]`)
).reverse()
for (const anchor of anchors) {
@ -35,87 +35,16 @@ export const getTopOmnivoreAnchorElement = (
}
export const useReadingProgressAnchor = (
articleContentRef: React.MutableRefObject<HTMLDivElement | null>,
setReadingAnchorIndex: React.Dispatch<React.SetStateAction<number>>
articleContentRef: React.MutableRefObject<HTMLDivElement | null>
): 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])
parseDomTree(articleContentRef.current)
}, [articleContentRef])
}
function parseDomTree(pageNode: HTMLDivElement | null): HTMLDivElement[] {
export function parseDomTree(
pageNode: HTMLDivElement | null
): HTMLDivElement[] {
if (!pageNode || pageNode.childNodes.length == 0) {
return []
}

View file

@ -13,6 +13,7 @@ export const articleFragment = gql`
contentReader
originalArticleUrl
readingProgressPercent
readingProgressTopPercent
readingProgressAnchorIndex
slug
isArchived
@ -52,6 +53,7 @@ export type ArticleFragmentData = {
contentReader?: ContentReader
originalArticleUrl: string
readingProgressPercent: number
readingProgressTopPercent?: number
readingProgressAnchorIndex: number
slug: string
isArchived: boolean

View file

@ -11,7 +11,7 @@ export async function searchQuery({
limit = 10,
searchQuery,
}: LibraryItemsQueryInput): Promise<LibraryItemsData | undefined> {
const query = gql`
const query = gql`
query Search($after: String, $first: Int, $query: String) {
search(first: $first, after: $after, query: $query) {
... on SearchSuccess {
@ -27,6 +27,7 @@ export async function searchQuery({
createdAt
isArchived
readingProgressPercent
readingProgressTopPercent
readingProgressAnchorIndex
author
image
@ -71,8 +72,8 @@ export async function searchQuery({
}
try {
const data = (await gqlFetcher(query, {...variables}))
return data as LibraryItemsData || undefined;
const data = await gqlFetcher(query, { ...variables })
return (data as LibraryItemsData) || undefined
} catch (error) {
console.log('search error', error)
return undefined

View file

@ -50,6 +50,7 @@ export type ArticleAttributes = {
description?: string
contentReader: ContentReader
readingProgressPercent: number
readingProgressTopPercent?: number
readingProgressAnchorIndex: number
slug: string
savedByViewer?: boolean

View file

@ -67,6 +67,7 @@ export type LibraryItemNode = {
contentReader?: ContentReader
originalArticleUrl: string
readingProgressPercent: number
readingProgressTopPercent?: number
readingProgressAnchorIndex: number
slug: string
isArchived: boolean