Merge pull request #2165 from Wolfsblvt/scored-search-sorting

Scored search sorting
This commit is contained in:
Cohee
2024-05-01 13:44:08 +03:00
committed by GitHub
7 changed files with 307 additions and 63 deletions

View File

@@ -3410,6 +3410,7 @@
<div id="world_popup_delete" class="menu_button fa-solid fa-trash-can redWarningBG" title="Delete World Info" data-i18n="[title]Delete World Info"></div>
<input type="search" class="text_pole textarea_compact" data-i18n="[placeholder]Search..." id="world_info_search" placeholder="Search...">
<select id="world_info_sort_order" class="margin0">
<option data-rule="search" value="14" data-i18n="Search" hidden>Search</option>
<option data-rule="priority" value="0" data-i18n="Priority">Priority</option>
<option data-rule="custom" value="13" data-i18n="Custom">Custom</option>
<option data-order="asc" data-field="comment" value="1" data-i18n="Title A-Z">Title A-Z</option>
@@ -4185,6 +4186,7 @@
</div>
<input id="persona_search_bar" class="text_pole width100p flex1 margin0" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." maxlength="100">
<select id="persona_sort_order" class="margin0">
<option value="search" data-i18n="Search" hidden>Search</option>
<option value="asc">A-Z</option>
<option value="desc">Z-A</option>
</select>
@@ -4580,6 +4582,7 @@
<div id="rm_button_group_chats" title="Create New Chat Group" data-i18n="[title]Create New Chat Group" class="menu_button fa-solid fa-users-gear "></div>
<input id="character_search_bar" class="text_pole width100p" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." maxlength="100" />
<select id="character_sort_order" title="Characters sorting order" data-i18n="[title]Characters sorting order">
<option data-field="search" data-order="desc" data-i18n="Search" hidden>Search</option>
<option data-field="name" data-order="asc" data-i18n="A-Z">A-Z</option>
<option data-field="name" data-order="desc" data-i18n="Z-A">Z-A</option>
<option data-field="create_date" data-order="desc" data-i18n="Newest">Newest</option>

View File

@@ -1310,7 +1310,6 @@ function getCharacterBlock(item, id) {
async function printCharacters(fullRefresh = false) {
const storageKey = 'Characters_PerPage';
const listId = '#rm_print_characters_block';
const entities = getEntitiesList({ doFilter: true });
let currentScrollTop = $(listId).scrollTop();
@@ -1320,10 +1319,15 @@ async function printCharacters(fullRefresh = false) {
await delay(1);
}
// Before printing the personas, we check if we should enable/disable search sorting
verifyCharactersSearchSortRule();
// We are actually always reprinting filters, as it "doesn't hurt", and this way they are always up to date
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
const entities = getEntitiesList({ doFilter: true });
$('#rm_print_characters_pagination').pagination({
dataSource: entities,
pageSize: Number(localStorage.getItem(storageKey)) || per_page_default,
@@ -1383,6 +1387,26 @@ async function printCharacters(fullRefresh = false) {
favsToHotswap();
}
/** Checks the state of the current search, and adds/removes the search sorting option accordingly */
function verifyCharactersSearchSortRule() {
const searchTerm = entitiesFilter.getFilterData(FILTER_TYPES.SEARCH);
const searchOption = $('#character_sort_order option[data-field="search"]');
const selector = $('#character_sort_order');
const isHidden = searchOption.attr('hidden') !== undefined;
// If we have a search term, we are displaying the sorting option for it
if (searchTerm && isHidden) {
searchOption.removeAttr('hidden');
searchOption.prop('selected', true);
flashHighlight(selector);
}
// If search got cleared, we make sure to hide the option and go back to the one before
if (!searchTerm && !isHidden) {
searchOption.attr('hidden', '');
$(`#character_sort_order option[data-order="${power_user.sort_order}"][data-field="${power_user.sort_field}"]`).prop('selected', true);
}
}
/** @typedef {object} Character - A character */
/** @typedef {object} Group - A group */
@@ -1465,7 +1489,7 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
const subCount = subEntities.length;
subEntities = filterByTagState(entities, { subForEntity: entity });
if (doFilter) {
subEntities = entitiesFilter.applyFilters(subEntities);
subEntities = entitiesFilter.applyFilters(subEntities, { clearScoreCache: false });
}
entity.entities = subEntities;
entity.hidden = subCount - subEntities.length;
@@ -5914,17 +5938,16 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
return [];
}
allEntities.sort((a, b) => {
const aName = String(power_user.personas[a] || a);
const bName = String(power_user.personas[b] || b);
return power_user.persona_sort_order === 'asc' ? aName.localeCompare(bName) : bName.localeCompare(aName);
});
if (!doRender) {
return allEntities;
}
const entities = personasFilter.applyFilters(allEntities);
// Before printing the personas, we check if we should enable/disable search sorting
verifyPersonaSearchSortRule();
let entities = personasFilter.applyFilters(allEntities);
entities = sortPersonas(entities);
const storageKey = 'Personas_PerPage';
const listId = '#user_avatar_block';
const perPage = Number(localStorage.getItem(storageKey)) || 5;
@@ -5978,6 +6001,50 @@ function highlightSelectedAvatar() {
$(`#user_avatar_block .avatar-container[imgfile="${user_avatar}"]`).addClass('selected');
}
/**
* Sort the given personas
* @param {string[]} personas - The persona names to sort
* @returns {string[]} The sorted persona names arrray, same reference as passed in
*/
function sortPersonas(personas) {
const option = $('#persona_sort_order').find(':selected');
if (option.attr('value') === 'search') {
personas.sort((a, b) => {
const aScore = personasFilter.getScore(FILTER_TYPES.PERSONA_SEARCH, a);
const bScore = personasFilter.getScore(FILTER_TYPES.PERSONA_SEARCH, b);
return (aScore - bScore);
});
} else {
personas.sort((a, b) => {
const aName = String(power_user.personas[a] || a);
const bName = String(power_user.personas[b] || b);
return power_user.persona_sort_order === 'asc' ? aName.localeCompare(bName) : bName.localeCompare(aName);
});
}
return personas;
}
/** Checks the state of the current search, and adds/removes the search sorting option accordingly */
function verifyPersonaSearchSortRule() {
const searchTerm = personasFilter.getFilterData(FILTER_TYPES.PERSONA_SEARCH);
const searchOption = $('#persona_sort_order option[value="search"]');
const selector = $('#persona_sort_order');
const isHidden = searchOption.attr('hidden') !== undefined;
// If we have a search term, we are displaying the sorting option for it
if (searchTerm && isHidden) {
searchOption.removeAttr('hidden');
selector.val(searchOption.attr('value'));
flashHighlight(selector);
}
// If search got cleared, we make sure to hide the option and go back to the one before
if (!searchTerm && !isHidden) {
searchOption.attr('hidden', '');
selector.val(power_user.persona_sort_order);
}
}
/**
* Gets a rendered avatar block.
* @param {string} name Avatar file name
@@ -8690,7 +8757,7 @@ jQuery(async function () {
entitiesFilter.setFilterData(FILTER_TYPES.SEARCH, searchQuery);
});
$('#character_search_bar').on('input', function () {
const searchQuery = String($(this).val()).toLowerCase();
const searchQuery = String($(this).val());
debouncedCharacterSearch(searchQuery);
});
@@ -8698,7 +8765,7 @@ jQuery(async function () {
personasFilter.setFilterData(FILTER_TYPES.PERSONA_SEARCH, searchQuery);
});
$('#persona_search_bar').on('input', function () {
const searchQuery = String($(this).val()).toLowerCase();
const searchQuery = String($(this).val());
debouncedPersonaSearch(searchQuery);
});

View File

@@ -1,9 +1,16 @@
import { fuzzySearchCharacters, fuzzySearchGroups, fuzzySearchPersonas, fuzzySearchTags, fuzzySearchWorldInfo, power_user } from './power-user.js';
import { tag_map } from './tags.js';
import { includesIgnoreCaseAndAccents } from './utils.js';
/**
* @typedef FilterType The filter type possible for this filter helper
* @type {'search'|'tag'|'folder'|'fav'|'group'|'world_info_search'|'persona_search'}
*/
/**
* The filter types
* @type {{ SEARCH: string, TAG: string, FOLDER: string, FAV: string, GROUP: string, WORLD_INFO_SEARCH: string, PERSONA_SEARCH: string, [key: string]: string }}
* @type {{ SEARCH: 'search', TAG: 'tag', FOLDER: 'folder', FAV: 'fav', GROUP: 'group', WORLD_INFO_SEARCH: 'world_info_search', PERSONA_SEARCH: 'persona_search'}}
*/
export const FILTER_TYPES = {
SEARCH: 'search',
@@ -56,12 +63,22 @@ export function isFilterState(a, b) {
* data = filterHelper.applyFilters(data);
*/
export class FilterHelper {
/**
* Cache fuzzy search weighting scores for re-usability, sorting and stuff
*
* Contains maps of weighting numbers assigned to their uid/id, for each of the different `FILTER_TYPES`
* @type {Map<FilterType, Map<string|number,number>>}
*/
scoreCache;
/**
* Creates a new FilterHelper
* @param {Function} onDataChanged Callback to trigger when the filter data changes
*/
constructor(onDataChanged) {
this.onDataChanged = onDataChanged;
this.scoreCache = new Map();
}
/**
@@ -135,7 +152,10 @@ export class FilterHelper {
}
const fuzzySearchResults = fuzzySearchWorldInfo(data, term);
return data.filter(entity => fuzzySearchResults.includes(entity.uid));
this.cacheScores(FILTER_TYPES.WORLD_INFO_SEARCH, new Map(fuzzySearchResults.map(i => [i.item?.uid, i.score])));
const filteredData = data.filter(entity => fuzzySearchResults.find(x => x.item === entity));
return filteredData;
}
/**
@@ -151,7 +171,10 @@ export class FilterHelper {
}
const fuzzySearchResults = fuzzySearchPersonas(data, term);
return data.filter(entity => fuzzySearchResults.includes(entity));
this.cacheScores(FILTER_TYPES.PERSONA_SEARCH, new Map(fuzzySearchResults.map(i => [i.item.key, i.score])));
const filteredData = data.filter(name => fuzzySearchResults.find(x => x.item.key === name));
return filteredData;
}
/**
@@ -263,29 +286,28 @@ export class FilterHelper {
return data;
}
const searchValue = this.filterData[FILTER_TYPES.SEARCH].trim().toLowerCase();
const fuzzySearchCharactersResults = power_user.fuzzy_search ? fuzzySearchCharacters(searchValue) : [];
const fuzzySearchGroupsResults = power_user.fuzzy_search ? fuzzySearchGroups(searchValue) : [];
const fuzzySearchTagsResult = power_user.fuzzy_search ? fuzzySearchTags(searchValue) : [];
const searchValue = this.filterData[FILTER_TYPES.SEARCH];
// Save fuzzy search results and scores if enabled
if (power_user.fuzzy_search) {
const fuzzySearchCharactersResults = fuzzySearchCharacters(searchValue);
const fuzzySearchGroupsResults = fuzzySearchGroups(searchValue);
const fuzzySearchTagsResult = fuzzySearchTags(searchValue);
this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchCharactersResults.map(i => [`character.${i.refIndex}`, i.score])));
this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchGroupsResults.map(i => [`group.${i.item.id}`, i.score])));
this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchTagsResult.map(i => [`tag.${i.item.id}`, i.score])));
}
const _this = this;
function getIsValidSearch(entity) {
const isGroup = entity.type === 'group';
const isCharacter = entity.type === 'character';
const isTag = entity.type === 'tag';
if (power_user.fuzzy_search) {
if (isCharacter) {
return fuzzySearchCharactersResults.includes(parseInt(entity.id));
} else if (isGroup) {
return fuzzySearchGroupsResults.includes(String(entity.id));
} else if (isTag) {
return fuzzySearchTagsResult.includes(String(entity.id));
} else {
return false;
}
// We can filter easily by checking if we have saved a score
const score = _this.getScore(FILTER_TYPES.SEARCH, `${entity.type}.${entity.id}`);
return score !== undefined;
}
else {
return entity.item?.name?.toLowerCase()?.includes(searchValue) || false;
// Compare insensitive and without accents
return includesIgnoreCaseAndAccents(entity.item?.name, searchValue);
}
}
@@ -310,7 +332,7 @@ export class FilterHelper {
/**
* Gets the filter data for the given filter type.
* @param {string} filterType The filter type to get data for.
* @param {FilterType} filterType The filter type to get data for.
*/
getFilterData(filterType) {
return this.filterData[filterType];
@@ -318,11 +340,51 @@ export class FilterHelper {
/**
* Applies all filters to the given data.
* @param {any[]} data The data to filter.
* @param {any[]} data - The data to filter.
* @param {object} options - Optional call parameters
* @param {boolean|FilterType} [options.clearScoreCache=true] - Whether the score
* @returns {any[]} The filtered data.
*/
applyFilters(data) {
applyFilters(data, { clearScoreCache = true } = {}) {
if (clearScoreCache) this.clearScoreCache();
return Object.values(this.filterFunctions)
.reduce((data, fn) => fn(data), data);
}
/**
* Cache scores for a specific filter type
* @param {FilterType} type - The type of data being cached
* @param {Map<string|number, number>} results - The search results containing mapped item identifiers and their scores
*/
cacheScores(type, results) {
/** @type {Map<string|number, number>} */
const typeScores = this.scoreCache.get(type) || new Map();
for (const [uid, score] of results) {
typeScores.set(uid, score);
}
this.scoreCache.set(type, typeScores);
console.debug('search scores chached', type, typeScores);
}
/**
* Get the cached score for an item by type and its identifier
* @param {FilterType} type The type of data
* @param {string|number} uid The unique identifier for an item
* @returns {number|undefined} The cached score, or `undefined` if no score is present
*/
getScore(type, uid) {
return this.scoreCache.get(type)?.get(uid) ?? undefined;
}
/**
* Clear the score cache for a specific type, or completely if no type is specified
* @param {FilterType} [type] The type of data to clear scores for. Clears all if unspecified.
*/
clearScoreCache(type) {
if (type) {
this.scoreCache.set(type, new Map());
} else {
this.scoreCache = new Map();
}
}
}

View File

@@ -635,7 +635,9 @@ export function initPersonas() {
$('#personas_restore').on('click', () => $('#personas_restore_input').trigger('click'));
$('#personas_restore_input').on('change', onPersonasRestoreInput);
$('#persona_sort_order').val(power_user.persona_sort_order).on('input', function () {
power_user.persona_sort_order = String($(this).val());
const value = String($(this).val());
// Save sort order, but do not save search sorting, as this is a temporary sorting option
if (value !== 'search') power_user.persona_sort_order = value;
getUserAvatars(true, user_avatar);
saveSettingsDebounced();
});

View File

@@ -22,6 +22,7 @@ import {
ANIMATION_DURATION_DEFAULT,
setActiveGroup,
setActiveCharacter,
entitiesFilter,
} from '../script.js';
import { isMobile, initMovingUI, favsToHotswap } from './RossAscends-mods.js';
import {
@@ -35,12 +36,13 @@ import {
} from './instruct-mode.js';
import { registerSlashCommand } from './slash-commands.js';
import { tag_map, tags } from './tags.js';
import { getTagsList, tag_map, tags } from './tags.js';
import { tokenizers } from './tokenizers.js';
import { BIAS_CACHE } from './logit-bias.js';
import { renderTemplateAsync } from './templates.js';
import { countOccurrences, debounce, delay, download, getFileText, isOdd, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
import { FILTER_TYPES } from './filters.js';
export {
loadPowerUserSettings,
@@ -1860,10 +1862,17 @@ function highlightDefaultContext() {
$('#context_delete_preset').toggleClass('disabled', power_user.default_context === power_user.context.preset);
}
/**
* Fuzzy search characters by a search term
* @param {string} searchValue - The search term
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
*/
export function fuzzySearchCharacters(searchValue) {
// @ts-ignore
const fuse = new Fuse(characters, {
keys: [
{ name: 'data.name', weight: 8 },
{ name: 'data.name', weight: 20 },
{ name: '#tags', weight: 10, getFn: (character) => getTagsList(character.avatar).map(x => x.name).join('||') },
{ name: 'data.description', weight: 3 },
{ name: 'data.mes_example', weight: 3 },
{ name: 'data.scenario', weight: 2 },
@@ -1876,82 +1885,114 @@ export function fuzzySearchCharacters(searchValue) {
],
includeScore: true,
ignoreLocation: true,
useExtendedSearch: true,
threshold: 0.2,
});
const results = fuse.search(searchValue);
console.debug('Characters fuzzy search results for ' + searchValue, results);
const indices = results.map(x => x.refIndex);
return indices;
return results;
}
/**
* Fuzzy search world info entries by a search term
* @param {*[]} data - WI items data array
* @param {string} searchValue - The search term
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
*/
export function fuzzySearchWorldInfo(data, searchValue) {
// @ts-ignore
const fuse = new Fuse(data, {
keys: [
{ name: 'key', weight: 3 },
{ name: 'key', weight: 20 },
{ name: 'group', weight: 15 },
{ name: 'comment', weight: 10 },
{ name: 'keysecondary', weight: 10 },
{ name: 'content', weight: 3 },
{ name: 'comment', weight: 2 },
{ name: 'keysecondary', weight: 2 },
{ name: 'uid', weight: 1 },
{ name: 'automationId', weight: 1 },
],
includeScore: true,
ignoreLocation: true,
useExtendedSearch: true,
threshold: 0.2,
});
const results = fuse.search(searchValue);
console.debug('World Info fuzzy search results for ' + searchValue, results);
return results.map(x => x.item?.uid);
return results;
}
/**
* Fuzzy search persona entries by a search term
* @param {*[]} data - persona data array
* @param {string} searchValue - The search term
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
*/
export function fuzzySearchPersonas(data, searchValue) {
data = data.map(x => ({ key: x, description: power_user.persona_descriptions[x]?.description ?? '', name: power_user.personas[x] ?? '' }));
data = data.map(x => ({ key: x, name: power_user.personas[x] ?? '', description: power_user.persona_descriptions[x]?.description ?? '' }));
// @ts-ignore
const fuse = new Fuse(data, {
keys: [
{ name: 'name', weight: 4 },
{ name: 'description', weight: 1 },
{ name: 'name', weight: 20 },
{ name: 'description', weight: 3 },
],
includeScore: true,
ignoreLocation: true,
useExtendedSearch: true,
threshold: 0.2,
});
const results = fuse.search(searchValue);
console.debug('Personas fuzzy search results for ' + searchValue, results);
return results.map(x => x.item?.key);
return results;
}
/**
* Fuzzy search tags by a search term
* @param {string} searchValue - The search term
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
*/
export function fuzzySearchTags(searchValue) {
// @ts-ignore
const fuse = new Fuse(tags, {
keys: [
{ name: 'name', weight: 1 },
],
includeScore: true,
ignoreLocation: true,
useExtendedSearch: true,
threshold: 0.2,
});
const results = fuse.search(searchValue);
console.debug('Tags fuzzy search results for ' + searchValue, results);
const ids = results.map(x => String(x.item?.id)).filter(x => x);
return ids;
return results;
}
/**
* Fuzzy search groups by a search term
* @param {string} searchValue - The search term
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
*/
export function fuzzySearchGroups(searchValue) {
// @ts-ignore
const fuse = new Fuse(groups, {
keys: [
{ name: 'name', weight: 3 },
{ name: 'members', weight: 1 },
{ name: 'name', weight: 20 },
{ name: 'members', weight: 15 },
{ name: '#tags', weight: 10, getFn: (group) => getTagsList(group.id).map(x => x.name).join('||') },
{ name: 'id', weight: 1 },
],
includeScore: true,
ignoreLocation: true,
useExtendedSearch: true,
threshold: 0.2,
});
const results = fuse.search(searchValue);
console.debug('Groups fuzzy search results for ' + searchValue, results);
const ids = results.map(x => String(x.item?.id)).filter(x => x);
return ids;
return results;
}
/**
@@ -2026,15 +2067,24 @@ function sortEntitiesList(entities) {
return;
}
const isSearch = $('#character_sort_order option[data-field="search"]').is(':selected');
entities.sort((a, b) => {
// Sort tags/folders will always be at the top
if (a.type === 'tag' && b.type !== 'tag') {
return -1;
}
if (a.type !== 'tag' && b.type === 'tag') {
return 1;
}
// If we have search sorting, we take scores and use those
if (isSearch) {
const aScore = entitiesFilter.getScore(FILTER_TYPES.SEARCH, `${a.type}.${a.id}`);
const bScore = entitiesFilter.getScore(FILTER_TYPES.SEARCH, `${b.type}.${b.id}`);
return (aScore - bScore);
}
return sortFunc(a.item, b.item);
});
}
@@ -2745,6 +2795,7 @@ function setAvgBG() {
}
async function setThemeCallback(_, text) {
// @ts-ignore
const fuse = new Fuse(themes, {
keys: [
{ name: 'name', weight: 1 },
@@ -2767,6 +2818,7 @@ async function setThemeCallback(_, text) {
}
async function setmovingUIPreset(_, text) {
// @ts-ignore
const fuse = new Fuse(movingUIPresets, {
keys: [
{ name: 'name', weight: 1 },
@@ -3208,9 +3260,13 @@ $(document).ready(() => {
});
$('#character_sort_order').on('change', function () {
power_user.sort_field = $(this).find(':selected').data('field');
power_user.sort_order = $(this).find(':selected').data('order');
power_user.sort_rule = $(this).find(':selected').data('rule');
const field = String($(this).find(':selected').data('field'));
// Save sort order, but do not save search sorting, as this is a temporary sorting option
if (field !== 'search') {
power_user.sort_field = field;
power_user.sort_order = $(this).find(':selected').data('order');
power_user.sort_rule = $(this).find(':selected').data('rule');
}
printCharactersDebounced();
saveSettingsDebounced();
});

View File

@@ -1430,3 +1430,22 @@ export function flashHighlight(element, timespan = 2000) {
element.addClass('flash animated');
setTimeout(() => element.removeClass('flash animated'), timespan);
}
/**
* Performs a case-insensitive and accent-insensitive substring search.
* This function normalizes the strings to remove diacritical marks and converts them to lowercase to ensure the search is insensitive to case and accents.
*
* @param {string} text - The text in which to search for the substring.
* @param {string} searchTerm - The substring to search for in the text.
* @returns {boolean} - Returns true if the searchTerm is found within the text, otherwise returns false.
*/
export function includesIgnoreCaseAndAccents(text, searchTerm) {
if (!text || !searchTerm) return false; // Return false if either string is empty
// Normalize and remove diacritics, then convert to lower case
const normalizedText = text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
const normalizedSearchTerm = searchTerm.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
// Check if the normalized text includes the normalized search term
return normalizedText.includes(normalizedSearchTerm);
}

View File

@@ -679,7 +679,17 @@ function sortEntries(data) {
const sortRule = option.data('rule');
const orderSign = sortOrder === 'asc' ? 1 : -1;
if (sortRule === 'custom') {
if (!data.length) return data;
// If we have a search term for WI, we are sorting by weighting scores
if (sortRule === 'search') {
data.sort((a, b) => {
const aScore = worldInfoFilter.getScore(FILTER_TYPES.WORLD_INFO_SEARCH, a.uid);
const bScore = worldInfoFilter.getScore(FILTER_TYPES.WORLD_INFO_SEARCH, b.uid);
return (aScore - bScore);
});
}
else if (sortRule === 'custom') {
// First by display index, then by order, then by uid
data.sort((a, b) => {
const aValue = a.displayIndex;
@@ -762,6 +772,9 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
return;
}
// Before printing the WI, we check if we should enable/disable search sorting
verifyWorldInfoSearchSortRule();
function getDataArray(callback) {
// Convert the data.entries object into an array
let entriesArray = Object.keys(data.entries).map(uid => {
@@ -770,10 +783,11 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
return entry;
});
// Sort the entries array by displayIndex and uid
entriesArray.sort((a, b) => a.displayIndex - b.displayIndex || a.uid - b.uid);
entriesArray = sortEntries(entriesArray);
// Apply the filter and do the chosen sorting
entriesArray = worldInfoFilter.applyFilters(entriesArray);
entriesArray = sortEntries(entriesArray)
// Run the callback for printing this
typeof callback === 'function' && callback(entriesArray);
return entriesArray;
}
@@ -1008,6 +1022,26 @@ const originalDataKeyMap = {
'groupOverride': 'extensions.group_override',
};
/** Checks the state of the current search, and adds/removes the search sorting option accordingly */
function verifyWorldInfoSearchSortRule() {
const searchTerm = worldInfoFilter.getFilterData(FILTER_TYPES.WORLD_INFO_SEARCH);
const searchOption = $('#world_info_sort_order option[data-rule="search"]');
const selector = $('#world_info_sort_order');
const isHidden = searchOption.attr('hidden') !== undefined;
// If we have a search term, we are displaying the sorting option for it
if (searchTerm && isHidden) {
searchOption.removeAttr('hidden');
selector.val(searchOption.attr('value') || '0');
flashHighlight(selector);
}
// If search got cleared, we make sure to hide the option and go back to the one before
if (!searchTerm && !isHidden) {
searchOption.attr('hidden', '');
selector.val(localStorage.getItem(SORT_ORDER_KEY) || '0');
}
}
function setOriginalDataValue(data, uid, key, value) {
if (data.originalData && Array.isArray(data.originalData.entries)) {
let originalEntry = data.originalData.entries.find(x => x.uid === uid);
@@ -3065,7 +3099,8 @@ jQuery(() => {
$('#world_info_sort_order').on('change', function () {
const value = String($(this).find(':selected').val());
localStorage.setItem(SORT_ORDER_KEY, value);
// Save sort order, but do not save search sorting, as this is a temporary sorting option
if (value !== 'search') localStorage.setItem(SORT_ORDER_KEY, value);
updateEditor(navigation_option.none);
});