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' ;
2023-08-18 22:13:15 +02:00
2023-08-22 13:30:49 +02:00
/ * *
2024-03-31 00:21:33 +01:00
* The filter types
* @ type { { SEARCH : string , TAG : string , FOLDER : string , FAV : string , GROUP : string , WORLD _INFO _SEARCH : string , PERSONA _SEARCH : string , [ key : string ] : string } }
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 {
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 ;
}
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 ) ;
return data . filter ( entity => fuzzySearchResults . includes ( entity . uid ) ) ;
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 ) ;
return data . filter ( entity => fuzzySearchResults . includes ( entity ) ) ;
}
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 ;
}
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 ) : [ ] ;
2023-11-10 20:56:25 +01:00
const fuzzySearchTagsResult = power _user . fuzzy _search ? fuzzySearchTags ( searchValue ) : [ ] ;
2023-08-18 22:13:15 +02:00
function getIsValidSearch ( entity ) {
const isGroup = entity . type === 'group' ;
const isCharacter = entity . type === 'character' ;
2023-11-10 20:56:25 +01:00
const isTag = entity . type === 'tag' ;
2023-08-18 22:13:15 +02:00
if ( power _user . fuzzy _search ) {
if ( isCharacter ) {
return fuzzySearchCharactersResults . includes ( parseInt ( entity . id ) ) ;
} else if ( isGroup ) {
return fuzzySearchGroupsResults . includes ( String ( entity . id ) ) ;
2023-11-10 20:56:25 +01:00
} else if ( isTag ) {
return fuzzySearchTagsResult . includes ( String ( entity . id ) ) ;
2023-08-18 22:13:15 +02:00
} else {
return false ;
}
}
else {
return entity . item ? . name ? . toLowerCase ( ) ? . includes ( searchValue ) || false ;
}
}
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 .
* @ param { string } filterType The filter type to get data for .
* /
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 .
* @ param { any [ ] } data The data to filter .
* @ returns { any [ ] } The filtered data .
* /
2023-08-18 22:13:15 +02:00
applyFilters ( data ) {
return Object . values ( this . filterFunctions )
. reduce ( ( data , fn ) => fn ( data ) , data ) ;
}
}