mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Bulk edit select improvements & bulk tag edit inline avatars
- bulk edit tags shows inline avatars for all selected characters - allow shift-click selecting/deselecting multiple characters on bulk edit - bulk select all button added - bulk select shows selected character count
This commit is contained in:
@ -4319,7 +4319,9 @@
|
||||
<div id="rm_print_characters_pagination">
|
||||
<i id="charListGridToggle" class="fa-solid fa-table-cells-large menu_button" title="Toggle character grid view" data-i18n="[title]Toggle character grid view"></i>
|
||||
<i id="bulkEditButton" class="fa-solid fa-edit menu_button bulkEditButton" title="Bulk edit characters" data-i18n="[title]Bulk edit characters"></i>
|
||||
<i id="bulkDeleteButton" class="fa-solid fa-trash menu_button bulkDeleteButton" title="Bulk delete characters" data-i18n="[title]Bulk delete characters" style="display: none;"></i>
|
||||
<div id="bulkSelectedCount" class="bulkEditOptionElement paginationjs-nav"></div>
|
||||
<i id="bulkSelectAllButton" class="fa-solid fa-check-double menu_button bulkEditOptionElement bulkSelectAllButton" title="Bulk select all characters" data-i18n="[title]Bulk select all characters" style="display: none;"></i>
|
||||
<i id="bulkDeleteButton" class="fa-solid fa-trash menu_button bulkEditOptionElement bulkDeleteButton" title="Bulk delete characters" data-i18n="[title]Bulk delete characters" style="display: none;"></i>
|
||||
</div>
|
||||
<div id="rm_print_characters_block" class="flexFlowColumn"></div>
|
||||
</div>
|
||||
|
@ -279,6 +279,7 @@ export {
|
||||
default_ch_mes,
|
||||
extension_prompt_types,
|
||||
mesForShowdownParse,
|
||||
characterGroupOverlay,
|
||||
printCharacters,
|
||||
isOdd,
|
||||
countOccurrences,
|
||||
@ -1343,19 +1344,19 @@ async function printCharacters(fullRefresh = false) {
|
||||
favsToHotswap();
|
||||
}
|
||||
|
||||
export function characterToEntity(character, id) {
|
||||
return { item: character, id, type: 'character' };
|
||||
}
|
||||
|
||||
export function groupToEntity(group) {
|
||||
return { item: group, id: group.id, type: 'group' };
|
||||
}
|
||||
|
||||
export function tagToEntity(tag) {
|
||||
return { item: structuredClone(tag), id: tag.id, type: 'tag', entities: [] };
|
||||
}
|
||||
|
||||
export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
|
||||
function characterToEntity(character, id) {
|
||||
return { item: character, id, type: 'character' };
|
||||
}
|
||||
|
||||
function groupToEntity(group) {
|
||||
return { item: group, id: group.id, type: 'group' };
|
||||
}
|
||||
|
||||
function tagToEntity(tag) {
|
||||
return { item: structuredClone(tag), id: tag.id, type: 'tag', entities: [] };
|
||||
}
|
||||
|
||||
let entities = [
|
||||
...characters.map((item, index) => characterToEntity(item, index)),
|
||||
...groups.map(item => groupToEntity(item)),
|
||||
|
@ -10,6 +10,8 @@ import {
|
||||
getPastCharacterChats,
|
||||
getRequestHeaders,
|
||||
printCharacters,
|
||||
buildAvatarList,
|
||||
characterToEntity,
|
||||
} from '../script.js';
|
||||
|
||||
import { favsToHotswap } from './RossAscends-mods.js';
|
||||
@ -194,7 +196,8 @@ class BulkTagPopupHandler {
|
||||
<div id="bulk_tag_popup">
|
||||
<div id="bulk_tag_popup_holder">
|
||||
<h3 class="marginBot5">Modify tags of ${characterIds.length} characters</h3>
|
||||
<small class="bulk_tags_desc m-b-1">This popup allows you to modify the mutual tags of all selected characters.</small>
|
||||
<small class="bulk_tags_desc m-b-1">Add or remove the mutual tags of all selected characters.</small>
|
||||
<div id="bulk_tags_avatars_block" class="avatars_inline avatars_inline_small tags tags_inline"></div>
|
||||
<br>
|
||||
<div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>
|
||||
<div class="tag_controls">
|
||||
@ -233,7 +236,8 @@ class BulkTagPopupHandler {
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', this.#getHtml(characterIds));
|
||||
|
||||
this.mutualTags = this.getMutualTags(characterIds);
|
||||
const entities = characterIds.map(id => characterToEntity(characters[id], id)).filter(entity => entity.item !== undefined);
|
||||
buildAvatarList($('#bulk_tags_avatars_block'), entities);
|
||||
|
||||
// Print the tag list with all mutuable tags, marking them as removable. That is the initial fill
|
||||
printTagList($('#bulkTagList'), { tags: () => this.getMutualTags(characterIds), tagOptions: { removable: true } });
|
||||
@ -257,7 +261,7 @@ class BulkTagPopupHandler {
|
||||
}
|
||||
|
||||
// Find mutual tags for multiple characters
|
||||
const allTags = characterIds.map(c => getTagsList(getTagKeyForEntity(c)));
|
||||
const allTags = characterIds.map(cid => getTagsList(getTagKeyForEntity(cid)));
|
||||
const mutualTags = allTags.reduce((mutual, characterTags) =>
|
||||
mutual.filter(tag => characterTags.some(cTag => cTag.id === tag.id))
|
||||
);
|
||||
@ -345,6 +349,7 @@ class BulkEditOverlay {
|
||||
static selectModeClass = 'group_overlay_mode_select';
|
||||
static selectedClass = 'character_selected';
|
||||
static legacySelectedClass = 'bulk_select_checkbox';
|
||||
static bulkSelectedCountId = 'bulkSelectedCount';
|
||||
|
||||
static longPressDelay = 2500;
|
||||
|
||||
@ -353,6 +358,17 @@ class BulkEditOverlay {
|
||||
#stateChangeCallbacks = [];
|
||||
#selectedCharacters = [];
|
||||
|
||||
/**
|
||||
* @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> if it was selected, <c>false</c> if it was deselected.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {LastSelected} - An object noting the last selected character and its state.
|
||||
*/
|
||||
lastSelected = { characterId: undefined, select: undefined };
|
||||
|
||||
/**
|
||||
* Locks other pointer actions when the context menu is open
|
||||
*
|
||||
@ -588,27 +604,80 @@ class BulkEditOverlay {
|
||||
event.stopPropagation();
|
||||
|
||||
const character = event.currentTarget;
|
||||
const characterId = character.getAttribute('chid');
|
||||
|
||||
const alreadySelected = this.selectedCharacters.includes(characterId);
|
||||
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();
|
||||
|
||||
const legacyBulkEditCheckbox = character.querySelector('.' + BulkEditOverlay.legacySelectedClass);
|
||||
|
||||
// Only toggle when context menu is closed and wasn't just closed.
|
||||
if (!this.#contextMenuOpen && !this.#cancelNextToggle)
|
||||
if (alreadySelected) {
|
||||
character.classList.remove(BulkEditOverlay.selectedClass);
|
||||
if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = false;
|
||||
this.dismissCharacter(characterId);
|
||||
this.handleShiftClick(character);
|
||||
} else {
|
||||
character.classList.add(BulkEditOverlay.selectedClass);
|
||||
if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = true;
|
||||
this.selectCharacter(characterId);
|
||||
this.toggleSingleCharacter(character);
|
||||
}
|
||||
}
|
||||
|
||||
this.#cancelNextToggle = false;
|
||||
};
|
||||
|
||||
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) {
|
||||
this.selectCharactersInRange(currentCharacter, select);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
this.selectCharacter(characterId);
|
||||
} else {
|
||||
character.classList.remove(BulkEditOverlay.selectedClass);
|
||||
if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = false;
|
||||
this.dismissCharacter(characterId);
|
||||
}
|
||||
|
||||
this.updateSelectedCount();
|
||||
|
||||
if (markState) {
|
||||
this.lastSelected.characterId = characterId;
|
||||
this.lastSelected.select = select;
|
||||
}
|
||||
};
|
||||
|
||||
updateSelectedCount = (countOverride = undefined) => {
|
||||
const count = countOverride ?? this.selectedCharacters.length;
|
||||
$(`#${BulkEditOverlay.bulkSelectedCountId}`).text(count).attr('title', `${count} characters selected`);
|
||||
};
|
||||
|
||||
selectCharactersInRange = (currentCharacter, select) => {
|
||||
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);
|
||||
|
||||
if (select && !isCharacterSelected || !select && isCharacterSelected) {
|
||||
this.toggleSingleCharacter(character, { markState: currentCharacterId == i });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleContextMenuShow = (event) => {
|
||||
event.preventDefault();
|
||||
CharacterContextMenu.show(...this.#getContextMenuPosition(event));
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { characters, getCharacters, handleDeleteCharacter, callPopup } from '../script.js';
|
||||
import { characters, getCharacters, handleDeleteCharacter, callPopup, characterGroupOverlay } from '../script.js';
|
||||
import { BulkEditOverlay, BulkEditOverlayState } from './BulkEditOverlay.js';
|
||||
|
||||
|
||||
@ -6,18 +6,20 @@ let is_bulk_edit = false;
|
||||
|
||||
const enableBulkEdit = () => {
|
||||
enableBulkSelect();
|
||||
(new BulkEditOverlay()).selectState();
|
||||
// show the delete button
|
||||
$('#bulkDeleteButton').show();
|
||||
characterGroupOverlay.selectState();
|
||||
// show the bulk edit option buttons
|
||||
$('.bulkEditOptionElement').show();
|
||||
is_bulk_edit = true;
|
||||
characterGroupOverlay.updateSelectedCount(0);
|
||||
};
|
||||
|
||||
const disableBulkEdit = () => {
|
||||
disableBulkSelect();
|
||||
(new BulkEditOverlay()).browseState();
|
||||
// hide the delete button
|
||||
$('#bulkDeleteButton').hide();
|
||||
characterGroupOverlay.browseState();
|
||||
// hide the bulk edit option buttons
|
||||
$('.bulkEditOptionElement').hide();
|
||||
is_bulk_edit = false;
|
||||
characterGroupOverlay.updateSelectedCount(0);
|
||||
};
|
||||
|
||||
const toggleBulkEditMode = (isBulkEdit) => {
|
||||
@ -41,6 +43,32 @@ function onEditButtonClick() {
|
||||
toggleBulkEditMode(is_bulk_edit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the select state of all characters in bulk edit mode to selected. If all are selected, they'll be deselected.
|
||||
*/
|
||||
function onSelectAllButtonClick() {
|
||||
console.log('Bulk select all button clicked');
|
||||
const characters = Array.from(document.querySelectorAll('#' + BulkEditOverlay.containerId + ' .' + BulkEditOverlay.characterClass));
|
||||
let atLeastOneSelected = false;
|
||||
for (const character of characters) {
|
||||
const checked = $(character).find('.bulk_select_checkbox:checked').length > 0;
|
||||
if (!checked) {
|
||||
characterGroupOverlay.toggleSingleCharacter(character);
|
||||
atLeastOneSelected = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!atLeastOneSelected) {
|
||||
// If none was selected, trigger click on all to deselect all of them
|
||||
for(const character of characters) {
|
||||
const checked = $(character).find('.bulk_select_checkbox:checked') ?? false;
|
||||
if (checked) {
|
||||
characterGroupOverlay.toggleSingleCharacter(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the character with the given chid.
|
||||
*
|
||||
@ -89,6 +117,10 @@ async function onDeleteButtonClick() {
|
||||
*/
|
||||
function enableBulkSelect() {
|
||||
$('#rm_print_characters_block .character_select').each((i, el) => {
|
||||
// Prevent checkbox from adding multiple times (because of stage change callback)
|
||||
if ($(el).find('.bulk_select_checkbox').length > 0) {
|
||||
return;
|
||||
}
|
||||
const checkbox = $('<input type=\'checkbox\' class=\'bulk_select_checkbox\'>');
|
||||
checkbox.on('change', () => {
|
||||
// Do something when the checkbox is changed
|
||||
@ -115,5 +147,6 @@ function disableBulkSelect() {
|
||||
*/
|
||||
jQuery(() => {
|
||||
$('#bulkEditButton').on('click', onEditButtonClick);
|
||||
$('#bulkSelectAllButton').on('click', onSelectAllButtonClick);
|
||||
$('#bulkDeleteButton').on('click', onDeleteButtonClick);
|
||||
});
|
||||
|
@ -2126,6 +2126,10 @@ grammarly-extension {
|
||||
background-color: var(--crimson-hover);
|
||||
}
|
||||
|
||||
#bulk_tags_avatars_block {
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
#dialogue_popup_input {
|
||||
margin: 10px 0;
|
||||
width: 100%;
|
||||
|
Reference in New Issue
Block a user