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:
parent
181657cede
commit
25b528ee4f
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue