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">
|
<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="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="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>
|
||||||
<div id="rm_print_characters_block" class="flexFlowColumn"></div>
|
<div id="rm_print_characters_block" class="flexFlowColumn"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -279,6 +279,7 @@ export {
|
|||||||
default_ch_mes,
|
default_ch_mes,
|
||||||
extension_prompt_types,
|
extension_prompt_types,
|
||||||
mesForShowdownParse,
|
mesForShowdownParse,
|
||||||
|
characterGroupOverlay,
|
||||||
printCharacters,
|
printCharacters,
|
||||||
isOdd,
|
isOdd,
|
||||||
countOccurrences,
|
countOccurrences,
|
||||||
@ -1343,19 +1344,19 @@ async function printCharacters(fullRefresh = false) {
|
|||||||
favsToHotswap();
|
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 } = {}) {
|
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 = [
|
let entities = [
|
||||||
...characters.map((item, index) => characterToEntity(item, index)),
|
...characters.map((item, index) => characterToEntity(item, index)),
|
||||||
...groups.map(item => groupToEntity(item)),
|
...groups.map(item => groupToEntity(item)),
|
||||||
|
@ -10,6 +10,8 @@ import {
|
|||||||
getPastCharacterChats,
|
getPastCharacterChats,
|
||||||
getRequestHeaders,
|
getRequestHeaders,
|
||||||
printCharacters,
|
printCharacters,
|
||||||
|
buildAvatarList,
|
||||||
|
characterToEntity,
|
||||||
} from '../script.js';
|
} from '../script.js';
|
||||||
|
|
||||||
import { favsToHotswap } from './RossAscends-mods.js';
|
import { favsToHotswap } from './RossAscends-mods.js';
|
||||||
@ -194,7 +196,8 @@ class BulkTagPopupHandler {
|
|||||||
<div id="bulk_tag_popup">
|
<div id="bulk_tag_popup">
|
||||||
<div id="bulk_tag_popup_holder">
|
<div id="bulk_tag_popup_holder">
|
||||||
<h3 class="marginBot5">Modify tags of ${characterIds.length} characters</h3>
|
<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>
|
<br>
|
||||||
<div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>
|
<div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>
|
||||||
<div class="tag_controls">
|
<div class="tag_controls">
|
||||||
@ -233,7 +236,8 @@ class BulkTagPopupHandler {
|
|||||||
|
|
||||||
document.body.insertAdjacentHTML('beforeend', this.#getHtml(characterIds));
|
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
|
// 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 } });
|
printTagList($('#bulkTagList'), { tags: () => this.getMutualTags(characterIds), tagOptions: { removable: true } });
|
||||||
@ -257,7 +261,7 @@ class BulkTagPopupHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find mutual tags for multiple characters
|
// 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) =>
|
const mutualTags = allTags.reduce((mutual, characterTags) =>
|
||||||
mutual.filter(tag => characterTags.some(cTag => cTag.id === tag.id))
|
mutual.filter(tag => characterTags.some(cTag => cTag.id === tag.id))
|
||||||
);
|
);
|
||||||
@ -345,6 +349,7 @@ class BulkEditOverlay {
|
|||||||
static selectModeClass = 'group_overlay_mode_select';
|
static selectModeClass = 'group_overlay_mode_select';
|
||||||
static selectedClass = 'character_selected';
|
static selectedClass = 'character_selected';
|
||||||
static legacySelectedClass = 'bulk_select_checkbox';
|
static legacySelectedClass = 'bulk_select_checkbox';
|
||||||
|
static bulkSelectedCountId = 'bulkSelectedCount';
|
||||||
|
|
||||||
static longPressDelay = 2500;
|
static longPressDelay = 2500;
|
||||||
|
|
||||||
@ -353,6 +358,17 @@ class BulkEditOverlay {
|
|||||||
#stateChangeCallbacks = [];
|
#stateChangeCallbacks = [];
|
||||||
#selectedCharacters = [];
|
#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
|
* Locks other pointer actions when the context menu is open
|
||||||
*
|
*
|
||||||
@ -588,27 +604,80 @@ class BulkEditOverlay {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
const character = event.currentTarget;
|
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);
|
this.handleShiftClick(character);
|
||||||
|
|
||||||
// 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);
|
|
||||||
} else {
|
} else {
|
||||||
character.classList.add(BulkEditOverlay.selectedClass);
|
this.toggleSingleCharacter(character);
|
||||||
if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = true;
|
|
||||||
this.selectCharacter(characterId);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.#cancelNextToggle = false;
|
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) => {
|
handleContextMenuShow = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
CharacterContextMenu.show(...this.#getContextMenuPosition(event));
|
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';
|
import { BulkEditOverlay, BulkEditOverlayState } from './BulkEditOverlay.js';
|
||||||
|
|
||||||
|
|
||||||
@ -6,18 +6,20 @@ let is_bulk_edit = false;
|
|||||||
|
|
||||||
const enableBulkEdit = () => {
|
const enableBulkEdit = () => {
|
||||||
enableBulkSelect();
|
enableBulkSelect();
|
||||||
(new BulkEditOverlay()).selectState();
|
characterGroupOverlay.selectState();
|
||||||
// show the delete button
|
// show the bulk edit option buttons
|
||||||
$('#bulkDeleteButton').show();
|
$('.bulkEditOptionElement').show();
|
||||||
is_bulk_edit = true;
|
is_bulk_edit = true;
|
||||||
|
characterGroupOverlay.updateSelectedCount(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const disableBulkEdit = () => {
|
const disableBulkEdit = () => {
|
||||||
disableBulkSelect();
|
disableBulkSelect();
|
||||||
(new BulkEditOverlay()).browseState();
|
characterGroupOverlay.browseState();
|
||||||
// hide the delete button
|
// hide the bulk edit option buttons
|
||||||
$('#bulkDeleteButton').hide();
|
$('.bulkEditOptionElement').hide();
|
||||||
is_bulk_edit = false;
|
is_bulk_edit = false;
|
||||||
|
characterGroupOverlay.updateSelectedCount(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleBulkEditMode = (isBulkEdit) => {
|
const toggleBulkEditMode = (isBulkEdit) => {
|
||||||
@ -41,6 +43,32 @@ function onEditButtonClick() {
|
|||||||
toggleBulkEditMode(is_bulk_edit);
|
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.
|
* Deletes the character with the given chid.
|
||||||
*
|
*
|
||||||
@ -89,6 +117,10 @@ async function onDeleteButtonClick() {
|
|||||||
*/
|
*/
|
||||||
function enableBulkSelect() {
|
function enableBulkSelect() {
|
||||||
$('#rm_print_characters_block .character_select').each((i, el) => {
|
$('#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\'>');
|
const checkbox = $('<input type=\'checkbox\' class=\'bulk_select_checkbox\'>');
|
||||||
checkbox.on('change', () => {
|
checkbox.on('change', () => {
|
||||||
// Do something when the checkbox is changed
|
// Do something when the checkbox is changed
|
||||||
@ -115,5 +147,6 @@ function disableBulkSelect() {
|
|||||||
*/
|
*/
|
||||||
jQuery(() => {
|
jQuery(() => {
|
||||||
$('#bulkEditButton').on('click', onEditButtonClick);
|
$('#bulkEditButton').on('click', onEditButtonClick);
|
||||||
|
$('#bulkSelectAllButton').on('click', onSelectAllButtonClick);
|
||||||
$('#bulkDeleteButton').on('click', onDeleteButtonClick);
|
$('#bulkDeleteButton').on('click', onDeleteButtonClick);
|
||||||
});
|
});
|
||||||
|
@ -2126,6 +2126,10 @@ grammarly-extension {
|
|||||||
background-color: var(--crimson-hover);
|
background-color: var(--crimson-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#bulk_tags_avatars_block {
|
||||||
|
max-height: 70vh;
|
||||||
|
}
|
||||||
|
|
||||||
#dialogue_popup_input {
|
#dialogue_popup_input {
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
Reference in New Issue
Block a user