mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-04 13:05:21 +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!"
|
:image="recipe.image!"
|
||||||
:tags="recipe.tags!"
|
:tags="recipe.tags!"
|
||||||
:recipe-id="recipe.id!"
|
:recipe-id="recipe.id!"
|
||||||
|
@click="handleRecipeNavigation"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
@ -147,6 +148,7 @@
|
||||||
:image="recipe.image!"
|
:image="recipe.image!"
|
||||||
:tags="recipe.tags!"
|
:tags="recipe.tags!"
|
||||||
:recipe-id="recipe.id!"
|
:recipe-id="recipe.id!"
|
||||||
|
@selected="handleRecipeNavigation"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
@ -171,6 +173,7 @@ import { useLazyRecipes } from "~/composables/recipes";
|
||||||
import type { Recipe } from "~/lib/api/types/recipe";
|
import type { Recipe } from "~/lib/api/types/recipe";
|
||||||
import { useUserSortPreferences } from "~/composables/use-users/preferences";
|
import { useUserSortPreferences } from "~/composables/use-users/preferences";
|
||||||
import type { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
|
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 REPLACE_RECIPES_EVENT = "replaceRecipes";
|
||||||
const APPEND_RECIPES_EVENT = "appendRecipes";
|
const APPEND_RECIPES_EVENT = "appendRecipes";
|
||||||
|
@ -241,9 +244,11 @@ export default defineNuxtComponent({
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
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 perPage = 32;
|
||||||
const hasMore = ref(true);
|
const hasMore = ref(recipeListState.state.hasMore);
|
||||||
const ready = ref(false);
|
const ready = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
@ -282,8 +287,33 @@ export default defineNuxtComponent({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save scroll position
|
||||||
|
const throttledScrollSave = useThrottleFn(() => {
|
||||||
|
recipeListState.saveScrollPosition();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
onMounted(async () => {
|
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;
|
ready.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -294,6 +324,10 @@ export default defineNuxtComponent({
|
||||||
const newValueString = JSON.stringify(newValue);
|
const newValueString = JSON.stringify(newValue);
|
||||||
if (lastQuery !== newValueString) {
|
if (lastQuery !== newValueString) {
|
||||||
lastQuery = newValueString;
|
lastQuery = newValueString;
|
||||||
|
|
||||||
|
// Save scroll position before query change
|
||||||
|
recipeListState.saveScrollPosition();
|
||||||
|
|
||||||
ready.value = false;
|
ready.value = false;
|
||||||
await initRecipes();
|
await initRecipes();
|
||||||
ready.value = true;
|
ready.value = true;
|
||||||
|
@ -315,6 +349,14 @@ export default defineNuxtComponent({
|
||||||
// since we doubled the first call, we also need to advance the page
|
// since we doubled the first call, we also need to advance the page
|
||||||
page.value = page.value + 1;
|
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);
|
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,6 +373,14 @@ export default defineNuxtComponent({
|
||||||
hasMore.value = false;
|
hasMore.value = false;
|
||||||
}
|
}
|
||||||
if (newRecipes.length) {
|
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);
|
context.emit(APPEND_RECIPES_EVENT, newRecipes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -408,6 +458,15 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
// fetch new recipes
|
// fetch new recipes
|
||||||
const newRecipes = await fetchRecipes();
|
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);
|
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||||
|
|
||||||
state.sortLoading = false;
|
state.sortLoading = false;
|
||||||
|
@ -427,6 +486,17 @@ export default defineNuxtComponent({
|
||||||
preferences.value.useMobileCards = !preferences.value.useMobileCards;
|
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 {
|
return {
|
||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
displayTitleIcon,
|
displayTitleIcon,
|
||||||
|
@ -439,6 +509,7 @@ export default defineNuxtComponent({
|
||||||
sortRecipes,
|
sortRecipes,
|
||||||
toggleMobileCards,
|
toggleMobileCards,
|
||||||
useMobileCards,
|
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({
|
export default defineNuxtComponent({
|
||||||
components: { RecipeExplorerPage },
|
components: { RecipeExplorerPage },
|
||||||
|
setup() {
|
||||||
|
// Enable scroll restoration for this page to work with our state management
|
||||||
|
definePageMeta({
|
||||||
|
scrollToTop: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue