Files
remanso/src/hooks/useRouteQueryStackedNotes.hook.ts
Julien Calixte 17f015b686 fix(notes): wait for stacked note element before scrolling on mobile
A single nextTick is not enough for a freshly added stacked note to be
in the DOM, so the mobile scroll target was computed against a null
element. Poll with requestAnimationFrame (mirroring scrollToHashInNote)
and use offsetTop, with an (index + 1) * height fallback.
2026-04-29 10:52:07 +02:00

151 lines
3.4 KiB
TypeScript

import { useWindowSize } from "@vueuse/core"
import { useRouteQuery } from "@vueuse/router"
import { nextTick, readonly } from "vue"
import { getBookmarkWidthPx } from "@/constants/bookmark-width"
import { getNoteWidth } from "@/constants/note-width"
import { useOverlay } from "@/hooks/useOverlay.hook"
export const useRouteQueryStackedNotes = () => {
const stackedNotes = useRouteQuery("stackedNotes", undefined, {
transform: (value: string | string[] | undefined) => {
if (!value) {
return []
}
return Array.isArray(value) ? value : [value]
}
})
const { height } = useWindowSize()
const { scrollToNote, isMobile } = useOverlay(false)
const scrollToHashInNote = (
cleanSha: string,
hash: string,
smooth: boolean,
attempts = 30
) => {
if (attempts <= 0) {
return
}
const heading = document.querySelector(
`.note-${cleanSha} #${CSS.escape(hash)}`
)
if (heading) {
heading.scrollIntoView({
block: "start",
inline: "nearest",
behavior: smooth ? "smooth" : "auto"
})
return
}
requestAnimationFrame(() => {
scrollToHashInNote(cleanSha, hash, smooth, attempts - 1)
})
}
const scrollToNoteElement = (
cleanNoteId: string,
index: number,
attempts = 30
) => {
if (attempts <= 0) {
scrollToNote((index + 1) * height.value)
return
}
const element = document.querySelector(
`.note-${cleanNoteId}`
) as HTMLElement | null
if (element) {
scrollToNote(element.offsetTop)
return
}
requestAnimationFrame(() => {
scrollToNoteElement(cleanNoteId, index, attempts - 1)
})
}
type ScrollToFocusedNoteOptions = {
noteId?: string | null
notes?: string[]
hash?: string
smoothHash?: boolean
}
const scrollToFocusedNote = ({
noteId = null,
notes = stackedNotes.value,
hash,
smoothHash = false
}: ScrollToFocusedNoteOptions = {}) => {
nextTick(() => {
const index = noteId ? notes.findIndex((nid) => nid === noteId) : 0
if (isMobile.value) {
if (noteId) {
scrollToNoteElement(noteId.replaceAll(":", "-"), index)
} else {
scrollToNote(0)
}
} else {
if (noteId) {
const left = (index + 1) * (getNoteWidth() - getBookmarkWidthPx())
scrollToNote(left)
} else {
scrollToNote(0)
}
}
if (hash && noteId) {
scrollToHashInNote(noteId.replaceAll(":", "-"), hash, smoothHash)
}
})
}
const addStackedNote = (
currentSha: string,
sha: string,
selector?: string,
hash?: string
) => {
if (stackedNotes.value.includes(sha)) {
scrollToFocusedNote({
noteId: selector ?? sha,
hash,
smoothHash: true
})
return
}
if (!currentSha) {
stackedNotes.value = [sha]
} else {
const [splittedStackedNotes] = stackedNotes.value
.join(";")
.split(currentSha)
const newStackedNotes = [
...splittedStackedNotes.replaceAll(";;", ";").split(";"),
currentSha,
sha
].filter((sha) => !!sha)
stackedNotes.value = newStackedNotes
}
scrollToFocusedNote({ noteId: selector ?? sha, hash })
}
return {
stackedNotes: readonly(stackedNotes),
addStackedNote,
scrollToFocusedNote
}
}