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