diff --git a/next-ui/src/pages/libraries/[id]/books.vue b/next-ui/src/pages/libraries/[id]/books.vue
index b3be651f..05191054 100644
--- a/next-ui/src/pages/libraries/[id]/books.vue
+++ b/next-ui/src/pages/libraries/[id]/books.vue
@@ -9,4 +9,5 @@ series.vue
meta:
requiresRole: USER
+ scrollable: true
diff --git a/next-ui/src/pages/libraries/[id]/collections.vue b/next-ui/src/pages/libraries/[id]/collections.vue
index 64e70f3e..de17f29a 100644
--- a/next-ui/src/pages/libraries/[id]/collections.vue
+++ b/next-ui/src/pages/libraries/[id]/collections.vue
@@ -10,4 +10,5 @@
meta:
requiresRole: USER
+ scrollable: true
diff --git a/next-ui/src/pages/libraries/[id]/readlists.vue b/next-ui/src/pages/libraries/[id]/readlists.vue
index 878f4e9c..b324c27b 100644
--- a/next-ui/src/pages/libraries/[id]/readlists.vue
+++ b/next-ui/src/pages/libraries/[id]/readlists.vue
@@ -10,4 +10,5 @@
meta:
requiresRole: USER
+ scrollable: true
diff --git a/next-ui/src/pages/libraries/[id]/series.vue b/next-ui/src/pages/libraries/[id]/series.vue
index 807678da..92c1fab5 100644
--- a/next-ui/src/pages/libraries/[id]/series.vue
+++ b/next-ui/src/pages/libraries/[id]/series.vue
@@ -123,4 +123,5 @@ watch(series, (newSeries) => {
meta:
requiresRole: USER
+ scrollable: true
diff --git a/next-ui/src/plugins/index.ts b/next-ui/src/plugins/index.ts
index 548cdd86..6f265c52 100644
--- a/next-ui/src/plugins/index.ts
+++ b/next-ui/src/plugins/index.ts
@@ -18,6 +18,7 @@ import type { App } from 'vue'
// Navigation guards
import { useLoginGuard } from '@/router/login-guard'
import { useRoleGuard } from '@/router/role-guard'
+import { useScroll } from '@/router/scroll'
export function registerPlugins(app: App) {
app
@@ -39,4 +40,5 @@ export function registerPlugins(app: App) {
// register navigation guards
useLoginGuard(router)
useRoleGuard(router)
+ useScroll(router)
}
diff --git a/next-ui/src/router/index.ts b/next-ui/src/router/index.ts
index e1e4d436..c1777017 100644
--- a/next-ui/src/router/index.ts
+++ b/next-ui/src/router/index.ts
@@ -9,11 +9,24 @@ import { createRouter, createWebHistory } from 'vue-router'
import { setupLayouts } from 'virtual:generated-layouts'
import { routes } from 'vue-router/auto-routes'
+import { scrollerQuery, scrollMap } from '@/router/scroll'
+
const router = createRouter({
history: createWebHistory(
import.meta.env.PROD ? window.resourceBaseUrl : import.meta.env.BASE_URL,
),
routes: setupLayouts(routes),
+ scrollBehavior(to) {
+ if (scrollMap.has(to.path)) {
+ const scrollTo = scrollMap.get(to.path)
+ return new Promise((resolve) => {
+ void nextTick(() => {
+ document.querySelector(scrollerQuery)?.scrollTo(0, scrollTo ?? 0)
+ resolve(false)
+ })
+ })
+ }
+ },
})
// Workaround for https://github.com/vitejs/vite/issues/11804
diff --git a/next-ui/src/router/login-guard.ts b/next-ui/src/router/login-guard.ts
index f7da1e9c..fed938f5 100644
--- a/next-ui/src/router/login-guard.ts
+++ b/next-ui/src/router/login-guard.ts
@@ -1,9 +1,11 @@
import type { Router } from 'vue-router'
import { useCurrentUser } from '@/colada/users'
-// check if the user is authenticated before navigating to any page
-// the authentication is cached by Pinia Colada
-// redirect to the startup page if not authenticated
+/**
+ * Check if the user is authenticated before navigating to any page.
+ * The authentication is cached by Pinia Colada.
+ * Redirect to the startup page if not authenticated.
+ */
export function useLoginGuard(router: Router) {
router.beforeEach((to) => {
if (!to.meta.noAuth) {
diff --git a/next-ui/src/router/role-guard.ts b/next-ui/src/router/role-guard.ts
index 1c3baa46..6ab75f66 100644
--- a/next-ui/src/router/role-guard.ts
+++ b/next-ui/src/router/role-guard.ts
@@ -1,9 +1,11 @@
import type { Router } from 'vue-router'
import { useCurrentUser } from '@/colada/users'
-// check if the user has the necessary role before navigating to restricted pages
-// the authentication is cached by Pinia Colada
-// redirect to the home page in case of insufficient permissions
+/**
+ * Check if the user has the necessary role before navigating to restricted pages.
+ * The authentication is cached by Pinia Colada.
+ * Redirect to the home page in case of insufficient permissions.
+ */
export function useRoleGuard(router: Router) {
router.beforeEach((to) => {
if (to.meta.requiresRole) {
diff --git a/next-ui/src/router/scroll.ts b/next-ui/src/router/scroll.ts
new file mode 100644
index 00000000..269ec2f1
--- /dev/null
+++ b/next-ui/src/router/scroll.ts
@@ -0,0 +1,23 @@
+import type { Router } from 'vue-router'
+
+/**
+ * Stores the `scrollTop` position per `route.path`
+ */
+export const scrollMap = new Map()
+/**
+ * Query to be used with `document.querySelector()` to get the main scroller.
+ */
+export const scrollerQuery = '.v-main__scroller'
+
+/**
+ * Persist the scroll position when navigating away from a page.
+ * Scroll restoration is handled in the router's `scrollBehavior` function.
+ */
+export function useScroll(router: Router) {
+ router.afterEach((to, from) => {
+ if (from.meta.scrollable) {
+ const scroller = document.querySelector(scrollerQuery)
+ if (scroller) scrollMap.set(from.path, scroller.scrollTop)
+ }
+ })
+}
diff --git a/next-ui/src/types/RouterMeta.d.ts b/next-ui/src/types/RouterMeta.d.ts
index fe2086c8..ab0337fb 100644
--- a/next-ui/src/types/RouterMeta.d.ts
+++ b/next-ui/src/types/RouterMeta.d.ts
@@ -11,5 +11,9 @@ declare module 'vue-router' {
interface RouteMeta {
noAuth?: boolean
requiresRole?: UserRoles
+ /**
+ * Indicates whether the scroll position should be persisted and restored for this page.
+ */
+ scrollable?: boolean
}
}