From 25b528ee4fcf87991db79c29a387567d4de725d0 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 18 Feb 2024 08:42:36 +0100 Subject: [PATCH] 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 --- public/css/tags.css | 46 +++++++++++++++ public/index.html | 2 + public/script.js | 5 +- public/scripts/tags.js | 123 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 168 insertions(+), 8 deletions(-) diff --git a/public/css/tags.css b/public/css/tags.css index 3ad18c468..9d11c45a0 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -174,3 +174,49 @@ 1px -1px 0px black; 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; +} diff --git a/public/index.html b/public/index.html index e91ea5cae..1252037b7 100644 --- a/public/index.html +++ b/public/index.html @@ -4542,6 +4542,8 @@
+
+
diff --git a/public/script.js b/public/script.js index dcab37d2a..a0c793f7b 100644 --- a/public/script.js +++ b/public/script.js @@ -163,6 +163,7 @@ import { renameTagKey, importTags, tag_filter_types, + compareTagsForSort, } from './scripts/tags.js'; import { SECRET_KEYS, @@ -1326,7 +1327,7 @@ export function getEntitiesList({ doFilter } = {}) { let entities = [ ...characters.map((item, index) => characterToEntity(item, index)), ...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) { @@ -1337,7 +1338,7 @@ export function getEntitiesList({ doFilter } = {}) { // Get tags of entities within the bogus folder const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.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 = []; for (const entity of entities) { for (const tag of otherTags) { diff --git a/public/scripts/tags.js b/public/scripts/tags.js index f6a756278..29163a34b 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -12,7 +12,8 @@ import { import { FILTER_TYPES, FilterHelper } from './filters.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 { tags, @@ -24,6 +25,8 @@ export { createTagMapFromList, renameTagKey, importTags, + sortTags, + compareTagsForSort, }; const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter'; @@ -111,7 +114,7 @@ function getTagsList(key) { return tag_map[key] .map(x => tags.find(y => y.id === x)) .filter(x => x) - .sort((a, b) => a.name.localeCompare(b.name)); + .sort(compareTagsForSort); } function getInlineListSelector() { @@ -245,7 +248,9 @@ async function importTags(imported_char) { } else { selected_tags = await callPopup(`

Importing Tags For ${imported_char.name}

${existingTags.length} existing tags have been found${existingTagsString}.

The following ${newTags.length} new tags will be imported.

`, 'input', newTags.join(', ')); } + // @ts-ignore selected_tags = existingTags.concat(selected_tags.split(',')); + // @ts-ignore selected_tags = selected_tags.map(t => t.trim()).filter(t => t !== ''); //Anti-troll measure if (selected_tags.length > 15) { @@ -276,6 +281,8 @@ function createNewTag(tagName) { const tag = { id: uuidv4(), name: tagName, + is_folder: false, + sort_order: tags.length, color: '', color2: '', create_date: Date.now(), @@ -379,7 +386,7 @@ function printTagFilters(type = tag_filter_types.character) { const characterTagIds = Object.values(tag_map).flat(); const tagsToDisplay = tags .filter(x => characterTagIds.includes(x.id)) - .sort((a, b) => a.name.localeCompare(b.name)); + .sort(compareTagsForSort); for (const tag of Object.values(ACTIONABLE_TAGS)) { appendTagToList(FILTER_SELECTOR, tag, { removable: false, selectable: false, action: tag.action, isGeneralList: true }); @@ -426,13 +433,16 @@ function onTagRemoveClick(event) { saveSettingsDebounced(); } +// @ts-ignore function onTagInput(event) { let val = $(this).val(); if (tags.find(t => t.name === val)) return; + // @ts-ignore $(this).autocomplete('search', val); } function onTagInputFocus() { + // @ts-ignore $(this).autocomplete('search', $(this).val()); } @@ -475,6 +485,7 @@ function applyTagsOnGroupSelect() { export function createTagInput(inputSelector, listSelector) { $(inputSelector) + // @ts-ignore .autocomplete({ source: (i, o) => findTag(i, o, listSelector), select: (e, u) => selectTag(e, u, listSelector), @@ -509,19 +520,94 @@ function onViewTagsListClick() {
+ Drag the handle to reorder.
+ ${(power_user.bogus_folders ? 'Click on the folder icon to use this tag as a folder.
' : '')} Click on the tag name to edit it.
Click on color box to assign new color.
`); - const sortedTags = tags.slice().sort((a, b) => a?.name?.toLowerCase()?.localeCompare(b?.name?.toLowerCase())); + const tagContainer = $('
'); + 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) { - 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'); } +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) { const file = e.target.files[0]; @@ -620,7 +706,7 @@ function onTagsBackupClick() { function onTagCreateClick() { const tag = createNewTag('New Tag'); - appendViewTagToList($('#tag_view_list'), tag, []); + appendViewTagToList($('#tag_view_list .tag_view_list_tags'), tag, []); printCharacters(false); 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('color', tag.color2); + const tagAsFolderId = tag.id + '-tag-folder'; const colorPickerId = tag.id + '-tag-color'; 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( ``, ); @@ -646,6 +738,7 @@ function appendViewTagToList(list, tag, everything) { ``, ); + template.find('.tag_as_folder').attr('id', tagAsFolderId); template.find('.tag-color').attr('id', colorPickerId); template.find('.tag-color2').attr('id', colorPicker2Id); @@ -663,10 +756,26 @@ function appendViewTagToList(list, tag, everything) { }); }, 100); + // @ts-ignore $(colorPickerId).color = tag.color; + // @ts-ignore $(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() { if (!confirm('Are you sure?')) { return; @@ -738,8 +847,10 @@ jQuery(() => { $(document).on('input', '.tag_input', onTagInput); $(document).on('click', '.tags_view', onViewTagsListClick); $(document).on('click', '.tag_delete', onTagDeleteClick); + $(document).on('click', '.tag_as_folder', onTagAsFolderClick); $(document).on('input', '.tag_view_name', onTagRenameInput); $(document).on('click', '.tag_view_create', onTagCreateClick); $(document).on('click', '.tag_view_backup', onTagsBackupClick); $(document).on('click', '.tag_view_restore', onBackupRestoreClick); }); +