Tag Folders: add tag folder sorting and enabling

- make tags sortable per drag&drop (then sorted everywhere)
- each tag can individually be enabled as folder
- fix redraw of tags/entity list on tag changes
This commit is contained in:
Wolfsblvt 2024-02-18 08:42:36 +01:00
parent 181657cede
commit 25b528ee4f
4 changed files with 168 additions and 8 deletions

View File

@ -174,3 +174,49 @@
1px -1px 0px black; 1px -1px 0px black;
opacity: 1; opacity: 1;
} }
.tag_as_folder {
filter: brightness(25%) saturate(0.25);
}
.tag_as_folder.yes_folder {
filter: brightness(75%) saturate(0.6);
}
.tag_as_folder:hover {
filter: brightness(150%) saturate(0.6);
}
.tag_as_folder.yes_folder:after {
position: absolute;
top: -8px;
bottom: 0;
left: 0;
right: -24px;
content: "\2714";
font-size: calc(var(--mainFontSize) * 1);
color: green;
line-height: calc(var(--mainFontSize) * 1.3);
text-align: center;
text-shadow: 1px 1px 0px black,
-1px -1px 0px black,
-1px 1px 0px black,
1px -1px 0px black;
opacity: 1;
}
.tag_as_folder.no_folder:after {
position: absolute;
top: -8px;
bottom: 0;
left: 0;
right: -24px;
content: "\2715";
font-size: calc(var(--mainFontSize) * 1);
color: red;
line-height: calc(var(--mainFontSize) * 1.3);
text-align: center;
text-shadow: 1px 1px 0px black,
-1px -1px 0px black,
-1px 1px 0px black,
1px -1px 0px black;
opacity: 1;
}

View File

@ -4542,6 +4542,8 @@
</div> </div>
<div id="tag_view_template" class="template_element"> <div id="tag_view_template" class="template_element">
<div class="tag_view_item"> <div class="tag_view_item">
<div class="drag-handle" data-i18n="[title]Drag to reorder tag"></div>
<div title="Tag as folder" class="tag_as_folder fa-solid fa-folder-open right_menu_button" data-i18n="[title]Use tag as folder"></div>
<div class="tagColorPickerHolder"></div> <div class="tagColorPickerHolder"></div>
<div class="tagColorPicker2Holder"></div> <div class="tagColorPicker2Holder"></div>
<div class="tag_view_name" contenteditable="true"></div> <div class="tag_view_name" contenteditable="true"></div>

View File

@ -163,6 +163,7 @@ import {
renameTagKey, renameTagKey,
importTags, importTags,
tag_filter_types, tag_filter_types,
compareTagsForSort,
} from './scripts/tags.js'; } from './scripts/tags.js';
import { import {
SECRET_KEYS, SECRET_KEYS,
@ -1326,7 +1327,7 @@ export function getEntitiesList({ doFilter } = {}) {
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)),
...(power_user.bogus_folders ? tags.map(item => tagToEntity(item)) : []), ...(power_user.bogus_folders ? tags.filter(x => x.is_folder).sort(compareTagsForSort).map(item => tagToEntity(item)) : []),
]; ];
if (doFilter) { if (doFilter) {
@ -1337,7 +1338,7 @@ export function getEntitiesList({ doFilter } = {}) {
// Get tags of entities within the bogus folder // Get tags of entities within the bogus folder
const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG)); const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
entities = entities.filter(x => x.type !== 'tag'); entities = entities.filter(x => x.type !== 'tag');
const otherTags = tags.filter(x => !filterData.selected.includes(x.id)); const otherTags = tags.filter(x => x.is_folder && !filterData.selected.includes(x.id)).sort(compareTagsForSort);
const bogusTags = []; const bogusTags = [];
for (const entity of entities) { for (const entity of entities) {
for (const tag of otherTags) { for (const tag of otherTags) {

View File

@ -12,7 +12,8 @@ import {
import { FILTER_TYPES, FilterHelper } from './filters.js'; import { FILTER_TYPES, FilterHelper } from './filters.js';
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js'; import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
import { download, onlyUnique, parseJsonFile, uuidv4 } from './utils.js'; import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay } from './utils.js';
import { power_user } from './power-user.js';
export { export {
tags, tags,
@ -24,6 +25,8 @@ export {
createTagMapFromList, createTagMapFromList,
renameTagKey, renameTagKey,
importTags, importTags,
sortTags,
compareTagsForSort,
}; };
const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter'; const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
@ -111,7 +114,7 @@ function getTagsList(key) {
return tag_map[key] return tag_map[key]
.map(x => tags.find(y => y.id === x)) .map(x => tags.find(y => y.id === x))
.filter(x => x) .filter(x => x)
.sort((a, b) => a.name.localeCompare(b.name)); .sort(compareTagsForSort);
} }
function getInlineListSelector() { function getInlineListSelector() {
@ -245,7 +248,9 @@ async function importTags(imported_char) {
} else { } else {
selected_tags = await callPopup(`<h3>Importing Tags For ${imported_char.name}</h3><p>${existingTags.length} existing tags have been found${existingTagsString}.</p><p>The following ${newTags.length} new tags will be imported.</p>`, 'input', newTags.join(', ')); selected_tags = await callPopup(`<h3>Importing Tags For ${imported_char.name}</h3><p>${existingTags.length} existing tags have been found${existingTagsString}.</p><p>The following ${newTags.length} new tags will be imported.</p>`, 'input', newTags.join(', '));
} }
// @ts-ignore
selected_tags = existingTags.concat(selected_tags.split(',')); selected_tags = existingTags.concat(selected_tags.split(','));
// @ts-ignore
selected_tags = selected_tags.map(t => t.trim()).filter(t => t !== ''); selected_tags = selected_tags.map(t => t.trim()).filter(t => t !== '');
//Anti-troll measure //Anti-troll measure
if (selected_tags.length > 15) { if (selected_tags.length > 15) {
@ -276,6 +281,8 @@ function createNewTag(tagName) {
const tag = { const tag = {
id: uuidv4(), id: uuidv4(),
name: tagName, name: tagName,
is_folder: false,
sort_order: tags.length,
color: '', color: '',
color2: '', color2: '',
create_date: Date.now(), create_date: Date.now(),
@ -379,7 +386,7 @@ function printTagFilters(type = tag_filter_types.character) {
const characterTagIds = Object.values(tag_map).flat(); const characterTagIds = Object.values(tag_map).flat();
const tagsToDisplay = tags const tagsToDisplay = tags
.filter(x => characterTagIds.includes(x.id)) .filter(x => characterTagIds.includes(x.id))
.sort((a, b) => a.name.localeCompare(b.name)); .sort(compareTagsForSort);
for (const tag of Object.values(ACTIONABLE_TAGS)) { for (const tag of Object.values(ACTIONABLE_TAGS)) {
appendTagToList(FILTER_SELECTOR, tag, { removable: false, selectable: false, action: tag.action, isGeneralList: true }); appendTagToList(FILTER_SELECTOR, tag, { removable: false, selectable: false, action: tag.action, isGeneralList: true });
@ -426,13 +433,16 @@ function onTagRemoveClick(event) {
saveSettingsDebounced(); saveSettingsDebounced();
} }
// @ts-ignore
function onTagInput(event) { function onTagInput(event) {
let val = $(this).val(); let val = $(this).val();
if (tags.find(t => t.name === val)) return; if (tags.find(t => t.name === val)) return;
// @ts-ignore
$(this).autocomplete('search', val); $(this).autocomplete('search', val);
} }
function onTagInputFocus() { function onTagInputFocus() {
// @ts-ignore
$(this).autocomplete('search', $(this).val()); $(this).autocomplete('search', $(this).val());
} }
@ -475,6 +485,7 @@ function applyTagsOnGroupSelect() {
export function createTagInput(inputSelector, listSelector) { export function createTagInput(inputSelector, listSelector) {
$(inputSelector) $(inputSelector)
// @ts-ignore
.autocomplete({ .autocomplete({
source: (i, o) => findTag(i, o, listSelector), source: (i, o) => findTag(i, o, listSelector),
select: (e, u) => selectTag(e, u, listSelector), select: (e, u) => selectTag(e, u, listSelector),
@ -509,19 +520,94 @@ function onViewTagsListClick() {
</div> </div>
<div class="justifyLeft m-b-1"> <div class="justifyLeft m-b-1">
<small> <small>
Drag the handle to reorder.<br>
${(power_user.bogus_folders ? 'Click on the folder icon to use this tag as a folder.<br>' : '')}
Click on the tag name to edit it.<br> Click on the tag name to edit it.<br>
Click on color box to assign new color. Click on color box to assign new color.
</small> </small>
</div>`); </div>`);
const sortedTags = tags.slice().sort((a, b) => a?.name?.toLowerCase()?.localeCompare(b?.name?.toLowerCase())); const tagContainer = $('<div class="tag_view_list_tags ui-sortable"></div>');
list.append(tagContainer);
const sortedTags = sortTags(tags);
// var highestSortOrder = sortedTags.reduce((max, tag) => tag.sort_order !== undefined ? Math.max(max, tag.sort_order) : max, -1);
for (const tag of sortedTags) { for (const tag of sortedTags) {
appendViewTagToList(list, tag, everything); // // For drag&drop to work we need a sort_order defined, so set it but not save. Gets persisted if there are any tag settings changes
// if (tag.sort_order === undefined) {
// tag.sort_order = ++highestSortOrder;
// }
appendViewTagToList(tagContainer, tag, everything);
} }
makeTagListDraggable(tagContainer);
callPopup(list, 'text'); callPopup(list, 'text');
} }
function makeTagListDraggable(tagContainer) {
const onTagsSort = () => {
tagContainer.find('.tag_view_item').each(function (i, tagElement) {
const id = $(tagElement).attr('id');
const tag = tags.find(x => x.id === id);
// Fix the defined colors, because if there is no color set, they seem to get automatically set to black
// based on the color picker after drag&drop, even if there was no color chosen. We just set them back.
const color = $(tagElement).find('.tagColorPickerHolder .tag-color').attr('color');
const color2 = $(tagElement).find('.tagColorPicker2Holder .tag-color2').attr('color');
if (color === '' || color === undefined) {
tag.color = '';
fixColor('background-color', tag.color);
}
if (color2 === '' || color2 === undefined) {
tag.color2 = '';
fixColor('color', tag.color2);
}
// Update the sort order
tag.sort_order = i;
function fixColor(property, color) {
$(tagElement).find('.tag_view_name').css(property, color);
$(`.tag[id="${id}"]`).css(property, color);
$(`.bogus_folder_select[tagid="${id}"] .avatar`).css(property, color);
}
});
saveSettingsDebounced();
// If the order of tags in display has changed, we need to redraw some UI elements
printCharacters(false);
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
};
// @ts-ignore
$(tagContainer).sortable({
delay: getSortableDelay(),
stop: () => onTagsSort(),
handle: '.drag-handle',
});
}
function sortTags(tags) {
return tags.slice().sort(compareTagsForSort);
}
function compareTagsForSort(a, b) {
if (a.sort_order !== undefined && b.sort_order !== undefined) {
return a.sort_order - b.sort_order;
} else if (a.sort_order !== undefined) {
return -1;
} else if (b.sort_order !== undefined) {
return 1;
} else {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
}
}
async function onTagRestoreFileSelect(e) { async function onTagRestoreFileSelect(e) {
const file = e.target.files[0]; const file = e.target.files[0];
@ -620,7 +706,7 @@ function onTagsBackupClick() {
function onTagCreateClick() { function onTagCreateClick() {
const tag = createNewTag('New Tag'); const tag = createNewTag('New Tag');
appendViewTagToList($('#tag_view_list'), tag, []); appendViewTagToList($('#tag_view_list .tag_view_list_tags'), tag, []);
printCharacters(false); printCharacters(false);
saveSettingsDebounced(); saveSettingsDebounced();
} }
@ -636,9 +722,15 @@ function appendViewTagToList(list, tag, everything) {
template.find('.tag_view_name').css('background-color', tag.color); template.find('.tag_view_name').css('background-color', tag.color);
template.find('.tag_view_name').css('color', tag.color2); template.find('.tag_view_name').css('color', tag.color2);
const tagAsFolderId = tag.id + '-tag-folder';
const colorPickerId = tag.id + '-tag-color'; const colorPickerId = tag.id + '-tag-color';
const colorPicker2Id = tag.id + '-tag-color2'; const colorPicker2Id = tag.id + '-tag-color2';
if (!power_user.bogus_folders) {
template.find('.tag_as_folder').hide();
}
template.find('.tag_as_folder').addClass(tag.is_folder == true ? 'yes_folder' : 'no_folder');
template.find('.tagColorPickerHolder').html( template.find('.tagColorPickerHolder').html(
`<toolcool-color-picker id="${colorPickerId}" color="${tag.color}" class="tag-color"></toolcool-color-picker>`, `<toolcool-color-picker id="${colorPickerId}" color="${tag.color}" class="tag-color"></toolcool-color-picker>`,
); );
@ -646,6 +738,7 @@ function appendViewTagToList(list, tag, everything) {
`<toolcool-color-picker id="${colorPicker2Id}" color="${tag.color2}" class="tag-color2"></toolcool-color-picker>`, `<toolcool-color-picker id="${colorPicker2Id}" color="${tag.color2}" class="tag-color2"></toolcool-color-picker>`,
); );
template.find('.tag_as_folder').attr('id', tagAsFolderId);
template.find('.tag-color').attr('id', colorPickerId); template.find('.tag-color').attr('id', colorPickerId);
template.find('.tag-color2').attr('id', colorPicker2Id); template.find('.tag-color2').attr('id', colorPicker2Id);
@ -663,10 +756,26 @@ function appendViewTagToList(list, tag, everything) {
}); });
}, 100); }, 100);
// @ts-ignore
$(colorPickerId).color = tag.color; $(colorPickerId).color = tag.color;
// @ts-ignore
$(colorPicker2Id).color = tag.color2; $(colorPicker2Id).color = tag.color2;
} }
function onTagAsFolderClick() {
const id = $(this).closest('.tag_view_item').attr('id');
const tag = tags.find(x => x.id === id);
// Toggle
tag.is_folder = tag.is_folder != true;
$(`.tag_view_item[id="${id}"] .tag_as_folder`).toggleClass('yes_folder').toggleClass('no_folder');
// If folder display has changed, we have to redraw the character list, otherwise this folders state would not change
printCharacters(true);
saveSettingsDebounced();
}
function onTagDeleteClick() { function onTagDeleteClick() {
if (!confirm('Are you sure?')) { if (!confirm('Are you sure?')) {
return; return;
@ -738,8 +847,10 @@ jQuery(() => {
$(document).on('input', '.tag_input', onTagInput); $(document).on('input', '.tag_input', onTagInput);
$(document).on('click', '.tags_view', onViewTagsListClick); $(document).on('click', '.tags_view', onViewTagsListClick);
$(document).on('click', '.tag_delete', onTagDeleteClick); $(document).on('click', '.tag_delete', onTagDeleteClick);
$(document).on('click', '.tag_as_folder', onTagAsFolderClick);
$(document).on('input', '.tag_view_name', onTagRenameInput); $(document).on('input', '.tag_view_name', onTagRenameInput);
$(document).on('click', '.tag_view_create', onTagCreateClick); $(document).on('click', '.tag_view_create', onTagCreateClick);
$(document).on('click', '.tag_view_backup', onTagsBackupClick); $(document).on('click', '.tag_view_backup', onTagsBackupClick);
$(document).on('click', '.tag_view_restore', onBackupRestoreClick); $(document).on('click', '.tag_view_restore', onBackupRestoreClick);
}); });