feat(web): persist scroll position on navigation back to album (#11388)

Co-authored-by: Calum Dingwall <caburum@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Calum Dingwall
2024-11-25 12:16:42 -06:00
committed by GitHub
parent 51de108d43
commit d277096d58
8 changed files with 234 additions and 7 deletions
+87
View File
@@ -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<void>;
}
/**
* @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();
},
};
}
+67
View File
@@ -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<P> = {
update?: (newParams?: P) => void;
destroy?: () => void;
} | void;
export type SvelteHTMLActionType<P> = (node: HTMLElement, params?: P) => SvelteActionReturnType<P>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type HTMLActionEntry<P = any> = SvelteHTMLActionType<P> | [SvelteHTMLActionType<P>, P];
export type HTMLActionArray = HTMLActionEntry[];
export type SvelteSVGActionType<P> = (node: SVGElement, params?: P) => SvelteActionReturnType<P>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type SVGActionEntry<P = any> = SvelteSVGActionType<P> | [SvelteSVGActionType<P>, P];
export type SVGActionArray = SVGActionEntry[];
export type ActionArray = HTMLActionArray | SVGActionArray;
export function useActions(node: HTMLElement | SVGElement, actions: ActionArray) {
const actionReturns: SvelteActionReturnType<unknown>[] = [];
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?.();
}
},
};
}