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:
parent
691300e481
commit
78a0f74f33
3 changed files with 174 additions and 3 deletions
|
@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
94
frontend/composables/recipe-page/use-recipe-list-state.ts
Normal file
94
frontend/composables/recipe-page/use-recipe-list-state.ts
Normal 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();
|
||||
}
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue