2023-07-20 19:32:15 +02:00
import {
characters ,
saveSettingsDebounced ,
this _chid ,
callPopup ,
menu _type ,
getCharacters ,
2023-08-18 22:13:15 +02:00
entitiesFilter ,
2023-11-10 20:56:25 +01:00
printCharacters ,
2024-03-06 04:59:39 +01:00
buildAvatarList ,
2023-12-02 19:04:51 +01:00
} from '../script.js' ;
2023-12-03 13:23:20 +01:00
// eslint-disable-next-line no-unused-vars
2024-03-06 23:13:22 +01:00
import { FILTER _TYPES , FILTER _STATES , isFilterState , FilterHelper } from './filters.js' ;
2023-07-20 19:32:15 +02:00
2024-03-06 04:59:39 +01:00
import { groupCandidatesFilter , groups , selected _group } from './group-chats.js' ;
2024-03-12 23:39:54 +01:00
import { download , onlyUnique , parseJsonFile , uuidv4 , getSortableDelay , debounce } from './utils.js' ;
2024-02-18 08:42:36 +01:00
import { power _user } from './power-user.js' ;
2023-07-20 19:32:15 +02:00
export {
2024-03-06 00:28:14 +01:00
TAG _FOLDER _TYPES ,
TAG _FOLDER _DEFAULT _TYPE ,
2023-07-20 19:32:15 +02:00
tags ,
tag _map ,
2024-03-06 00:28:14 +01:00
filterByTagState ,
2024-03-06 01:05:08 +01:00
isBogusFolder ,
isBogusFolderOpen ,
chooseBogusFolder ,
getTagBlock ,
2023-07-20 19:32:15 +02:00
loadTagsSettings ,
printTagFilters ,
getTagsList ,
2024-03-07 23:48:50 +01:00
printTagList ,
2023-07-20 19:32:15 +02:00
appendTagToList ,
createTagMapFromList ,
renameTagKey ,
importTags ,
2024-02-18 08:42:36 +01:00
sortTags ,
compareTagsForSort ,
2023-07-20 19:32:15 +02:00
} ;
const CHARACTER _FILTER _SELECTOR = '#rm_characters_block .rm_tag_filter' ;
const GROUP _FILTER _SELECTOR = '#rm_group_chats_block .rm_tag_filter' ;
2023-08-18 22:13:15 +02:00
function getFilterHelper ( listSelector ) {
2023-09-06 00:47:55 +02:00
return $ ( listSelector ) . is ( GROUP _FILTER _SELECTOR ) ? groupCandidatesFilter : entitiesFilter ;
2023-07-20 19:32:15 +02:00
}
2024-03-13 02:18:25 +01:00
const redrawCharsAndFiltersDebounced = debounce ( ( ) => {
printCharacters ( false ) ;
printTagFilters ( tag _filter _types . character ) ;
printTagFilters ( tag _filter _types . group _member ) ;
} , 100 ) ;
2023-07-20 19:32:15 +02:00
export const tag _filter _types = {
character : 0 ,
group _member : 1 ,
} ;
const ACTIONABLE _TAGS = {
2024-03-06 23:13:22 +01:00
FAV : { id : 1 , name : 'Show only favorites' , color : 'rgba(255, 255, 0, 0.5)' , action : filterByFav , icon : 'fa-solid fa-star' , class : 'filterByFavorites' } ,
2023-07-20 19:32:15 +02:00
GROUP : { id : 0 , name : 'Show only groups' , color : 'rgba(100, 100, 100, 0.5)' , action : filterByGroups , icon : 'fa-solid fa-users' , class : 'filterByGroups' } ,
2024-03-06 23:13:22 +01:00
FOLDER : { id : 4 , name : 'Always show folders' , color : 'rgba(120, 120, 120, 0.5)' , action : filterByFolder , icon : 'fa-solid fa-folder-plus' , class : 'filterByFolder' } ,
2023-11-11 13:53:08 +01:00
VIEW : { id : 2 , name : 'Manage tags' , color : 'rgba(150, 100, 100, 0.5)' , action : onViewTagsListClick , icon : 'fa-solid fa-gear' , class : 'manageTags' } ,
2023-07-20 19:32:15 +02:00
HINT : { id : 3 , name : 'Show Tag List' , color : 'rgba(150, 100, 100, 0.5)' , action : onTagListHintClick , icon : 'fa-solid fa-tags' , class : 'showTagList' } ,
2024-03-07 04:26:33 +01:00
UNFILTER : { id : 5 , name : 'Clear all filters' , action : onClearAllFiltersClick , icon : 'fa-solid fa-filter-circle-xmark' , class : 'clearAllFilters' } ,
2023-12-02 20:11:06 +01:00
} ;
2023-07-20 19:32:15 +02:00
const InListActionable = {
2023-12-02 20:11:06 +01:00
} ;
2023-07-20 19:32:15 +02:00
const DEFAULT _TAGS = [
2023-12-02 19:04:51 +01:00
{ id : uuidv4 ( ) , name : 'Plain Text' , create _date : Date . now ( ) } ,
{ id : uuidv4 ( ) , name : 'OpenAI' , create _date : Date . now ( ) } ,
{ id : uuidv4 ( ) , name : 'W++' , create _date : Date . now ( ) } ,
{ id : uuidv4 ( ) , name : 'Boostyle' , create _date : Date . now ( ) } ,
{ id : uuidv4 ( ) , name : 'PList' , create _date : Date . now ( ) } ,
{ id : uuidv4 ( ) , name : 'AliChat' , create _date : Date . now ( ) } ,
2023-07-20 19:32:15 +02:00
] ;
2024-03-06 00:28:14 +01:00
const TAG _FOLDER _TYPES = {
OPEN : { icon : '✔' , class : 'folder_open' , fa _icon : 'fa-folder-open' , tooltip : 'Open Folder (Show all characters even if not selected)' , color : 'green' , size : '1' } ,
CLOSED : { icon : '👁' , class : 'folder_closed' , fa _icon : 'fa-eye-slash' , tooltip : 'Closed Folder (Hide all characters unless selected)' , color : 'lightgoldenrodyellow' , size : '0.7' } ,
NONE : { icon : '✕' , class : 'no_folder' , tooltip : 'No Folder' , color : 'red' , size : '1' } ,
} ;
const TAG _FOLDER _DEFAULT _TYPE = 'NONE' ;
2023-07-20 19:32:15 +02:00
let tags = [ ] ;
let tag _map = { } ;
2024-03-06 00:28:14 +01:00
/ * *
* Applies the basic filter for the current state of the tags and their selection on an entity list .
2024-03-06 23:13:22 +01:00
* @ param { Array < Object > } entities List of entities for display , consisting of tags , characters and groups .
* @ param { Object } param1 Optional parameters , explained below .
* @ param { Boolean } [ param1 . globalDisplayFilters ] When enabled , applies the final filter for the global list . Icludes filtering out entities in closed / hidden folders and empty folders .
* @ param { Object } [ param1 . subForEntity ] When given an entity , the list of entities gets filtered specifically for that one as a "sub list" , filtering out other tags , elements not tagged for this and hidden elements .
2024-03-07 02:15:41 +01:00
* @ param { Boolean } [ param1 . filterHidden ] Optional switch with which filtering out hidden items ( from closed folders ) can be disabled .
2024-03-06 23:13:22 +01:00
* @ returns The filtered list of entities
2024-03-06 00:28:14 +01:00
* /
2024-03-07 02:15:41 +01:00
function filterByTagState ( entities , { globalDisplayFilters = false , subForEntity = undefined , filterHidden = true } = { } ) {
2024-03-06 00:28:14 +01:00
const filterData = structuredClone ( entitiesFilter . getFilterData ( FILTER _TYPES . TAG ) ) ;
entities = entities . filter ( entity => {
if ( entity . type === 'tag' ) {
// Remove folders that are already filtered on
if ( filterData . selected . includes ( entity . id ) || filterData . excluded . includes ( entity . id ) ) {
return false ;
}
}
return true ;
} ) ;
if ( globalDisplayFilters ) {
// Prepare some data for caching and performance
const closedFolders = entities . filter ( x => x . type === 'tag' && TAG _FOLDER _TYPES [ x . item . folder _type ] === TAG _FOLDER _TYPES . CLOSED ) ;
entities = entities . filter ( entity => {
// Hide entities that are in a closed folder, unless that one is opened
2024-03-07 02:15:41 +01:00
if ( filterHidden && entity . type !== 'tag' && closedFolders . some ( f => entitiesFilter . isElementTagged ( entity , f . id ) && ! filterData . selected . includes ( f . id ) ) ) {
2024-03-06 00:28:14 +01:00
return false ;
}
// Hide folders that have 0 visible sub entities after the first filtering round
2024-03-06 23:13:22 +01:00
const alwaysFolder = isFilterState ( entitiesFilter . getFilterData ( FILTER _TYPES . FOLDER ) , FILTER _STATES . SELECTED ) ;
2024-03-06 00:28:14 +01:00
if ( entity . type === 'tag' ) {
2024-03-06 23:13:22 +01:00
return alwaysFolder || entity . entities . length > 0 ;
2024-03-06 00:28:14 +01:00
}
return true ;
} ) ;
}
if ( subForEntity !== undefined && subForEntity . type === 'tag' ) {
2024-03-07 02:15:41 +01:00
entities = filterTagSubEntities ( subForEntity . item , entities , { filterHidden : filterHidden } ) ;
2024-03-06 00:28:14 +01:00
}
return entities ;
}
2024-03-07 02:15:41 +01:00
function filterTagSubEntities ( tag , entities , { filterHidden = true } = { } ) {
2024-03-06 00:28:14 +01:00
const filterData = structuredClone ( entitiesFilter . getFilterData ( FILTER _TYPES . TAG ) ) ;
const closedFolders = entities . filter ( x => x . type === 'tag' && TAG _FOLDER _TYPES [ x . item . folder _type ] === TAG _FOLDER _TYPES . CLOSED ) ;
entities = entities . filter ( sub => {
// Filter out all tags and and all who isn't tagged for this item
if ( sub . type === 'tag' || ! entitiesFilter . isElementTagged ( sub , tag . id ) ) {
return false ;
}
// Hide entities that are in a closed folder, unless the closed folder is opened or we display a closed folder
2024-03-07 02:15:41 +01:00
if ( filterHidden && sub . type !== 'tag' && TAG _FOLDER _TYPES [ tag . folder _type ] !== TAG _FOLDER _TYPES . CLOSED && closedFolders . some ( f => entitiesFilter . isElementTagged ( sub , f . id ) && ! filterData . selected . includes ( f . id ) ) ) {
2024-03-06 00:28:14 +01:00
return false ;
}
return true ;
} ) ;
return entities ;
}
2024-03-06 01:05:08 +01:00
/ * *
* Indicates whether a given tag is defined as a folder . Meaning it 's neither undefined nor ' NONE ' .
* @ returns { boolean } If it ' s a tag folder
* /
function isBogusFolder ( tag ) {
return tag ? . folder _type !== undefined && tag . folder _type !== TAG _FOLDER _DEFAULT _TYPE ;
}
/ * *
* Indicates whether a user is currently in a bogus folder .
* @ returns { boolean } If currently viewing a folder
* /
function isBogusFolderOpen ( ) {
const anyIsFolder = entitiesFilter . getFilterData ( FILTER _TYPES . TAG ) ? . selected
. map ( tagId => tags . find ( x => x . id === tagId ) )
. some ( isBogusFolder ) ;
return ! ! anyIsFolder ;
}
/ * *
* Function to be called when a specific tag / folder is chosen to "drill down" .
* @ param { * } source The jQuery element clicked when choosing the folder
* @ param { string } tagId The tag id that is behind the chosen folder
* @ param { boolean } remove Whether the given tag should be removed ( otherwise it is added / chosen )
* /
function chooseBogusFolder ( source , tagId , remove = false ) {
// If we are here via the 'back' action, we implicitly take the last filtered folder as one to remove
const isBack = tagId === 'back' ;
if ( isBack ) {
const drilldown = $ ( source ) . closest ( '#rm_characters_block' ) . find ( '.rm_tag_bogus_drilldown' ) ;
const lastTag = drilldown . find ( '.tag:last' ) . last ( ) ;
tagId = lastTag . attr ( 'id' ) ;
remove = true ;
}
// Instead of manually updating the filter conditions, we just "click" on the filter tag
// We search inside which filter block we are located in and use that one
const FILTER _SELECTOR = ( $ ( source ) . closest ( '#rm_characters_block' ) ? ? $ ( source ) . closest ( '#rm_group_chats_block' ) ) . find ( '.rm_tag_filter' ) ;
if ( remove ) {
// Click twice to skip over the 'excluded' state
$ ( FILTER _SELECTOR ) . find ( ` .tag[id= ${ tagId } ] ` ) . trigger ( 'click' ) . trigger ( 'click' ) ;
} else {
$ ( FILTER _SELECTOR ) . find ( ` .tag[id= ${ tagId } ] ` ) . trigger ( 'click' ) ;
}
}
2024-03-06 00:28:14 +01:00
2024-03-07 02:15:41 +01:00
/ * *
* Builds the tag block for the specified item .
* @ param { Object } item The tag item
* @ param { * } entities The list ob sub items for this tag
* @ param { * } hidden A count of how many sub items are hidden
* @ returns The html for the tag block
* /
function getTagBlock ( item , entities , hidden = 0 ) {
2024-03-06 01:05:08 +01:00
let count = entities . length ;
const tagFolder = TAG _FOLDER _TYPES [ item . folder _type ] ;
const template = $ ( '#bogus_folder_template .bogus_folder_select' ) . clone ( ) ;
template . addClass ( tagFolder . class ) ;
template . attr ( { 'tagid' : item . id , 'id' : ` BogusFolder ${ item . id } ` } ) ;
template . find ( '.avatar' ) . css ( { 'background-color' : item . color , 'color' : item . color2 } ) . attr ( 'title' , ` [Folder] ${ item . name } ` ) ;
template . find ( '.ch_name' ) . text ( item . name ) . attr ( 'title' , ` [Folder] ${ item . name } ` ) ;
2024-03-07 02:15:41 +01:00
template . find ( '.bogus_folder_hidden_counter' ) . text ( hidden > 0 ? ` ${ hidden } hidden ` : '' ) ;
template . find ( '.bogus_folder_counter' ) . text ( ` ${ count } ${ count != 1 ? 'characters' : 'character' } ` ) ;
2024-03-06 01:05:08 +01:00
template . find ( '.bogus_folder_icon' ) . addClass ( tagFolder . fa _icon ) ;
// Fill inline character images
2024-03-06 04:59:39 +01:00
buildAvatarList ( template . find ( '.bogus_folder_avatars_block' ) , entities ) ;
2024-03-06 01:05:08 +01:00
return template ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Applies the favorite filter to the character list .
* @ param { FilterHelper } filterHelper Instance of FilterHelper class .
* /
2024-03-06 23:13:22 +01:00
function filterByFav ( filterHelper ) {
const state = toggleTagThreeState ( $ ( this ) ) ;
filterHelper . setFilterData ( FILTER _TYPES . FAV , state ) ;
2023-07-20 19:32:15 +02:00
}
2023-08-22 12:07:24 +02:00
/ * *
* Applies the "is group" filter to the character list .
* @ param { FilterHelper } filterHelper Instance of FilterHelper class .
* /
2023-08-18 22:13:15 +02:00
function filterByGroups ( filterHelper ) {
2024-03-06 23:13:22 +01:00
const state = toggleTagThreeState ( $ ( this ) ) ;
filterHelper . setFilterData ( FILTER _TYPES . GROUP , state ) ;
}
2023-07-20 19:32:15 +02:00
2024-03-06 23:13:22 +01:00
/ * *
* Applies the "only folder" filter to the character list .
* @ param { FilterHelper } filterHelper Instance of FilterHelper class .
* /
function filterByFolder ( filterHelper ) {
const state = toggleTagThreeState ( $ ( this ) ) ;
filterHelper . setFilterData ( FILTER _TYPES . FOLDER , state ) ;
2023-07-20 19:32:15 +02:00
}
function loadTagsSettings ( settings ) {
tags = settings . tags !== undefined ? settings . tags : DEFAULT _TAGS ;
tag _map = settings . tag _map !== undefined ? settings . tag _map : Object . create ( null ) ;
}
function renameTagKey ( oldKey , newKey ) {
const value = tag _map [ oldKey ] ;
tag _map [ newKey ] = value || [ ] ;
delete tag _map [ oldKey ] ;
saveSettingsDebounced ( ) ;
}
function createTagMapFromList ( listElement , key ) {
2023-12-02 19:04:51 +01:00
const tagIds = [ ... ( $ ( listElement ) . find ( '.tag' ) . map ( ( _ , el ) => $ ( el ) . attr ( 'id' ) ) ) ] ;
2023-07-20 19:32:15 +02:00
tag _map [ key ] = tagIds ;
saveSettingsDebounced ( ) ;
}
function getTagsList ( key ) {
if ( ! Array . isArray ( tag _map [ key ] ) ) {
tag _map [ key ] = [ ] ;
return [ ] ;
}
return tag _map [ key ]
. map ( x => tags . find ( y => y . id === x ) )
. filter ( x => x )
2024-02-18 08:42:36 +01:00
. sort ( compareTagsForSort ) ;
2023-07-20 19:32:15 +02:00
}
function getInlineListSelector ( ) {
2023-12-02 19:04:51 +01:00
if ( selected _group && menu _type === 'group_edit' ) {
2023-07-20 19:32:15 +02:00
return ` .group_select[grid=" ${ selected _group } "] .tags ` ;
}
2023-12-02 19:04:51 +01:00
if ( this _chid && menu _type === 'character_edit' ) {
2023-07-20 19:32:15 +02:00
return ` .character_select[chid=" ${ this _chid } "] .tags ` ;
}
return null ;
}
function getTagKey ( ) {
2023-12-02 19:04:51 +01:00
if ( selected _group && menu _type === 'group_edit' ) {
2023-07-20 19:32:15 +02:00
return selected _group ;
}
2023-12-02 19:04:51 +01:00
if ( this _chid && menu _type === 'character_edit' ) {
2023-07-20 19:32:15 +02:00
return characters [ this _chid ] . avatar ;
}
return null ;
}
2024-03-07 23:48:50 +01:00
/ * *
* Gets the tag key for any provided entity / id / key . If a valid tag key is provided , it just returns this .
* Robust method to find a valid tag key for any entity
* @ param { object | number | string } entityOrKey An entity with id property ( character , group , tag ) , or directly an id or tag key .
* @ returns { string } The tag key that can be found .
* /
export function getTagKeyForEntity ( entityOrKey ) {
let x = entityOrKey ;
// If it's an object and has an 'id' property, we take this for further processing
if ( typeof x === 'object' && x !== null && 'id' in x ) {
x = x . id ;
}
// Next lets check if its a valid character or character id, so we can swith it to its tag
2024-03-09 20:58:13 +01:00
const character = characters . indexOf ( x ) >= 0 ? x : characters [ x ] ;
2024-03-07 23:48:50 +01:00
if ( character ) {
x = character . avatar ;
}
// We should hopefully have a key now. Let's check
if ( x in tag _map ) {
return x ;
}
// If none of the above, we cannot find a valid tag key
return undefined ;
2023-11-04 19:33:15 +01:00
}
function addTagToMap ( tagId , characterId = null ) {
2024-03-07 23:48:50 +01:00
const key = getTagKey ( ) ? ? getTagKeyForEntity ( characterId ) ;
2023-07-20 19:32:15 +02:00
if ( ! key ) {
return ;
}
if ( ! Array . isArray ( tag _map [ key ] ) ) {
tag _map [ key ] = [ tagId ] ;
}
else {
tag _map [ key ] . push ( tagId ) ;
2023-11-09 22:41:26 +01:00
tag _map [ key ] = tag _map [ key ] . filter ( onlyUnique ) ;
2023-07-20 19:32:15 +02:00
}
}
2023-11-04 19:33:15 +01:00
function removeTagFromMap ( tagId , characterId = null ) {
2024-03-07 23:48:50 +01:00
const key = getTagKey ( ) ? ? getTagKeyForEntity ( characterId ) ;
2023-07-20 19:32:15 +02:00
if ( ! key ) {
return ;
}
if ( ! Array . isArray ( tag _map [ key ] ) ) {
tag _map [ key ] = [ ] ;
}
else {
const indexOf = tag _map [ key ] . indexOf ( tagId ) ;
tag _map [ key ] . splice ( indexOf , 1 ) ;
}
}
function findTag ( request , resolve , listSelector ) {
2023-12-02 19:04:51 +01:00
const skipIds = [ ... ( $ ( listSelector ) . find ( '.tag' ) . map ( ( _ , el ) => $ ( el ) . attr ( 'id' ) ) ) ] ;
2023-09-06 00:47:55 +02:00
const haystack = tags . filter ( t => ! skipIds . includes ( t . id ) ) . map ( t => t . name ) . sort ( ( a , b ) => a . toLowerCase ( ) . localeCompare ( b . toLowerCase ( ) ) ) ;
2023-07-20 19:32:15 +02:00
const needle = request . term . toLowerCase ( ) ;
const hasExactMatch = haystack . findIndex ( x => x . toLowerCase ( ) == needle ) !== - 1 ;
const result = haystack . filter ( x => x . toLowerCase ( ) . includes ( needle ) ) ;
if ( request . term && ! hasExactMatch ) {
result . unshift ( request . term ) ;
}
resolve ( result ) ;
}
function selectTag ( event , ui , listSelector ) {
let tagName = ui . item . value ;
let tag = tags . find ( t => t . name === tagName ) ;
// create new tag if it doesn't exist
if ( ! tag ) {
tag = createNewTag ( tagName ) ;
}
// unfocus and clear the input
2023-12-02 19:04:51 +01:00
$ ( event . target ) . val ( '' ) . trigger ( 'input' ) ;
2023-07-20 19:32:15 +02:00
2023-11-04 19:33:15 +01:00
// Optional, check for multiple character ids being present.
const characterData = event . target . closest ( '#bulk_tags_div' ) ? . dataset . characters ;
const characterIds = characterData ? JSON . parse ( characterData ) . characterIds : null ;
if ( characterIds ) {
2023-11-10 20:56:25 +01:00
characterIds . forEach ( ( characterId ) => addTagToMap ( tag . id , characterId ) ) ;
} else {
2023-11-04 19:33:15 +01:00
addTagToMap ( tag . id ) ;
}
2023-07-20 19:32:15 +02:00
saveSettingsDebounced ( ) ;
2024-03-07 23:48:50 +01:00
// add tag to the UI and internal map - we reprint so sorting and new markup is done correctly
printTagList ( listSelector , { tagOptions : { removable : true } } ) ;
printTagList ( $ ( getInlineListSelector ( ) ) ) ;
2023-07-20 19:32:15 +02:00
printTagFilters ( tag _filter _types . character ) ;
printTagFilters ( tag _filter _types . group _member ) ;
// need to return false to keep the input clear
return false ;
}
function getExistingTags ( new _tags ) {
let existing _tags = [ ] ;
for ( let tag of new _tags ) {
2023-12-02 20:11:06 +01:00
let foundTag = tags . find ( t => t . name . toLowerCase ( ) === tag . toLowerCase ( ) ) ;
2023-07-20 19:32:15 +02:00
if ( foundTag ) {
existing _tags . push ( foundTag . name ) ;
}
}
2023-12-02 20:11:06 +01:00
return existing _tags ;
2023-07-20 19:32:15 +02:00
}
async function importTags ( imported _char ) {
2023-12-02 19:04:51 +01:00
let imported _tags = imported _char . tags . filter ( t => t !== 'ROOT' && t !== 'TAVERN' ) ;
2023-07-20 19:32:15 +02:00
let existingTags = await getExistingTags ( imported _tags ) ;
//make this case insensitive
let newTags = imported _tags . filter ( t => ! existingTags . some ( existingTag => existingTag . toLowerCase ( ) === t . toLowerCase ( ) ) ) ;
2023-12-02 19:04:51 +01:00
let selected _tags = '' ;
2023-07-20 19:32:15 +02:00
const existingTagsString = existingTags . length ? ( ': ' + existingTags . join ( ', ' ) ) : '' ;
if ( newTags . length === 0 ) {
await callPopup ( ` <h3>Importing Tags For ${ imported _char . name } </h3><p> ${ existingTags . length } existing tags have been found ${ existingTagsString } .</p> ` , 'text' ) ;
} else {
selected _tags = await callPopup ( ` <h3>Importing Tags For ${ imported _char . name } </h3><p> ${ existingTags . length } existing tags have been found ${ existingTagsString } .</p><p>The following ${ newTags . length } new tags will be imported.</p> ` , 'input' , newTags . join ( ', ' ) ) ;
}
2024-02-18 08:42:36 +01:00
// @ts-ignore
2023-07-20 19:32:15 +02:00
selected _tags = existingTags . concat ( selected _tags . split ( ',' ) ) ;
2024-02-18 08:42:36 +01:00
// @ts-ignore
2023-12-02 19:04:51 +01:00
selected _tags = selected _tags . map ( t => t . trim ( ) ) . filter ( t => t !== '' ) ;
2023-07-20 19:32:15 +02:00
//Anti-troll measure
if ( selected _tags . length > 15 ) {
selected _tags = selected _tags . slice ( 0 , 15 ) ;
}
for ( let tagName of selected _tags ) {
let tag = tags . find ( t => t . name === tagName ) ;
if ( ! tag ) {
tag = createNewTag ( tagName ) ;
}
if ( ! tag _map [ imported _char . avatar ] . includes ( tag . id ) ) {
tag _map [ imported _char . avatar ] . push ( tag . id ) ;
console . debug ( 'added tag to map' , tag , imported _char . name ) ;
}
2023-12-02 16:15:03 +01:00
}
2023-07-20 19:32:15 +02:00
saveSettingsDebounced ( ) ;
await getCharacters ( ) ;
printTagFilters ( tag _filter _types . character ) ;
printTagFilters ( tag _filter _types . group _member ) ;
// need to return false to keep the input clear
return false ;
}
function createNewTag ( tagName ) {
const tag = {
2023-08-22 12:07:24 +02:00
id : uuidv4 ( ) ,
2023-07-20 19:32:15 +02:00
name : tagName ,
2024-03-06 00:28:14 +01:00
folder _type : TAG _FOLDER _DEFAULT _TYPE ,
2024-02-18 08:42:36 +01:00
sort _order : tags . length ,
2023-07-20 19:32:15 +02:00
color : '' ,
2023-09-16 11:37:19 +02:00
color2 : '' ,
2023-11-10 20:56:25 +01:00
create _date : Date . now ( ) ,
2023-07-20 19:32:15 +02:00
} ;
tags . push ( tag ) ;
return tag ;
}
2024-03-07 23:48:50 +01:00
/ * *
* @ typedef { object } TagOptions
* @ property { boolean } [ removable = false ] - Whether tags can be removed .
* @ property { boolean } [ selectable = false ] - Whether tags can be selected .
* @ property { function } [ action = undefined ] - Action to perform on tag interaction .
* @ property { boolean } [ isGeneralList = false ] - If true , indicates that this is the general list of tags .
* @ property { boolean } [ skipExistsCheck = false ] - If true , the tag gets added even if a tag with the same id already exists .
* /
/ * *
* Prints the list of tags .
* @ param { JQuery < HTMLElement > } element - The container element where the tags are to be printed .
* @ param { object } [ options ] - Optional parameters for printing the tag list .
* @ param { Array < object > } [ options . tags ] Optional override of tags that should be printed . Those will not be sorted . If no supplied , tags for the relevant character are printed .
* @ param { object | number | string } [ options . forEntityOrKey = undefined ] - Optional override for the chosen entity , otherwise the currently selected is chosen . Can be an entity with id property ( character , group , tag ) , or directly an id or tag key .
* @ param { boolean } [ options . empty = true ] - Whether the list should be initially empty .
* @ param { function ( object ) : function } [ options . tagActionSelector = undefined ] - An optional override for the action property that can be assigned to each tag via tagOptions .
* If set , the selector is executed on each tag as input argument . This allows a list of tags to be provided and each tag can have it ' s action based on the tag object itself .
* @ param { TagOptions } [ options . tagOptions = { } ] - Options for tag behavior . ( Same object will be passed into "appendTagToList" )
* /
function printTagList ( element , { tags = undefined , forEntityOrKey = undefined , empty = true , tagActionSelector = undefined , tagOptions = { } } = { } ) {
const key = forEntityOrKey !== undefined ? getTagKeyForEntity ( forEntityOrKey ) : getTagKey ( ) ;
const printableTags = tags ? ? getTagsList ( key ) ;
if ( empty ) {
$ ( element ) . empty ( ) ;
}
for ( const tag of printableTags ) {
// If we have a custom action selector, we override that tag options for each tag
if ( tagActionSelector && typeof tagActionSelector === 'function' ) {
const action = tagActionSelector ( tag ) ;
if ( action && typeof action !== 'function' ) {
console . error ( 'The action parameter must return a function for tag.' , tag ) ;
} else {
tagOptions . action = action ;
}
}
appendTagToList ( element , tag , tagOptions ) ;
}
}
2023-12-03 13:23:20 +01:00
/ * *
* Appends a tag to the list element .
2024-03-07 23:48:50 +01:00
* @ param { JQuery < HTMLElement > } listElement List element .
* @ param { object } tag Tag object to append .
* @ param { TagOptions } [ options = { } ] - Options for tag behavior .
2023-12-03 13:23:20 +01:00
* @ returns { void }
* /
2024-03-07 23:48:50 +01:00
function appendTagToList ( listElement , tag , { removable = false , selectable = false , action = undefined , isGeneralList = false , skipExistsCheck = false } = { } ) {
2023-07-20 19:32:15 +02:00
if ( ! listElement ) {
return ;
}
2024-03-07 23:48:50 +01:00
if ( ! skipExistsCheck && $ ( listElement ) . find ( ` .tag[id=" ${ tag . id } "] ` ) . length > 0 ) {
return ;
}
2023-07-20 19:32:15 +02:00
let tagElement = $ ( '#tag_template .tag' ) . clone ( ) ;
tagElement . attr ( 'id' , tag . id ) ;
2023-09-16 11:37:19 +02:00
//tagElement.css('color', 'var(--SmartThemeBodyColor)');
2023-07-20 19:32:15 +02:00
tagElement . css ( 'background-color' , tag . color ) ;
2023-09-16 11:37:19 +02:00
tagElement . css ( 'color' , tag . color2 ) ;
2023-07-20 19:32:15 +02:00
tagElement . find ( '.tag_name' ) . text ( tag . name ) ;
2023-12-02 19:04:51 +01:00
const removeButton = tagElement . find ( '.tag_remove' ) ;
2023-07-20 19:32:15 +02:00
removable ? removeButton . show ( ) : removeButton . hide ( ) ;
if ( tag . class ) {
tagElement . addClass ( tag . class ) ;
}
if ( tag . icon ) {
tagElement . find ( '.tag_name' ) . text ( '' ) . attr ( 'title' , tag . name ) . addClass ( tag . icon ) ;
}
2023-08-19 20:08:35 +02:00
if ( tag . excluded && isGeneralList ) {
2024-03-07 04:26:33 +01:00
toggleTagThreeState ( tagElement , { stateOverride : FILTER _STATES . EXCLUDED } ) ;
2023-07-20 19:32:15 +02:00
}
if ( selectable ) {
2023-08-18 22:13:15 +02:00
tagElement . on ( 'click' , ( ) => onTagFilterClick . bind ( tagElement ) ( listElement ) ) ;
2023-07-20 19:32:15 +02:00
}
if ( action ) {
2023-08-18 22:13:15 +02:00
const filter = getFilterHelper ( $ ( listElement ) ) ;
tagElement . on ( 'click' , ( ) => action . bind ( tagElement ) ( filter ) ) ;
2023-07-20 19:32:15 +02:00
tagElement . addClass ( 'actionable' ) ;
}
2023-11-11 13:53:08 +01:00
/ * i f ( a c t i o n & & t a g . i d = = = 2 ) {
2023-07-20 19:32:15 +02:00
tagElement . addClass ( 'innerActionable hidden' ) ;
2023-11-11 13:53:08 +01:00
} * /
2023-07-20 19:32:15 +02:00
$ ( listElement ) . append ( tagElement ) ;
}
2023-08-18 22:13:15 +02:00
function onTagFilterClick ( listElement ) {
2024-02-27 23:32:21 +01:00
const tagId = $ ( this ) . attr ( 'id' ) ;
const existingTag = tags . find ( ( tag ) => tag . id === tagId ) ;
2024-03-06 23:13:22 +01:00
let state = toggleTagThreeState ( $ ( this ) ) ;
2023-07-20 19:32:15 +02:00
// Manual undefined check required for three-state boolean
2024-03-06 23:13:22 +01:00
if ( existingTag ) {
existingTag . excluded = isFilterState ( state , FILTER _STATES . EXCLUDED ) ;
2023-07-20 19:32:15 +02:00
2024-03-06 23:13:22 +01:00
saveSettingsDebounced ( ) ;
2023-07-20 19:32:15 +02:00
}
2024-02-27 23:32:21 +01:00
// Update bogus folder if applicable
2024-03-06 00:28:14 +01:00
if ( isBogusFolder ( existingTag ) ) {
2024-02-27 23:32:21 +01:00
// Update bogus drilldown
if ( $ ( this ) . hasClass ( 'selected' ) ) {
2024-03-07 23:48:50 +01:00
appendTagToList ( $ ( '.rm_tag_controls .rm_tag_bogus_drilldown' ) , existingTag , { removable : true } ) ;
2024-02-27 23:32:21 +01:00
} else {
$ ( listElement ) . closest ( '.rm_tag_controls' ) . find ( ` .rm_tag_bogus_drilldown .tag[id= ${ tagId } ] ` ) . remove ( ) ;
}
}
2023-08-25 06:29:10 +02:00
runTagFilters ( listElement ) ;
2024-02-19 03:15:45 +01:00
updateTagFilterIndicator ( ) ;
2023-08-25 06:29:10 +02:00
}
2024-03-07 04:26:33 +01:00
function toggleTagThreeState ( element , { stateOverride = undefined , simulateClick = false } = { } ) {
2024-03-06 23:13:22 +01:00
const states = Object . keys ( FILTER _STATES ) ;
2024-03-07 04:26:33 +01:00
const overrideKey = states . includes ( stateOverride ) ? stateOverride : Object . keys ( FILTER _STATES ) . find ( key => FILTER _STATES [ key ] === stateOverride ) ;
2024-03-06 23:13:22 +01:00
2024-03-07 04:26:33 +01:00
const currentStateIndex = states . indexOf ( element . attr ( 'data-toggle-state' ) ) ? ? states . length - 1 ;
const targetStateIndex = overrideKey !== undefined ? states . indexOf ( overrideKey ) : ( currentStateIndex + 1 ) % states . length ;
2024-03-06 23:13:22 +01:00
2024-03-07 04:26:33 +01:00
if ( simulateClick ) {
// Calculate how many clicks are needed to go from the current state to the target state
let clickCount = 0 ;
if ( targetStateIndex >= currentStateIndex ) {
clickCount = targetStateIndex - currentStateIndex ;
} else {
clickCount = ( states . length - currentStateIndex ) + targetStateIndex ;
}
2024-03-06 23:13:22 +01:00
2024-03-07 04:26:33 +01:00
for ( let i = 0 ; i < clickCount ; i ++ ) {
$ ( element ) . trigger ( 'click' ) ;
2024-03-06 23:13:22 +01:00
}
2024-03-07 04:26:33 +01:00
console . debug ( 'manually click-toggle three-way filter from' , states [ currentStateIndex ] , 'to' , states [ targetStateIndex ] , 'on' , element ) ;
} else {
element . attr ( 'data-toggle-state' , states [ targetStateIndex ] ) ;
// Update css class and remove all others
states . forEach ( state => {
element . toggleClass ( FILTER _STATES [ state ] . class , state === states [ targetStateIndex ] ) ;
} ) ;
console . debug ( 'toggle three-way filter from' , states [ currentStateIndex ] , 'to' , states [ targetStateIndex ] , 'on' , element ) ;
}
return states [ targetStateIndex ] ;
2024-03-06 23:13:22 +01:00
}
2023-08-25 06:29:10 +02:00
function runTagFilters ( listElement ) {
2023-12-02 19:04:51 +01:00
const tagIds = [ ... ( $ ( listElement ) . find ( '.tag.selected:not(.actionable)' ) . map ( ( _ , el ) => $ ( el ) . attr ( 'id' ) ) ) ] ;
const excludedTagIds = [ ... ( $ ( listElement ) . find ( '.tag.excluded:not(.actionable)' ) . map ( ( _ , el ) => $ ( el ) . attr ( 'id' ) ) ) ] ;
2023-08-18 22:13:15 +02:00
const filterHelper = getFilterHelper ( $ ( listElement ) ) ;
filterHelper . setFilterData ( FILTER _TYPES . TAG , { excluded : excludedTagIds , selected : tagIds } ) ;
2023-07-20 19:32:15 +02:00
}
function printTagFilters ( type = tag _filter _types . character ) {
2024-03-07 23:48:50 +01:00
const filterData = structuredClone ( entitiesFilter . getFilterData ( FILTER _TYPES . TAG ) ) ;
2023-07-20 19:32:15 +02:00
const FILTER _SELECTOR = type === tag _filter _types . character ? CHARACTER _FILTER _SELECTOR : GROUP _FILTER _SELECTOR ;
$ ( FILTER _SELECTOR ) . empty ( ) ;
2024-02-27 23:32:21 +01:00
$ ( FILTER _SELECTOR ) . siblings ( '.rm_tag_bogus_drilldown' ) . empty ( ) ;
2024-03-07 23:48:50 +01:00
// Print all action tags. (Exclude folder if that setting isn't chosen)
const actionTags = Object . values ( ACTIONABLE _TAGS ) . filter ( tag => power _user . bogus _folders || tag . id != ACTIONABLE _TAGS . FOLDER . id ) ;
printTagList ( $ ( FILTER _SELECTOR ) , { empty : false , tags : actionTags , tagActionSelector : tag => tag . action , tagOptions : { isGeneralList : true } } ) ;
const inListActionTags = Object . values ( InListActionable ) ;
printTagList ( $ ( FILTER _SELECTOR ) , { empty : false , tags : inListActionTags , tagActionSelector : tag => tag . action , tagOptions : { isGeneralList : true } } ) ;
2023-07-20 19:32:15 +02:00
const characterTagIds = Object . values ( tag _map ) . flat ( ) ;
const tagsToDisplay = tags
. filter ( x => characterTagIds . includes ( x . id ) )
2024-02-18 08:42:36 +01:00
. sort ( compareTagsForSort ) ;
2024-03-07 23:48:50 +01:00
printTagList ( $ ( FILTER _SELECTOR ) , { empty : false , tags : tagsToDisplay , tagOptions : { selectable : true , isGeneralList : true } } ) ;
2023-07-20 19:32:15 +02:00
2024-03-07 23:48:50 +01:00
runTagFilters ( FILTER _SELECTOR ) ;
2023-07-20 19:32:15 +02:00
2024-03-07 23:48:50 +01:00
// Simulate clicks on all "selected" tags when we reprint, otherwise their filter gets lost. "excluded" is persisted.
for ( const tagId of filterData . selected ) {
toggleTagThreeState ( $ ( ` ${ FILTER _SELECTOR } .tag[id=" ${ tagId } "] ` ) , { stateOverride : FILTER _STATES . SELECTED , simulateClick : true } ) ;
2023-07-20 19:32:15 +02:00
}
2024-02-19 03:15:45 +01:00
if ( power _user . show _tag _filters ) {
$ ( '.rm_tag_controls .showTagList' ) . addClass ( 'selected' ) ;
$ ( '.rm_tag_controls' ) . find ( '.tag:not(.actionable)' ) . show ( ) ;
}
updateTagFilterIndicator ( ) ;
}
function updateTagFilterIndicator ( ) {
if ( $ ( '.rm_tag_controls' ) . find ( '.tag:not(.actionable)' ) . is ( '.selected, .excluded' ) ) {
$ ( '.rm_tag_controls .showTagList' ) . addClass ( 'indicator' ) ;
} else {
$ ( '.rm_tag_controls .showTagList' ) . removeClass ( 'indicator' ) ;
}
2023-07-20 19:32:15 +02:00
}
function onTagRemoveClick ( event ) {
event . stopPropagation ( ) ;
2023-12-02 19:04:51 +01:00
const tag = $ ( this ) . closest ( '.tag' ) ;
const tagId = tag . attr ( 'id' ) ;
2023-11-04 19:33:15 +01:00
2024-02-19 05:30:42 +01:00
// Check if we are inside the drilldown. If so, we call remove on the bogus folder
if ( $ ( this ) . closest ( '.rm_tag_bogus_drilldown' ) . length > 0 ) {
console . debug ( 'Bogus drilldown remove' , tagId ) ;
2024-02-27 23:32:21 +01:00
chooseBogusFolder ( $ ( this ) , tagId , true ) ;
2024-02-19 05:30:42 +01:00
return ;
}
2023-11-04 19:33:15 +01:00
// Optional, check for multiple character ids being present.
const characterData = event . target . closest ( '#bulk_tags_div' ) ? . dataset . characters ;
const characterIds = characterData ? JSON . parse ( characterData ) . characterIds : null ;
2023-07-20 19:32:15 +02:00
tag . remove ( ) ;
2023-11-04 19:33:15 +01:00
if ( characterIds ) {
characterIds . forEach ( ( characterId ) => removeTagFromMap ( tagId , characterId ) ) ;
} else {
removeTagFromMap ( tagId ) ;
}
2023-07-20 19:32:15 +02:00
$ ( ` ${ getInlineListSelector ( ) } .tag[id=" ${ tagId } "] ` ) . remove ( ) ;
printTagFilters ( tag _filter _types . character ) ;
printTagFilters ( tag _filter _types . group _member ) ;
saveSettingsDebounced ( ) ;
}
2024-02-18 08:42:36 +01:00
// @ts-ignore
2023-07-20 19:32:15 +02:00
function onTagInput ( event ) {
let val = $ ( this ) . val ( ) ;
if ( tags . find ( t => t . name === val ) ) return ;
2024-02-18 08:42:36 +01:00
// @ts-ignore
2023-12-02 19:04:51 +01:00
$ ( this ) . autocomplete ( 'search' , val ) ;
2023-07-20 19:32:15 +02:00
}
function onTagInputFocus ( ) {
2024-02-18 08:42:36 +01:00
// @ts-ignore
2023-07-20 19:32:15 +02:00
$ ( this ) . autocomplete ( 'search' , $ ( this ) . val ( ) ) ;
}
function onCharacterCreateClick ( ) {
2023-12-02 19:04:51 +01:00
$ ( '#tagList' ) . empty ( ) ;
2023-07-20 19:32:15 +02:00
}
function onGroupCreateClick ( ) {
2024-03-07 23:48:50 +01:00
// Nothing to do here at the moment. Tags in group interface get automatically redrawn.
2023-07-20 19:32:15 +02:00
}
export function applyTagsOnCharacterSelect ( ) {
//clearTagsFilter();
const chid = Number ( $ ( this ) . attr ( 'chid' ) ) ;
2024-03-07 23:48:50 +01:00
printTagList ( $ ( '#tagList' ) , { forEntityOrKey : chid , tagOptions : { removable : true } } ) ;
2023-07-20 19:32:15 +02:00
}
function applyTagsOnGroupSelect ( ) {
//clearTagsFilter();
2024-03-07 23:48:50 +01:00
// Nothing to do here at the moment. Tags in group interface get automatically redrawn.
2023-07-20 19:32:15 +02:00
}
2023-11-04 19:33:15 +01:00
export function createTagInput ( inputSelector , listSelector ) {
2023-07-20 19:32:15 +02:00
$ ( inputSelector )
2024-02-18 08:42:36 +01:00
// @ts-ignore
2023-07-20 19:32:15 +02:00
. autocomplete ( {
source : ( i , o ) => findTag ( i , o , listSelector ) ,
select : ( e , u ) => selectTag ( e , u , listSelector ) ,
minLength : 0 ,
} )
. focus ( onTagInputFocus ) ; // <== show tag list on click
}
function onViewTagsListClick ( ) {
$ ( '#dialogue_popup' ) . addClass ( 'large_dialogue_popup' ) ;
2023-11-10 20:56:25 +01:00
const list = $ ( document . createElement ( 'div' ) ) ;
list . attr ( 'id' , 'tag_view_list' ) ;
2023-07-20 19:32:15 +02:00
const everything = Object . values ( tag _map ) . flat ( ) ;
2023-11-10 20:56:25 +01:00
$ ( list ) . append ( `
< div class = "title_restorable alignItemsBaseline" >
< h3 > Tag Management < / h 3 >
2023-11-14 23:59:44 +01:00
< div class = "flex-container alignItemsBaseline" >
< div class = "menu_button menu_button_icon tag_view_backup" title = "Save your tags to a file" >
< i class = "fa-solid fa-file-export" > < / i >
< span data - i18n = "Backup" > Backup < / s p a n >
< / d i v >
< div class = "menu_button menu_button_icon tag_view_restore" title = "Restore tags from a file" >
< i class = "fa-solid fa-file-import" > < / i >
< span data - i18n = "Restore" > Restore < / s p a n >
< / d i v >
< div class = "menu_button menu_button_icon tag_view_create" title = "Create a new tag" >
< i class = "fa-solid fa-plus" > < / i >
< span data - i18n = "Create" > Create < / s p a n >
< / d i v >
< input type = "file" id = "tag_view_restore_input" hidden accept = ".json" >
2023-11-10 20:56:25 +01:00
< / d i v >
< / d i v >
< div class = "justifyLeft m-b-1" >
< small >
2024-02-18 08:42:36 +01:00
Drag the handle to reorder . < br >
$ { ( power _user . bogus _folders ? 'Click on the folder icon to use this tag as a folder.<br>' : '' ) }
2023-11-10 20:56:25 +01:00
Click on the tag name to edit it . < br >
Click on color box to assign new color .
< / s m a l l >
< / d i v > ` ) ;
2023-07-20 19:32:15 +02:00
2024-02-18 08:42:36 +01:00
const tagContainer = $ ( '<div class="tag_view_list_tags ui-sortable"></div>' ) ;
list . append ( tagContainer ) ;
const sortedTags = sortTags ( tags ) ;
2023-11-14 23:59:44 +01:00
for ( const tag of sortedTags ) {
2024-02-18 08:42:36 +01:00
appendViewTagToList ( tagContainer , tag , everything ) ;
2023-11-10 20:56:25 +01:00
}
2023-09-16 11:37:19 +02:00
2024-02-18 08:42:36 +01:00
makeTagListDraggable ( tagContainer ) ;
2023-11-10 20:56:25 +01:00
callPopup ( list , 'text' ) ;
}
2023-09-16 11:37:19 +02:00
2024-02-18 08:42:36 +01:00
function makeTagListDraggable ( tagContainer ) {
const onTagsSort = ( ) => {
tagContainer . find ( '.tag_view_item' ) . each ( function ( i , tagElement ) {
const id = $ ( tagElement ) . attr ( 'id' ) ;
const tag = tags . find ( x => x . id === id ) ;
// Fix the defined colors, because if there is no color set, they seem to get automatically set to black
// based on the color picker after drag&drop, even if there was no color chosen. We just set them back.
const color = $ ( tagElement ) . find ( '.tagColorPickerHolder .tag-color' ) . attr ( 'color' ) ;
const color2 = $ ( tagElement ) . find ( '.tagColorPicker2Holder .tag-color2' ) . attr ( 'color' ) ;
if ( color === '' || color === undefined ) {
tag . color = '' ;
fixColor ( 'background-color' , tag . color ) ;
}
if ( color2 === '' || color2 === undefined ) {
tag . color2 = '' ;
fixColor ( 'color' , tag . color2 ) ;
}
// Update the sort order
tag . sort _order = i ;
function fixColor ( property , color ) {
$ ( tagElement ) . find ( '.tag_view_name' ) . css ( property , color ) ;
$ ( ` .tag[id=" ${ id } "] ` ) . css ( property , color ) ;
$ ( ` .bogus_folder_select[tagid=" ${ id } "] .avatar ` ) . css ( property , color ) ;
}
} ) ;
saveSettingsDebounced ( ) ;
2024-03-12 23:39:54 +01:00
// If the order of tags in display has changed, we need to redraw some UI elements. Do it debounced so it doesn't block and you can drag multiple tags.
2024-03-13 02:18:25 +01:00
redrawCharsAndFiltersDebounced ( ) ;
2024-02-18 08:42:36 +01:00
} ;
// @ts-ignore
$ ( tagContainer ) . sortable ( {
delay : getSortableDelay ( ) ,
stop : ( ) => onTagsSort ( ) ,
handle : '.drag-handle' ,
} ) ;
}
function sortTags ( tags ) {
return tags . slice ( ) . sort ( compareTagsForSort ) ;
}
function compareTagsForSort ( a , b ) {
if ( a . sort _order !== undefined && b . sort _order !== undefined ) {
return a . sort _order - b . sort _order ;
} else if ( a . sort _order !== undefined ) {
return - 1 ;
} else if ( b . sort _order !== undefined ) {
return 1 ;
} else {
return a . name . toLowerCase ( ) . localeCompare ( b . name . toLowerCase ( ) ) ;
}
}
2023-11-14 23:59:44 +01:00
async function onTagRestoreFileSelect ( e ) {
const file = e . target . files [ 0 ] ;
if ( ! file ) {
console . log ( 'Tag restore: No file selected.' ) ;
return ;
}
const data = await parseJsonFile ( file ) ;
if ( ! data ) {
2023-11-15 01:09:40 +01:00
toastr . warning ( 'Empty file data' , 'Tag restore' ) ;
2023-11-14 23:59:44 +01:00
console . log ( 'Tag restore: File data empty.' ) ;
return ;
}
if ( ! data . tags || ! data . tag _map || ! Array . isArray ( data . tags ) || typeof data . tag _map !== 'object' ) {
2023-11-15 01:09:40 +01:00
toastr . warning ( 'Invalid file format' , 'Tag restore' ) ;
2023-11-14 23:59:44 +01:00
console . log ( 'Tag restore: Invalid file format.' ) ;
return ;
}
const warnings = [ ] ;
// Import tags
for ( const tag of data . tags ) {
if ( ! tag . id || ! tag . name ) {
warnings . push ( ` Tag object is invalid: ${ JSON . stringify ( tag ) } . ` ) ;
continue ;
}
if ( tags . find ( x => x . id === tag . id ) ) {
warnings . push ( ` Tag with id ${ tag . id } already exists. ` ) ;
continue ;
}
tags . push ( tag ) ;
}
// Import tag_map
for ( const key of Object . keys ( data . tag _map ) ) {
const tagIds = data . tag _map [ key ] ;
if ( ! Array . isArray ( tagIds ) ) {
warnings . push ( ` Tag map for key ${ key } is invalid: ${ JSON . stringify ( tagIds ) } . ` ) ;
continue ;
}
// Verify that the key points to a valid character or group.
const characterExists = characters . some ( x => String ( x . avatar ) === String ( key ) ) ;
const groupExists = groups . some ( x => String ( x . id ) === String ( key ) ) ;
if ( ! characterExists && ! groupExists ) {
warnings . push ( ` Tag map key ${ key } does not exist. ` ) ;
continue ;
}
// Get existing tag ids for this key or empty array.
const existingTagIds = tag _map [ key ] || [ ] ;
// Merge existing and new tag ids. Remove duplicates.
tag _map [ key ] = existingTagIds . concat ( tagIds ) . filter ( onlyUnique ) ;
// Verify that all tags exist. Remove tags that don't exist.
tag _map [ key ] = tag _map [ key ] . filter ( x => tags . some ( y => String ( y . id ) === String ( x ) ) ) ;
}
if ( warnings . length ) {
toastr . success ( 'Tags restored with warnings. Check console for details.' ) ;
console . warn ( ` TAG RESTORE REPORT \n ==================== \n ${ warnings . join ( '\n' ) } ` ) ;
} else {
toastr . success ( 'Tags restored successfully.' ) ;
}
$ ( '#tag_view_restore_input' ) . val ( '' ) ;
saveSettingsDebounced ( ) ;
printCharacters ( true ) ;
onViewTagsListClick ( ) ;
}
function onBackupRestoreClick ( ) {
$ ( '#tag_view_restore_input' )
. off ( 'change' )
. on ( 'change' , onTagRestoreFileSelect )
. trigger ( 'click' ) ;
}
function onTagsBackupClick ( ) {
const timestamp = new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] . replace ( /-/g , '' ) ;
const filename = ` tags_ ${ timestamp } .json ` ;
const data = {
tags : tags ,
tag _map : tag _map ,
} ;
const blob = new Blob ( [ JSON . stringify ( data , null , 2 ) ] , { type : 'application/json' } ) ;
download ( blob , filename , 'application/json' ) ;
}
2023-11-10 20:56:25 +01:00
function onTagCreateClick ( ) {
const tag = createNewTag ( 'New Tag' ) ;
2024-02-18 08:42:36 +01:00
appendViewTagToList ( $ ( '#tag_view_list .tag_view_list_tags' ) , tag , [ ] ) ;
2023-11-10 20:56:25 +01:00
printCharacters ( false ) ;
saveSettingsDebounced ( ) ;
}
2023-09-16 11:37:19 +02:00
2023-11-10 20:56:25 +01:00
function appendViewTagToList ( list , tag , everything ) {
const count = everything . filter ( x => x == tag . id ) . length ;
const template = $ ( '#tag_view_template .tag_view_item' ) . clone ( ) ;
template . attr ( 'id' , tag . id ) ;
template . find ( '.tag_view_counter_value' ) . text ( count ) ;
template . find ( '.tag_view_name' ) . text ( tag . name ) ;
template . find ( '.tag_view_name' ) . addClass ( 'tag' ) ;
2023-07-20 19:32:15 +02:00
2023-11-10 20:56:25 +01:00
template . find ( '.tag_view_name' ) . css ( 'background-color' , tag . color ) ;
template . find ( '.tag_view_name' ) . css ( 'color' , tag . color2 ) ;
2023-09-16 11:37:19 +02:00
2024-02-18 08:42:36 +01:00
const tagAsFolderId = tag . id + '-tag-folder' ;
2023-12-02 19:04:51 +01:00
const colorPickerId = tag . id + '-tag-color' ;
const colorPicker2Id = tag . id + '-tag-color2' ;
2023-07-20 19:32:15 +02:00
2024-02-18 08:42:36 +01:00
if ( ! power _user . bogus _folders ) {
template . find ( '.tag_as_folder' ) . hide ( ) ;
}
2023-11-10 20:56:25 +01:00
template . find ( '.tagColorPickerHolder' ) . html (
2023-12-02 21:06:57 +01:00
` <toolcool-color-picker id=" ${ colorPickerId } " color=" ${ tag . color } " class="tag-color"></toolcool-color-picker> ` ,
2023-11-10 20:56:25 +01:00
) ;
template . find ( '.tagColorPicker2Holder' ) . html (
2023-12-02 21:06:57 +01:00
` <toolcool-color-picker id=" ${ colorPicker2Id } " color=" ${ tag . color2 } " class="tag-color2"></toolcool-color-picker> ` ,
2023-11-10 20:56:25 +01:00
) ;
2023-09-16 11:37:19 +02:00
2024-02-18 08:42:36 +01:00
template . find ( '.tag_as_folder' ) . attr ( 'id' , tagAsFolderId ) ;
2023-11-10 20:56:25 +01:00
template . find ( '.tag-color' ) . attr ( 'id' , colorPickerId ) ;
template . find ( '.tag-color2' ) . attr ( 'id' , colorPicker2Id ) ;
2023-09-16 11:37:19 +02:00
2023-11-10 20:56:25 +01:00
list . append ( template ) ;
2023-07-20 19:32:15 +02:00
2023-11-10 20:56:25 +01:00
setTimeout ( function ( ) {
document . querySelector ( ` .tag-color[id=" ${ colorPickerId } " ` ) . addEventListener ( 'change' , ( evt ) => {
onTagColorize ( evt ) ;
} ) ;
} , 100 ) ;
2023-07-20 19:32:15 +02:00
2023-11-10 20:56:25 +01:00
setTimeout ( function ( ) {
document . querySelector ( ` .tag-color2[id=" ${ colorPicker2Id } " ` ) . addEventListener ( 'change' , ( evt ) => {
onTagColorize2 ( evt ) ;
} ) ;
} , 100 ) ;
2024-03-06 00:28:14 +01:00
updateDrawTagFolder ( template , tag ) ;
2024-02-18 08:42:36 +01:00
// @ts-ignore
2023-11-10 20:56:25 +01:00
$ ( colorPickerId ) . color = tag . color ;
2024-02-18 08:42:36 +01:00
// @ts-ignore
2023-11-10 20:56:25 +01:00
$ ( colorPicker2Id ) . color = tag . color2 ;
2023-07-20 19:32:15 +02:00
}
2024-02-18 08:42:36 +01:00
function onTagAsFolderClick ( ) {
2024-03-06 00:28:14 +01:00
const element = $ ( this ) . closest ( '.tag_view_item' ) ;
const id = element . attr ( 'id' ) ;
2024-02-18 08:42:36 +01:00
const tag = tags . find ( x => x . id === id ) ;
2024-03-06 00:28:14 +01:00
// Cycle through folder types
const types = Object . keys ( TAG _FOLDER _TYPES ) ;
2024-03-06 23:13:22 +01:00
const currentTypeIndex = types . indexOf ( tag . folder _type ) ;
2024-03-06 00:28:14 +01:00
tag . folder _type = types [ ( currentTypeIndex + 1 ) % types . length ] ;
updateDrawTagFolder ( element , tag ) ;
2024-02-18 08:42:36 +01:00
// If folder display has changed, we have to redraw the character list, otherwise this folders state would not change
printCharacters ( true ) ;
saveSettingsDebounced ( ) ;
}
2024-03-06 00:28:14 +01:00
function updateDrawTagFolder ( element , tag ) {
const tagFolder = TAG _FOLDER _TYPES [ tag . folder _type ] || TAG _FOLDER _TYPES [ TAG _FOLDER _DEFAULT _TYPE ] ;
const folderElement = element . find ( '.tag_as_folder' ) ;
// Update css class and remove all others
Object . keys ( TAG _FOLDER _TYPES ) . forEach ( x => {
folderElement . toggleClass ( TAG _FOLDER _TYPES [ x ] . class , TAG _FOLDER _TYPES [ x ] === tagFolder ) ;
} ) ;
// Draw/update css attributes for this class
folderElement . attr ( 'title' , tagFolder . tooltip ) ;
const indicator = folderElement . find ( '.tag_folder_indicator' ) ;
indicator . text ( tagFolder . icon ) ;
indicator . css ( 'color' , tagFolder . color ) ;
indicator . css ( 'font-size' , ` calc(var(--mainFontSize) * ${ tagFolder . size } ) ` ) ;
}
2023-07-20 19:32:15 +02:00
function onTagDeleteClick ( ) {
2023-12-02 19:04:51 +01:00
if ( ! confirm ( 'Are you sure?' ) ) {
2023-07-20 19:32:15 +02:00
return ;
}
const id = $ ( this ) . closest ( '.tag_view_item' ) . attr ( 'id' ) ;
for ( const key of Object . keys ( tag _map ) ) {
tag _map [ key ] = tag _map [ key ] . filter ( x => x . id !== id ) ;
}
const index = tags . findIndex ( x => x . id === id ) ;
tags . splice ( index , 1 ) ;
$ ( ` .tag[id=" ${ id } "] ` ) . remove ( ) ;
$ ( ` .tag_view_item[id=" ${ id } "] ` ) . remove ( ) ;
2023-11-10 20:56:25 +01:00
printCharacters ( false ) ;
2023-07-20 19:32:15 +02:00
saveSettingsDebounced ( ) ;
}
function onTagRenameInput ( ) {
const id = $ ( this ) . closest ( '.tag_view_item' ) . attr ( 'id' ) ;
const newName = $ ( this ) . text ( ) ;
const tag = tags . find ( x => x . id === id ) ;
tag . name = newName ;
$ ( ` .tag[id=" ${ id } "] .tag_name ` ) . text ( newName ) ;
saveSettingsDebounced ( ) ;
}
function onTagColorize ( evt ) {
console . debug ( evt ) ;
const id = $ ( evt . target ) . closest ( '.tag_view_item' ) . attr ( 'id' ) ;
const newColor = evt . detail . rgba ;
$ ( evt . target ) . parent ( ) . parent ( ) . find ( '.tag_view_name' ) . css ( 'background-color' , newColor ) ;
$ ( ` .tag[id=" ${ id } "] ` ) . css ( 'background-color' , newColor ) ;
2023-11-10 20:56:25 +01:00
$ ( ` .bogus_folder_select[tagid=" ${ id } "] .avatar ` ) . css ( 'background-color' , newColor ) ;
2023-07-20 19:32:15 +02:00
const tag = tags . find ( x => x . id === id ) ;
tag . color = newColor ;
2023-09-16 11:37:19 +02:00
console . debug ( tag ) ;
saveSettingsDebounced ( ) ;
}
function onTagColorize2 ( evt ) {
console . debug ( evt ) ;
const id = $ ( evt . target ) . closest ( '.tag_view_item' ) . attr ( 'id' ) ;
const newColor = evt . detail . rgba ;
$ ( evt . target ) . parent ( ) . parent ( ) . find ( '.tag_view_name' ) . css ( 'color' , newColor ) ;
$ ( ` .tag[id=" ${ id } "] ` ) . css ( 'color' , newColor ) ;
2023-11-10 20:56:25 +01:00
$ ( ` .bogus_folder_select[tagid=" ${ id } "] .avatar ` ) . css ( 'color' , newColor ) ;
2023-09-16 11:37:19 +02:00
const tag = tags . find ( x => x . id === id ) ;
tag . color2 = newColor ;
2023-07-20 19:32:15 +02:00
console . debug ( tag ) ;
saveSettingsDebounced ( ) ;
}
function onTagListHintClick ( ) {
$ ( this ) . toggleClass ( 'selected' ) ;
2023-12-02 19:04:51 +01:00
$ ( this ) . siblings ( '.tag:not(.actionable)' ) . toggle ( 100 ) ;
$ ( this ) . siblings ( '.innerActionable' ) . toggleClass ( 'hidden' ) ;
2024-02-19 03:15:45 +01:00
power _user . show _tag _filters = $ ( this ) . hasClass ( 'selected' ) ;
saveSettingsDebounced ( ) ;
2024-03-06 00:28:14 +01:00
console . debug ( 'show_tag_filters' , power _user . show _tag _filters ) ;
2023-07-20 19:32:15 +02:00
}
2024-03-07 04:26:33 +01:00
function onClearAllFiltersClick ( ) {
console . debug ( 'clear all filters clicked' ) ;
// We have to manually go through the elements and unfilter by clicking...
// Thankfully nearly all filter controls are three-state-toggles
const filterTags = $ ( '.rm_tag_controls .rm_tag_filter' ) . find ( '.tag' ) ;
for ( const tag of filterTags ) {
const toggleState = $ ( tag ) . attr ( 'data-toggle-state' ) ;
if ( toggleState !== undefined && ! isFilterState ( toggleState ? ? FILTER _STATES . UNDEFINED , FILTER _STATES . UNDEFINED ) ) {
toggleTagThreeState ( $ ( tag ) , { stateOverride : FILTER _STATES . UNDEFINED , simulateClick : true } ) ;
}
}
// Reset search too
$ ( '#character_search_bar' ) . val ( '' ) . trigger ( 'input' ) ;
}
2023-11-15 00:06:27 +01:00
jQuery ( ( ) => {
2023-07-20 19:32:15 +02:00
createTagInput ( '#tagInput' , '#tagList' ) ;
createTagInput ( '#groupTagInput' , '#groupTagList' ) ;
2023-12-02 19:04:51 +01:00
$ ( document ) . on ( 'click' , '#rm_button_create' , onCharacterCreateClick ) ;
$ ( document ) . on ( 'click' , '#rm_button_group_chats' , onGroupCreateClick ) ;
$ ( document ) . on ( 'click' , '.character_select' , applyTagsOnCharacterSelect ) ;
$ ( document ) . on ( 'click' , '.group_select' , applyTagsOnGroupSelect ) ;
$ ( document ) . on ( 'click' , '.tag_remove' , onTagRemoveClick ) ;
$ ( document ) . on ( 'input' , '.tag_input' , onTagInput ) ;
$ ( document ) . on ( 'click' , '.tags_view' , onViewTagsListClick ) ;
$ ( document ) . on ( 'click' , '.tag_delete' , onTagDeleteClick ) ;
2024-02-18 08:42:36 +01:00
$ ( document ) . on ( 'click' , '.tag_as_folder' , onTagAsFolderClick ) ;
2023-12-02 19:04:51 +01:00
$ ( document ) . on ( 'input' , '.tag_view_name' , onTagRenameInput ) ;
$ ( document ) . on ( 'click' , '.tag_view_create' , onTagCreateClick ) ;
$ ( document ) . on ( 'click' , '.tag_view_backup' , onTagsBackupClick ) ;
$ ( document ) . on ( 'click' , '.tag_view_restore' , onBackupRestoreClick ) ;
2023-07-20 19:32:15 +02:00
} ) ;
2024-02-18 08:42:36 +01:00