From c6a682e41efe213dd0b05fb39518d2316903f11c Mon Sep 17 00:00:00 2001 From: Calum Dingwall Date: Wed, 4 Sep 2024 15:25:16 -0500 Subject: [PATCH] Move scroll memory into reusable action. --- web/src/lib/actions/scroll-memory.ts | 87 ++++++++++++++ web/src/lib/actions/use-actions.ts | 67 +++++++++++ .../layouts/user-page-layout.svelte | 9 +- web/src/routes/(user)/albums/+page.svelte | 27 +---- .../[[assetId=id]]/+page.svelte | 18 ++- web/src/routes/(user)/people/+page.svelte | 106 +++++++++--------- .../[[assetId=id]]/+page.svelte | 21 ++-- 7 files changed, 232 insertions(+), 103 deletions(-) create mode 100644 web/src/lib/actions/scroll-memory.ts create mode 100644 web/src/lib/actions/use-actions.ts diff --git a/web/src/lib/actions/scroll-memory.ts b/web/src/lib/actions/scroll-memory.ts new file mode 100644 index 0000000000000..1c19fdd8ab2d0 --- /dev/null +++ b/web/src/lib/actions/scroll-memory.ts @@ -0,0 +1,87 @@ +import { navigating } from '$app/stores'; +import { AppRoute, SessionStorageKey } from '$lib/constants'; +import { handlePromiseError } from '$lib/utils'; + +interface Options { + /** + * {@link AppRoute} for subpages that scroll state should be kept while visiting. + * + * This must be kept the same in all subpages of this route for the scroll memory clearer to work. + */ + routeStartsWith: AppRoute; + /** + * Function to clear additional data/state before scrolling (ex infinite scroll). + */ + beforeClear?: () => void; +} + +interface PageOptions extends Options { + /** + * Function to save additional data/state before scrolling (ex infinite scroll). + */ + beforeSave?: () => void; + /** + * Function to load additional data/state before scrolling (ex infinite scroll). + */ + beforeScroll?: () => Promise; +} + +/** + * @param node The scroll slot element, typically from {@link UserPageLayout} + */ +export function scrollMemory( + node: HTMLElement, + { routeStartsWith, beforeSave, beforeClear, beforeScroll }: PageOptions, +) { + const unsubscribeNavigating = navigating.subscribe((navigation) => { + const existingScroll = sessionStorage.getItem(SessionStorageKey.SCROLL_POSITION); + if (navigation?.to && !existingScroll) { + // Save current scroll information when going into a subpage. + if (navigation.to.url.pathname.startsWith(routeStartsWith)) { + beforeSave?.(); + sessionStorage.setItem(SessionStorageKey.SCROLL_POSITION, node.scrollTop.toString()); + } else { + beforeClear?.(); + sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION); + } + } + }); + + handlePromiseError( + (async () => { + await beforeScroll?.(); + + const newScroll = sessionStorage.getItem(SessionStorageKey.SCROLL_POSITION); + if (newScroll) { + node.scroll({ + top: Number.parseFloat(newScroll), + behavior: 'instant', + }); + } + beforeClear?.(); + sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION); + })(), + ); + + return { + destroy() { + unsubscribeNavigating(); + }, + }; +} + +export function scrollMemoryClearer(_node: HTMLElement, { routeStartsWith, beforeClear }: Options) { + const unsubscribeNavigating = navigating.subscribe((navigation) => { + // Forget scroll position from main page if going somewhere else. + if (navigation?.to && !navigation?.to.url.pathname.startsWith(routeStartsWith)) { + beforeClear?.(); + sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION); + } + }); + + return { + destroy() { + unsubscribeNavigating(); + }, + }; +} diff --git a/web/src/lib/actions/use-actions.ts b/web/src/lib/actions/use-actions.ts new file mode 100644 index 0000000000000..762cfdccf775b --- /dev/null +++ b/web/src/lib/actions/use-actions.ts @@ -0,0 +1,67 @@ +/** + * @license Apache-2.0 + * https://github.com/hperrin/svelte-material-ui/blob/master/packages/common/src/internal/useActions.ts + */ + +export type SvelteActionReturnType

= { + update?: (newParams?: P) => void; + destroy?: () => void; +} | void; + +export type SvelteHTMLActionType

= (node: HTMLElement, params?: P) => SvelteActionReturnType

; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type HTMLActionEntry

= SvelteHTMLActionType

| [SvelteHTMLActionType

, P]; + +export type HTMLActionArray = HTMLActionEntry[]; + +export type SvelteSVGActionType

= (node: SVGElement, params?: P) => SvelteActionReturnType

; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type SVGActionEntry

= SvelteSVGActionType

| [SvelteSVGActionType

, P]; + +export type SVGActionArray = SVGActionEntry[]; + +export type ActionArray = HTMLActionArray | SVGActionArray; + +export function useActions(node: HTMLElement | SVGElement, actions: ActionArray) { + const actionReturns: SvelteActionReturnType[] = []; + + if (actions) { + for (const actionEntry of actions) { + const action = Array.isArray(actionEntry) ? actionEntry[0] : actionEntry; + if (Array.isArray(actionEntry) && actionEntry.length > 1) { + actionReturns.push(action(node as HTMLElement & SVGElement, actionEntry[1])); + } else { + actionReturns.push(action(node as HTMLElement & SVGElement)); + } + } + } + + return { + update(actions: ActionArray) { + if ((actions?.length || 0) != actionReturns.length) { + throw new Error('You must not change the length of an actions array.'); + } + + if (actions) { + for (const [i, returnEntry] of actionReturns.entries()) { + if (returnEntry && returnEntry.update) { + const actionEntry = actions[i]; + if (Array.isArray(actionEntry) && actionEntry.length > 1) { + returnEntry.update(actionEntry[1]); + } else { + returnEntry.update(); + } + } + } + } + }, + + destroy() { + for (const returnEntry of actionReturns) { + returnEntry?.destroy?.(); + } + }, + }; +} diff --git a/web/src/lib/components/layouts/user-page-layout.svelte b/web/src/lib/components/layouts/user-page-layout.svelte index bad07767519aa..bdeeaaab9aa55 100644 --- a/web/src/lib/components/layouts/user-page-layout.svelte +++ b/web/src/lib/components/layouts/user-page-layout.svelte @@ -3,6 +3,7 @@ import NavigationBar from '../shared-components/navigation-bar/navigation-bar.svelte'; import SideBar from '../shared-components/side-bar/side-bar.svelte'; import AdminSideBar from '../shared-components/side-bar/admin-side-bar.svelte'; + import { useActions, type ActionArray } from '$lib/actions/use-actions'; export let hideNavbar = false; export let showUploadButton = false; @@ -10,8 +11,7 @@ export let description: string | undefined = undefined; export let scrollbar = true; export let admin = false; - - export let scrollSlot: HTMLDivElement | undefined = undefined; + export let use: ActionArray = []; $: scrollbarClass = scrollbar ? 'immich-scrollbar p-2 pb-8' : 'scrollbar-hidden'; $: hasTitleClass = title ? 'top-16 h-[calc(100%-theme(spacing.16))]' : 'top-0 h-full'; @@ -55,10 +55,7 @@ {/if} -

+
diff --git a/web/src/routes/(user)/albums/+page.svelte b/web/src/routes/(user)/albums/+page.svelte index ce3050ee0175f..4f378ed0dabf7 100644 --- a/web/src/routes/(user)/albums/+page.svelte +++ b/web/src/routes/(user)/albums/+page.svelte @@ -1,6 +1,6 @@ - +
diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index d22f59d379928..029b5093e194a 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -1,5 +1,6 @@ -
+
{#if $isMultiSelectState} assetInteractionStore.clearMultiselect()}> diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index a7a773211d102..b3349e2ddd0dc 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -1,7 +1,8 @@