Global refactor of printCharacter and filter print

- (!) Refactor character list and filter redrawing to one global debounce
- Refactor all places where character list and filters where redrawn to the correct usage (hope I didn't miss any)
- Automatically redraw character list on each tag bulk edit
- Fix tags not being sorted in bulk edit mutual tags list
- Refactor bulk tag edit class to actually be an instance object
- Remember scroll position on character list redraw - unless it's a full refresh
This commit is contained in:
Wolfsblvt 2024-03-30 03:06:40 +01:00
parent 6a688cc383
commit 80f4bd4d9e
5 changed files with 122 additions and 78 deletions

View File

@ -282,6 +282,7 @@ export {
mesForShowdownParse,
characterGroupOverlay,
printCharacters,
printCharactersDebounced,
isOdd,
countOccurrences,
};
@ -498,6 +499,14 @@ const durationSaveEdit = 1000;
const saveSettingsDebounced = debounce(() => saveSettings(), durationSaveEdit);
export const saveCharacterDebounced = debounce(() => $('#create_button').trigger('click'), durationSaveEdit);
/**
* Prints the character list in a debounced fashion without blocking, with a delay of 100 milliseconds.
* Use this function instead of a direct `printCharacters()` whenever the reprinting of the character list is not the primary focus.
*
* The printing will also always reprint all filter options of the global list, to keep them up to date.
*/
const printCharactersDebounced = debounce(() => { printCharacters(false); }, 100);
/**
* @enum {string} System message types
*/
@ -836,7 +845,7 @@ export let active_character = '';
/** The tag of the active group. (Coincidentally also the id) */
export let active_group = '';
export const entitiesFilter = new FilterHelper(debounce(printCharacters, 100));
export const entitiesFilter = new FilterHelper(printCharactersDebounced);
export const personasFilter = new FilterHelper(debounce(getUserAvatars, 100));
export function getRequestHeaders() {
@ -1275,19 +1284,31 @@ function getCharacterBlock(item, id) {
return template;
}
/**
* Prints the global character list, optionally doing a full refresh of the list
* Use this function whenever the reprinting of the character list is the primary focus, otherwise using `printCharactersDebounced` is preferred for a cleaner, non-blocking experience.
*
* The printing will also always reprint all filter options of the global list, to keep them up to date.
*
* @param {boolean} fullRefresh - If true, the list is fully refreshed and the navigation is being reset
*/
async function printCharacters(fullRefresh = false) {
if (fullRefresh) {
saveCharactersPage = 0;
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
await delay(1);
}
const storageKey = 'Characters_PerPage';
const listId = '#rm_print_characters_block';
const entities = getEntitiesList({ doFilter: true });
let currentScrollTop = $(listId).scrollTop();
if (fullRefresh) {
saveCharactersPage = 0;
currentScrollTop = 0;
await delay(1);
}
// We are actually always reprinting filters, as it "doesn't hurt", and this way they are always up to date
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
$('#rm_print_characters_pagination').pagination({
dataSource: entities,
pageSize: Number(localStorage.getItem(storageKey)) || per_page_default,
@ -1340,7 +1361,7 @@ async function printCharacters(fullRefresh = false) {
saveCharactersPage = e;
},
afterRender: function () {
$(listId).scrollTop(0);
$(listId).scrollTop(currentScrollTop);
},
});

View File

@ -1,6 +1,7 @@
'use strict';
import {
characterGroupOverlay,
callPopup,
characters,
deleteCharacter,
@ -9,9 +10,9 @@ import {
getCharacters,
getPastCharacterChats,
getRequestHeaders,
printCharacters,
buildAvatarList,
characterToEntity,
printCharactersDebounced,
} from '../script.js';
import { favsToHotswap } from './RossAscends-mods.js';
@ -31,7 +32,7 @@ class CharacterContextMenu {
* @param {Array<number>} selectedCharacters
*/
static tag = (selectedCharacters) => {
BulkTagPopupHandler.show(selectedCharacters);
characterGroupOverlay.bulkTagPopupHandler.show(selectedCharacters);
};
/**
@ -186,18 +187,36 @@ class CharacterContextMenu {
* Represents a tag control not bound to a single character
*/
class BulkTagPopupHandler {
/**
* 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() { }
/**
* Gets the HTML as a string that is going to be the popup for the bulk tag edit
*
* @param {Array<number>} characterIds - The characters that are shown inside the popup
* @returns String containing the html for the popup
*/
static #getHtml = (characterIds) => {
const characterData = JSON.stringify({ characterIds: characterIds });
#getHtml = () => {
const characterData = JSON.stringify({ characterIds: this.characterIds });
return `<div id="bulk_tag_shadow_popup">
<div id="bulk_tag_popup">
<div id="bulk_tag_popup_holder">
<h3 class="marginBot5">Modify tags of ${characterIds.length} characters</h3>
<h3 class="marginBot5">Modify tags of ${this.characterIds.length} characters</h3>
<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>
@ -227,93 +246,91 @@ class BulkTagPopupHandler {
/**
* Append and show the tag control
*
* @param {Array<number>} characterIds - The characters assigned to this control
* @param {number[]} characterIds - The characters that are shown inside the popup
*/
static show(characterIds) {
if (characterIds.length == 0) {
show(characterIds) {
// shallow copy character ids persistently into this tooltip
this.characterIds = characterIds.slice();
if (this.characterIds.length == 0) {
console.log('No characters selected for bulk edit tags.');
return;
}
document.body.insertAdjacentHTML('beforeend', this.#getHtml(characterIds));
document.body.insertAdjacentHTML('beforeend', this.#getHtml());
const entities = characterIds.map(id => characterToEntity(characters[id], id)).filter(entity => entity.item !== undefined);
const entities = this.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 } });
printTagList($('#bulkTagList'), { tags: () => this.getMutualTags(), tagOptions: { removable: true } });
// Tag input with resolvable list for the mutual tags to get redrawn, so that newly added tags get sorted correctly
createTagInput('#bulkTagInput', '#bulkTagList', { tags: () => this.getMutualTags(characterIds), tagOptions: { removable: true }});
createTagInput('#bulkTagInput', '#bulkTagList', { tags: () => this.getMutualTags(), tagOptions: { removable: true }});
document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this, characterIds));
document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this, characterIds));
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));
document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this));
}
/**
* Builds a list of all tags that the provided characters have in common.
*
* @param {Array<number>} characterIds - The characters to find mutual tags for
* @returns {Array<object>} A list of mutual tags
*/
static getMutualTags(characterIds) {
if (characterIds.length == 0) {
getMutualTags() {
if (this.characterIds.length == 0) {
return [];
}
if (characterIds.length === 1) {
if (this.characterIds.length === 1) {
// Just use tags of the single character
return getTagsList(getTagKeyForEntity(characterIds[0]));
return getTagsList(getTagKeyForEntity(this.characterIds[0]));
}
// Find mutual tags for multiple characters
const allTags = characterIds.map(cid => getTagsList(getTagKeyForEntity(cid)));
const allTags = this.characterIds.map(cid => getTagsList(getTagKeyForEntity(cid)));
const mutualTags = allTags.reduce((mutual, characterTags) =>
mutual.filter(tag => characterTags.some(cTag => cTag.id === tag.id))
);
this.mutualTags = mutualTags.sort(compareTagsForSort);
return this.mutualTags;
this.currentMutualTags = mutualTags.sort(compareTagsForSort);
return this.currentMutualTags;
}
/**
* Hide and remove the tag control
*/
static hide() {
hide() {
let popupElement = document.querySelector('#bulk_tag_shadow_popup');
if (popupElement) {
document.body.removeChild(popupElement);
}
printCharacters(true);
// No need to redraw here, all tags actions were redrawn when they happened
}
/**
* Empty the tag map for the given characters
*
* @param {Array<number>} characterIds
*/
static resetTags(characterIds) {
for (const characterId of characterIds) {
resetTags() {
for (const characterId of this.characterIds) {
const key = getTagKeyForEntity(characterId);
if (key) tag_map[key] = [];
}
$('#bulkTagList').empty();
printCharacters(true);
printCharactersDebounced();
}
/**
* Remove the mutual tags for all given characters
*
* @param {Array<number>} characterIds
*/
static removeMutual(characterIds) {
const mutualTags = this.getMutualTags(characterIds);
removeMutual() {
const mutualTags = this.getMutualTags();
for (const characterId of characterIds) {
for (const characterId of this.characterIds) {
for(const tag of mutualTags) {
removeTagFromMap(tag.id, characterId);
}
@ -321,7 +338,7 @@ class BulkTagPopupHandler {
$('#bulkTagList').empty();
printCharacters(true);
printCharactersDebounced();
}
}
@ -364,6 +381,7 @@ class BulkEditOverlay {
#longPress = false;
#stateChangeCallbacks = [];
#selectedCharacters = [];
#bulkTagPopupHandler = new BulkTagPopupHandler();
/**
* @typedef {object} LastSelected - An object noting the last selected character and its state.
@ -429,6 +447,15 @@ class BulkEditOverlay {
return this.#selectedCharacters;
}
/**
* The instance of the bulk tag popup handler that handles tagging of all selected characters
*
* @returns {BulkTagPopupHandler}
*/
get bulkTagPopupHandler() {
return this.#bulkTagPopupHandler;
}
constructor() {
if (bulkEditOverlayInstance instanceof BulkEditOverlay)
return bulkEditOverlayInstance;

View File

@ -30,7 +30,7 @@ const toggleBulkEditMode = (isBulkEdit) => {
}
};
(new BulkEditOverlay()).addStateChangeCallback((state) => {
characterGroupOverlay.addStateChangeCallback((state) => {
if (state === BulkEditOverlayState.select) enableBulkEdit();
if (state === BulkEditOverlayState.browse) disableBulkEdit();
});
@ -52,7 +52,7 @@ function onSelectAllButtonClick() {
let atLeastOneSelected = false;
for (const character of characters) {
const checked = $(character).find('.bulk_select_checkbox:checked').length > 0;
if (!checked) {
if (!checked && character instanceof HTMLElement) {
characterGroupOverlay.toggleSingleCharacter(character);
atLeastOneSelected = true;
}
@ -62,7 +62,7 @@ function onSelectAllButtonClick() {
// 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) {
if (checked && character instanceof HTMLElement) {
characterGroupOverlay.toggleSingleCharacter(character);
}
}

View File

@ -10,7 +10,7 @@ import {
eventSource,
event_types,
getCurrentChatId,
printCharacters,
printCharactersDebounced,
setCharacterId,
setEditedMessageId,
renderTemplate,
@ -1288,7 +1288,7 @@ async function applyTheme(name) {
key: 'bogus_folders',
action: async () => {
$('#bogus_folders').prop('checked', power_user.bogus_folders);
await printCharacters(true);
printCharactersDebounced();
},
},
{
@ -3045,7 +3045,7 @@ $(document).ready(() => {
$('#show_card_avatar_urls').on('input', function () {
power_user.show_card_avatar_urls = !!$(this).prop('checked');
printCharacters();
printCharactersDebounced();
saveSettingsDebounced();
});
@ -3068,7 +3068,7 @@ $(document).ready(() => {
power_user.sort_field = $(this).find(':selected').data('field');
power_user.sort_order = $(this).find(':selected').data('order');
power_user.sort_rule = $(this).find(':selected').data('rule');
printCharacters();
printCharactersDebounced();
saveSettingsDebounced();
});
@ -3365,15 +3365,15 @@ $(document).ready(() => {
$('#bogus_folders').on('input', function () {
const value = !!$(this).prop('checked');
power_user.bogus_folders = value;
printCharactersDebounced();
saveSettingsDebounced();
printCharacters(true);
});
$('#aux_field').on('change', function () {
const value = $(this).find(':selected').val();
power_user.aux_field = String(value);
printCharactersDebounced();
saveSettingsDebounced();
printCharacters(false);
});
$('#restore_user_input').on('input', function () {

View File

@ -7,6 +7,7 @@ import {
getCharacters,
entitiesFilter,
printCharacters,
printCharactersDebounced,
buildAvatarList,
eventSource,
event_types,
@ -48,12 +49,6 @@ function getFilterHelper(listSelector) {
return $(listSelector).is(GROUP_FILTER_SELECTOR) ? groupCandidatesFilter : entitiesFilter;
}
const redrawCharsAndFiltersDebounced = debounce(() => {
printCharacters(false);
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
}, 100);
export const tag_filter_types = {
character: 0,
group_member: 1,
@ -406,10 +401,11 @@ function findTag(request, resolve, listSelector) {
* @param {*} event - The event that fired on autocomplete select
* @param {*} ui - An Object with label and value properties for the selected option
* @param {*} listSelector - The selector of the list to print/add to
* @param {PrintTagListOptions} [tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before.
* @param {object} param1 - Optional parameters for this method call
* @param {PrintTagListOptions} [param1.tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before.
* @returns {boolean} <c>false</c>, to keep the input clear
*/
function selectTag(event, ui, listSelector, tagListOptions = {}) {
function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) {
let tagName = ui.item.value;
let tag = tags.find(t => t.name === tagName);
@ -431,6 +427,7 @@ function selectTag(event, ui, listSelector, tagListOptions = {}) {
addTagToMap(tag.id);
}
printCharactersDebounced();
saveSettingsDebounced();
// We should manually add the selected tag to the print tag function, so we cover places where the tag list did not automatically include it
@ -443,9 +440,6 @@ function selectTag(event, ui, listSelector, tagListOptions = {}) {
printTagList($(inlineSelector), tagListOptions);
}
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
// need to return false to keep the input clear
return false;
}
@ -493,10 +487,11 @@ async function importTags(imported_char) {
console.debug('added tag to map', tag, imported_char.name);
}
}
saveSettingsDebounced();
// Await the character list, which will automatically reprint it and all tag filters
await getCharacters();
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
// need to return false to keep the input clear
return false;
@ -767,8 +762,7 @@ function onTagRemoveClick(event) {
$(`${getInlineListSelector()} .tag[id="${tagId}"]`).remove();
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
printCharactersDebounced();
saveSettingsDebounced();
@ -818,7 +812,7 @@ export function createTagInput(inputSelector, listSelector, tagListOptions = {})
// @ts-ignore
.autocomplete({
source: (i, o) => findTag(i, o, listSelector),
select: (e, u) => selectTag(e, u, listSelector, tagListOptions),
select: (e, u) => selectTag(e, u, listSelector, { tagListOptions: tagListOptions }),
minLength: 0,
})
.focus(onTagInputFocus); // <== show tag list on click
@ -900,10 +894,9 @@ function makeTagListDraggable(tagContainer) {
}
});
saveSettingsDebounced();
// 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.
redrawCharsAndFiltersDebounced();
printCharactersDebounced();
saveSettingsDebounced();
};
// @ts-ignore
@ -1003,8 +996,9 @@ async function onTagRestoreFileSelect(e) {
}
$('#tag_view_restore_input').val('');
printCharactersDebounced();
saveSettingsDebounced();
printCharacters(true);
onViewTagsListClick();
}
@ -1029,7 +1023,8 @@ function onTagsBackupClick() {
function onTagCreateClick() {
const tag = createNewTag('New Tag');
appendViewTagToList($('#tag_view_list .tag_view_list_tags'), tag, []);
printCharacters(false);
printCharactersDebounced();
saveSettingsDebounced();
}
@ -1098,7 +1093,7 @@ function onTagAsFolderClick() {
updateDrawTagFolder(element, tag);
// If folder display has changed, we have to redraw the character list, otherwise this folders state would not change
printCharacters(true);
printCharactersDebounced();
saveSettingsDebounced();
}
@ -1133,7 +1128,8 @@ function onTagDeleteClick() {
tags.splice(index, 1);
$(`.tag[id="${id}"]`).remove();
$(`.tag_view_item[id="${id}"]`).remove();
printCharacters(false);
printCharactersDebounced();
saveSettingsDebounced();
}