1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-02 03:55:22 +02:00

reset scroll position

This commit is contained in:
Kuchenpirat 2025-07-11 23:29:09 +00:00
parent 691300e481
commit 78a0f74f33
3 changed files with 174 additions and 3 deletions

View file

@ -123,6 +123,7 @@
:image="recipe.image!"
:tags="recipe.tags!"
:recipe-id="recipe.id!"
@click="handleRecipeNavigation"
/>
</v-col>
</v-row>
@ -147,6 +148,7 @@
:image="recipe.image!"
:tags="recipe.tags!"
:recipe-id="recipe.id!"
@selected="handleRecipeNavigation"
/>
</v-col>
</v-row>
@ -171,6 +173,7 @@ import { useLazyRecipes } from "~/composables/recipes";
import type { Recipe } from "~/lib/api/types/recipe";
import { useUserSortPreferences } from "~/composables/use-users/preferences";
import type { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
import { useRecipeListState } from "~/composables/recipe-page/use-recipe-list-state";
const REPLACE_RECIPES_EVENT = "replaceRecipes";
const APPEND_RECIPES_EVENT = "appendRecipes";
@ -241,9 +244,11 @@ export default defineNuxtComponent({
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
const page = ref(1);
const recipeListState = useRecipeListState(props.query);
const page = ref(recipeListState.state.page || 1);
const perPage = 32;
const hasMore = ref(true);
const hasMore = ref(recipeListState.state.hasMore);
const ready = ref(false);
const loading = ref(false);
@ -282,8 +287,33 @@ export default defineNuxtComponent({
);
}
// Save scroll position
const throttledScrollSave = useThrottleFn(() => {
recipeListState.saveScrollPosition();
}, 1000);
onMounted(async () => {
await initRecipes();
window.addEventListener("scroll", throttledScrollSave);
// cached state with scroll position
if (recipeListState.hasValidState() && recipeListState.isQueryMatch(props.query)) {
// Restore from cached state
page.value = recipeListState.state.page;
hasMore.value = recipeListState.state.hasMore;
ready.value = recipeListState.state.ready;
// Emit cached recipes
context.emit(REPLACE_RECIPES_EVENT, recipeListState.state.recipes);
// Restore scroll position after recipes are rendered
nextTick(() => {
recipeListState.restoreScrollPosition();
});
}
else {
// Initialize fresh recipes
await initRecipes();
}
ready.value = true;
});
@ -294,6 +324,10 @@ export default defineNuxtComponent({
const newValueString = JSON.stringify(newValue);
if (lastQuery !== newValueString) {
lastQuery = newValueString;
// Save scroll position before query change
recipeListState.saveScrollPosition();
ready.value = false;
await initRecipes();
ready.value = true;
@ -315,6 +349,14 @@ export default defineNuxtComponent({
// since we doubled the first call, we also need to advance the page
page.value = page.value + 1;
// Save state after fetching recipes
recipeListState.saveState({
recipes: newRecipes,
page: page.value,
hasMore: hasMore.value,
ready: true,
});
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
}
@ -331,6 +373,14 @@ export default defineNuxtComponent({
hasMore.value = false;
}
if (newRecipes.length) {
// Update cached state with new recipes
const allRecipes = [...(recipeListState.state.recipes || []), ...newRecipes] as Recipe[];
recipeListState.saveState({
recipes: allRecipes,
page: page.value,
hasMore: hasMore.value,
});
context.emit(APPEND_RECIPES_EVENT, newRecipes);
}
@ -408,6 +458,15 @@ export default defineNuxtComponent({
// fetch new recipes
const newRecipes = await fetchRecipes();
// Update cached state
recipeListState.saveState({
recipes: newRecipes,
page: page.value,
hasMore: hasMore.value,
ready: true,
});
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
state.sortLoading = false;
@ -427,6 +486,17 @@ export default defineNuxtComponent({
preferences.value.useMobileCards = !preferences.value.useMobileCards;
}
// Save scroll position when component is unmounted or when navigating away
onBeforeUnmount(() => {
recipeListState.saveScrollPosition();
window.removeEventListener("scroll", throttledScrollSave);
});
// Save scroll position when navigating to recipe pages
function handleRecipeNavigation() {
recipeListState.saveScrollPosition();
}
return {
...toRefs(state),
displayTitleIcon,
@ -439,6 +509,7 @@ export default defineNuxtComponent({
sortRecipes,
toggleMobileCards,
useMobileCards,
handleRecipeNavigation,
};
},
});

View file

@ -0,0 +1,94 @@
import type { Recipe } from "~/lib/api/types/recipe";
import type { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
interface RecipeListState {
recipes: Recipe[];
page: number;
hasMore: boolean;
scrollPosition: number;
query: RecipeSearchQuery | null;
ready: boolean;
}
const recipeListStates = new Map<string, RecipeListState>();
function generateStateKey(query: RecipeSearchQuery | null): string {
if (!query) return "default";
const keyParts = [
query.search || "",
query.orderBy || "",
query.orderDirection || "",
query.queryFilter || "",
JSON.stringify(query.categories || []),
JSON.stringify(query.tags || []),
JSON.stringify(query.tools || []),
JSON.stringify(query.foods || []),
JSON.stringify(query.households || []),
];
return keyParts.join("|");
}
export function useRecipeListState(query: RecipeSearchQuery | null) {
const stateKey = generateStateKey(query);
// Initialize state if it doesn't exist
if (!recipeListStates.has(stateKey)) {
recipeListStates.set(stateKey, {
recipes: [],
page: 1,
hasMore: true,
scrollPosition: 0,
query,
ready: false,
});
}
const state = recipeListStates.get(stateKey)!;
function saveState(newState: Partial<RecipeListState>) {
Object.assign(state, newState);
}
function saveScrollPosition() {
state.scrollPosition = window.scrollY || document.documentElement.scrollTop || 0;
}
function restoreScrollPosition() {
if (state.scrollPosition > 0) {
// Use nextTick to ensure DOM is updated before scrolling
nextTick(() => {
window.scrollTo(0, state.scrollPosition);
});
}
}
function clearState() {
recipeListStates.delete(stateKey);
}
function hasValidState(): boolean {
return state.recipes.length > 0 && state.ready;
}
function isQueryMatch(newQuery: RecipeSearchQuery | null): boolean {
const newKey = generateStateKey(newQuery);
return newKey === stateKey;
}
return {
state: readonly(state),
saveState,
saveScrollPosition,
restoreScrollPosition,
clearState,
hasValidState,
isQueryMatch,
};
}
// Clean up old states when navigating away from recipe sections
export function cleanupRecipeListStates() {
recipeListStates.clear();
}

View file

@ -9,5 +9,11 @@ import RecipeExplorerPage from "~/components/Domain/Recipe/RecipeExplorerPage.vu
export default defineNuxtComponent({
components: { RecipeExplorerPage },
setup() {
// Enable scroll restoration for this page to work with our state management
definePageMeta({
scrollToTop: false,
});
},
});
</script>