mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 10:13:32 -04:00 
			
		
		
		
	reset scroll position
This commit is contained in:
		| @@ -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 () => { | ||||||
|  |       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(); |         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> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user