2023-12-02 19:04:51 +01:00
'use strict' ;
2023-10-21 15:12:09 +02:00
import {
2024-03-30 03:06:40 +01:00
characterGroupOverlay ,
2023-10-21 15:12:09 +02:00
callPopup ,
2023-11-04 19:33:15 +01:00
characters ,
2023-10-21 15:12:09 +02:00
event _types ,
eventSource ,
2023-12-09 15:09:10 +01:00
getCharacters ,
2023-11-04 19:33:15 +01:00
getRequestHeaders ,
2024-03-27 08:22:03 +01:00
buildAvatarList ,
characterToEntity ,
2024-03-30 03:06:40 +01:00
printCharactersDebounced ,
2024-05-22 23:52:35 +02:00
deleteCharacter ,
2023-12-02 19:04:51 +01:00
} from '../script.js' ;
2023-11-05 18:23:14 +01:00
2023-12-02 19:04:51 +01:00
import { favsToHotswap } from './RossAscends-mods.js' ;
import { hideLoader , showLoader } from './loader.js' ;
import { convertCharacterToPersona } from './personas.js' ;
2024-08-16 22:43:22 +02:00
import { createTagInput , getTagKeyForEntity , getTagsList , printTagList , tag _map , compareTagsForSort , removeTagFromMap , importTags , tag _import _setting } from './tags.js' ;
2023-10-21 15:12:09 +02:00
2023-11-05 18:23:14 +01:00
/ * *
* Static object representing the actions of the
* character context menu override .
* /
2023-10-21 15:12:09 +02:00
class CharacterContextMenu {
2023-11-05 18:23:14 +01:00
/ * *
* Tag one or more characters ,
* opens a popup .
*
2024-03-29 04:41:16 +01:00
* @ param { Array < number > } selectedCharacters
2023-11-05 18:23:14 +01:00
* /
2023-11-04 19:33:15 +01:00
static tag = ( selectedCharacters ) => {
2024-03-30 03:06:40 +01:00
characterGroupOverlay . bulkTagPopupHandler . show ( selectedCharacters ) ;
2023-12-02 20:11:06 +01:00
} ;
2023-11-04 19:33:15 +01:00
2023-10-21 15:12:09 +02:00
/ * *
2023-11-05 18:23:14 +01:00
* Duplicate one or more characters
2023-10-21 15:12:09 +02:00
*
2024-03-29 04:41:16 +01:00
* @ param { number } characterId
2024-03-28 00:13:54 +01:00
* @ returns { Promise < any > }
2023-10-21 15:12:09 +02:00
* /
static duplicate = async ( characterId ) => {
2023-11-05 18:23:14 +01:00
const character = CharacterContextMenu . # getCharacter ( characterId ) ;
2024-03-28 00:13:54 +01:00
const body = { avatar _url : character . avatar } ;
2023-10-21 15:12:09 +02:00
2024-03-28 00:13:54 +01:00
const result = await fetch ( '/api/characters/duplicate' , {
2023-10-21 15:12:09 +02:00
method : 'POST' ,
headers : getRequestHeaders ( ) ,
2024-03-28 00:15:14 +01:00
body : JSON . stringify ( body ) ,
2023-10-21 15:12:09 +02:00
} ) ;
2024-03-28 00:13:54 +01:00
if ( ! result . ok ) {
throw new Error ( 'Character not duplicated' ) ;
}
const data = await result . json ( ) ;
await eventSource . emit ( event _types . CHARACTER _DUPLICATED , { oldAvatar : body . avatar _url , newAvatar : data . path } ) ;
2023-12-02 20:11:06 +01:00
} ;
2023-10-21 15:12:09 +02:00
/ * *
* Favorite a character
2023-11-05 18:23:14 +01:00
* and highlight it .
2023-10-21 15:12:09 +02:00
*
2024-03-29 04:41:16 +01:00
* @ param { number } characterId
2023-10-21 15:12:09 +02:00
* @ returns { Promise < void > }
* /
static favorite = async ( characterId ) => {
2023-11-05 18:23:14 +01:00
const character = CharacterContextMenu . # getCharacter ( characterId ) ;
2023-12-06 00:56:07 +01:00
const newFavState = ! character . data . extensions . fav ;
2023-11-05 18:23:14 +01:00
2023-10-21 15:12:09 +02:00
const data = {
name : character . name ,
avatar : character . avatar ,
data : {
extensions : {
2023-12-06 00:56:07 +01:00
fav : newFavState ,
2023-12-02 21:06:57 +01:00
} ,
} ,
2023-12-06 00:56:07 +01:00
fav : newFavState ,
2023-10-21 15:12:09 +02:00
} ;
2023-12-06 00:56:07 +01:00
const mergeResponse = await fetch ( '/api/characters/merge-attributes' , {
2023-12-02 19:04:51 +01:00
method : 'POST' ,
2023-10-21 15:12:09 +02:00
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( data ) ,
} ) ;
2023-12-06 00:56:07 +01:00
if ( ! mergeResponse . ok ) {
mergeResponse . json ( ) . then ( json => toastr . error ( ` Character not saved. Error: ${ json . message } . Field: ${ json . error } ` ) ) ;
}
const element = document . getElementById ( ` CharID ${ characterId } ` ) ;
element . classList . toggle ( 'is_fav' ) ;
2023-12-02 20:11:06 +01:00
} ;
2023-10-21 15:12:09 +02:00
2023-11-05 17:43:35 +01:00
/ * *
2023-11-05 18:23:14 +01:00
* Convert one or more characters to persona ,
* may open a popup for one or more characters .
2023-11-05 17:43:35 +01:00
*
2024-03-29 04:41:16 +01:00
* @ param { number } characterId
2023-11-05 17:43:35 +01:00
* @ returns { Promise < void > }
* /
static persona = async ( characterId ) => await convertCharacterToPersona ( characterId ) ;
2023-10-21 15:12:09 +02:00
2023-11-05 18:23:14 +01:00
/ * *
* Delete one or more characters ,
* opens a popup .
*
2024-08-30 18:52:57 +02:00
* @ param { string | string [ ] } characterKey
2024-03-29 04:41:16 +01:00
* @ param { boolean } [ deleteChats ]
2023-11-05 18:23:14 +01:00
* @ returns { Promise < void > }
* /
2024-08-30 18:52:57 +02:00
static delete = async ( characterKey , deleteChats = false ) => {
await deleteCharacter ( characterKey , { deleteChats : deleteChats } ) ;
2023-12-02 20:11:06 +01:00
} ;
2023-10-21 15:12:09 +02:00
2023-11-05 18:23:14 +01:00
static # getCharacter = ( characterId ) => characters [ characterId ] ? ? null ;
2023-10-21 15:12:09 +02:00
2023-11-05 18:23:14 +01:00
/ * *
* Show the context menu at the given position
*
* @ param positionX
* @ param positionY
* /
2023-10-21 15:12:09 +02:00
static show = ( positionX , positionY ) => {
2023-10-30 19:26:41 +01:00
let contextMenu = document . getElementById ( BulkEditOverlay . contextMenuId ) ;
2023-10-21 15:12:09 +02:00
contextMenu . style . left = ` ${ positionX } px ` ;
contextMenu . style . top = ` ${ positionY } px ` ;
2023-10-30 19:26:41 +01:00
document . getElementById ( BulkEditOverlay . contextMenuId ) . classList . remove ( 'hidden' ) ;
2023-11-09 22:55:14 +01:00
// Adjust position if context menu is outside of viewport
const boundingRect = contextMenu . getBoundingClientRect ( ) ;
if ( boundingRect . right > window . innerWidth ) {
contextMenu . style . left = ` ${ positionX - ( boundingRect . right - window . innerWidth ) } px ` ;
}
if ( boundingRect . bottom > window . innerHeight ) {
contextMenu . style . top = ` ${ positionY - ( boundingRect . bottom - window . innerHeight ) } px ` ;
}
2023-12-02 20:11:06 +01:00
} ;
2023-10-21 15:12:09 +02:00
2023-11-05 18:23:14 +01:00
/ * *
* Hide the context menu
* /
2023-10-30 19:26:41 +01:00
static hide = ( ) => document . getElementById ( BulkEditOverlay . contextMenuId ) . classList . add ( 'hidden' ) ;
2023-10-21 15:12:09 +02:00
2023-11-05 18:23:14 +01:00
/ * *
* Sets up the context menu for the given overlay
*
* @ param characterGroupOverlay
* /
2023-10-21 15:12:09 +02:00
constructor ( characterGroupOverlay ) {
const contextMenuItems = [
2023-11-08 00:10:51 +01:00
{ id : 'character_context_menu_favorite' , callback : characterGroupOverlay . handleContextMenuFavorite } ,
{ id : 'character_context_menu_duplicate' , callback : characterGroupOverlay . handleContextMenuDuplicate } ,
{ id : 'character_context_menu_delete' , callback : characterGroupOverlay . handleContextMenuDelete } ,
{ id : 'character_context_menu_persona' , callback : characterGroupOverlay . handleContextMenuPersona } ,
2023-12-02 21:06:57 +01:00
{ id : 'character_context_menu_tag' , callback : characterGroupOverlay . handleContextMenuTag } ,
2023-10-21 15:12:09 +02:00
] ;
2023-12-02 20:11:06 +01:00
contextMenuItems . forEach ( contextMenuItem => document . getElementById ( contextMenuItem . id ) . addEventListener ( 'click' , contextMenuItem . callback ) ) ;
2023-10-21 15:12:09 +02:00
}
}
2023-11-04 19:33:15 +01:00
/ * *
2023-11-05 18:23:14 +01:00
* Represents a tag control not bound to a single character
2023-11-04 19:33:15 +01:00
* /
class BulkTagPopupHandler {
2024-03-30 03:06:40 +01:00
/ * *
* The characters for this popup
* @ type { number [ ] }
* /
characterIds ;
/ * *
* A storage of the current mutual tags , as calculated by getMutualTags ( )
* @ type { object [ ] }
* /
currentMutualTags ;
/ * *
* Sets up the bulk popup menu handler for the given overlay .
*
* Characters can be passed in with the show ( ) call .
* /
constructor ( ) { }
2024-03-29 05:53:26 +01:00
/ * *
* Gets the HTML as a string that is going to be the popup for the bulk tag edit
*
* @ returns String containing the html for the popup
* /
2024-03-30 03:06:40 +01:00
# getHtml = ( ) => {
const characterData = JSON . stringify ( { characterIds : this . characterIds } ) ;
2023-11-04 19:33:15 +01:00
return ` <div id="bulk_tag_shadow_popup">
2024-08-16 22:00:01 +02:00
< div id = "bulk_tag_popup" class = "wider_dialogue_popup" >
2023-11-04 19:33:15 +01:00
< div id = "bulk_tag_popup_holder" >
2024-03-30 03:06:40 +01:00
< h3 class = "marginBot5" > Modify tags of $ { this . characterIds . length } characters < / h 3 >
2024-08-15 15:47:04 +02:00
< small class = "bulk_tags_desc m-b-1" > Add or remove the mutual tags of all selected characters . Import all or existing tags for all selected characters . < / s m a l l >
2024-03-27 08:22:03 +01:00
< div id = "bulk_tags_avatars_block" class = "avatars_inline avatars_inline_small tags tags_inline" > < / d i v >
2024-03-27 03:36:09 +01:00
< br >
2023-11-04 19:33:15 +01:00
< div id = "bulk_tags_div" class = "marginBot5" data - characters = '${characterData}' >
< div class = "tag_controls" >
< input id = "bulkTagInput" class = "text_pole tag_input wide100p margin0" data - i18n = "[placeholder]Search / Create Tags" placeholder = "Search / Create tags" maxlength = "25" / >
< div class = "tags_view menu_button fa-solid fa-tags" title = "View all tags" data - i18n = "[title]View all tags" > < / d i v >
< / d i v >
< div id = "bulkTagList" class = "m-t-1 tags" > < / d i v >
< / d i v >
< div id = "dialogue_popup_controls" class = "m-t-1" >
2024-03-27 03:36:09 +01:00
< div id = "bulk_tag_popup_reset" class = "menu_button" title = "Remove all tags from the selected characters" data - i18n = "[title]Remove all tags from the selected characters" >
< i class = "fa-solid fa-trash-can margin-right-10px" > < / i >
All
< / d i v >
< div id = "bulk_tag_popup_remove_mutual" class = "menu_button" title = "Remove all mutual tags from the selected characters" data - i18n = "[title]Remove all mutual tags from the selected characters" >
< i class = "fa-solid fa-trash-can margin-right-10px" > < / i >
Mutual
< / d i v >
2024-08-15 15:36:46 +02:00
< div id = "bulk_tag_popup_import_all_tags" class = "menu_button" title = "Import all tags from selected characters" data - i18n = "[title]Import all tags from selected characters" >
Import All
< / d i v >
2024-08-15 15:47:04 +02:00
< div id = "bulk_tag_popup_import_existing_tags" class = "menu_button" title = "Import existing tags from selected characters" data - i18n = "[title]Import existing tags from selected characters" >
Import Existing
< / d i v >
2023-11-04 19:33:15 +01:00
< div id = "bulk_tag_popup_cancel" class = "menu_button" data - i18n = "Cancel" > Close < / d i v >
< / d i v >
< / d i v >
< / d i v >
2024-03-29 05:53:26 +01:00
< / d i v > ` ;
2023-11-04 19:33:15 +01:00
} ;
2023-11-05 18:23:14 +01:00
/ * *
* Append and show the tag control
*
2024-03-30 03:06:40 +01:00
* @ param { number [ ] } characterIds - The characters that are shown inside the popup
2023-11-05 18:23:14 +01:00
* /
2024-03-30 03:06:40 +01:00
show ( characterIds ) {
// shallow copy character ids persistently into this tooltip
this . characterIds = characterIds . slice ( ) ;
if ( this . characterIds . length == 0 ) {
2024-03-27 03:36:09 +01:00
console . log ( 'No characters selected for bulk edit tags.' ) ;
return ;
}
2024-03-30 03:06:40 +01:00
document . body . insertAdjacentHTML ( 'beforeend' , this . # getHtml ( ) ) ;
2024-03-27 03:36:09 +01:00
2024-03-30 03:06:40 +01:00
const entities = this . characterIds . map ( id => characterToEntity ( characters [ id ] , id ) ) . filter ( entity => entity . item !== undefined ) ;
2024-03-27 08:22:03 +01:00
buildAvatarList ( $ ( '#bulk_tags_avatars_block' ) , entities ) ;
2024-03-27 03:36:09 +01:00
// Print the tag list with all mutuable tags, marking them as removable. That is the initial fill
2024-03-30 03:06:40 +01:00
printTagList ( $ ( '#bulkTagList' ) , { tags : ( ) => this . getMutualTags ( ) , tagOptions : { removable : true } } ) ;
2024-03-27 03:36:09 +01:00
2024-03-29 04:41:16 +01:00
// Tag input with resolvable list for the mutual tags to get redrawn, so that newly added tags get sorted correctly
2024-04-12 13:22:12 +02:00
createTagInput ( '#bulkTagInput' , '#bulkTagList' , { tags : ( ) => this . getMutualTags ( ) , tagOptions : { removable : true } } ) ;
2024-03-27 03:36:09 +01:00
2024-03-30 03:06:40 +01:00
document . querySelector ( '#bulk_tag_popup_reset' ) . addEventListener ( 'click' , this . resetTags . bind ( this ) ) ;
document . querySelector ( '#bulk_tag_popup_remove_mutual' ) . addEventListener ( 'click' , this . removeMutual . bind ( this ) ) ;
2023-11-04 19:33:15 +01:00
document . querySelector ( '#bulk_tag_popup_cancel' ) . addEventListener ( 'click' , this . hide . bind ( this ) ) ;
2024-08-15 15:36:46 +02:00
document . querySelector ( '#bulk_tag_popup_import_all_tags' ) . addEventListener ( 'click' , this . importAllTags . bind ( this ) ) ;
2024-08-15 15:47:04 +02:00
document . querySelector ( '#bulk_tag_popup_import_existing_tags' ) . addEventListener ( 'click' , this . importExistingTags . bind ( this ) ) ;
}
/ * *
* Import existing tags for all selected characters
* /
async importExistingTags ( ) {
for ( const characterId of this . characterIds ) {
2024-08-16 22:43:22 +02:00
await importTags ( characters [ characterId ] , { importSetting : tag _import _setting . ONLY _EXISTING } ) ;
2024-08-15 15:47:04 +02:00
}
$ ( '#bulkTagList' ) . empty ( ) ;
2024-08-15 15:36:46 +02:00
}
/ * *
* Import all tags for all selected characters
* /
async importAllTags ( ) {
for ( const characterId of this . characterIds ) {
2024-08-16 22:43:22 +02:00
await importTags ( characters [ characterId ] , { importSetting : tag _import _setting . ALL } ) ;
2024-08-15 15:36:46 +02:00
}
$ ( '#bulkTagList' ) . empty ( ) ;
2024-03-27 03:36:09 +01:00
}
2024-03-29 04:41:16 +01:00
/ * *
* Builds a list of all tags that the provided characters have in common .
*
* @ returns { Array < object > } A list of mutual tags
* /
2024-03-30 03:06:40 +01:00
getMutualTags ( ) {
if ( this . characterIds . length == 0 ) {
2024-03-27 03:36:09 +01:00
return [ ] ;
}
2024-03-30 03:06:40 +01:00
if ( this . characterIds . length === 1 ) {
2024-03-27 03:36:09 +01:00
// Just use tags of the single character
2024-03-30 03:06:40 +01:00
return getTagsList ( getTagKeyForEntity ( this . characterIds [ 0 ] ) ) ;
2024-03-27 03:36:09 +01:00
}
// Find mutual tags for multiple characters
2024-03-30 03:06:40 +01:00
const allTags = this . characterIds . map ( cid => getTagsList ( getTagKeyForEntity ( cid ) ) ) ;
2024-03-27 03:36:09 +01:00
const mutualTags = allTags . reduce ( ( mutual , characterTags ) =>
2024-04-12 13:22:12 +02:00
mutual . filter ( tag => characterTags . some ( cTag => cTag . id === tag . id ) ) ,
2024-03-27 03:36:09 +01:00
) ;
2024-03-30 03:06:40 +01:00
this . currentMutualTags = mutualTags . sort ( compareTagsForSort ) ;
return this . currentMutualTags ;
2023-11-04 19:33:15 +01:00
}
2023-11-05 18:23:14 +01:00
/ * *
* Hide and remove the tag control
* /
2024-03-30 03:06:40 +01:00
hide ( ) {
2023-11-04 19:33:15 +01:00
let popupElement = document . querySelector ( '#bulk_tag_shadow_popup' ) ;
if ( popupElement ) {
document . body . removeChild ( popupElement ) ;
}
2023-11-07 19:43:24 +01:00
2024-03-30 03:06:40 +01:00
// No need to redraw here, all tags actions were redrawn when they happened
2023-11-04 19:33:15 +01:00
}
2023-11-05 18:23:14 +01:00
/ * *
* Empty the tag map for the given characters
* /
2024-03-30 03:06:40 +01:00
resetTags ( ) {
for ( const characterId of this . characterIds ) {
2024-03-07 23:48:50 +01:00
const key = getTagKeyForEntity ( characterId ) ;
2023-11-04 19:33:15 +01:00
if ( key ) tag _map [ key ] = [ ] ;
2024-03-27 03:36:09 +01:00
}
$ ( '#bulkTagList' ) . empty ( ) ;
2024-03-30 03:06:40 +01:00
printCharactersDebounced ( ) ;
2024-03-27 03:36:09 +01:00
}
/ * *
2024-03-29 04:41:16 +01:00
* Remove the mutual tags for all given characters
2024-03-27 03:36:09 +01:00
* /
2024-03-30 03:06:40 +01:00
removeMutual ( ) {
const mutualTags = this . getMutualTags ( ) ;
2024-03-27 03:36:09 +01:00
2024-03-30 03:06:40 +01:00
for ( const characterId of this . characterIds ) {
2024-08-30 18:52:57 +02:00
for ( const tag of mutualTags ) {
2024-03-27 03:36:09 +01:00
removeTagFromMap ( tag . id , characterId ) ;
}
}
$ ( '#bulkTagList' ) . empty ( ) ;
2023-11-07 19:43:24 +01:00
2024-03-30 03:06:40 +01:00
printCharactersDebounced ( ) ;
2023-11-04 19:33:15 +01:00
}
}
2023-11-06 17:20:18 +01:00
class BulkEditOverlayState {
2023-11-05 18:23:14 +01:00
/ * *
*
* @ type { number }
* /
static browse = 0 ;
/ * *
*
* @ type { number }
* /
static select = 1 ;
}
2023-11-04 19:33:15 +01:00
/ * *
* Implement a SingletonPattern , allowing access to the group overlay instance
* from everywhere via ( new CharacterGroupOverlay ( ) )
*
* @ type BulkEditOverlay
* /
let bulkEditOverlayInstance = null ;
2023-10-30 19:26:41 +01:00
class BulkEditOverlay {
2023-10-21 15:12:09 +02:00
static containerId = 'rm_print_characters_block' ;
static contextMenuId = 'character_context_menu' ;
static characterClass = 'character_select' ;
2023-11-08 20:48:48 +01:00
static groupClass = 'group_select' ;
2023-11-10 21:18:48 +01:00
static bogusFolderClass = 'bogus_folder_select' ;
2023-10-21 15:12:09 +02:00
static selectModeClass = 'group_overlay_mode_select' ;
static selectedClass = 'character_selected' ;
2023-11-05 17:18:46 +01:00
static legacySelectedClass = 'bulk_select_checkbox' ;
2024-03-27 08:22:03 +01:00
static bulkSelectedCountId = 'bulkSelectedCount' ;
2023-10-21 15:12:09 +02:00
2023-11-08 00:10:51 +01:00
static longPressDelay = 2500 ;
2023-11-06 15:31:27 +01:00
2023-11-06 17:20:18 +01:00
# state = BulkEditOverlayState . browse ;
2023-10-21 15:12:09 +02:00
# longPress = false ;
# stateChangeCallbacks = [ ] ;
# selectedCharacters = [ ] ;
2024-03-30 03:06:40 +01:00
# bulkTagPopupHandler = new BulkTagPopupHandler ( ) ;
2023-10-21 15:12:09 +02:00
2024-03-27 08:22:03 +01:00
/ * *
* @ typedef { object } LastSelected - An object noting the last selected character and its state .
* @ property { string } [ characterId ] - The character id of the last selected character .
* @ property { boolean } [ select ] - The selected state of the last selected character . < c > true < / c > i f i t w a s s e l e c t e d , < c > f a l s e < / c > i f i t w a s d e s e l e c t e d .
* /
/ * *
* @ type { LastSelected } - An object noting the last selected character and its state .
* /
lastSelected = { characterId : undefined , select : undefined } ;
2023-11-08 21:08:31 +01:00
/ * *
2023-11-09 15:24:24 +01:00
* Locks other pointer actions when the context menu is open
2023-11-08 21:08:31 +01:00
*
* @ type { boolean }
* /
2023-11-09 15:18:59 +01:00
# contextMenuOpen = false ;
/ * *
* Whether the next character select should be skipped
*
* @ type { boolean }
* /
# cancelNextToggle = false ;
2023-11-08 21:08:31 +01:00
2023-10-21 15:12:09 +02:00
/ * *
* @ type HTMLElement
* /
container = null ;
get state ( ) {
return this . # state ;
}
set state ( newState ) {
if ( this . # state === newState ) return ;
eventSource . emit ( event _types . CHARACTER _GROUP _OVERLAY _STATE _CHANGE _BEFORE , newState )
. then ( ( ) => {
this . # state = newState ;
2023-12-02 20:11:06 +01:00
eventSource . emit ( event _types . CHARACTER _GROUP _OVERLAY _STATE _CHANGE _AFTER , this . state ) ;
2023-10-21 15:12:09 +02:00
} ) ;
}
get isLongPress ( ) {
return this . # longPress ;
}
set isLongPress ( longPress ) {
this . # longPress = longPress ;
}
get stateChangeCallbacks ( ) {
return this . # stateChangeCallbacks ;
}
2023-11-04 19:33:15 +01:00
/ * *
*
2024-03-29 05:53:26 +01:00
* @ returns { number [ ] }
2023-11-04 19:33:15 +01:00
* /
2023-10-21 15:12:09 +02:00
get selectedCharacters ( ) {
return this . # selectedCharacters ;
}
2024-03-30 03:06:40 +01:00
/ * *
* The instance of the bulk tag popup handler that handles tagging of all selected characters
*
* @ returns { BulkTagPopupHandler }
* /
get bulkTagPopupHandler ( ) {
return this . # bulkTagPopupHandler ;
}
2023-10-21 15:12:09 +02:00
constructor ( ) {
2023-11-04 19:33:15 +01:00
if ( bulkEditOverlayInstance instanceof BulkEditOverlay )
2023-12-02 20:11:06 +01:00
return bulkEditOverlayInstance ;
2023-10-21 15:12:09 +02:00
2023-10-30 19:26:41 +01:00
this . container = document . getElementById ( BulkEditOverlay . containerId ) ;
2023-10-21 15:12:09 +02:00
eventSource . on ( event _types . CHARACTER _GROUP _OVERLAY _STATE _CHANGE _AFTER , this . handleStateChange ) ;
2023-11-04 19:33:15 +01:00
bulkEditOverlayInstance = Object . freeze ( this ) ;
2023-10-21 15:12:09 +02:00
}
2023-11-05 18:23:14 +01:00
/ * *
* Set the overlay to browse mode
* /
2023-11-06 17:20:18 +01:00
browseState = ( ) => this . state = BulkEditOverlayState . browse ;
2023-11-05 18:23:14 +01:00
/ * *
* Set the overlay to select mode
* /
2023-11-06 17:20:18 +01:00
selectState = ( ) => this . state = BulkEditOverlayState . select ;
2023-10-21 15:12:09 +02:00
/ * *
* Set up a Sortable grid for the loaded page
* /
onPageLoad = ( ) => {
2023-10-30 19:24:45 +01:00
this . browseState ( ) ;
2023-10-21 15:12:09 +02:00
2023-11-06 15:31:27 +01:00
const elements = this . # getEnabledElements ( ) ;
2023-10-21 15:12:09 +02:00
elements . forEach ( element => element . addEventListener ( 'touchstart' , this . handleHold ) ) ;
elements . forEach ( element => element . addEventListener ( 'mousedown' , this . handleHold ) ) ;
2023-11-08 00:10:51 +01:00
elements . forEach ( element => element . addEventListener ( 'contextmenu' , this . handleDefaultContextMenu ) ) ;
2023-11-02 17:54:17 +01:00
2023-10-21 15:12:09 +02:00
elements . forEach ( element => element . addEventListener ( 'touchend' , this . handleLongPressEnd ) ) ;
elements . forEach ( element => element . addEventListener ( 'mouseup' , this . handleLongPressEnd ) ) ;
elements . forEach ( element => element . addEventListener ( 'dragend' , this . handleLongPressEnd ) ) ;
2023-11-08 00:10:51 +01:00
elements . forEach ( element => element . addEventListener ( 'touchmove' , this . handleLongPressEnd ) ) ;
2023-10-21 15:12:09 +02:00
2023-11-09 22:55:14 +01:00
// Cohee: It only triggers when clicking on a margin between the elements?
// Feel free to fix or remove this, I'm not sure how to.
//this.container.addEventListener('click', this.handleCancelClick);
2023-12-02 20:11:06 +01:00
} ;
2023-10-21 15:12:09 +02:00
2023-11-05 18:23:14 +01:00
/ * *
* Handle state changes
*
*
* /
handleStateChange = ( ) => {
switch ( this . state ) {
2023-11-06 17:20:18 +01:00
case BulkEditOverlayState . browse :
2023-11-05 18:23:14 +01:00
this . container . classList . remove ( BulkEditOverlay . selectModeClass ) ;
2023-11-09 15:18:59 +01:00
this . # contextMenuOpen = false ;
2023-11-05 18:23:14 +01:00
this . # enableClickEventsForCharacters ( ) ;
2023-11-08 20:48:48 +01:00
this . # enableClickEventsForGroups ( ) ;
2023-11-05 18:23:14 +01:00
this . clearSelectedCharacters ( ) ;
this . disableContextMenu ( ) ;
this . # disableBulkEditButtonHighlight ( ) ;
CharacterContextMenu . hide ( ) ;
break ;
2023-11-06 17:20:18 +01:00
case BulkEditOverlayState . select :
2023-11-05 18:23:14 +01:00
this . container . classList . add ( BulkEditOverlay . selectModeClass ) ;
this . # disableClickEventsForCharacters ( ) ;
2023-11-08 20:48:48 +01:00
this . # disableClickEventsForGroups ( ) ;
2023-11-05 18:23:14 +01:00
this . enableContextMenu ( ) ;
this . # enableBulkEditButtonHighlight ( ) ;
break ;
}
this . stateChangeCallbacks . forEach ( callback => callback ( this . state ) ) ;
2023-12-02 20:11:06 +01:00
} ;
2023-11-05 18:23:14 +01:00
2023-10-21 15:12:09 +02:00
/ * *
* Block the browsers native context menu and
* set a click event to hide the custom context menu .
* /
enableContextMenu = ( ) => {
2023-11-08 19:29:50 +01:00
this . container . addEventListener ( 'contextmenu' , this . handleContextMenuShow ) ;
2023-10-21 15:12:09 +02:00
document . addEventListener ( 'click' , this . handleContextMenuHide ) ;
2023-12-02 20:11:06 +01:00
} ;
2023-10-21 15:12:09 +02:00
/ * *
* Remove event listeners , allowing the native browser context
* menu to be opened .
* /
disableContextMenu = ( ) => {
2023-11-08 19:29:50 +01:00
this . container . removeEventListener ( 'contextmenu' , this . handleContextMenuShow ) ;
2023-10-21 15:12:09 +02:00
document . removeEventListener ( 'click' , this . handleContextMenuHide ) ;
2023-12-02 20:11:06 +01:00
} ;
2023-10-21 15:12:09 +02:00
2023-11-08 00:10:51 +01:00
handleDefaultContextMenu = ( event ) => {
if ( this . isLongPress ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
return false ;
}
2023-12-02 20:11:06 +01:00
} ;
2023-11-08 00:10:51 +01:00
2023-11-08 15:18:53 +01:00
/ * *
* Opens menu on long - press .
*
* @ param event - Pointer event
* /
2023-10-31 19:18:31 +01:00
handleHold = ( event ) => {
2023-11-06 15:31:27 +01:00
if ( 0 !== event . button && event . type !== 'touchstart' ) return ;
2023-11-09 15:18:59 +01:00
if ( this . # contextMenuOpen ) {
this . # contextMenuOpen = false ;
this . # cancelNextToggle = true ;
2023-11-09 15:03:49 +01:00
CharacterContextMenu . hide ( ) ;
return ;
}
2023-11-06 15:31:27 +01:00
2023-11-08 15:18:53 +01:00
let cancel = false ;
const cancelHold = ( event ) => cancel = true ;
2023-11-08 19:29:50 +01:00
this . container . addEventListener ( 'mouseup' , cancelHold ) ;
this . container . addEventListener ( 'touchend' , cancelHold ) ;
2023-11-08 15:18:53 +01:00
2023-10-21 15:12:09 +02:00
this . isLongPress = true ;
2023-11-08 15:18:53 +01:00
2023-10-21 15:12:09 +02:00
setTimeout ( ( ) => {
2023-12-09 15:09:10 +01:00
if ( this . isLongPress && ! cancel ) {
if ( this . state === BulkEditOverlayState . browse ) {
this . selectState ( ) ;
} else if ( this . state === BulkEditOverlayState . select ) {
this . # contextMenuOpen = true ;
CharacterContextMenu . show ( ... this . # getContextMenuPosition ( event ) ) ;
2023-11-08 21:08:31 +01:00
}
2023-12-09 15:09:10 +01:00
}
2023-11-08 15:18:53 +01:00
2023-12-09 15:09:10 +01:00
this . container . removeEventListener ( 'mouseup' , cancelHold ) ;
this . container . removeEventListener ( 'touchend' , cancelHold ) ;
2024-08-30 18:52:57 +02:00
} , BulkEditOverlay . longPressDelay ) ;
2023-12-02 20:11:06 +01:00
} ;
2023-10-21 15:12:09 +02:00
2023-11-09 23:04:16 +01:00
handleLongPressEnd = ( event ) => {
2023-10-21 15:12:09 +02:00
this . isLongPress = false ;
2023-11-09 15:18:59 +01:00
if ( this . # contextMenuOpen ) event . stopPropagation ( ) ;
2023-12-02 20:11:06 +01:00
} ;
2023-10-21 15:12:09 +02:00
handleCancelClick = ( ) => {
2023-11-09 15:18:59 +01:00
if ( false === this . # contextMenuOpen ) this . state = BulkEditOverlayState . browse ;
this . # contextMenuOpen = false ;
2023-12-02 20:11:06 +01:00
} ;
2023-10-21 15:12:09 +02:00
2023-11-06 15:31:27 +01:00
/ * *
* Returns the position of the mouse / touch location
*
* @ param event
* @ returns { ( boolean | number | * ) [ ] }
* /
# getContextMenuPosition = ( event ) => [
event . clientX || event . touches [ 0 ] . clientX ,
event . clientY || event . touches [ 0 ] . clientY ,
] ;
2023-11-09 23:04:16 +01:00
# stopEventPropagation = ( event ) => {
if ( this . # contextMenuOpen ) {
this . handleContextMenuHide ( event ) ;
}
event . stopPropagation ( ) ;
2023-12-02 20:11:06 +01:00
} ;
2023-11-08 20:48:48 +01:00
# enableClickEventsForGroups = ( ) => this . # getDisabledElements ( ) . forEach ( ( element ) => element . removeEventListener ( 'click' , this . # stopEventPropagation ) ) ;
# disableClickEventsForGroups = ( ) => this . # getDisabledElements ( ) . forEach ( ( element ) => element . addEventListener ( 'click' , this . # stopEventPropagation ) ) ;
2023-11-06 15:31:27 +01:00
# enableClickEventsForCharacters = ( ) => this . # getEnabledElements ( ) . forEach ( element => element . removeEventListener ( 'click' , this . toggleCharacterSelected ) ) ;
2023-10-28 12:50:42 +02:00
2023-11-06 15:31:27 +01:00
# disableClickEventsForCharacters = ( ) => this . # getEnabledElements ( ) . forEach ( element => element . addEventListener ( 'click' , this . toggleCharacterSelected ) ) ;
2023-10-28 12:50:42 +02:00
2023-10-30 19:33:18 +01:00
# enableBulkEditButtonHighlight = ( ) => document . getElementById ( 'bulkEditButton' ) . classList . add ( 'bulk_edit_overlay_active' ) ;
# disableBulkEditButtonHighlight = ( ) => document . getElementById ( 'bulkEditButton' ) . classList . remove ( 'bulk_edit_overlay_active' ) ;
2023-11-06 15:31:27 +01:00
# getEnabledElements = ( ) => [ ... this . container . getElementsByClassName ( BulkEditOverlay . characterClass ) ] ;
2023-12-06 00:56:07 +01:00
# getDisabledElements = ( ) => [ ... this . container . getElementsByClassName ( BulkEditOverlay . groupClass ) , ... this . container . getElementsByClassName ( BulkEditOverlay . bogusFolderClass ) ] ;
2023-11-08 20:48:48 +01:00
2023-10-21 15:12:09 +02:00
toggleCharacterSelected = event => {
event . stopPropagation ( ) ;
const character = event . currentTarget ;
2024-03-27 08:22:03 +01:00
if ( ! this . # contextMenuOpen && ! this . # cancelNextToggle ) {
if ( event . shiftKey ) {
// Shift click might have selected text that we don't want to. Unselect it.
document . getSelection ( ) . removeAllRanges ( ) ;
2023-11-06 15:31:27 +01:00
2024-03-27 08:22:03 +01:00
this . handleShiftClick ( character ) ;
2023-11-08 21:08:31 +01:00
} else {
2024-03-27 08:22:03 +01:00
this . toggleSingleCharacter ( character ) ;
2023-11-08 21:08:31 +01:00
}
2024-03-27 08:22:03 +01:00
}
2023-11-09 15:18:59 +01:00
this . # cancelNextToggle = false ;
2023-12-02 20:11:06 +01:00
} ;
2023-10-21 15:12:09 +02:00
2024-03-29 04:41:16 +01:00
/ * *
* When shift click was held down , this function handles the multi select of characters in a single click .
*
* If the last clicked character was deselected , and the current one was deselected too , it will deselect all currently selected characters between those two .
* If the last clicked character was selected , and the current one was selected too , it will select all currently not selected characters between those two .
* If the states do not match , nothing will happen .
*
* @ param { HTMLElement } currentCharacter - The html element of the currently toggled character
* /
2024-03-27 08:22:03 +01:00
handleShiftClick = ( currentCharacter ) => {
const characterId = currentCharacter . getAttribute ( 'chid' ) ;
const select = ! this . selectedCharacters . includes ( characterId ) ;
if ( this . lastSelected . characterId && this . lastSelected . select !== undefined ) {
// Only if select state and the last select state match we execute the range select
if ( select === this . lastSelected . select ) {
2024-03-29 04:41:16 +01:00
this . toggleCharactersInRange ( currentCharacter , select ) ;
2024-03-27 08:22:03 +01:00
}
}
} ;
2024-03-29 04:41:16 +01:00
/ * *
* Toggles the selection of a given characters
*
* @ param { HTMLElement } character - The html element of a character
* @ param { object } param1 - Optional params
* @ param { boolean } [ param1 . markState ] - Whether the toggle of this character should be remembered as the last done toggle
* /
2024-03-27 08:22:03 +01:00
toggleSingleCharacter = ( character , { markState = true } = { } ) => {
const characterId = character . getAttribute ( 'chid' ) ;
const select = ! this . selectedCharacters . includes ( characterId ) ;
const legacyBulkEditCheckbox = character . querySelector ( '.' + BulkEditOverlay . legacySelectedClass ) ;
if ( select ) {
character . classList . add ( BulkEditOverlay . selectedClass ) ;
if ( legacyBulkEditCheckbox ) legacyBulkEditCheckbox . checked = true ;
2024-03-29 04:41:16 +01:00
this . # selectedCharacters . push ( String ( characterId ) ) ;
2024-03-27 08:22:03 +01:00
} else {
character . classList . remove ( BulkEditOverlay . selectedClass ) ;
if ( legacyBulkEditCheckbox ) legacyBulkEditCheckbox . checked = false ;
2024-04-12 13:22:12 +02:00
this . # selectedCharacters = this . # selectedCharacters . filter ( item => String ( characterId ) !== item ) ;
2024-03-27 08:22:03 +01:00
}
this . updateSelectedCount ( ) ;
if ( markState ) {
this . lastSelected . characterId = characterId ;
this . lastSelected . select = select ;
}
} ;
2024-03-29 04:41:16 +01:00
/ * *
* Updates the selected count element with the current count
*
* @ param { number } [ countOverride ] - optional override for a manual number to set
* /
2024-03-27 08:22:03 +01:00
updateSelectedCount = ( countOverride = undefined ) => {
const count = countOverride ? ? this . selectedCharacters . length ;
$ ( ` # ${ BulkEditOverlay . bulkSelectedCountId } ` ) . text ( count ) . attr ( 'title' , ` ${ count } characters selected ` ) ;
} ;
2024-03-29 04:41:16 +01:00
/ * *
* Toggles the selection of characters in a given range .
* The range is provided by the given character and the last selected one remembered in the selection state .
*
* @ param { HTMLElement } currentCharacter - The html element of the currently toggled character
* @ param { boolean } select - < c > true < / c > i f t h e c h a r a c t e r s i n t h e r a n g e a r e t o b e s e l e c t e d , < c > f a l s e < / c > i f d e s e l e c t e d
* /
toggleCharactersInRange = ( currentCharacter , select ) => {
2024-03-27 08:22:03 +01:00
const currentCharacterId = currentCharacter . getAttribute ( 'chid' ) ;
const characters = Array . from ( document . querySelectorAll ( '#' + BulkEditOverlay . containerId + ' .' + BulkEditOverlay . characterClass ) ) ;
const startIndex = characters . findIndex ( c => c . getAttribute ( 'chid' ) === this . lastSelected . characterId ) ;
const endIndex = characters . findIndex ( c => c . getAttribute ( 'chid' ) === currentCharacterId ) ;
for ( let i = Math . min ( startIndex , endIndex ) ; i <= Math . max ( startIndex , endIndex ) ; i ++ ) {
const character = characters [ i ] ;
const characterId = character . getAttribute ( 'chid' ) ;
const isCharacterSelected = this . selectedCharacters . includes ( characterId ) ;
2024-03-29 04:41:16 +01:00
// Only toggle the character if it wasn't on the state we have are toggling towards.
// Also doing a weird type check, because typescript checker doesn't like the return of 'querySelectorAll'.
if ( ( select && ! isCharacterSelected || ! select && isCharacterSelected ) && character instanceof HTMLElement ) {
this . toggleSingleCharacter ( character , { markState : currentCharacterId == characterId } ) ;
2024-03-27 08:22:03 +01:00
}
}
} ;
2023-10-21 15:12:09 +02:00
handleContextMenuShow = ( event ) => {
event . preventDefault ( ) ;
2023-11-06 15:31:27 +01:00
CharacterContextMenu . show ( ... this . # getContextMenuPosition ( event ) ) ;
2023-11-09 15:18:59 +01:00
this . # contextMenuOpen = true ;
2023-12-02 20:11:06 +01:00
} ;
2023-10-21 15:12:09 +02:00
handleContextMenuHide = ( event ) => {
2023-10-30 19:26:41 +01:00
let contextMenu = document . getElementById ( BulkEditOverlay . contextMenuId ) ;
2023-10-21 15:12:09 +02:00
if ( false === contextMenu . contains ( event . target ) ) {
CharacterContextMenu . hide ( ) ;
}
2023-12-02 20:11:06 +01:00
} ;
2023-10-21 15:12:09 +02:00
2023-11-06 15:31:27 +01:00
/ * *
* Concurrently handle character favorite requests .
*
2023-12-06 00:56:07 +01:00
* @ returns { Promise < void > }
2023-11-06 15:31:27 +01:00
* /
2023-12-06 00:56:07 +01:00
handleContextMenuFavorite = async ( ) => {
const promises = [ ] ;
for ( const characterId of this . selectedCharacters ) {
promises . push ( CharacterContextMenu . favorite ( characterId ) ) ;
}
await Promise . allSettled ( promises ) ;
await getCharacters ( ) ;
await favsToHotswap ( ) ;
this . browseState ( ) ;
} ;
2023-10-21 15:12:09 +02:00
2023-11-06 15:31:27 +01:00
/ * *
* Concurrently handle character duplicate requests .
*
* @ returns { Promise < number > }
* /
2023-10-21 15:12:09 +02:00
handleContextMenuDuplicate = ( ) => Promise . all ( this . selectedCharacters . map ( async characterId => CharacterContextMenu . duplicate ( characterId ) ) )
. then ( ( ) => getCharacters ( ) )
2023-12-02 20:11:06 +01:00
. then ( ( ) => this . browseState ( ) ) ;
2023-10-21 15:12:09 +02:00
2023-11-05 17:43:35 +01:00
/ * *
* Sequentially handle all character - to - persona conversions .
*
* @ returns { Promise < void > }
* /
handleContextMenuPersona = async ( ) => {
for ( const characterId of this . selectedCharacters ) {
2023-12-02 20:11:06 +01:00
await CharacterContextMenu . persona ( characterId ) ;
2023-11-05 17:43:35 +01:00
}
this . browseState ( ) ;
2023-12-02 20:11:06 +01:00
} ;
2023-10-21 15:12:09 +02:00
2024-03-29 05:53:26 +01:00
/ * *
* Gets the HTML as a string that is displayed inside the popup for the bulk delete
*
* @ param { Array < number > } characterIds - The characters that are shown inside the popup
* @ returns String containing the html for the popup content
* /
static # getDeletePopupContentHtml = ( characterIds ) => {
return `
< h3 class = "marginBot5" > Delete $ { characterIds . length } characters ? < / h 3 >
< span class = "bulk_delete_note" >
< i class = "fa-solid fa-triangle-exclamation warning margin-r5" > < / i >
< b > THIS IS PERMANENT ! < / b >
< / s p a n >
< div id = "bulk_delete_avatars_block" class = "avatars_inline avatars_inline_small tags tags_inline m-t-1" > < / d i v >
< br >
< div id = "bulk_delete_options" class = "m-b-1" >
< label for = "del_char_checkbox" class = "checkbox_label justifyCenter" >
< input type = "checkbox" id = "del_char_checkbox" / >
< span > Also delete the chat files < / s p a n >
< / l a b e l >
< / d i v > ` ;
2024-04-12 13:22:12 +02:00
} ;
2024-03-29 05:53:26 +01:00
2023-11-06 15:31:27 +01:00
/ * *
* Request user input before concurrently handle deletion
* requests .
*
* @ returns { Promise < number > }
* /
2023-10-21 15:12:09 +02:00
handleContextMenuDelete = ( ) => {
2024-03-29 05:53:26 +01:00
const characterIds = this . selectedCharacters ;
const popupContent = BulkEditOverlay . # getDeletePopupContentHtml ( characterIds ) ;
const promise = callPopup ( popupContent , null )
2023-11-06 15:56:47 +01:00
. then ( ( accept ) => {
if ( true !== accept ) return ;
const deleteChats = document . getElementById ( 'del_char_checkbox' ) . checked ? ? false ;
2023-11-12 20:58:43 +01:00
showLoader ( ) ;
2024-08-30 18:52:57 +02:00
const toast = toastr . info ( 'We\'re deleting your characters, please wait...' , 'Working on it' ) ;
const avatarList = characterIds . map ( id => characters [ id ] ? . avatar ) . filter ( a => a ) ;
return CharacterContextMenu . delete ( avatarList , deleteChats )
2023-11-08 00:10:51 +01:00
. then ( ( ) => this . browseState ( ) )
2024-08-30 18:52:57 +02:00
. finally ( ( ) => {
toastr . clear ( toast ) ;
hideLoader ( ) ;
} ) ;
2023-12-09 14:36:37 +01:00
} ) ;
2024-03-29 05:53:26 +01:00
// At this moment the popup is already changed in the dom, but not yet closed/resolved. We build the avatar list here
const entities = characterIds . map ( id => characterToEntity ( characters [ id ] , id ) ) . filter ( entity => entity . item !== undefined ) ;
buildAvatarList ( $ ( '#bulk_delete_avatars_block' ) , entities ) ;
return promise ;
2023-12-02 20:11:06 +01:00
} ;
2023-10-21 15:12:09 +02:00
2023-11-06 15:31:27 +01:00
/ * *
* Attaches and opens the tag menu
* /
2023-11-04 19:33:15 +01:00
handleContextMenuTag = ( ) => {
CharacterContextMenu . tag ( this . selectedCharacters ) ;
2024-03-31 09:54:23 +02:00
this . browseState ( ) ;
2023-12-02 20:11:06 +01:00
} ;
2023-11-04 19:33:15 +01:00
2023-10-21 15:12:09 +02:00
addStateChangeCallback = callback => this . stateChangeCallbacks . push ( callback ) ;
2023-11-05 16:57:14 +01:00
/ * *
* Clears internal character storage and
* removes visual highlight .
* /
2023-10-21 15:12:09 +02:00
clearSelectedCharacters = ( ) => {
2023-11-05 16:57:14 +01:00
document . querySelectorAll ( '#' + BulkEditOverlay . containerId + ' .' + BulkEditOverlay . selectedClass )
2023-11-08 00:10:51 +01:00
. forEach ( element => element . classList . remove ( BulkEditOverlay . selectedClass ) ) ;
2023-10-21 15:12:09 +02:00
this . selectedCharacters . length = 0 ;
2023-12-02 20:11:06 +01:00
} ;
2023-10-21 15:12:09 +02:00
}
2023-11-08 00:10:51 +01:00
export { BulkEditOverlayState , CharacterContextMenu , BulkEditOverlay } ;