feat(recipes): wire recipes layout switch event and rebuild

This commit is contained in:
Will Miao
2026-08-07 12:49:30 +08:00
parent 76a913f5e0
commit 87e3d4dea9
4 changed files with 65 additions and 1 deletions

View File

@@ -2,6 +2,7 @@
import { showToast } from '../utils/uiHelpers.js';
import { RecipeCard } from './RecipeCard.js';
import { state, getCurrentPageState } from '../state/index.js';
import { recreateVirtualScroll } from '../utils/infiniteScroll.js';
export class DuplicatesManager {
constructor(recipeManager) {
@@ -96,6 +97,12 @@ export class DuplicatesManager {
// Re-enable virtual scrolling
state.virtualScroller.enable();
// Apply a layout switch that was deferred while duplicates mode was active
if (state.pendingLayoutRecreate) {
state.pendingLayoutRecreate = false;
recreateVirtualScroll('recipes');
}
}
renderDuplicateGroups() {

View File

@@ -1017,6 +1017,12 @@ export class SettingsManager {
displayDensitySelect.value = state.global.settings.display_density || 'default';
}
// Set recipes layout setting
const recipesLayoutSelect = document.getElementById('recipesLayout');
if (recipesLayoutSelect) {
recipesLayoutSelect.value = state.global.settings.recipes_layout || 'grid';
}
// Set card info display setting
const cardInfoDisplaySelect = document.getElementById('cardInfoDisplay');
if (cardInfoDisplaySelect) {
@@ -2288,6 +2294,13 @@ export class SettingsManager {
// Apply frontend settings immediately
this.applyFrontendSettings();
// Dispatch layout change event; the scroller instance is about to be rebuilt,
// so calculateLayout() must NOT run on the old instance here
if (settingKey === 'recipes_layout') {
window.dispatchEvent(new CustomEvent('lm:recipes-layout-changed'));
return;
}
// Recalculate layout when display density changes
if (settingKey === 'display_density' && state.virtualScroller) {
state.virtualScroller.calculateLayout();

View File

@@ -7,7 +7,7 @@ import { state, getCurrentPageState } from './state/index.js';
import { getStorageItem, setStorageItem, getSessionItem, removeSessionItem } from './utils/storageHelpers.js';
import { RecipeContextMenu } from './components/ContextMenu/index.js';
import { DuplicatesManager } from './components/DuplicatesManager.js';
import { refreshVirtualScroll } from './utils/infiniteScroll.js';
import { refreshVirtualScroll, recreateVirtualScroll } from './utils/infiniteScroll.js';
import { refreshRecipes, RecipeSidebarApiClient } from './api/recipeApi.js';
import { sidebarManager } from './components/SidebarManager.js';
import { initSortDropdown } from './components/controls/SortDropdown.js';
@@ -272,6 +272,20 @@ class RecipeManager {
});
}
// Rebuild the scroller on layout switch; in duplicates mode defer until
// exitDuplicateMode re-enables the scroller (direct recreation would dispose
// the old instance while initializeVirtualScroll skips duplicates mode)
window.addEventListener('lm:recipes-layout-changed', () => {
const pageState = getCurrentPageState();
if (pageState.duplicatesMode) {
state.pendingLayoutRecreate = true;
return;
}
if (typeof recreateVirtualScroll === 'function') {
recreateVirtualScroll('recipes');
}
});
// Initialize dropdown functionality for refresh button
this.initDropdowns();
}

View File

@@ -501,3 +501,33 @@ describe('SettingsManager library controls', () => {
expect(document.getElementById('exampleImagesUriTemplateSetting').style.display).toBe('none');
});
});
describe('SettingsManager recipes layout switch', () => {
it('dispatches lm:recipes-layout-changed without recalculating the old scroller', async () => {
const manager = createManager();
const select = document.createElement('select');
select.id = 'recipesLayout';
const option = document.createElement('option');
option.value = 'masonry';
select.appendChild(option);
select.value = 'masonry';
document.body.appendChild(select);
const calculateLayout = vi.fn();
state.virtualScroller = { calculateLayout };
const dispatchSpy = vi.spyOn(window, 'dispatchEvent');
await manager.saveSelectSetting('recipesLayout', 'recipes_layout');
const layoutEvent = dispatchSpy.mock.calls
.map(([event]) => event)
.find(event => event.type === 'lm:recipes-layout-changed');
expect(layoutEvent).toBeInstanceOf(CustomEvent);
expect(calculateLayout).not.toHaveBeenCalled();
expect(showToast).not.toHaveBeenCalled();
dispatchSpy.mockRestore();
delete state.virtualScroller;
});
});