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:
Wolfsblvt
2024-03-27 08:22:03 +01:00
parent 4547e68497
commit a4c4f36fc6
5 changed files with 145 additions and 36 deletions

View File

@ -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>

View File

@ -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)),

View File

@ -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));

View File

@ -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);
});

View File

@ -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%;