| | import { fuzzySearchCharacters, fuzzySearchGroups, fuzzySearchPersonas, fuzzySearchTags, fuzzySearchWorldInfo, power_user } from './power-user.js'; |
| | import { tag_map } from './tags.js'; |
| | import { includesIgnoreCaseAndAccents } from './utils.js'; |
| |
|
| |
|
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | export const FILTER_TYPES = { |
| | SEARCH: 'search', |
| | TAG: 'tag', |
| | FOLDER: 'folder', |
| | FAV: 'fav', |
| | GROUP: 'group', |
| | WORLD_INFO_SEARCH: 'world_info_search', |
| | PERSONA_SEARCH: 'persona_search', |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | export const FILTER_STATES = { |
| | SELECTED: { key: 'SELECTED', class: 'selected' }, |
| | EXCLUDED: { key: 'EXCLUDED', class: 'excluded' }, |
| | UNDEFINED: { key: 'UNDEFINED', class: 'undefined' }, |
| | }; |
| | |
| | export const DEFAULT_FILTER_STATE = FILTER_STATES.UNDEFINED.key; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | export function isFilterState(a, b) { |
| | const states = Object.keys(FILTER_STATES); |
| |
|
| | const aKey = typeof a == 'string' && states.includes(a) ? a : states.find(key => FILTER_STATES[key] === a); |
| | const bKey = typeof b == 'string' && states.includes(b) ? b : states.find(key => FILTER_STATES[key] === b); |
| |
|
| | return aKey === bKey; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | export const fuzzySearchCategories = Object.freeze({ |
| | characters: 'characters', |
| | worldInfo: 'worldInfo', |
| | personas: 'personas', |
| | tags: 'tags', |
| | groups: 'groups', |
| | }); |
| |
|
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export class FilterHelper { |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | scoreCache; |
| |
|
| | |
| | |
| | |
| | |
| | fuzzySearchCaches; |
| |
|
| | |
| | |
| | |
| | |
| | constructor(onDataChanged) { |
| | this.onDataChanged = onDataChanged; |
| | this.scoreCache = new Map(); |
| | this.fuzzySearchCaches = { |
| | [fuzzySearchCategories.characters]: { resultMap: new Map() }, |
| | [fuzzySearchCategories.worldInfo]: { resultMap: new Map() }, |
| | [fuzzySearchCategories.personas]: { resultMap: new Map() }, |
| | [fuzzySearchCategories.tags]: { resultMap: new Map() }, |
| | [fuzzySearchCategories.groups]: { resultMap: new Map() }, |
| | }; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | hasAnyFilter() { |
| | |
| | |
| | |
| | |
| | |
| | function checkRecursive(obj) { |
| | if (typeof obj === 'string' && obj.length > 0 && obj !== 'UNDEFINED') { |
| | return true; |
| | } else if (typeof obj === 'boolean' && obj) { |
| | return true; |
| | } else if (Array.isArray(obj) && obj.length > 0) { |
| | return true; |
| | } else if (typeof obj === 'object' && obj !== null && Object.keys(obj.length > 0)) { |
| | for (const key in obj) { |
| | if (checkRecursive(obj[key])) { |
| | return true; |
| | } |
| | } |
| | } |
| | return false; |
| | } |
| |
|
| | return checkRecursive(this.filterData); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | filterFunctions = { |
| | [FILTER_TYPES.SEARCH]: this.searchFilter.bind(this), |
| | [FILTER_TYPES.FAV]: this.favFilter.bind(this), |
| | [FILTER_TYPES.GROUP]: this.groupFilter.bind(this), |
| | [FILTER_TYPES.FOLDER]: this.folderFilter.bind(this), |
| | [FILTER_TYPES.TAG]: this.tagFilter.bind(this), |
| | [FILTER_TYPES.WORLD_INFO_SEARCH]: this.wiSearchFilter.bind(this), |
| | [FILTER_TYPES.PERSONA_SEARCH]: this.personaSearchFilter.bind(this), |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | filterData = { |
| | [FILTER_TYPES.SEARCH]: '', |
| | [FILTER_TYPES.FAV]: false, |
| | [FILTER_TYPES.GROUP]: false, |
| | [FILTER_TYPES.FOLDER]: false, |
| | [FILTER_TYPES.TAG]: { excluded: [], selected: [] }, |
| | [FILTER_TYPES.WORLD_INFO_SEARCH]: '', |
| | [FILTER_TYPES.PERSONA_SEARCH]: '', |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | wiSearchFilter(data) { |
| | const term = this.filterData[FILTER_TYPES.WORLD_INFO_SEARCH]; |
| |
|
| | if (!term) { |
| | return data; |
| | } |
| |
|
| | const fuzzySearchResults = fuzzySearchWorldInfo(data, term, this.fuzzySearchCaches); |
| | 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; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | personaSearchFilter(data) { |
| | const term = this.filterData[FILTER_TYPES.PERSONA_SEARCH]; |
| |
|
| | if (!term) { |
| | return data; |
| | } |
| |
|
| | const fuzzySearchResults = fuzzySearchPersonas(data, term, this.fuzzySearchCaches); |
| | 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; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | isElementTagged(entity, tagId) { |
| | const isCharacter = entity.type === 'character'; |
| | const lookupValue = isCharacter ? entity.item.avatar : String(entity.id); |
| | const isTagged = Array.isArray(tag_map[lookupValue]) && tag_map[lookupValue].includes(tagId); |
| |
|
| | return isTagged; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | tagFilter(data) { |
| | const TAG_LOGIC_AND = true; |
| | const { selected, excluded } = this.filterData[FILTER_TYPES.TAG]; |
| |
|
| | if (!selected.length && !excluded.length) { |
| | return data; |
| | } |
| |
|
| | const getIsTagged = (entity) => { |
| | const isTag = entity.type === 'tag'; |
| | const tagFlags = selected.map(tagId => this.isElementTagged(entity, tagId)); |
| | const trueFlags = tagFlags.filter(x => x); |
| | const isTagged = TAG_LOGIC_AND ? tagFlags.length === trueFlags.length : trueFlags.length > 0; |
| |
|
| | const excludedTagFlags = excluded.map(tagId => this.isElementTagged(entity, tagId)); |
| | const isExcluded = excludedTagFlags.includes(true); |
| |
|
| | if (isTag) { |
| | return true; |
| | } else if (isExcluded) { |
| | return false; |
| | } else if (selected.length > 0 && !isTagged) { |
| | return false; |
| | } else { |
| | return true; |
| | } |
| | }; |
| |
|
| | return data.filter(entity => getIsTagged(entity)); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | favFilter(data) { |
| | const state = this.filterData[FILTER_TYPES.FAV]; |
| | const isFav = entity => entity.item.fav || entity.item.fav == 'true'; |
| |
|
| | return this.filterDataByState(data, state, isFav, { includeFolders: true }); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | groupFilter(data) { |
| | const state = this.filterData[FILTER_TYPES.GROUP]; |
| | const isGroup = entity => entity.type === 'group'; |
| |
|
| | return this.filterDataByState(data, state, isGroup, { includeFolders: true }); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | folderFilter(data) { |
| | const state = this.filterData[FILTER_TYPES.FOLDER]; |
| | |
| | const isFolder = entity => entity.type === 'tag'; |
| |
|
| | return this.filterDataByState(data, state, isFolder); |
| | } |
| |
|
| | filterDataByState(data, state, filterFunc, { includeFolders = false } = {}) { |
| | if (isFilterState(state, FILTER_STATES.SELECTED)) { |
| | return data.filter(entity => filterFunc(entity) || (includeFolders && entity.type == 'tag')); |
| | } |
| | if (isFilterState(state, FILTER_STATES.EXCLUDED)) { |
| | return data.filter(entity => !filterFunc(entity) || (includeFolders && entity.type == 'tag')); |
| | } |
| |
|
| | return data; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | searchFilter(data) { |
| | if (!this.filterData[FILTER_TYPES.SEARCH]) { |
| | return data; |
| | } |
| |
|
| | const searchValue = this.filterData[FILTER_TYPES.SEARCH]; |
| |
|
| | |
| | if (power_user.fuzzy_search) { |
| | const fuzzySearchCharactersResults = fuzzySearchCharacters(searchValue, this.fuzzySearchCaches); |
| | const fuzzySearchGroupsResults = fuzzySearchGroups(searchValue, this.fuzzySearchCaches); |
| | const fuzzySearchTagsResult = fuzzySearchTags(searchValue, this.fuzzySearchCaches); |
| | 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) { |
| | if (power_user.fuzzy_search) { |
| | |
| | const score = _this.getScore(FILTER_TYPES.SEARCH, `${entity.type}.${entity.id}`); |
| | return score !== undefined; |
| | } |
| | else { |
| | |
| | return includesIgnoreCaseAndAccents(entity.item?.name, searchValue); |
| | } |
| | } |
| |
|
| | return data.filter(entity => getIsValidSearch(entity)); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | setFilterData(filterType, data, suppressDataChanged = false) { |
| | const oldData = this.filterData[filterType]; |
| | this.filterData[filterType] = data; |
| |
|
| | |
| | if (JSON.stringify(oldData) !== JSON.stringify(data) && !suppressDataChanged) { |
| | this.onDataChanged(); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | getFilterData(filterType) { |
| | return this.filterData[filterType]; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | applyFilters(data, { clearScoreCache = true, tempOverrides = {}, clearFuzzySearchCaches = true } = {}) { |
| | if (clearScoreCache) this.clearScoreCache(); |
| |
|
| | if (clearFuzzySearchCaches) this.clearFuzzySearchCaches(); |
| |
|
| | |
| | const originalStates = {}; |
| | for (const key in tempOverrides) { |
| | originalStates[key] = this.filterData[key]; |
| | this.filterData[key] = tempOverrides[key]; |
| | } |
| |
|
| | try { |
| | const result = Object.values(this.filterFunctions) |
| | .reduce((data, fn) => fn(data), data); |
| |
|
| | |
| | for (const key in originalStates) { |
| | this.filterData[key] = originalStates[key]; |
| | } |
| |
|
| | return result; |
| | } catch (error) { |
| | |
| | for (const key in originalStates) { |
| | this.filterData[key] = originalStates[key]; |
| | } |
| | throw error; |
| | } |
| | } |
| |
|
| |
|
| | |
| | |
| | |
| | |
| | |
| | cacheScores(type, results) { |
| | |
| | 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); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | getScore(type, uid) { |
| | return this.scoreCache.get(type)?.get(uid) ?? undefined; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | clearScoreCache(type) { |
| | if (type) { |
| | this.scoreCache.set(type, new Map()); |
| | } else { |
| | this.scoreCache = new Map(); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | clearFuzzySearchCaches() { |
| | for (const cache of Object.values(this.fuzzySearchCaches)) { |
| | cache.resultMap.clear(); |
| | } |
| | } |
| | } |
| |
|