refactor(sort): extract seeded random sort helpers into SortDropdown

This commit is contained in:
Will Miao
2026-08-15 09:53:28 +08:00
parent 93472e5d67
commit 34c87d4934
5 changed files with 56 additions and 92 deletions
+9 -47
View File
@@ -4,7 +4,7 @@ import { getStorageItem, setStorageItem, removeStorageItem, getSessionItem, setS
import { showToast, openCivitaiByMetadata } from '../../utils/uiHelpers.js'; import { showToast, openCivitaiByMetadata } from '../../utils/uiHelpers.js';
import { performModelUpdateCheck } from '../../utils/updateCheckHelpers.js'; import { performModelUpdateCheck } from '../../utils/updateCheckHelpers.js';
import { sidebarManager } from '../SidebarManager.js'; import { sidebarManager } from '../SidebarManager.js';
import { initSortDropdown } from './SortDropdown.js'; import { initSortDropdown, applySortToSelect, randomizeSortValue } from './SortDropdown.js';
/** /**
* PageControls class - Unified control management for model pages * PageControls class - Unified control management for model pages
@@ -108,20 +108,20 @@ export class PageControls {
const sortSelect = document.getElementById('sortSelect'); const sortSelect = document.getElementById('sortSelect');
if (sortSelect) { if (sortSelect) {
initSortDropdown(sortSelect); initSortDropdown(sortSelect);
this.applySortToSelect(this.pageState.sortBy); applySortToSelect(this.pageState.sortBy);
sortSelect.addEventListener('change', async (e) => { sortSelect.addEventListener('change', async (e) => {
let value = e.target.value; let value = e.target.value;
if (value.startsWith('random')) { if (value.startsWith('random')) {
// Every pick of Random reshuffles the list: generate a // Every pick of Random reshuffles the list: generate a
// fresh seed so the backend keeps a stable order across // fresh seed so the backend keeps a stable order across
// paginated requests. // paginated requests.
value = this._randomizeSortValue(); value = randomizeSortValue();
} }
this.pageState.sortBy = value; this.pageState.sortBy = value;
this.saveSortPreference(value); this.saveSortPreference(value);
// Reset the seeded Random option when switching away from // Reset the seeded Random option when switching away from
// Random, or re-apply the fresh seed when picking it again. // Random, or re-apply the fresh seed when picking it again.
this.applySortToSelect(value); applySortToSelect(value);
await this.resetAndReload(); await this.resetAndReload();
}); });
} }
@@ -322,44 +322,6 @@ export class PageControls {
} }
} }
/**
* Apply a sort value to the native sort <select>, keeping the Random
* option's value in sync when the persisted value carries a seed
* (e.g. "random:abc123"). Must be used instead of assigning
* sortSelect.value directly whenever the value may be a seeded random
* sort, otherwise the native select has no matching option.
* @param {string} sortValue - Sort value like "name:asc" or "random:<seed>"
*/
applySortToSelect(sortValue) {
const sortSelect = document.getElementById('sortSelect');
if (!sortSelect) return;
const randomOpt = sortSelect.querySelector('option[value="random"], option[value^="random:"]');
if (randomOpt) {
randomOpt.value = String(sortValue).startsWith('random') ? sortValue : 'random';
}
sortSelect.value = sortValue;
}
/**
* Generate a fresh seeded random sort value ("random:<seed>") and keep
* the native <select> in sync so its value matches the persisted sort
* string and the dropdown shows the selected label.
* @returns {string} The new sort value, e.g. "random:abc123xyz"
*/
_randomizeSortValue() {
const seed = Math.random().toString(36).slice(2, 12);
const value = `random:${seed}`;
const sortSelect = document.getElementById('sortSelect');
if (sortSelect) {
const randomOpt = sortSelect.querySelector('option[value="random"], option[value^="random:"]');
if (randomOpt) {
randomOpt.value = value;
}
sortSelect.value = value;
}
return value;
}
/** /**
* Load sort preference from storage * Load sort preference from storage
*/ */
@@ -374,7 +336,7 @@ export class PageControls {
// Handle legacy format conversion // Handle legacy format conversion
const convertedSort = this.convertLegacySortFormat(savedSort); const convertedSort = this.convertLegacySortFormat(savedSort);
this.pageState.sortBy = convertedSort; this.pageState.sortBy = convertedSort;
this.applySortToSelect(convertedSort); applySortToSelect(convertedSort);
} }
} }
@@ -568,7 +530,7 @@ export class PageControls {
this.pageState.sortBy = restoredSort; this.pageState.sortBy = restoredSort;
this.saveSortPreference(restoredSort); this.saveSortPreference(restoredSort);
this._removeVlmSortOption(); this._removeVlmSortOption();
this.applySortToSelect(restoredSort); applySortToSelect(restoredSort);
const sortSelect = document.getElementById('sortSelect'); const sortSelect = document.getElementById('sortSelect');
if (sortSelect) { if (sortSelect) {
sortSelect.disabled = false; sortSelect.disabled = false;
@@ -620,7 +582,7 @@ export class PageControls {
const savedGroupedSort = getStorageItem(groupedKey); const savedGroupedSort = getStorageItem(groupedKey);
if (savedGroupedSort) { if (savedGroupedSort) {
this.pageState.sortBy = savedGroupedSort; this.pageState.sortBy = savedGroupedSort;
this.applySortToSelect(savedGroupedSort); applySortToSelect(savedGroupedSort);
} }
} else { } else {
// Leaving group mode: persist current sort for next time, restore non-group sort // Leaving group mode: persist current sort for next time, restore non-group sort
@@ -628,7 +590,7 @@ export class PageControls {
const savedNormalSort = getStorageItem(`${this.pageType}_sort`); const savedNormalSort = getStorageItem(`${this.pageType}_sort`);
if (savedNormalSort) { if (savedNormalSort) {
this.pageState.sortBy = savedNormalSort; this.pageState.sortBy = savedNormalSort;
this.applySortToSelect(savedNormalSort); applySortToSelect(savedNormalSort);
} }
} }
} }
@@ -913,7 +875,7 @@ export class PageControls {
} }
if (sortSelect) { if (sortSelect) {
this.applySortToSelect(this.pageState.sortBy); applySortToSelect(this.pageState.sortBy);
} }
if (searchInput) { if (searchInput) {
searchInput.value = this.pageState.filters?.search || ''; searchInput.value = this.pageState.filters?.search || '';
@@ -18,6 +18,44 @@
const SORT_GROUP_SELECTOR = '.sort-dropdown-group'; const SORT_GROUP_SELECTOR = '.sort-dropdown-group';
const ACTIVE_GROUP_SELECTOR = '.sort-dropdown-group.active, .dropdown-group.active'; const ACTIVE_GROUP_SELECTOR = '.sort-dropdown-group.active, .dropdown-group.active';
/**
* Apply a sort value to the page's native sort <select>, keeping the Random
* option's value in sync when the persisted value carries a seed
* (e.g. "random:abc123"). Must be used instead of assigning
* sortSelect.value directly whenever the value may be a seeded random
* sort, otherwise the native select has no matching option.
* @param {string} sortValue - Sort value like "name:asc" or "random:<seed>"
*/
export function applySortToSelect(sortValue) {
const sortSelect = document.getElementById('sortSelect');
if (!sortSelect) return;
const randomOpt = sortSelect.querySelector('option[value="random"], option[value^="random:"]');
if (randomOpt) {
randomOpt.value = String(sortValue).startsWith('random') ? sortValue : 'random';
}
sortSelect.value = sortValue;
}
/**
* Generate a fresh seeded random sort value ("random:<seed>") and keep the
* native <select> in sync so its value matches the persisted sort string and
* the dropdown shows the selected label.
* @returns {string} The new sort value, e.g. "random:abc123xyz"
*/
export function randomizeSortValue() {
const seed = Math.random().toString(36).slice(2, 12);
const value = `random:${seed}`;
const sortSelect = document.getElementById('sortSelect');
if (sortSelect) {
const randomOpt = sortSelect.querySelector('option[value="random"], option[value^="random:"]');
if (randomOpt) {
randomOpt.value = value;
}
sortSelect.value = value;
}
return value;
}
/** /**
* Initialize a decoupled sort dropdown around a native <select>. * Initialize a decoupled sort dropdown around a native <select>.
* Idempotent: safe to call more than once on the same element. * Idempotent: safe to call more than once on the same element.
+4 -42
View File
@@ -10,7 +10,7 @@ import { DuplicatesManager } from './components/DuplicatesManager.js';
import { refreshVirtualScroll, recreateVirtualScroll } from './utils/infiniteScroll.js'; import { refreshVirtualScroll, recreateVirtualScroll } from './utils/infiniteScroll.js';
import { refreshRecipes, RecipeSidebarApiClient } from './api/recipeApi.js'; import { refreshRecipes, RecipeSidebarApiClient } from './api/recipeApi.js';
import { sidebarManager } from './components/SidebarManager.js'; import { sidebarManager } from './components/SidebarManager.js';
import { initSortDropdown } from './components/controls/SortDropdown.js'; import { initSortDropdown, applySortToSelect, randomizeSortValue } from './components/controls/SortDropdown.js';
class RecipePageControls { class RecipePageControls {
constructor() { constructor() {
@@ -245,20 +245,20 @@ class RecipeManager {
this.pageState.sortBy = savedSort; this.pageState.sortBy = savedSort;
} }
initSortDropdown(sortSelect); initSortDropdown(sortSelect);
this.applySortToSelect(this.pageState.sortBy || 'date:desc'); applySortToSelect(this.pageState.sortBy || 'date:desc');
sortSelect.addEventListener('change', () => { sortSelect.addEventListener('change', () => {
let value = sortSelect.value; let value = sortSelect.value;
if (value.startsWith('random')) { if (value.startsWith('random')) {
// Every pick of Random reshuffles the list: generate a // Every pick of Random reshuffles the list: generate a
// fresh seed so the backend keeps a stable order across // fresh seed so the backend keeps a stable order across
// paginated requests. // paginated requests.
value = this._randomizeSortValue(); value = randomizeSortValue();
} }
this.pageState.sortBy = value; this.pageState.sortBy = value;
setStorageItem('recipes_sort', value); setStorageItem('recipes_sort', value);
// Reset the seeded Random option when switching away from // Reset the seeded Random option when switching away from
// Random, or re-apply the fresh seed when picking it again. // Random, or re-apply the fresh seed when picking it again.
this.applySortToSelect(value); applySortToSelect(value);
refreshVirtualScroll(); refreshVirtualScroll();
}); });
} }
@@ -351,44 +351,6 @@ class RecipeManager {
}); });
} }
/**
* Apply a sort value to the native sort <select>, keeping the Random
* option's value in sync when the persisted value carries a seed
* (e.g. "random:abc123"). Must be used instead of assigning
* sortSelect.value directly whenever the value may be a seeded random
* sort, otherwise the native select has no matching option.
* @param {string} sortValue - Sort value like "date:desc" or "random:<seed>"
*/
applySortToSelect(sortValue) {
const sortSelect = document.getElementById('sortSelect');
if (!sortSelect) return;
const randomOpt = sortSelect.querySelector('option[value="random"], option[value^="random:"]');
if (randomOpt) {
randomOpt.value = String(sortValue).startsWith('random') ? sortValue : 'random';
}
sortSelect.value = sortValue;
}
/**
* Generate a fresh seeded random sort value ("random:<seed>") and keep
* the native <select> in sync so its value matches the persisted sort
* string and the dropdown shows the selected label.
* @returns {string} The new sort value, e.g. "random:abc123xyz"
*/
_randomizeSortValue() {
const seed = Math.random().toString(36).slice(2, 12);
const value = `random:${seed}`;
const sortSelect = document.getElementById('sortSelect');
if (sortSelect) {
const randomOpt = sortSelect.querySelector('option[value="random"], option[value^="random:"]');
if (randomOpt) {
randomOpt.value = value;
}
sortSelect.value = value;
}
return value;
}
normalizeLoadRecipesOptions(options = true) { normalizeLoadRecipesOptions(options = true) {
if (typeof options === 'boolean') { if (typeof options === 'boolean') {
return { return {
@@ -1,4 +1,5 @@
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest'; import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
import { applySortToSelect } from '../../../static/js/components/controls/SortDropdown.js';
const resetAndReloadMock = vi.fn(); const resetAndReloadMock = vi.fn();
const getModelApiClientMock = vi.fn(); const getModelApiClientMock = vi.fn();
@@ -190,7 +191,7 @@ describe('Random sort option', () => {
sortSelect.value = 'random'; sortSelect.value = 'random';
sortSelect.dispatchEvent(new Event('change', { bubbles: true })); sortSelect.dispatchEvent(new Event('change', { bubbles: true }));
await Promise.resolve(); await Promise.resolve();
controls.applySortToSelect('name:desc'); applySortToSelect('name:desc');
expect(sortSelect.value).toBe('name:desc'); expect(sortSelect.value).toBe('name:desc');
expect(randomOpt.value).toBe('random'); expect(randomOpt.value).toBe('random');
@@ -1,5 +1,6 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { renderRecipesPage } from '../utils/pageFixtures.js'; import { renderRecipesPage } from '../utils/pageFixtures.js';
import { applySortToSelect } from '../../../static/js/components/controls/SortDropdown.js';
const initializeAppMock = vi.fn(); const initializeAppMock = vi.fn();
const initializePageFeaturesMock = vi.fn(); const initializePageFeaturesMock = vi.fn();
@@ -203,12 +204,12 @@ describe('RecipeManager Random sort', () => {
it('applies a non-random sort back to the plain random option', async () => { it('applies a non-random sort back to the plain random option', async () => {
const sortSelect = renderSortSelect(); const sortSelect = renderSortSelect();
const randomOpt = sortSelect.querySelector('option[value="random"]'); const randomOpt = sortSelect.querySelector('option[value="random"]');
const manager = await createManager(); await createManager();
sortSelect.value = 'random'; sortSelect.value = 'random';
sortSelect.dispatchEvent(new Event('change', { bubbles: true })); sortSelect.dispatchEvent(new Event('change', { bubbles: true }));
await Promise.resolve(); await Promise.resolve();
manager.applySortToSelect('name:asc'); applySortToSelect('name:asc');
expect(sortSelect.value).toBe('name:asc'); expect(sortSelect.value).toBe('name:asc');
expect(randomOpt.value).toBe('random'); expect(randomOpt.value).toBe('random');