mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	Fix and improve more tag popups
- Rework tag color pickers to... actually work without hacks - Color picker default to main text color and tag default background. If default color is chosen, sets "empty" in tag, for possible style changes - Fix tabbing on tag name in tag view list being broken - Unique names on new tag click - Several fixes on tags popups - Animation utility functions (for popup, heh) - Utility function to get free (unique) name
This commit is contained in:
		| @@ -14,13 +14,13 @@ import { | |||||||
| import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js'; | import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js'; | ||||||
|  |  | ||||||
| import { groupCandidatesFilter, groups, select_group_chats, selected_group } from './group-chats.js'; | import { groupCandidatesFilter, groups, select_group_chats, selected_group } from './group-chats.js'; | ||||||
| import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray } from './utils.js'; | import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray, getFreeName } from './utils.js'; | ||||||
| import { power_user } from './power-user.js'; | import { power_user } from './power-user.js'; | ||||||
| import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | ||||||
| import { SlashCommand } from './slash-commands/SlashCommand.js'; | import { SlashCommand } from './slash-commands/SlashCommand.js'; | ||||||
| import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; | ||||||
| import { isMobile } from './RossAscends-mods.js'; | import { isMobile } from './RossAscends-mods.js'; | ||||||
| import { POPUP_TYPE, callGenericPopup } from './popup.js'; | import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js'; | ||||||
|  |  | ||||||
| export { | export { | ||||||
|     TAG_FOLDER_TYPES, |     TAG_FOLDER_TYPES, | ||||||
| @@ -330,7 +330,7 @@ function filterByFolder(filterHelper) { | |||||||
|     if (!power_user.bogus_folders) { |     if (!power_user.bogus_folders) { | ||||||
|         $('#bogus_folders').prop('checked', true).trigger('input'); |         $('#bogus_folders').prop('checked', true).trigger('input'); | ||||||
|         onViewTagsListClick(); |         onViewTagsListClick(); | ||||||
|         flashHighlight($('#dialogue_popup .tag_as_folder, #dialogue_popup .tag_folder_indicator')); |         flashHighlight($('#tag_view_list .tag_as_folder, #tag_view_list .tag_folder_indicator')); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -814,7 +814,7 @@ function getTag(tagName, { createNew = false } = {}) { | |||||||
| function createNewTag(tagName) { | function createNewTag(tagName) { | ||||||
|     const existing = getTag(tagName); |     const existing = getTag(tagName); | ||||||
|     if (existing) { |     if (existing) { | ||||||
|         toastr.warning(`Cannot create new tag. A tag with the name already exists:<br />${existing}`, 'Creating Tag', { escapeHtml: false }); |         toastr.warning(`Cannot create new tag. A tag with the name already exists:<br />${existing.name}`, 'Creating Tag', { escapeHtml: false }); | ||||||
|         return existing; |         return existing; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -1225,9 +1225,7 @@ export function createTagInput(inputSelector, listSelector, tagListOptions = {}) | |||||||
|         .focus(onTagInputFocus); // <== show tag list on click |         .focus(onTagInputFocus); // <== show tag list on click | ||||||
| } | } | ||||||
|  |  | ||||||
| function onViewTagsListClick() { | async function onViewTagsListClick() { | ||||||
|     const popup = $('#dialogue_popup'); |  | ||||||
|     popup.addClass('large_dialogue_popup'); |  | ||||||
|     const html = $(document.createElement('div')); |     const html = $(document.createElement('div')); | ||||||
|     html.attr('id', 'tag_view_list'); |     html.attr('id', 'tag_view_list'); | ||||||
|     html.append(` |     html.append(` | ||||||
| @@ -1268,13 +1266,10 @@ function onViewTagsListClick() { | |||||||
|     const tagContainer = $('<div class="tag_view_list_tags ui-sortable"></div>'); |     const tagContainer = $('<div class="tag_view_list_tags ui-sortable"></div>'); | ||||||
|     html.append(tagContainer); |     html.append(tagContainer); | ||||||
|  |  | ||||||
|     const result = callGenericPopup(html, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true }); |     printViewTagList(tagContainer); | ||||||
|  |  | ||||||
|     printViewTagList(html); |  | ||||||
|     makeTagListDraggable(tagContainer); |     makeTagListDraggable(tagContainer); | ||||||
|  |  | ||||||
|     $('#dialogue_popup  .tag-color').on('change', (evt) => onTagColorize(evt)); |     await callGenericPopup(html, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true }); | ||||||
|     $('#dialogue_popup  .tag-color2').on('change', (evt) => onTagColorize2(evt)); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -1336,7 +1331,7 @@ function makeTagListDraggable(tagContainer) { | |||||||
|         // If tags were dragged manually, we have to disable auto sorting |         // If tags were dragged manually, we have to disable auto sorting | ||||||
|         if (power_user.auto_sort_tags) { |         if (power_user.auto_sort_tags) { | ||||||
|             power_user.auto_sort_tags = false; |             power_user.auto_sort_tags = false; | ||||||
|             $('#dialogue_popup input[name="auto_sort_tags"]').prop('checked', false); |             $('#tag_view_list input[name="auto_sort_tags"]').prop('checked', false); | ||||||
|             toastr.info('Automatic sorting of tags deactivated.'); |             toastr.info('Automatic sorting of tags deactivated.'); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -1463,7 +1458,7 @@ async function onTagRestoreFileSelect(e) { | |||||||
|     printCharactersDebounced(); |     printCharactersDebounced(); | ||||||
|     saveSettingsDebounced(); |     saveSettingsDebounced(); | ||||||
|  |  | ||||||
|     onViewTagsListClick(); |     await onViewTagsListClick(); | ||||||
| } | } | ||||||
|  |  | ||||||
| function onBackupRestoreClick() { | function onBackupRestoreClick() { | ||||||
| @@ -1485,14 +1480,18 @@ function onTagsBackupClick() { | |||||||
| } | } | ||||||
|  |  | ||||||
| function onTagCreateClick() { | function onTagCreateClick() { | ||||||
|     const tag = createNewTag('New Tag'); |     const tagName = getFreeName('New Tag', tags.map(x => x.name)); | ||||||
|     printViewTagList(); |     const tag = createNewTag(tagName); | ||||||
|  |     printViewTagList($('#tag_view_list .tag_view_list_tags')); | ||||||
|  |  | ||||||
|     const tagElement = ($('#dialogue_popup .tag_view_list_tags')).find(`.tag_view_item[id="${tag.id}"]`); |     const tagElement = ($('#tag_view_list .tag_view_list_tags')).find(`.tag_view_item[id="${tag.id}"]`); | ||||||
|  |     tagElement[0]?.scrollIntoView(); | ||||||
|     flashHighlight(tagElement); |     flashHighlight(tagElement); | ||||||
|  |  | ||||||
|     printCharactersDebounced(); |     printCharactersDebounced(); | ||||||
|     saveSettingsDebounced(); |     saveSettingsDebounced(); | ||||||
|  |  | ||||||
|  |     toastr.success('Tag created', 'Create Tag', { showDuration: 60000 }); | ||||||
| } | } | ||||||
|  |  | ||||||
| function appendViewTagToList(list, tag, everything) { | function appendViewTagToList(list, tag, everything) { | ||||||
| @@ -1516,25 +1515,23 @@ function appendViewTagToList(list, tag, everything) { | |||||||
|  |  | ||||||
|     const primaryColorPicker = $('<toolcool-color-picker></toolcool-color-picker>') |     const primaryColorPicker = $('<toolcool-color-picker></toolcool-color-picker>') | ||||||
|         .addClass('tag-color') |         .addClass('tag-color') | ||||||
|         .attr({ id: colorPickerId, color: tag.color }); |         .attr({ id: colorPickerId, color: tag.color || 'rgba(0, 0, 0, 0.3)', 'data-default-color': 'rgba(0, 0, 0, 0.3)' }); | ||||||
|  |  | ||||||
|     const secondaryColorPicker = $('<toolcool-color-picker></toolcool-color-picker>') |     const secondaryColorPicker = $('<toolcool-color-picker></toolcool-color-picker>') | ||||||
|         .addClass('tag-color2') |         .addClass('tag-color2') | ||||||
|         .attr({ id: colorPicker2Id, color: tag.color2 }); |         .attr({ id: colorPicker2Id, color: tag.color2 || power_user.main_text_color, 'data-default-color': power_user.main_text_color }); | ||||||
|  |  | ||||||
|     template.find('.tagColorPickerHolder').append(primaryColorPicker); |     template.find('.tagColorPickerHolder').append(primaryColorPicker); | ||||||
|     template.find('.tagColorPicker2Holder').append(secondaryColorPicker); |     template.find('.tagColorPicker2Holder').append(secondaryColorPicker); | ||||||
|  |  | ||||||
|     template.find('.tag_as_folder').attr('id', tagAsFolderId); |     template.find('.tag_as_folder').attr('id', tagAsFolderId); | ||||||
|  |  | ||||||
|  |     primaryColorPicker.on('change', (evt) => onTagColorize(evt)); | ||||||
|  |     secondaryColorPicker.on('change', (evt) => onTagColorize2(evt)); | ||||||
|  |  | ||||||
|     list.append(template); |     list.append(template); | ||||||
|  |  | ||||||
|     updateDrawTagFolder(template, tag); |     updateDrawTagFolder(template, tag); | ||||||
|  |  | ||||||
|     // @ts-ignore |  | ||||||
|     $(colorPickerId).color = tag.color; |  | ||||||
|     // @ts-ignore |  | ||||||
|     $(colorPicker2Id).color = tag.color2; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| function onTagAsFolderClick() { | function onTagAsFolderClick() { | ||||||
| @@ -1588,21 +1585,19 @@ async function onTagDeleteClick() { | |||||||
|  |  | ||||||
|     appendTagToList(popupContent.find('#tag_to_delete'), tag); |     appendTagToList(popupContent.find('#tag_to_delete'), tag); | ||||||
|  |  | ||||||
|     const result = callGenericPopup(popupContent, POPUP_TYPE.CONFIRM); |  | ||||||
|  |  | ||||||
|     // Make the select control more fancy on not mobile |     // Make the select control more fancy on not mobile | ||||||
|     if (!isMobile()) { |     if (!isMobile()) { | ||||||
|         // Delete the empty option in the dropdown, and make the select2 be empty by default |         // Delete the empty option in the dropdown, and make the select2 be empty by default | ||||||
|         $('#merge_tag_select option[value=""]').remove(); |         popupContent.find('#merge_tag_select option[value=""]').remove(); | ||||||
|         $('#merge_tag_select').select2({ |         popupContent.find('#merge_tag_select').select2({ | ||||||
|             width: '50%', |             width: '50%', | ||||||
|             placeholder: 'Select tag to merge into', |             placeholder: 'Select tag to merge into', | ||||||
|             allowClear: true, |             allowClear: true, | ||||||
|         }).val(null).trigger('change'); |         }).val(null).trigger('change'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const confirm = await result; |     const result = await callGenericPopup(popupContent, POPUP_TYPE.CONFIRM); | ||||||
|     if (!confirm) { |     if (result !== POPUP_RESULT.AFFIRMATIVE) { | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -1633,14 +1628,22 @@ function onTagRenameInput() { | |||||||
|     const newName = $(this).text(); |     const newName = $(this).text(); | ||||||
|     const tag = tags.find(x => x.id === id); |     const tag = tags.find(x => x.id === id); | ||||||
|     tag.name = newName; |     tag.name = newName; | ||||||
|  |     $(this).attr('dirty', ''); | ||||||
|     $(`.tag[id="${id}"] .tag_name`).text(newName); |     $(`.tag[id="${id}"] .tag_name`).text(newName); | ||||||
|     saveSettingsDebounced(); |     saveSettingsDebounced(); | ||||||
| } | } | ||||||
|  |  | ||||||
| function onTagColorize(evt) { | function onTagColorize(evt) { | ||||||
|     console.debug(evt); |     console.debug(evt); | ||||||
|  |     const isDefaultColor = $(evt.target).data('default-color') === evt.detail.rgba; | ||||||
|  |  | ||||||
|  |     if (evt.detail.rgba === evt.detail.color.originalInput) | ||||||
|  |         return; | ||||||
|  |  | ||||||
|     const id = $(evt.target).closest('.tag_view_item').attr('id'); |     const id = $(evt.target).closest('.tag_view_item').attr('id'); | ||||||
|     const newColor = evt.detail.rgba; |     let newColor = evt.detail.rgba; | ||||||
|  |     if (isDefaultColor) newColor = ''; | ||||||
|  |  | ||||||
|     $(evt.target).parent().parent().find('.tag_view_name').css('background-color', newColor); |     $(evt.target).parent().parent().find('.tag_view_name').css('background-color', newColor); | ||||||
|     $(`.tag[id="${id}"]`).css('background-color', newColor); |     $(`.tag[id="${id}"]`).css('background-color', newColor); | ||||||
|     $(`.bogus_folder_select[tagid="${id}"] .avatar`).css('background-color', newColor); |     $(`.bogus_folder_select[tagid="${id}"] .avatar`).css('background-color', newColor); | ||||||
| @@ -1652,8 +1655,17 @@ function onTagColorize(evt) { | |||||||
|  |  | ||||||
| function onTagColorize2(evt) { | function onTagColorize2(evt) { | ||||||
|     console.debug(evt); |     console.debug(evt); | ||||||
|  |     if (evt.detail.rgba === evt.detail.color.originalInput) | ||||||
|  |         return; | ||||||
|  |  | ||||||
|     const id = $(evt.target).closest('.tag_view_item').attr('id'); |     const id = $(evt.target).closest('.tag_view_item').attr('id'); | ||||||
|     const newColor = evt.detail.rgba; |     let newColor = evt.detail.rgba; | ||||||
|  |  | ||||||
|  |     // If new color is same as "data-default-color", we set it to empty string | ||||||
|  |     const defaultColor = $(evt.target).data('default-color'); | ||||||
|  |     if (newColor === defaultColor) newColor = ''; | ||||||
|  |  | ||||||
|  |  | ||||||
|     $(evt.target).parent().parent().find('.tag_view_name').css('color', newColor); |     $(evt.target).parent().parent().find('.tag_view_name').css('color', newColor); | ||||||
|     $(`.tag[id="${id}"]`).css('color', newColor); |     $(`.tag[id="${id}"]`).css('color', newColor); | ||||||
|     $(`.bogus_folder_select[tagid="${id}"] .avatar`).css('color', newColor); |     $(`.bogus_folder_select[tagid="${id}"] .avatar`).css('color', newColor); | ||||||
| @@ -1701,9 +1713,7 @@ function copyTags(data) { | |||||||
|     tag_map[data.newAvatar] = Array.from(new Set([...prevTagMap, ...newTagMap])); |     tag_map[data.newAvatar] = Array.from(new Set([...prevTagMap, ...newTagMap])); | ||||||
| } | } | ||||||
|  |  | ||||||
| function printViewTagList(html, empty = true) { | function printViewTagList(tagContainer, empty = true) { | ||||||
|     const tagContainer = html.find('.tag_view_list_tags'); |  | ||||||
|  |  | ||||||
|     if (empty) tagContainer.empty(); |     if (empty) tagContainer.empty(); | ||||||
|     const everything = Object.values(tag_map).flat(); |     const everything = Object.values(tag_map).flat(); | ||||||
|     const sortedTags = sortTags(tags); |     const sortedTags = sortTags(tags); | ||||||
| @@ -1901,22 +1911,31 @@ export function initTags() { | |||||||
|     eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags); |     eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags); | ||||||
|     eventSource.makeFirst(event_types.CHAT_CHANGED, () => selected_group ? applyTagsOnGroupSelect() : applyTagsOnCharacterSelect()); |     eventSource.makeFirst(event_types.CHAT_CHANGED, () => selected_group ? applyTagsOnGroupSelect() : applyTagsOnCharacterSelect()); | ||||||
|  |  | ||||||
|     $(document).on('input', '#dialogue_popup input[name="auto_sort_tags"]', (evt) => { |     $(document).on('input', '#tag_view_list input[name="auto_sort_tags"]', (evt) => { | ||||||
|         const toggle = $(evt.target).is(':checked'); |         const toggle = $(evt.target).is(':checked'); | ||||||
|         toggleAutoSortTags(evt.originalEvent, toggle); |         toggleAutoSortTags(evt.originalEvent, toggle); | ||||||
|         printViewTagList(); |         printViewTagList($('#tag_view_list .tag_view_list_tags')); | ||||||
|     }); |     }); | ||||||
|     $(document).on('focusout', '#dialogue_popup .tag_view_name', (evt) => { |     $(document).on('focusout', '#tag_view_list .tag_view_name', (evt) => { | ||||||
|  |         // Reorder/reprint tags, but only if the name actually has changed, and only if we auto sort tags | ||||||
|  |         if (!power_user.auto_sort_tags || !$(evt.target).is('[dirty]')) return; | ||||||
|  |  | ||||||
|         // Remember the order, so we can flash highlight if it changed after reprinting |         // Remember the order, so we can flash highlight if it changed after reprinting | ||||||
|         const tagId = $(evt.target).parent('.tag_view_item').attr('id'); |         const tagId = ($(evt.target).closest('.tag_view_item')).attr('id'); | ||||||
|         const oldOrder = $('#dialogue_popup .tag_view_item').map((_, el) => el.id).get(); |         const oldOrder = $('#tag_view_list .tag_view_item').map((_, el) => el.id).get(); | ||||||
|  |  | ||||||
|         printViewTagList(); |         printViewTagList($('#tag_view_list .tag_view_list_tags')); | ||||||
|  |  | ||||||
|         const newOrder = $('#dialogue_popup .tag_view_item').map((_, el) => el.id).get(); |         // If the new focus would've been inside the now redrawn tag list, we should at least move back the focus to the current name | ||||||
|  |         // Otherwise tab-navigation gets a bit weird | ||||||
|  |         if (evt.relatedTarget instanceof HTMLElement && $(evt.relatedTarget).closest('#tag_view_list')) { | ||||||
|  |             $(`#tag_view_list .tag_view_item[id="${tagId}"] .tag_view_name`)[0]?.focus(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const newOrder = $('#tag_view_list .tag_view_item').map((_, el) => el.id).get(); | ||||||
|         const orderChanged = !oldOrder.every((id, index) => id === newOrder[index]); |         const orderChanged = !oldOrder.every((id, index) => id === newOrder[index]); | ||||||
|         if (orderChanged) { |         if (orderChanged) { | ||||||
|             flashHighlight($(`#dialogue_popup .tag_view_item[id="${tagId}"]`)); |             flashHighlight($(`#tag_view_list .tag_view_item[id="${tagId}"]`)); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -74,6 +74,20 @@ export function onlyUnique(value, index, array) { | |||||||
|     return array.indexOf(value) === index; |     return array.indexOf(value) === index; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Removes the first occurrence of a specified item from an array | ||||||
|  |  * | ||||||
|  |  * @param {*[]} array - The array from which to remove the item | ||||||
|  |  * @param {*} item - The item to remove from the array | ||||||
|  |  * @returns {boolean} - Returns true if the item was successfully removed, false otherwise. | ||||||
|  |  */ | ||||||
|  | export function removeFromArray(array, item) { | ||||||
|  |     const index = array.indexOf(item); | ||||||
|  |     if (index === -1) return false; | ||||||
|  |     array.splice(index, 1); | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Checks if a string only contains digits. |  * Checks if a string only contains digits. | ||||||
|  * @param {string} str The string to check. |  * @param {string} str The string to check. | ||||||
| @@ -1499,6 +1513,35 @@ export function flashHighlight(element, timespan = 2000) { | |||||||
|     setTimeout(() => element.removeClass('flash animated'), timespan); |     setTimeout(() => element.removeClass('flash animated'), timespan); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Checks if the given control has an animation applied to it | ||||||
|  |  * | ||||||
|  |  * @param {HTMLElement} control - The control element to check for animation | ||||||
|  |  * @returns {boolean} Whether the control has an animation applied | ||||||
|  |  */ | ||||||
|  | export function hasAnimation(control) { | ||||||
|  |     const animatioName = getComputedStyle(control, null)["animation-name"]; | ||||||
|  |     return animatioName != "none"; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Run an action once an animation on a control ends. If the control has no animation, the action will be executed immediately. | ||||||
|  |  * | ||||||
|  |  * @param {HTMLElement} control - The control element to listen for animation end event | ||||||
|  |  * @param {(control:*?) => void} callback - The callback function to be executed when the animation ends | ||||||
|  |  */ | ||||||
|  | export function runAfterAnimation(control, callback) { | ||||||
|  |     if (hasAnimation(control)) { | ||||||
|  |         const onAnimationEnd = () => { | ||||||
|  |             control.removeEventListener('animationend', onAnimationEnd); | ||||||
|  |             callback(control); | ||||||
|  |         }; | ||||||
|  |         control.addEventListener('animationend', onAnimationEnd); | ||||||
|  |     } else { | ||||||
|  |         callback(control); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * A common base function for case-insensitive and accent-insensitive string comparisons. |  * A common base function for case-insensitive and accent-insensitive string comparisons. | ||||||
|  * |  * | ||||||
| @@ -1755,3 +1798,22 @@ export async function checkOverwriteExistingData(type, existingNames, name, { in | |||||||
|  |  | ||||||
|     return true; |     return true; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Generates a free name by appending a counter to the given name if it already exists in the list | ||||||
|  |  * | ||||||
|  |  * @param {string} name - The original name to check for existence in the list | ||||||
|  |  * @param {string[]} list - The list of names to check for existence | ||||||
|  |  * @param {(n: number) => string} [numberFormatter=(n) => ` #${n}`] - The function used to format the counter | ||||||
|  |  * @returns {string} The generated free name | ||||||
|  |  */ | ||||||
|  | export function getFreeName(name, list, numberFormatter = (n) => ` #${n}`) { | ||||||
|  |     if (!list.includes(name)) { | ||||||
|  |         return name; | ||||||
|  |     } | ||||||
|  |     let counter = 1; | ||||||
|  |     while (list.includes(`${name} #${counter}`)) { | ||||||
|  |         counter++; | ||||||
|  |     } | ||||||
|  |     return `${name}${numberFormatter(counter)}`; | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user