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:
parent
6a688cc383
commit
80f4bd4d9e
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue