forked from Mirrors/elk
Compare commits
22 commits
main
...
userquin/f
Author | SHA1 | Date | |
---|---|---|---|
|
6ce5eadcac | ||
|
4ab1de45f4 | ||
|
6d2f70b2ba | ||
|
fcf712ef42 | ||
|
531392f967 | ||
|
1caa59acbe | ||
|
a217c59590 | ||
|
086a2940f7 | ||
|
2531bd8f45 | ||
|
674adb48ba | ||
|
2303700b63 | ||
|
be30faaa0f | ||
|
cd03f3fa1e | ||
|
1e505711a1 | ||
|
a2a6b84ba7 | ||
|
7c355eeec7 | ||
|
8e19574ea1 | ||
|
c9d70de38e | ||
|
5893cf1a6e | ||
|
b6eed6b46b | ||
|
2a30510127 | ||
|
c546408326 |
11 changed files with 114 additions and 7 deletions
|
@ -17,6 +17,7 @@ cacheAccount(account)
|
||||||
shrink
|
shrink
|
||||||
overflow-hidden
|
overflow-hidden
|
||||||
:to="getAccountRoute(account)"
|
:to="getAccountRoute(account)"
|
||||||
|
@click="$rememberAccountPosition(getAccountRoute(account).fullPath)"
|
||||||
/>
|
/>
|
||||||
<div h-full p1 shrink-0>
|
<div h-full p1 shrink-0>
|
||||||
<AccountFollowButton :account="account" />
|
<AccountFollowButton :account="account" />
|
||||||
|
|
|
@ -15,6 +15,7 @@ const { options, command, replace, preventScrollTop = false } = $defineProps<{
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
|
||||||
useCommands(() => command
|
useCommands(() => command
|
||||||
? options.map(tab => ({
|
? options.map(tab => ({
|
||||||
|
@ -24,7 +25,15 @@ useCommands(() => command
|
||||||
icon: tab.icon ?? 'i-ri:file-list-2-line',
|
icon: tab.icon ?? 'i-ri:file-list-2-line',
|
||||||
onActivate: () => router.replace(tab.to),
|
onActivate: () => router.replace(tab.to),
|
||||||
}))
|
}))
|
||||||
: [])
|
: [],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleClick = (to: RouteLocationRaw) => {
|
||||||
|
if (preventScrollTop || nuxtApp.$preventScrollToTop(router.resolve(to).fullPath))
|
||||||
|
return
|
||||||
|
|
||||||
|
nuxtApp.$scrollToTop()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -41,7 +50,7 @@ useCommands(() => command
|
||||||
tabindex="1"
|
tabindex="1"
|
||||||
hover:bg-active transition-100
|
hover:bg-active transition-100
|
||||||
exact-active-class="children:(text-secondary !border-primary !op100 !text-base)"
|
exact-active-class="children:(text-secondary !border-primary !op100 !text-base)"
|
||||||
@click="!preventScrollTop && $scrollToTop()"
|
@click="handleClick(option.to)"
|
||||||
>
|
>
|
||||||
<span ws-nowrap mxa sm:px2 sm:py3 xl:pb4 xl:pt5 py2 text-center border-b-3 text-secondary-light hover:text-secondary border-transparent>{{ option.display }}</span>
|
<span ws-nowrap mxa sm:px2 sm:py3 xl:pb4 xl:pt5 py2 text-center border-b-3 text-secondary-light hover:text-secondary border-transparent>{{ option.display }}</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
|
@ -15,6 +15,7 @@ defineSlots<{
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
|
||||||
useCommand({
|
useCommand({
|
||||||
scope: 'Navigation',
|
scope: 'Navigation',
|
||||||
|
@ -41,6 +42,13 @@ onMastoInit(async () => {
|
||||||
// when we know there is no user.
|
// when we know there is no user.
|
||||||
const noUserDisable = computed(() => !isMastoInitialised.value || (props.userOnly && !currentUser.value))
|
const noUserDisable = computed(() => !isMastoInitialised.value || (props.userOnly && !currentUser.value))
|
||||||
const noUserVisual = computed(() => isMastoInitialised.value && props.userOnly && !currentUser.value)
|
const noUserVisual = computed(() => isMastoInitialised.value && props.userOnly && !currentUser.value)
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (nuxtApp.$preventScrollToTop(router.resolve(props.to).fullPath))
|
||||||
|
return
|
||||||
|
|
||||||
|
nuxtApp.$scrollToTop()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -51,7 +59,7 @@ const noUserVisual = computed(() => isMastoInitialised.value && props.userOnly &
|
||||||
:active-class="activeClass"
|
:active-class="activeClass"
|
||||||
group focus:outline-none disabled:pointer-events-none
|
group focus:outline-none disabled:pointer-events-none
|
||||||
:tabindex="noUserDisable ? -1 : null"
|
:tabindex="noUserDisable ? -1 : null"
|
||||||
@click="$scrollToTop"
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
<CommonTooltip :disabled="!isMediumScreen" :content="text" placement="right">
|
<CommonTooltip :disabled="!isMediumScreen" :content="text" placement="right">
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -9,7 +9,10 @@ const { notification } = defineProps<{
|
||||||
<template>
|
<template>
|
||||||
<article flex flex-col relative>
|
<article flex flex-col relative>
|
||||||
<template v-if="notification.type === 'follow'">
|
<template v-if="notification.type === 'follow'">
|
||||||
<NuxtLink :to="getAccountRoute(notification.account)">
|
<NuxtLink
|
||||||
|
:to="getAccountRoute(notification.account)"
|
||||||
|
@click="$rememberAccountPosition(getAccountRoute(notification.account).fullPath)"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
flex items-center absolute
|
flex items-center absolute
|
||||||
ps-3 pe-4 inset-is-0
|
ps-3 pe-4 inset-is-0
|
||||||
|
|
|
@ -7,6 +7,7 @@ const props = defineProps<{
|
||||||
command?: boolean
|
command?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
const focusEditor = inject<typeof noop>('focus-editor', noop)
|
const focusEditor = inject<typeof noop>('focus-editor', noop)
|
||||||
|
|
||||||
const { details, command } = $(props)
|
const { details, command } = $(props)
|
||||||
|
|
|
@ -42,6 +42,7 @@ const statusRoute = $computed(() => getStatusRoute(status))
|
||||||
|
|
||||||
const el = ref<HTMLElement>()
|
const el = ref<HTMLElement>()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
|
||||||
function onclick(evt: MouseEvent | KeyboardEvent) {
|
function onclick(evt: MouseEvent | KeyboardEvent) {
|
||||||
const path = evt.composedPath() as HTMLElement[]
|
const path = evt.composedPath() as HTMLElement[]
|
||||||
|
@ -56,6 +57,7 @@ function go(evt: MouseEvent | KeyboardEvent) {
|
||||||
window.open(statusRoute.href)
|
window.open(statusRoute.href)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
nuxtApp.$rememberStatusPosition(status)
|
||||||
cacheStatus(status)
|
cacheStatus(status)
|
||||||
router.push(statusRoute)
|
router.push(statusRoute)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { Paginator, WsEvents, mastodon } from 'masto'
|
import type { Paginator, WsEvents, mastodon } from 'masto'
|
||||||
import type { PaginatorState } from '~/types'
|
import type { PaginatorState } from '~/types'
|
||||||
|
import { onReactivated } from '~/composables/vue'
|
||||||
|
|
||||||
export function usePaginator<T, P, U = T>(
|
export function usePaginator<T, P, U = T>(
|
||||||
paginator: Paginator<T[], P>,
|
paginator: Paginator<T[], P>,
|
||||||
|
@ -17,7 +18,10 @@ export function usePaginator<T, P, U = T>(
|
||||||
const bound = reactive(useElementBounding(endAnchor))
|
const bound = reactive(useElementBounding(endAnchor))
|
||||||
const isInScreen = $computed(() => bound.top < window.innerHeight * 2)
|
const isInScreen = $computed(() => bound.top < window.innerHeight * 2)
|
||||||
const error = ref<unknown | undefined>()
|
const error = ref<unknown | undefined>()
|
||||||
|
const loaded = ref(false)
|
||||||
|
|
||||||
const deactivated = useDeactivated()
|
const deactivated = useDeactivated()
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
|
||||||
async function update() {
|
async function update() {
|
||||||
(items.value as U[]).unshift(...preprocess(prevItems.value as T[]))
|
(items.value as U[]).unshift(...preprocess(prevItems.value as T[]))
|
||||||
|
@ -87,13 +91,37 @@ export function usePaginator<T, P, U = T>(
|
||||||
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
bound.update()
|
bound.update()
|
||||||
|
if (!loaded.value) {
|
||||||
|
loaded.value = true
|
||||||
|
await nextTick()
|
||||||
|
nuxtApp.$restoreScrollPosition()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.client) {
|
if (process.client) {
|
||||||
|
const timeout = ref()
|
||||||
useIntervalFn(() => {
|
useIntervalFn(() => {
|
||||||
bound.update()
|
bound.update()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
window.clearTimeout(timeout.value)
|
||||||
|
loaded.value = false
|
||||||
|
})
|
||||||
|
onReactivated(() => {
|
||||||
|
window.clearTimeout(timeout.value)
|
||||||
|
if (isMastoInitialised.value) {
|
||||||
|
if (!loaded.value)
|
||||||
|
loaded.value = true
|
||||||
|
|
||||||
|
// TODO: apply timeout based in items length: be conservative for long lists on slow devices
|
||||||
|
timeout.value = setTimeout(() => nuxtApp.$restoreScrollPosition(), 600)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
loaded.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (!isMastoInitialised.value) {
|
if (!isMastoInitialised.value) {
|
||||||
onMastoInit(() => {
|
onMastoInit(() => {
|
||||||
state.value = 'idle'
|
state.value = 'idle'
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
import type { mastodon } from 'masto'
|
import type { mastodon } from 'masto'
|
||||||
|
|
||||||
export const navigateToStatus = ({ status, focusReply = false }: { status: mastodon.v1.Status; focusReply?: boolean }) => navigateTo({ path: getStatusRoute(status).href, state: { focusReply } })
|
export const navigateToStatus = ({ status, focusReply = false }: { status: mastodon.v1.Status; focusReply?: boolean }) => {
|
||||||
|
useNuxtApp().$rememberStatusPosition(status)
|
||||||
|
navigateTo({ path: getStatusRoute(status).href, state: { focusReply } })
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ export const STORAGE_KEY_HIDE_EXPLORE_NEWS_TIPS = 'elk-hide-explore-news-tips'
|
||||||
export const STORAGE_KEY_HIDE_EXPLORE_TAGS_TIPS = 'elk-hide-explore-tags-tips'
|
export const STORAGE_KEY_HIDE_EXPLORE_TAGS_TIPS = 'elk-hide-explore-tags-tips'
|
||||||
export const STORAGE_KEY_NOTIFICATION = 'elk-notification'
|
export const STORAGE_KEY_NOTIFICATION = 'elk-notification'
|
||||||
export const STORAGE_KEY_NOTIFICATION_POLICY = 'elk-notification-policy'
|
export const STORAGE_KEY_NOTIFICATION_POLICY = 'elk-notification-policy'
|
||||||
|
export const STORAGE_KEY_LAST_SCROLL_POSITION = 'elk-last-scroll-position'
|
||||||
|
|
||||||
export const COOKIE_MAX_AGE = 10 * 365 * 24 * 60 * 60 * 1000
|
export const COOKIE_MAX_AGE = 10 * 365 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
|
44
plugins/remember-scroll-position.client.ts
Normal file
44
plugins/remember-scroll-position.client.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import type { mastodon } from 'masto'
|
||||||
|
|
||||||
|
import { STORAGE_KEY_LAST_SCROLL_POSITION } from '~/constants'
|
||||||
|
|
||||||
|
interface RestoreScroll {
|
||||||
|
id: string
|
||||||
|
type: 'status' | 'account'
|
||||||
|
position: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
const lastStatus = useSessionStorage<Record<string, RestoreScroll>>(STORAGE_KEY_LAST_SCROLL_POSITION, {})
|
||||||
|
return {
|
||||||
|
provide: {
|
||||||
|
preventScrollToTop: (path: string) => {
|
||||||
|
return !!lastStatus.value[path]
|
||||||
|
},
|
||||||
|
restoreScrollPosition: () => {
|
||||||
|
const fullPath = useRoute().fullPath
|
||||||
|
const restore = lastStatus.value[fullPath]
|
||||||
|
if (restore) {
|
||||||
|
const el = restore.type === 'status'
|
||||||
|
? document.getElementById(`status-${restore.id}`)
|
||||||
|
: document.querySelector(`a[href="${restore.id}"]`)
|
||||||
|
if (el) {
|
||||||
|
if (typeof restore.position === 'undefined')
|
||||||
|
el.scrollIntoView()
|
||||||
|
else
|
||||||
|
window.scrollTo(0, restore.position)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
delete lastStatus.value[fullPath]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rememberAccountPosition: (account: string) => {
|
||||||
|
lastStatus.value[useRoute().fullPath] = { id: account, type: 'account', position: window.scrollY }
|
||||||
|
},
|
||||||
|
rememberStatusPosition: (status: mastodon.v1.Status) => {
|
||||||
|
lastStatus.value[useRoute().fullPath] = { id: status.id, type: 'status', position: window.scrollY }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
|
@ -1,7 +1,14 @@
|
||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin((nuxt) => {
|
||||||
return {
|
return {
|
||||||
provide: {
|
provide: {
|
||||||
scrollToTop: () => {
|
scrollToTop: (evt?: MouseEvent | KeyboardEvent) => {
|
||||||
|
const path = evt?.composedPath?.() as HTMLElement[]
|
||||||
|
const el = path?.find(el => el.tagName?.toUpperCase() === 'A') as HTMLAnchorElement
|
||||||
|
if (el?.href) {
|
||||||
|
if (nuxt.$preventScrollToTop(new URL(el.href, import.meta.url).pathname))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
|
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue