2024-01-30 18:12:56 +01:00
import { fuzzySearchCharacters , fuzzySearchGroups , fuzzySearchPersonas , fuzzySearchTags , fuzzySearchWorldInfo , power _user } from './power-user.js' ;
2023-12-02 19:04:51 +01:00
import { tag _map } from './tags.js' ;
2024-04-30 06:03:41 +02:00
import { includesIgnoreCaseAndAccents } from './utils.js' ;
2023-08-18 22:13:15 +02:00
2024-04-30 01:39:47 +02:00
/ * *
* @ typedef FilterType The filter type possible for this filter helper
* @ type { 'search' | 'tag' | 'folder' | 'fav' | 'group' | 'world_info_search' | 'persona_search' }
* /
2023-08-22 13:30:49 +02:00
/ * *
2024-03-31 00:21:33 +01:00
* The filter types
2024-04-30 01:39:47 +02:00
* @ type { { SEARCH : 'search' , TAG : 'tag' , FOLDER : 'folder' , FAV : 'fav' , GROUP : 'group' , WORLD _INFO _SEARCH : 'world_info_search' , PERSONA _SEARCH : 'persona_search' } }
2023-08-22 13:30:49 +02:00
* /
2023-08-18 22:13:15 +02:00
export const FILTER _TYPES = {
SEARCH : 'search' ,
TAG : 'tag' ,
2024-03-06 23:13:22 +01:00
FOLDER : 'folder' ,
2023-08-18 22:13:15 +02:00
FAV : 'fav' ,
GROUP : 'group' ,
2023-08-22 00:51:31 +02:00
WORLD _INFO _SEARCH : 'world_info_search' ,
2024-01-30 18:12:56 +01:00
PERSONA _SEARCH : 'persona_search' ,
2023-08-18 22:13:15 +02:00
} ;
2024-03-06 23:13:22 +01:00
/ * *
2024-03-31 00:21:33 +01:00
* @ typedef FilterState One of the filter states
* @ property { string } key - The key of the state
* @ property { string } class - The css class for this state
* /
/ * *
* The filter states
* @ type { { SELECTED : FilterState , EXCLUDED : FilterState , UNDEFINED : FilterState , [ key : string ] : FilterState } }
2024-03-06 23:13:22 +01:00
* /
export const FILTER _STATES = {
SELECTED : { key : 'SELECTED' , class : 'selected' } ,
EXCLUDED : { key : 'EXCLUDED' , class : 'excluded' } ,
2024-03-07 04:26:33 +01:00
UNDEFINED : { key : 'UNDEFINED' , class : 'undefined' } ,
2024-03-06 23:13:22 +01:00
} ;
2024-03-31 00:21:33 +01:00
/** @type {string} the default filter state of `FILTER_STATES` */
2024-03-30 20:33:08 +01:00
export const DEFAULT _FILTER _STATE = FILTER _STATES . UNDEFINED . key ;
2024-03-06 23:13:22 +01:00
/ * *
* Robust check if one state equals the other . It does not care whether it ' s the state key or the state value object .
2024-03-31 00:21:33 +01:00
* @ param { FilterState | string } a First state
* @ param { FilterState | string } b Second state
* @ returns { boolean }
2024-03-06 23:13:22 +01:00
* /
export function isFilterState ( a , b ) {
const states = Object . keys ( FILTER _STATES ) ;
2024-03-31 00:21:33 +01:00
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 ) ;
2024-03-06 23:13:22 +01:00
return aKey === bKey ;
}
2023-08-22 13:30:49 +02:00
/ * *
* Helper class for filtering data .
* @ example
* const filterHelper = new FilterHelper ( ( ) => console . log ( 'data changed' ) ) ;
* filterHelper . setFilterData ( FILTER _TYPES . SEARCH , 'test' ) ;
* data = filterHelper . applyFilters ( data ) ;
* /
2023-08-18 22:13:15 +02:00
export class FilterHelper {
2024-04-30 01:39:47 +02:00
/ * *
* 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 ;
2023-08-22 13:30:49 +02:00
/ * *
* Creates a new FilterHelper
* @ param { Function } onDataChanged Callback to trigger when the filter data changes
* /
2023-08-18 22:13:15 +02:00
constructor ( onDataChanged ) {
this . onDataChanged = onDataChanged ;
2024-04-30 01:39:47 +02:00
this . scoreCache = new Map ( ) ;
2023-08-18 22:13:15 +02:00
}
2024-04-28 20:33:37 +02:00
/ * *
* Checks if the filter data has any values .
* @ returns { boolean } Whether the filter data has any values
* /
hasAnyFilter ( ) {
/ * *
* Checks if the object has any values .
* @ param { object } obj The object to check for values
* @ returns { boolean } Whether the object has any values
* /
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 ) ;
}
2023-08-22 13:30:49 +02:00
/ * *
* The filter functions .
* @ type { Object . < string , Function > }
* /
2023-08-18 22:13:15 +02:00
filterFunctions = {
[ FILTER _TYPES . SEARCH ] : this . searchFilter . bind ( this ) ,
[ FILTER _TYPES . FAV ] : this . favFilter . bind ( this ) ,
2024-03-06 23:13:22 +01:00
[ FILTER _TYPES . GROUP ] : this . groupFilter . bind ( this ) ,
[ FILTER _TYPES . FOLDER ] : this . folderFilter . bind ( this ) ,
2023-08-18 22:13:15 +02:00
[ FILTER _TYPES . TAG ] : this . tagFilter . bind ( this ) ,
2023-08-22 00:51:31 +02:00
[ FILTER _TYPES . WORLD _INFO _SEARCH ] : this . wiSearchFilter . bind ( this ) ,
2024-01-30 18:12:56 +01:00
[ FILTER _TYPES . PERSONA _SEARCH ] : this . personaSearchFilter . bind ( this ) ,
2023-12-02 20:11:06 +01:00
} ;
2023-08-18 22:13:15 +02:00
2023-08-22 13:30:49 +02:00
/ * *
* The filter data .
* @ type { Object . < string , any > }
* /
2023-08-18 22:13:15 +02:00
filterData = {
[ FILTER _TYPES . SEARCH ] : '' ,
[ FILTER _TYPES . FAV ] : false ,
2024-03-06 23:13:22 +01:00
[ FILTER _TYPES . GROUP ] : false ,
[ FILTER _TYPES . FOLDER ] : false ,
2023-08-18 22:13:15 +02:00
[ FILTER _TYPES . TAG ] : { excluded : [ ] , selected : [ ] } ,
2023-08-22 00:51:31 +02:00
[ FILTER _TYPES . WORLD _INFO _SEARCH ] : '' ,
2024-01-30 18:12:56 +01:00
[ FILTER _TYPES . PERSONA _SEARCH ] : '' ,
2023-12-02 20:11:06 +01:00
} ;
2023-08-22 00:51:31 +02:00
2023-08-22 13:30:49 +02:00
/ * *
* Applies a fuzzy search filter to the World Info data .
* @ param { any [ ] } data The data to filter . Must have a uid property .
* @ returns { any [ ] } The filtered data .
* /
2023-08-22 00:51:31 +02:00
wiSearchFilter ( data ) {
const term = this . filterData [ FILTER _TYPES . WORLD _INFO _SEARCH ] ;
if ( ! term ) {
return data ;
}
const fuzzySearchResults = fuzzySearchWorldInfo ( data , term ) ;
2024-04-30 02:27:44 +02:00
this . cacheScores ( FILTER _TYPES . WORLD _INFO _SEARCH , new Map ( fuzzySearchResults . map ( i => [ i . item ? . uid , i . score ] ) ) ) ;
2024-04-30 01:39:47 +02:00
const filteredData = data . filter ( entity => fuzzySearchResults . find ( x => x . item === entity ) ) ;
return filteredData ;
2023-08-18 22:13:15 +02:00
}
2024-01-30 18:12:56 +01:00
/ * *
* Applies a search filter to Persona data .
* @ param { string [ ] } data The data to filter .
* @ returns { string [ ] } The filtered data .
* /
personaSearchFilter ( data ) {
const term = this . filterData [ FILTER _TYPES . PERSONA _SEARCH ] ;
if ( ! term ) {
return data ;
}
const fuzzySearchResults = fuzzySearchPersonas ( data , term ) ;
2024-04-30 02:27:44 +02:00
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 ;
2024-01-30 18:12:56 +01:00
}
2023-11-11 13:53:08 +01:00
/ * *
* Checks if the given entity is tagged with the given tag ID .
* @ param { object } entity Searchable entity
* @ param { string } tagId Tag ID to check
* @ returns { boolean } Whether the entity is tagged with the given tag ID
* /
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 ;
}
2023-08-22 13:30:49 +02:00
/ * *
* Applies a tag filter to the data .
* @ param { any [ ] } data The data to filter .
* @ returns { any [ ] } The filtered data .
* /
2023-08-18 22:13:15 +02:00
tagFilter ( data ) {
const TAG _LOGIC _AND = true ; // switch to false to use OR logic for combining tags
const { selected , excluded } = this . filterData [ FILTER _TYPES . TAG ] ;
if ( ! selected . length && ! excluded . length ) {
return data ;
}
2023-11-11 13:53:08 +01:00
const getIsTagged = ( entity ) => {
2024-02-19 03:15:45 +01:00
const isTag = entity . type === 'tag' ;
2023-11-11 13:53:08 +01:00
const tagFlags = selected . map ( tagId => this . isElementTagged ( entity , tagId ) ) ;
2023-08-18 22:13:15 +02:00
const trueFlags = tagFlags . filter ( x => x ) ;
const isTagged = TAG _LOGIC _AND ? tagFlags . length === trueFlags . length : trueFlags . length > 0 ;
2023-11-11 13:53:08 +01:00
const excludedTagFlags = excluded . map ( tagId => this . isElementTagged ( entity , tagId ) ) ;
2023-08-18 22:13:15 +02:00
const isExcluded = excludedTagFlags . includes ( true ) ;
2024-02-19 03:15:45 +01:00
if ( isTag ) {
return true ;
} else if ( isExcluded ) {
2023-08-18 22:13:15 +02:00
return false ;
} else if ( selected . length > 0 && ! isTagged ) {
return false ;
} else {
return true ;
}
2023-12-02 20:11:06 +01:00
} ;
2023-08-18 22:13:15 +02:00
return data . filter ( entity => getIsTagged ( entity ) ) ;
}
2023-08-22 13:30:49 +02:00
/ * *
* Applies a favorite filter to the data .
* @ param { any [ ] } data The data to filter .
* @ returns { any [ ] } The filtered data .
* /
2023-08-18 22:13:15 +02:00
favFilter ( data ) {
2024-03-06 23:13:22 +01:00
const state = this . filterData [ FILTER _TYPES . FAV ] ;
const isFav = entity => entity . item . fav || entity . item . fav == 'true' ;
2023-08-18 22:13:15 +02:00
2024-03-06 23:13:22 +01:00
return this . filterDataByState ( data , state , isFav , { includeFolders : true } ) ;
2023-08-18 22:13:15 +02:00
}
2023-08-22 13:30:49 +02:00
/ * *
* Applies a group type filter to the data .
* @ param { any [ ] } data The data to filter .
* @ returns { any [ ] } The filtered data .
* /
2023-08-18 22:13:15 +02:00
groupFilter ( data ) {
2024-03-06 23:13:22 +01:00
const state = this . filterData [ FILTER _TYPES . GROUP ] ;
const isGroup = entity => entity . type === 'group' ;
return this . filterDataByState ( data , state , isGroup , { includeFolders : true } ) ;
}
/ * *
* Applies a "folder" filter to the data .
* @ param { any [ ] } data The data to filter .
* @ returns { any [ ] } The filtered data .
* /
folderFilter ( data ) {
const state = this . filterData [ FILTER _TYPES . FOLDER ] ;
// Slightly different than the other filters, as a positive folder filter means it doesn't filter anything (folders get "not hidden" at another place),
// while a negative state should then filter out all folders.
const isFolder = entity => isFilterState ( state , FILTER _STATES . SELECTED ) ? true : entity . type === 'tag' ;
return this . filterDataByState ( data , state , isFolder ) ;
}
2024-03-30 20:33:08 +01:00
filterDataByState ( data , state , filterFunc , { includeFolders = false } = { } ) {
2024-03-06 23:13:22 +01:00
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' ) ) ;
2023-08-18 22:13:15 +02:00
}
2024-03-06 23:13:22 +01:00
return data ;
2023-08-18 22:13:15 +02:00
}
2023-08-22 13:30:49 +02:00
/ * *
* Applies a search filter to the data . Uses fuzzy search if enabled .
* @ param { any [ ] } data The data to filter .
* @ returns { any [ ] } The filtered data .
* /
2023-08-18 22:13:15 +02:00
searchFilter ( data ) {
if ( ! this . filterData [ FILTER _TYPES . SEARCH ] ) {
return data ;
}
2024-04-30 04:30:39 +02:00
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 ] ) ) ) ;
}
2023-08-18 22:13:15 +02:00
2024-04-30 04:30:39 +02:00
const _this = this ;
2023-08-18 22:13:15 +02:00
function getIsValidSearch ( entity ) {
if ( power _user . fuzzy _search ) {
2024-04-30 04:30:39 +02:00
// 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 ;
2023-08-18 22:13:15 +02:00
}
else {
2024-04-30 04:30:39 +02:00
// Compare insensitive and without accents
2024-04-30 06:03:41 +02:00
return includesIgnoreCaseAndAccents ( entity . item ? . name , searchValue ) ;
2023-08-18 22:13:15 +02:00
}
}
return data . filter ( entity => getIsValidSearch ( entity ) ) ;
}
2023-08-22 13:30:49 +02:00
/ * *
* Sets the filter data for the given filter type .
* @ param { string } filterType The filter type to set data for .
* @ param { any } data The data to set .
* @ param { boolean } suppressDataChanged Whether to suppress the data changed callback .
* /
2023-08-22 00:51:31 +02:00
setFilterData ( filterType , data , suppressDataChanged = false ) {
2023-08-18 22:13:15 +02:00
const oldData = this . filterData [ filterType ] ;
this . filterData [ filterType ] = data ;
// only trigger a data change if the data actually changed
2023-08-22 00:51:31 +02:00
if ( JSON . stringify ( oldData ) !== JSON . stringify ( data ) && ! suppressDataChanged ) {
2023-08-18 22:13:15 +02:00
this . onDataChanged ( ) ;
}
}
2023-08-22 13:30:49 +02:00
/ * *
* Gets the filter data for the given filter type .
2024-04-30 01:39:47 +02:00
* @ param { FilterType } filterType The filter type to get data for .
2023-08-22 13:30:49 +02:00
* /
2023-08-18 22:13:15 +02:00
getFilterData ( filterType ) {
return this . filterData [ filterType ] ;
}
2023-08-22 13:30:49 +02:00
/ * *
* Applies all filters to the given data .
2024-04-30 01:39:47 +02:00
* @ param { any [ ] } data - The data to filter .
* @ param { object } options - Optional call parameters
* @ param { boolean | FilterType } [ options . clearScoreCache = true ] - Whether the score
2023-08-22 13:30:49 +02:00
* @ returns { any [ ] } The filtered data .
* /
2024-04-30 01:39:47 +02:00
applyFilters ( data , { clearScoreCache = true } = { } ) {
if ( clearScoreCache ) this . clearScoreCache ( ) ;
2023-08-18 22:13:15 +02:00
return Object . values ( this . filterFunctions )
. reduce ( ( data , fn ) => fn ( data ) , data ) ;
}
2024-04-30 01:39:47 +02:00
/ * *
* 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 ) ;
2024-04-30 02:27:44 +02:00
console . debug ( 'search scores chached' , type , typeScores ) ;
2024-04-30 01:39:47 +02:00
}
/ * *
* 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
2024-04-30 04:30:39 +02:00
* @ returns { number | undefined } The cached score , or ` undefined ` if no score is present
2024-04-30 01:39:47 +02:00
* /
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 ( ) ;
}
}
2023-08-18 22:13:15 +02:00
}