Merge pull request #2330 from Wolfsblvt/smol-tag-improvements

Smol tag improvements & Huuuuuge popup/dialog rework (heh)
This commit is contained in:
Cohee 2024-06-18 01:58:52 +03:00 committed by GitHub
commit c3cbf33ba0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 2007 additions and 764 deletions

122
public/css/animations.css Normal file
View File

@ -0,0 +1,122 @@
/* Fade animations with opacity */
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
/* Pop animations with opacity and vertical scaling */
@keyframes pop-in {
0% {
opacity: 0;
transform: scaleY(0);
}
/* Make the scaling faster on pop-in, otherwise it looks a bit weird */
33% {
transform: scaleY(1);
}
100% {
opacity: 1;
transform: scaleY(1);
}
}
@keyframes pop-out {
0% {
opacity: 1;
transform: scaleY(1);
}
100% {
opacity: 0;
transform: scaleY(0);
}
}
/* Flashing for highlighting animation */
@keyframes flash {
20%,
60%,
100% {
opacity: 1;
}
0%,
40%,
80% {
opacity: 0.2;
}
}
/* Pulsing highlight, slightly resizing the element */
@keyframes pulse {
from {
transform: scale(1);
filter: brightness(1.1);
}
to {
transform: scale(1.01);
filter: brightness(1.3);
}
}
/* Ellipsis animation */
@keyframes ellipsis {
0% {
content: ""
}
25% {
content: "."
}
50% {
content: ".."
}
75% {
content: "..."
}
}
/* HEINOUS */
@keyframes infinite-spinning {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* STscript animation */
@keyframes script_progress_pulse {
0%,
100% {
border-top-color: var(--progColor);
}
50% {
border-top-color: var(--progFlashColor);
}
}

View File

@ -98,7 +98,7 @@
font-weight: bold; font-weight: bold;
} }
.logprobs_top_candidate:not([disabled]):hover, .logprobs_top_candidate:not([disabled]):focus { .logprobs_top_candidate:not([disabled]):hover {
background-color: rgba(0, 0, 0, 0.3); background-color: rgba(0, 0, 0, 0.3);
} }

131
public/css/popup.css Normal file
View File

@ -0,0 +1,131 @@
dialog {
color: var(--SmartThemeBodyColor);
}
/* Closed state of the dialog */
.popup {
width: 500px;
text-align: center;
box-shadow: 0px 0px 14px var(--black70a);
border: 1px solid var(--SmartThemeBorderColor);
padding: 4px 14px;
background-color: var(--SmartThemeBlurTintColor);
border-radius: 10px;
display: flex;
flex-direction: column;
/* Overflow visible so elements (like toasts) can appear outside of the dialog. '.popup-body' is hiding overflow for the real content. */
overflow: visible;
/* Fix weird animation issue with font-scaling during popup open */
backface-visibility: hidden;
transform: translateZ(0);
-webkit-font-smoothing: subpixel-antialiased;
}
.popup .popup-body {
display: flex;
flex-direction: column;
overflow: hidden;
width: 100%;
height: 100%;
padding: 1px;
}
.popup .popup-content {
margin-top: 10px;
padding: 0 8px;
overflow: hidden;
flex-grow: 1;
}
.popup .popup-content h3:first-child {
/* No double spacing for the first heading needed, the .popup-content already has margin */
margin-top: 0px;
}
.popup.vertical_scrolling_dialogue_popup .popup-content {
overflow-y: auto;
}
.popup.horizontal_scrolling_dialogue_popup .popup-content {
overflow-x: auto;
}
/* Opening animation */
.popup[opening] {
animation: pop-in var(--animation-duration-slow) ease-in-out;
}
.popup[opening]::backdrop {
animation: fade-in var(--animation-duration-slow) ease-in-out;
}
/* Open state of the dialog */
.popup[open] {
color: var(--SmartThemeBodyColor);
}
.popup[open]::backdrop {
backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
-webkit-backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
background-color: var(--black30a);
}
/* Closing animation */
.popup[closing] {
animation: pop-out var(--animation-duration-slow) ease-in-out;
}
.popup[closing]::backdrop {
animation: fade-out var(--animation-duration-slow) ease-in-out;
}
/* Fix toastr in dialogs by actually placing it at the top of the screen via transform */
.popup #toast-container {
height: 100svh;
top: calc(50% + var(--topBarBlockSize));
left: 50%;
transform: translate(-50%, -50%);
}
.popup-input {
margin-top: 10px;
}
.popup-controls {
margin-top: 10px;
display: flex;
align-self: center;
gap: 20px;
}
.menu_button.menu_button_default {
box-shadow: 0 0 5px var(--white20a);
}
.menu_button.popup-button-ok {
background-color: var(--crimson70a);
cursor: pointer;
}
.menu_button.popup-button-ok:hover {
background-color: var(--crimson-hover);
}
.menu_button.popup-button-custom {
/* Custom buttons should not scale to smallest size, otherwise they will always break to multiline */
width: unset;
}
.popup-controls .menu_button {
/* Fix weird animation issue with fonts on brightness filter */
backface-visibility: hidden;
transform: translateZ(0);
-webkit-font-smoothing: subpixel-antialiased;
}
.popup-controls .menu_button:hover:focus-visible {
filter: brightness(1.3) saturate(1.3);
}

View File

@ -181,8 +181,9 @@
} }
.select2-selection__choice__display { .select2-selection__choice__display {
/* Fix weird alignment on the left side */ /* Fix weird alignment of the inside block */
margin-left: 1px; margin-left: 3px;
margin-right: 1px;
} }
/* Styling for choice remove icon */ /* Styling for choice remove icon */

View File

@ -292,6 +292,14 @@
flex-wrap: nowrap; flex-wrap: nowrap;
} }
.inline-flex {
display: inline-flex;
}
.inline-block {
display: inline-block;
}
.alignitemscenter, .alignitemscenter,
.alignItemsCenter { .alignItemsCenter {
align-items: center; align-items: center;
@ -348,6 +356,10 @@
margin-right: 5px; margin-right: 5px;
} }
.margin-r2 {
margin-right: 2px;
}
.flex0 { .flex0 {
flex: 0; flex: 0;
} }

View File

@ -14,7 +14,7 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 10px; gap: 6px;
margin-bottom: 5px; margin-bottom: 5px;
} }
@ -27,8 +27,19 @@
flex: 1; flex: 1;
} }
.tag_view_color_picker {
position: relative;
}
.tag_view_color_picker .link_icon {
position: absolute;
top: 50%;
right: 0px;
opacity: 0.5;
}
.tag_delete { .tag_delete {
padding-right: 0; padding: 2px 4px;
color: var(--SmartThemeBodyColor) !important; color: var(--SmartThemeBodyColor) !important;
} }
@ -108,6 +119,14 @@
opacity: 0.6; opacity: 0.6;
} }
#tagList .tag:has(.tag_remove:hover) {
opacity: 1;
}
#tagList .tag:has(.tag_remove:hover) .tag_name {
opacity: 0.6;
}
.tags.tags_inline { .tags.tags_inline {
opacity: 0.6; opacity: 0.6;
column-gap: 0.2rem; column-gap: 0.2rem;
@ -164,6 +183,7 @@
.tag.selected { .tag.selected {
opacity: 1 !important; opacity: 1 !important;
filter: none !important; filter: none !important;
border: 1px solid lightgreen;
} }
.tag.excluded { .tag.excluded {

View File

@ -33,7 +33,6 @@
<link rel="apple-touch-icon" sizes="144x144" href="img/apple-icon-144x144.png" /> <link rel="apple-touch-icon" sizes="144x144" href="img/apple-icon-144x144.png" />
<link rel="stylesheet" type="text/css" href="style.css"> <link rel="stylesheet" type="text/css" href="style.css">
<link rel="stylesheet" type="text/css" href="css/st-tailwind.css"> <link rel="stylesheet" type="text/css" href="css/st-tailwind.css">
<link rel="stylesheet" type="text/css" href="css/tags.css">
<link rel="stylesheet" type="text/css" href="css/rm-groups.css"> <link rel="stylesheet" type="text/css" href="css/rm-groups.css">
<link rel="stylesheet" type="text/css" href="css/group-avatars.css"> <link rel="stylesheet" type="text/css" href="css/group-avatars.css">
<link rel="stylesheet" type="text/css" href="css/toggle-dependent.css"> <link rel="stylesheet" type="text/css" href="css/toggle-dependent.css">
@ -73,8 +72,8 @@
<div id="lm_button_panel_pin_div" title="Locked = AI Configuration panel will stay open" data-i18n="[title]AI Configuration panel will stay open"> <div id="lm_button_panel_pin_div" title="Locked = AI Configuration panel will stay open" data-i18n="[title]AI Configuration panel will stay open">
<input type="checkbox" id="lm_button_panel_pin"> <input type="checkbox" id="lm_button_panel_pin">
<label for="lm_button_panel_pin"> <label for="lm_button_panel_pin">
<div class="unchecked fa-solid fa-unlock "></div> <div class="unchecked fa-solid fa-unlock right_menu_button"></div>
<div class="checked fa-solid fa-lock "></div> <div class="checked fa-solid fa-lock right_menu_button"></div>
</label> </label>
</div> </div>
<div id="clickSlidersTips" data-i18n="clickslidertips" class="toggle-description wide100p editable-slider-notification"> <div id="clickSlidersTips" data-i18n="clickslidertips" class="toggle-description wide100p editable-slider-notification">
@ -4476,14 +4475,14 @@
<div id="rm_button_panel_pin_div" class="alignitemsflexstart" title="Locked = Character Management panel will stay open" data-i18n="[title]Locked = Character Management panel will stay open"> <div id="rm_button_panel_pin_div" class="alignitemsflexstart" title="Locked = Character Management panel will stay open" data-i18n="[title]Locked = Character Management panel will stay open">
<input type="checkbox" id="rm_button_panel_pin"> <input type="checkbox" id="rm_button_panel_pin">
<label for="rm_button_panel_pin"> <label for="rm_button_panel_pin">
<div class="fa-solid unchecked fa-unlock" alt=""></div> <div class="fa-solid unchecked fa-unlock right_menu_button" alt=""></div>
<div class="fa-solid checked fa-lock" alt=""></div> <div class="fa-solid checked fa-lock right_menu_button" alt=""></div>
</label> </label>
</div> </div>
<div class="right_menu_button fa-solid fa-list-ul" id="rm_button_characters" title="Select/Create Characters" data-i18n="[title]Select/Create Characters"></div> <div class="right_menu_button fa-solid fa-list-ul" id="rm_button_characters" title="Select/Create Characters" data-i18n="[title]Select/Create Characters"></div>
</div> </div>
<div id="HotSwapWrapper" class="alignitemscenter flex-container margin0auto wide100p"> <div id="HotSwapWrapper" class="alignitemscenter flex-container margin0auto wide100p">
<div class="hotswap avatars_inline flex-container expander" data-i18n="[no_favs]Favorite characters to add them to HotSwaps" no_favs="Favorite characters to add them to HotSwaps"></div> <div class="hotswap avatars_inline flex-container scroll-reset-container expander" data-i18n="[no_favs]Favorite characters to add them to HotSwaps" no_favs="Favorite characters to add them to HotSwaps"></div>
</div> </div>
</div> </div>
<hr> <hr>
@ -4491,7 +4490,7 @@
<div id="rm_PinAndTabs"> <div id="rm_PinAndTabs">
<div id="right-nav-panel-tabs" class=""> <div id="right-nav-panel-tabs" class="">
<div id="rm_button_selected_ch"> <div id="rm_button_selected_ch">
<h2></h2> <h2 class="interactable"></h2>
</div> </div>
<div id="result_info" class="flex-container" style="display: none;"> <div id="result_info" class="flex-container" style="display: none;">
<div id="result_info_text" title="Token counts may be inaccurate and provided just for reference." data-i18n="[title]Token counts may be inaccurate and provided just for reference."> <div id="result_info_text" title="Token counts may be inaccurate and provided just for reference." data-i18n="[title]Token counts may be inaccurate and provided just for reference.">
@ -4842,21 +4841,19 @@
</div> </div>
</div> </div>
<!-- various fullscreen popups --> <!-- various fullscreen popups -->
<template id="shadow_popup_template" data-i18n="[popup_text_save]popup_text_save;[popup_text_yes]popup_text_yes;[popup_text_no]popup_text_no;[popup_text_cancel]popup_text_cancel;[popup_text_import]popup_text_import" popup_text_save="Save" popup_text_yes="Yes" popup_text_no="No" popup_text_cancel="Cancel" popup_text_import="Import"> <!-- localization data holder for popups --> <template id="popup_template" data-i18n="[popup-button-save]popup-button-save;[popup-button-yes]popup-button-yes;[popup-button-no]popup-button-no;[popup-button-cancel]popup-button-cancel;[popup-button-import]popup-button-import" popup-button-save="Save" popup-button-yes="Yes" popup-button-no="No" popup-button-cancel="Cancel" popup-button-import="Import"> <!-- localization data holder for popups -->
<div class="shadow_popup"> <dialog class="popup">
<div class="dialogue_popup"> <div class="popup-body">
<div class="dialogue_popup_holder"> <div class="popup-content">
<div class="dialogue_popup_text"> <h3 class="popup-header">text</h3>
<h3 class="margin0">text</h3>
</div>
<textarea class="dialogue_popup_input text_pole" rows="1"></textarea>
<div class="dialogue_popup_controls">
<div class="dialogue_popup_ok menu_button" data-i18n="Delete">Delete</div>
<div class="dialogue_popup_cancel menu_button" data-i18n="Cancel">Cancel</div>
</div>
</div> </div>
<textarea class="popup-input text_pole result-control" rows="1" data-result="1"></textarea>
<div class="popup-controls">
<div class="popup-button-ok menu_button result-control" data-i18n="Delete" data-result="1" tabindex="0">Delete</div>
<div class="popup-button-cancel menu_button result-control" data-i18n="Cancel" data-result="0" tabindex="0">Cancel</div>
</div> </div>
</div> </div>
</dialog>
</template> </template>
<div id="shadow_popup"> <div id="shadow_popup">
<div id="dialogue_popup"> <div id="dialogue_popup">
@ -4866,15 +4863,15 @@
</div> </div>
<textarea id="dialogue_popup_input" class="text_pole" rows="1"></textarea> <textarea id="dialogue_popup_input" class="text_pole" rows="1"></textarea>
<div id="dialogue_popup_controls"> <div id="dialogue_popup_controls">
<div id="dialogue_popup_ok" class="menu_button" data-i18n="Delete">Delete</div> <div id="dialogue_popup_ok" class="menu_button" data-i18n="Delete" data-result="1">Delete</div>
<div id="dialogue_popup_cancel" class="menu_button" data-i18n="Cancel">Cancel</div> <div id="dialogue_popup_cancel" class="menu_button" data-i18n="Cancel" data-result="0">Cancel</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="character_popup" class="flex-container flexFlowColumn flexNoGap"> <div id="character_popup" class="flex-container flexFlowColumn flexNoGap">
<div id="character_popup_text"> <div id="character_popup_text">
<h3 id="character_popup_text_h3" class="margin0"></h3> <span data-i18n="Advanced Defininitions">- Advanced <h3 id="character_popup-button-h3" class="margin0"></h3> <span data-i18n="Advanced Defininitions">- Advanced
Definitions</span> Definitions</span>
</div> </div>
<hr class="margin-bot-10px"> <hr class="margin-bot-10px">
@ -5193,8 +5190,8 @@
<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 title="Tag as folder" class="tag_as_folder fa-solid fa-folder-open right_menu_button" data-i18n="[title]Use tag as folder">
<span class="tag_folder_indicator"></span> <span class="tag_folder_indicator"></span>
</div> </div>
<div class="tagColorPickerHolder"></div> <div class="tag_view_color_picker" data-value="color"></div>
<div class="tagColorPicker2Holder"></div> <div class="tag_view_color_picker" data-value="color2"></div>
<div class="tag_view_name" contenteditable="true"></div> <div class="tag_view_name" contenteditable="true"></div>
<div class="tag_view_counter"><span class="tag_view_counter_value"></span>&nbsp;entries</div> <div class="tag_view_counter"><span class="tag_view_counter_value"></span>&nbsp;entries</div>
<div title="Delete tag" class="tag_delete fa-solid fa-trash-can right_menu_button" data-i18n="[title]Delete tag"></div> <div title="Delete tag" class="tag_delete fa-solid fa-trash-can right_menu_button" data-i18n="[title]Delete tag"></div>
@ -5617,21 +5614,21 @@
</div> </div>
</div> </div>
<div class="mes_buttons"> <div class="mes_buttons">
<div title="Message Actions" class="extraMesButtonsHint fa-solid fa-ellipsis" data-i18n="[title]Message Actions"></div> <div title="Message Actions" class="mes_button extraMesButtonsHint fa-solid fa-ellipsis" data-i18n="[title]Message Actions"></div>
<div class="extraMesButtons"> <div class="extraMesButtons">
<div title="Translate message" class="mes_translate fa-solid fa-language" data-i18n="[title]Translate message"></div> <div title="Translate message" class="mes_button mes_translate fa-solid fa-language" data-i18n="[title]Translate message"></div>
<div title="Generate Image" class="sd_message_gen fa-solid fa-paintbrush" data-i18n="[title]Generate Image"></div> <div title="Generate Image" class="mes_button sd_message_gen fa-solid fa-paintbrush" data-i18n="[title]Generate Image"></div>
<div title="Narrate" class="mes_narrate fa-solid fa-bullhorn" data-i18n="[title]Narrate"></div> <div title="Narrate" class="mes_button mes_narrate fa-solid fa-bullhorn" data-i18n="[title]Narrate"></div>
<div title="Prompt" class="mes_prompt fa-solid fa-square-poll-horizontal " data-i18n="[title]Prompt" style="display: none;"></div> <div title="Prompt" class="mes_button mes_prompt fa-solid fa-square-poll-horizontal " data-i18n="[title]Prompt" style="display: none;"></div>
<div title="Exclude message from prompts" class="mes_hide fa-solid fa-eye" data-i18n="[title]Exclude message from prompts"></div> <div title="Exclude message from prompts" class="mes_button mes_hide fa-solid fa-eye" data-i18n="[title]Exclude message from prompts"></div>
<div title="Include message in prompts" class="mes_unhide fa-solid fa-eye-slash" data-i18n="[title]Include message in prompts"></div> <div title="Include message in prompts" class="mes_button mes_unhide fa-solid fa-eye-slash" data-i18n="[title]Include message in prompts"></div>
<div title="Embed file or image" class="mes_embed fa-solid fa-paperclip" data-i18n="[title]Embed file or image"></div> <div title="Embed file or image" class="mes_button mes_embed fa-solid fa-paperclip" data-i18n="[title]Embed file or image"></div>
<div title="Create checkpoint" class="mes_create_bookmark fa-regular fa-solid fa-flag-checkered" data-i18n="[title]Create checkpoint"></div> <div title="Create checkpoint" class="mes_button mes_create_bookmark fa-regular fa-solid fa-flag-checkered" data-i18n="[title]Create checkpoint"></div>
<div title="Create branch" class="mes_create_branch fa-regular fa-code-branch" data-i18n="[title]Create Branch"></div> <div title="Create branch" class="mes_button mes_create_branch fa-regular fa-code-branch" data-i18n="[title]Create Branch"></div>
<div title="Copy" class="mes_copy fa-solid fa-copy " data-i18n="[title]Copy"></div> <div title="Copy" class="mes_button mes_copy fa-solid fa-copy " data-i18n="[title]Copy"></div>
</div> </div>
<div title="Open checkpoint chat" class="mes_bookmark fa-solid fa-flag" data-i18n="[title]Open checkpoint chat"></div> <div title="Open checkpoint chat" class="mes_button mes_bookmark fa-solid fa-flag" data-i18n="[title]Open checkpoint chat"></div>
<div title="Edit" class="mes_edit fa-solid fa-pencil " data-i18n="[title]Edit"></div> <div title="Edit" class="mes_button mes_edit fa-solid fa-pencil " data-i18n="[title]Edit"></div>
</div> </div>
<div class="mes_edit_buttons"> <div class="mes_edit_buttons">
<div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div> <div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div>
@ -6251,7 +6248,7 @@
</form> </form>
<div id="nonQRFormItems"> <div id="nonQRFormItems">
<div id="leftSendForm" class="alignContentCenter"> <div id="leftSendForm" class="alignContentCenter">
<div id="options_button" class="fa-solid fa-bars"></div> <div id="options_button" class="fa-solid fa-bars interactable"></div>
</div> </div>
<textarea id="send_textarea" name="text" data-i18n="[no_connection_text]Not connected to API!;[connected_text]Type a message, or /? for help" placeholder="Not connected to API!" no_connection_text="Not connected to API!" connected_text="Type a message, or /? for help"></textarea> <textarea id="send_textarea" name="text" data-i18n="[no_connection_text]Not connected to API!;[connected_text]Type a message, or /? for help" placeholder="Not connected to API!" no_connection_text="Not connected to API!" connected_text="Type a message, or /? for help"></textarea>
<div id="rightSendForm" class="alignContentCenter"> <div id="rightSendForm" class="alignContentCenter">
@ -6267,8 +6264,8 @@
<div id="mes_stop" title="Abort request" class="mes_stop" data-i18n="[title]Abort request"> <div id="mes_stop" title="Abort request" class="mes_stop" data-i18n="[title]Abort request">
<i class="fa-solid fa-circle-stop"></i> <i class="fa-solid fa-circle-stop"></i>
</div> </div>
<div id="mes_continue" class="fa-fw fa-solid fa-arrow-right displayNone" title="Continue the last message" data-i18n="[title]Continue the last message"></div> <div id="mes_continue" class="fa-fw fa-solid fa-arrow-right interactable displayNone" title="Continue the last message" data-i18n="[title]Continue the last message"></div>
<div id="send_but" class="fa-solid fa-paper-plane displayNone" title="Send a message" data-i18n="[title]Send a message"></div> <div id="send_but" class="fa-solid fa-paper-plane interactable displayNone" title="Send a message" data-i18n="[title]Send a message"></div>
</div> </div>
</div> </div>
</div> </div>
@ -6432,15 +6429,6 @@
<script type="module" src="scripts/setting-search.js"></script> <script type="module" src="scripts/setting-search.js"></script>
<script type="module" src="scripts/server-history.js"></script> <script type="module" src="scripts/server-history.js"></script>
<script type="module" src="script.js"></script> <script type="module" src="script.js"></script>
<script>
// Configure toast library:
toastr.options.escapeHtml = true; // Prevent raw HTML inserts
toastr.options.timeOut = 4000; // How long the toast will display without user interaction
toastr.options.extendedTimeOut = 10000; // How long the toast will display after a user hovers over it
toastr.options.progressBar = true; // Visually indicate how long before a toast expires.
toastr.options.closeButton = true; // enable a close button
toastr.options.positionClass = "toast-top-center"; // Where to position the toast container
</script>
<script> <script>
window.addEventListener('load', (event) => { window.addEventListener('load', (event) => {
const documentHeight = () => { const documentHeight = () => {

View File

@ -885,11 +885,11 @@
"Bulk_edit_characters": "تحرير الشخصيات جميعها", "Bulk_edit_characters": "تحرير الشخصيات جميعها",
"Bulk select all characters": "تحديد كافة الشخصيات بالجملة", "Bulk select all characters": "تحديد كافة الشخصيات بالجملة",
"Bulk delete characters": "حذف الشخصيات جميعها", "Bulk delete characters": "حذف الشخصيات جميعها",
"popup_text_save": "يحفظ", "popup-button-save": "يحفظ",
"popup_text_yes": "نعم", "popup-button-yes": "نعم",
"popup_text_no": "لا", "popup-button-no": "لا",
"popup_text_cancel": "يلغي", "popup-button-cancel": "يلغي",
"popup_text_import": "يستورد", "popup-button-import": "يستورد",
"Advanced Defininitions": "تعريفات متقدمة", "Advanced Defininitions": "تعريفات متقدمة",
"Prompt Overrides": "التجاوزات السريعة", "Prompt Overrides": "التجاوزات السريعة",
"(For Chat Completion and Instruct Mode)": "(لاستكمال الدردشة ووضع التعليمات)", "(For Chat Completion and Instruct Mode)": "(لاستكمال الدردشة ووضع التعليمات)",

View File

@ -885,11 +885,11 @@
"Bulk_edit_characters": "Massenbearbeitung von Charakteren", "Bulk_edit_characters": "Massenbearbeitung von Charakteren",
"Bulk select all characters": "Massenauswahl aller Zeichen", "Bulk select all characters": "Massenauswahl aller Zeichen",
"Bulk delete characters": "Massenlöschung von Charakteren", "Bulk delete characters": "Massenlöschung von Charakteren",
"popup_text_save": "Speichern", "popup-button-save": "Speichern",
"popup_text_yes": "Ja", "popup-button-yes": "Ja",
"popup_text_no": "NEIN", "popup-button-no": "NEIN",
"popup_text_cancel": "Stornieren", "popup-button-cancel": "Stornieren",
"popup_text_import": "Importieren", "popup-button-import": "Importieren",
"Advanced Defininitions": "Erweiterte Definitionen", "Advanced Defininitions": "Erweiterte Definitionen",
"Prompt Overrides": "Eingabeaufforderungsüberschreibungen", "Prompt Overrides": "Eingabeaufforderungsüberschreibungen",
"(For Chat Completion and Instruct Mode)": "(Für Chat-Abschluss und Anweisungsmodus)", "(For Chat Completion and Instruct Mode)": "(Für Chat-Abschluss und Anweisungsmodus)",

View File

@ -885,11 +885,11 @@
"Bulk_edit_characters": "Editar personajes masivamente", "Bulk_edit_characters": "Editar personajes masivamente",
"Bulk select all characters": "Seleccionar de forma masiva todos los personajes", "Bulk select all characters": "Seleccionar de forma masiva todos los personajes",
"Bulk delete characters": "Eliminar personajes masivamente", "Bulk delete characters": "Eliminar personajes masivamente",
"popup_text_save": "Ahorrar", "popup-button-save": "Ahorrar",
"popup_text_yes": "Sí", "popup-button-yes": "Sí",
"popup_text_no": "No", "popup-button-no": "No",
"popup_text_cancel": "Cancelar", "popup-button-cancel": "Cancelar",
"popup_text_import": "Importar", "popup-button-import": "Importar",
"Advanced Defininitions": "Definiciones avanzadas", "Advanced Defininitions": "Definiciones avanzadas",
"Prompt Overrides": "Anulaciones rápidas", "Prompt Overrides": "Anulaciones rápidas",
"(For Chat Completion and Instruct Mode)": "(Para completar el chat y el modo de instrucción)", "(For Chat Completion and Instruct Mode)": "(Para completar el chat y el modo de instrucción)",

View File

@ -885,11 +885,11 @@
"Bulk_edit_characters": "Édition en masse des personnages", "Bulk_edit_characters": "Édition en masse des personnages",
"Bulk select all characters": "Sélection groupée de tous les caractères", "Bulk select all characters": "Sélection groupée de tous les caractères",
"Bulk delete characters": "Suppression en masse des personnages", "Bulk delete characters": "Suppression en masse des personnages",
"popup_text_save": "Sauvegarder", "popup-button-save": "Sauvegarder",
"popup_text_yes": "Oui", "popup-button-yes": "Oui",
"popup_text_no": "Non", "popup-button-no": "Non",
"popup_text_cancel": "Annuler", "popup-button-cancel": "Annuler",
"popup_text_import": "Importer", "popup-button-import": "Importer",
"Advanced Defininitions": "Définitions avancées", "Advanced Defininitions": "Définitions avancées",
"Prompt Overrides": "Remplacements d'invite", "Prompt Overrides": "Remplacements d'invite",
"(For Chat Completion and Instruct Mode)": "(Pour l'achèvement du chat et le mode instruction)", "(For Chat Completion and Instruct Mode)": "(Pour l'achèvement du chat et le mode instruction)",

View File

@ -885,11 +885,11 @@
"Bulk_edit_characters": "Breyta mörgum persónum í einu", "Bulk_edit_characters": "Breyta mörgum persónum í einu",
"Bulk select all characters": "Velja alla stafi í magni", "Bulk select all characters": "Velja alla stafi í magni",
"Bulk delete characters": "Eyða mörgum persónum í einu", "Bulk delete characters": "Eyða mörgum persónum í einu",
"popup_text_save": "Vista", "popup-button-save": "Vista",
"popup_text_yes": "Já", "popup-button-yes": "Já",
"popup_text_no": "Nei", "popup-button-no": "Nei",
"popup_text_cancel": "Hætta við", "popup-button-cancel": "Hætta við",
"popup_text_import": "Flytja inn", "popup-button-import": "Flytja inn",
"Advanced Defininitions": "Ítarleg skilgreiningar", "Advanced Defininitions": "Ítarleg skilgreiningar",
"Prompt Overrides": "Hnekkja hvetjandi", "Prompt Overrides": "Hnekkja hvetjandi",
"(For Chat Completion and Instruct Mode)": "(Til að ljúka spjalli og leiðbeiningarham)", "(For Chat Completion and Instruct Mode)": "(Til að ljúka spjalli og leiðbeiningarham)",

View File

@ -885,11 +885,11 @@
"Bulk_edit_characters": "Modifica personaggi in blocco", "Bulk_edit_characters": "Modifica personaggi in blocco",
"Bulk select all characters": "Seleziona in blocco tutti i personaggi", "Bulk select all characters": "Seleziona in blocco tutti i personaggi",
"Bulk delete characters": "Elimina personaggi in blocco", "Bulk delete characters": "Elimina personaggi in blocco",
"popup_text_save": "Salva", "popup-button-save": "Salva",
"popup_text_yes": "SÌ", "popup-button-yes": "SÌ",
"popup_text_no": "NO", "popup-button-no": "NO",
"popup_text_cancel": "Annulla", "popup-button-cancel": "Annulla",
"popup_text_import": "Importare", "popup-button-import": "Importare",
"Advanced Defininitions": "Definizioni avanzate", "Advanced Defininitions": "Definizioni avanzate",
"Prompt Overrides": "Sostituzioni richieste", "Prompt Overrides": "Sostituzioni richieste",
"(For Chat Completion and Instruct Mode)": "(Per il completamento della chat e la modalità istruzione)", "(For Chat Completion and Instruct Mode)": "(Per il completamento della chat e la modalità istruzione)",

View File

@ -885,11 +885,11 @@
"Bulk_edit_characters": "キャラクターを一括編集", "Bulk_edit_characters": "キャラクターを一括編集",
"Bulk select all characters": "すべての文字を一括選択", "Bulk select all characters": "すべての文字を一括選択",
"Bulk delete characters": "キャラクターを一括削除", "Bulk delete characters": "キャラクターを一括削除",
"popup_text_save": "保存", "popup-button-save": "保存",
"popup_text_yes": "はい", "popup-button-yes": "はい",
"popup_text_no": "いいえ", "popup-button-no": "いいえ",
"popup_text_cancel": "キャンセル", "popup-button-cancel": "キャンセル",
"popup_text_import": "輸入", "popup-button-import": "輸入",
"Advanced Defininitions": "高度な定義", "Advanced Defininitions": "高度な定義",
"Prompt Overrides": "プロンプトのオーバーライド", "Prompt Overrides": "プロンプトのオーバーライド",
"(For Chat Completion and Instruct Mode)": "(チャット補完と指示モード用)", "(For Chat Completion and Instruct Mode)": "(チャット補完と指示モード用)",

View File

@ -885,11 +885,11 @@
"Bulk_edit_characters": "대량 캐릭터 편집", "Bulk_edit_characters": "대량 캐릭터 편집",
"Bulk select all characters": "모든 문자 일괄 선택", "Bulk select all characters": "모든 문자 일괄 선택",
"Bulk delete characters": "대량 캐릭터 삭제", "Bulk delete characters": "대량 캐릭터 삭제",
"popup_text_save": "구하다", "popup-button-save": "구하다",
"popup_text_yes": "예", "popup-button-yes": "예",
"popup_text_no": "아니요", "popup-button-no": "아니요",
"popup_text_cancel": "취소", "popup-button-cancel": "취소",
"popup_text_import": "수입", "popup-button-import": "수입",
"Advanced Defininitions": "고급 정의", "Advanced Defininitions": "고급 정의",
"Prompt Overrides": "프롬프트 무시", "Prompt Overrides": "프롬프트 무시",
"(For Chat Completion and Instruct Mode)": "(채팅 완료 및 지시 모드의 경우)", "(For Chat Completion and Instruct Mode)": "(채팅 완료 및 지시 모드의 경우)",

View File

@ -885,11 +885,11 @@
"Bulk_edit_characters": "Massaal bewerken personages", "Bulk_edit_characters": "Massaal bewerken personages",
"Bulk select all characters": "Selecteer alle tekens in bulk", "Bulk select all characters": "Selecteer alle tekens in bulk",
"Bulk delete characters": "Massaal verwijderen personages", "Bulk delete characters": "Massaal verwijderen personages",
"popup_text_save": "Redden", "popup-button-save": "Redden",
"popup_text_yes": "Ja", "popup-button-yes": "Ja",
"popup_text_no": "Nee", "popup-button-no": "Nee",
"popup_text_cancel": "Annuleren", "popup-button-cancel": "Annuleren",
"popup_text_import": "Importeren", "popup-button-import": "Importeren",
"Advanced Defininitions": "Geavanceerde definities", "Advanced Defininitions": "Geavanceerde definities",
"Prompt Overrides": "Prompt-overschrijvingen", "Prompt Overrides": "Prompt-overschrijvingen",
"(For Chat Completion and Instruct Mode)": "(Voor voltooiing van chat en instructiemodus)", "(For Chat Completion and Instruct Mode)": "(Voor voltooiing van chat en instructiemodus)",

View File

@ -885,11 +885,11 @@
"Bulk_edit_characters": "Editar personagens em massa", "Bulk_edit_characters": "Editar personagens em massa",
"Bulk select all characters": "Selecione em massa todos os caracteres", "Bulk select all characters": "Selecione em massa todos os caracteres",
"Bulk delete characters": "Excluir personagens em massa", "Bulk delete characters": "Excluir personagens em massa",
"popup_text_save": "Salvar", "popup-button-save": "Salvar",
"popup_text_yes": "Sim", "popup-button-yes": "Sim",
"popup_text_no": "Não", "popup-button-no": "Não",
"popup_text_cancel": "Cancelar", "popup-button-cancel": "Cancelar",
"popup_text_import": "Importar", "popup-button-import": "Importar",
"Advanced Defininitions": "Definições Avançadas", "Advanced Defininitions": "Definições Avançadas",
"Prompt Overrides": "Substituições de prompt", "Prompt Overrides": "Substituições de prompt",
"(For Chat Completion and Instruct Mode)": "(Para conclusão de bate-papo e modo de instrução)", "(For Chat Completion and Instruct Mode)": "(Para conclusão de bate-papo e modo de instrução)",

View File

@ -962,10 +962,11 @@
"onboarding_import": "Импортируйте", "onboarding_import": "Импортируйте",
"from supported sources or view": "из источника или посмотрите", "from supported sources or view": "из источника или посмотрите",
"Sample characters": "Стандартных персонажей", "Sample characters": "Стандартных персонажей",
"popup_text_save": "Сохранить", "popup-button-save": "Сохранить",
"popup_text_yes": "Да", "popup-button-yes": "Да",
"popup_text_no": "Нет", "popup-button-no": "Нет",
"popup_text_cancel": "Отмена", "popup-button-cancel": "Отмена",
"popup-button-import": "Импортировать",
"Enter the URL of the content to import": "Введите URL-адрес импортируемого контента", "Enter the URL of the content to import": "Введите URL-адрес импортируемого контента",
"Supported sources:": "Поддерживаются следующие источники:", "Supported sources:": "Поддерживаются следующие источники:",
"char_import_example": "Пример:", "char_import_example": "Пример:",
@ -976,7 +977,6 @@
"char_import_5": "Персонаж с AICharacterCard.com (прямая ссылка или ID)", "char_import_5": "Персонаж с AICharacterCard.com (прямая ссылка или ID)",
"char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в", "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в",
"char_import_7": ")", "char_import_7": ")",
"popup_text_import": "Импортировать",
"Grammar String": "Грамматика", "Grammar String": "Грамматика",
"GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF или ENBF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.", "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF или ENBF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",
"Account": "Аккаунт", "Account": "Аккаунт",

View File

@ -885,11 +885,11 @@
"Bulk_edit_characters": "Масове редагування персонажів", "Bulk_edit_characters": "Масове редагування персонажів",
"Bulk select all characters": "Масове виділення всіх символів", "Bulk select all characters": "Масове виділення всіх символів",
"Bulk delete characters": "Масове видалення персонажів", "Bulk delete characters": "Масове видалення персонажів",
"popup_text_save": "зберегти", "popup-button-save": "зберегти",
"popup_text_yes": "Так", "popup-button-yes": "Так",
"popup_text_no": "Немає", "popup-button-no": "Немає",
"popup_text_cancel": "Скасувати", "popup-button-cancel": "Скасувати",
"popup_text_import": "Імпорт", "popup-button-import": "Імпорт",
"Advanced Defininitions": "Розширені визначення", "Advanced Defininitions": "Розширені визначення",
"Prompt Overrides": "Перевизначення підказок", "Prompt Overrides": "Перевизначення підказок",
"(For Chat Completion and Instruct Mode)": "(Для завершення чату та режиму інструктажу)", "(For Chat Completion and Instruct Mode)": "(Для завершення чату та режиму інструктажу)",

View File

@ -885,11 +885,11 @@
"Bulk_edit_characters": "Chỉnh sửa nhân vật theo lô", "Bulk_edit_characters": "Chỉnh sửa nhân vật theo lô",
"Bulk select all characters": "Chọn hàng loạt tất cả các ký tự", "Bulk select all characters": "Chọn hàng loạt tất cả các ký tự",
"Bulk delete characters": "Xóa nhân vật theo lô", "Bulk delete characters": "Xóa nhân vật theo lô",
"popup_text_save": "Cứu", "popup-button-save": "Cứu",
"popup_text_yes": "Đúng", "popup-button-yes": "Đúng",
"popup_text_no": "KHÔNG", "popup-button-no": "KHÔNG",
"popup_text_cancel": "Hủy bỏ", "popup-button-cancel": "Hủy bỏ",
"popup_text_import": "Nhập khẩu", "popup-button-import": "Nhập khẩu",
"Advanced Defininitions": "Các Định nghĩa Nâng cao", "Advanced Defininitions": "Các Định nghĩa Nâng cao",
"Prompt Overrides": "Ghi đè nhắc nhở", "Prompt Overrides": "Ghi đè nhắc nhở",
"(For Chat Completion and Instruct Mode)": "(Đối với chế độ hoàn thành trò chuyện và hướng dẫn)", "(For Chat Completion and Instruct Mode)": "(Đối với chế độ hoàn thành trò chuyện và hướng dẫn)",

View File

@ -899,11 +899,11 @@
"Bulk_edit_characters": "批量编辑角色", "Bulk_edit_characters": "批量编辑角色",
"Bulk select all characters": "批量选择所有角色", "Bulk select all characters": "批量选择所有角色",
"Bulk delete characters": "批量删除角色", "Bulk delete characters": "批量删除角色",
"popup_text_save": "保存", "popup-button-save": "保存",
"popup_text_yes": "是", "popup-button-yes": "是",
"popup_text_no": "否", "popup-button-no": "否",
"popup_text_cancel": "取消", "popup-button-cancel": "取消",
"popup_text_import": "导入", "popup-button-import": "导入",
"Advanced Defininitions": "高级定义", "Advanced Defininitions": "高级定义",
"Prompt Overrides": "提示词覆盖", "Prompt Overrides": "提示词覆盖",
"(For Chat Completion and Instruct Mode)": "(用于聊天补全和指导模式)", "(For Chat Completion and Instruct Mode)": "(用于聊天补全和指导模式)",

View File

@ -886,11 +886,11 @@
"Bulk_edit_characters": "批量編輯角色人物\n\n點選以切換角色人物\nShift + 點選以選擇/取消選擇一範圍的角色人物\n右鍵以查看動作", "Bulk_edit_characters": "批量編輯角色人物\n\n點選以切換角色人物\nShift + 點選以選擇/取消選擇一範圍的角色人物\n右鍵以查看動作",
"Bulk select all characters": "全選所有角色人物", "Bulk select all characters": "全選所有角色人物",
"Bulk delete characters": "批量刪除角色人物", "Bulk delete characters": "批量刪除角色人物",
"popup_text_save": "儲存", "popup-button-save": "儲存",
"popup_text_yes": "是", "popup-button-yes": "是",
"popup_text_no": "否", "popup-button-no": "否",
"popup_text_cancel": "取消", "popup-button-cancel": "取消",
"popup_text_import": "匯入", "popup-button-import": "匯入",
"Advanced Defininitions": "- 進階定義", "Advanced Defininitions": "- 進階定義",
"Prompt Overrides": "提示詞覆寫", "Prompt Overrides": "提示詞覆寫",
"(For Chat Completion and Instruct Mode)": "(用於聊天補充和指令模式)", "(For Chat Completion and Instruct Mode)": "(用於聊天補充和指令模式)",

View File

@ -227,7 +227,7 @@ import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, de
import { initPresetManager } from './scripts/preset-manager.js'; import { initPresetManager } from './scripts/preset-manager.js';
import { evaluateMacros } from './scripts/macros.js'; import { evaluateMacros } from './scripts/macros.js';
import { currentUser, setUserControls } from './scripts/user.js'; import { currentUser, setUserControls } from './scripts/user.js';
import { POPUP_TYPE, callGenericPopup } from './scripts/popup.js'; import { POPUP_TYPE, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
import { renderTemplate, renderTemplateAsync } from './scripts/templates.js'; import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
import { ScraperManager } from './scripts/scrapers.js'; import { ScraperManager } from './scripts/scrapers.js';
import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js'; import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
@ -235,6 +235,9 @@ import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';
import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.js'; import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.js';
import { initCustomSelectedSamplers, validateDisabledSamplers } from './scripts/samplerSelect.js'; import { initCustomSelectedSamplers, validateDisabledSamplers } from './scripts/samplerSelect.js';
import { DragAndDropHandler } from './scripts/dragdrop.js';
import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js';
import { initDynamicStyles } from './scripts/dynamic-styles.js';
//exporting functions and vars for mods //exporting functions and vars for mods
export { export {
@ -263,6 +266,19 @@ showLoader();
// Yoink preloader entirely; it only exists to cover up unstyled content while loading JS // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
document.getElementById('preloader').remove(); document.getElementById('preloader').remove();
// Configure toast library:
toastr.options.escapeHtml = true; // Prevent raw HTML inserts
toastr.options.timeOut = 4000; // How long the toast will display without user interaction
toastr.options.extendedTimeOut = 10000; // How long the toast will display after a user hovers over it
toastr.options.progressBar = true; // Visually indicate how long before a toast expires.
toastr.options.closeButton = true; // enable a close button
toastr.options.positionClass = "toast-top-center"; // Where to position the toast container
toastr.options.onHidden = () => {
// If we have any dialog still open, the last "hidden" toastr will remove the toastr-container. We need to keep it alive inside the dialog though
// so the toasts still show up inside there.
fixToastrForDialogs();
}
// Allow target="_blank" in links // Allow target="_blank" in links
DOMPurify.addHook('afterSanitizeAttributes', function (node) { DOMPurify.addHook('afterSanitizeAttributes', function (node) {
if ('target' in node) { if ('target' in node) {
@ -509,11 +525,15 @@ let is_delete_mode = false;
let fav_ch_checked = false; let fav_ch_checked = false;
let scrollLock = false; let scrollLock = false;
export let abortStatusCheck = new AbortController(); export let abortStatusCheck = new AbortController();
let charDragDropHandler = null;
/** @type {number} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */ /** @type {debounce_timeout} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */
const durationSaveEdit = debounce_timeout.relaxed; export const DEFAULT_SAVE_EDIT_TIMEOUT = debounce_timeout.relaxed;
export const saveSettingsDebounced = debounce(() => saveSettings(), durationSaveEdit); /** @type {debounce_timeout} The debounce timeout used for printing. debounce_timeout.quick: 100 ms */
export const saveCharacterDebounced = debounce(() => $('#create_button').trigger('click'), durationSaveEdit); export const DEFAULT_PRINT_TIMEOUT = debounce_timeout.quick;
export const saveSettingsDebounced = debounce(() => saveSettings(), DEFAULT_SAVE_EDIT_TIMEOUT);
export const saveCharacterDebounced = debounce(() => $('#create_button').trigger('click'), DEFAULT_SAVE_EDIT_TIMEOUT);
/** /**
* Prints the character list in a debounced fashion without blocking, with a delay of 100 milliseconds. * Prints the character list in a debounced fashion without blocking, with a delay of 100 milliseconds.
@ -521,7 +541,7 @@ export const saveCharacterDebounced = debounce(() => $('#create_button').trigger
* *
* The printing will also always reprint all filter options of the global list, to keep them up to date. * The printing will also always reprint all filter options of the global list, to keep them up to date.
*/ */
export const printCharactersDebounced = debounce(() => { printCharacters(false); }, debounce_timeout.quick); export const printCharactersDebounced = debounce(() => { printCharacters(false); }, DEFAULT_PRINT_TIMEOUT);
/** /**
* @enum {string} System message types * @enum {string} System message types
@ -885,6 +905,8 @@ async function firstLoadInit() {
await getSystemMessages(); await getSystemMessages();
sendSystemMessage(system_message_types.WELCOME); sendSystemMessage(system_message_types.WELCOME);
await getSettings(); await getSettings();
initKeyboard();
initDynamicStyles();
initTags(); initTags();
await getUserAvatars(true, user_avatar); await getUserAvatars(true, user_avatar);
await getCharacters(); await getCharacters();
@ -925,6 +947,8 @@ export function displayOnlineStatus() {
*/ */
export function setAnimationDuration(ms = null) { export function setAnimationDuration(ms = null) {
animation_duration = ms ?? ANIMATION_DURATION_DEFAULT; animation_duration = ms ?? ANIMATION_DURATION_DEFAULT;
// Set CSS variable to document
document.documentElement.style.setProperty('--animation-duration', `${animation_duration}ms`);
} }
export function setActiveCharacter(entityOrKey) { export function setActiveCharacter(entityOrKey) {
@ -5482,7 +5506,7 @@ export async function renameCharacter(name = null, { silent = false, renameChats
} }
const oldAvatar = characters[this_chid].avatar; const oldAvatar = characters[this_chid].avatar;
const newValue = name || await callPopup('<h3>New name:</h3>', 'input', characters[this_chid].name); const newValue = name || await callGenericPopup('<h3>New name:</h3>', POPUP_TYPE.INPUT, characters[this_chid].name);
if (!newValue) { if (!newValue) {
toastr.warning('No character name provided.', 'Rename Character'); toastr.warning('No character name provided.', 'Rename Character');
@ -5730,7 +5754,7 @@ async function read_avatar_load(input) {
} }
await createOrEditCharacter(); await createOrEditCharacter();
await delay(durationSaveEdit); await delay(DEFAULT_SAVE_EDIT_TIMEOUT);
const formData = new FormData($('#form_create').get(0)); const formData = new FormData($('#form_create').get(0));
await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), { await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), {
@ -5774,7 +5798,7 @@ export function getThumbnailUrl(type, file) {
return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`; return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`;
} }
export function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, selectable = false, highlightFavs = true } = {}) { export function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, interactable = false, highlightFavs = true } = {}) {
if (empty) { if (empty) {
block.empty(); block.empty();
} }
@ -5809,8 +5833,8 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t
avatarTemplate.attr('title', `[Group] ${entity.item.name}`); avatarTemplate.attr('title', `[Group] ${entity.item.name}`);
} }
if (selectable) { if (interactable) {
avatarTemplate.addClass('selectable'); avatarTemplate.addClass(INTERACTABLE_CONTROL_CLASS);
avatarTemplate.toggleClass('character_select', entity.type === 'character'); avatarTemplate.toggleClass('character_select', entity.type === 'character');
avatarTemplate.toggleClass('group_select', entity.type === 'group'); avatarTemplate.toggleClass('group_select', entity.type === 'group');
} }
@ -6847,7 +6871,7 @@ export function select_selected_character(chid) {
$('#add_avatar_button').val(''); $('#add_avatar_button').val('');
$('#character_popup_text_h3').text(characters[chid].name); $('#character_popup-button-h3').text(characters[chid].name);
$('#character_name_pole').val(characters[chid].name); $('#character_name_pole').val(characters[chid].name);
$('#description_textarea').val(characters[chid].description); $('#description_textarea').val(characters[chid].description);
$('#character_world').val(characters[chid].data?.extensions?.world || ''); $('#character_world').val(characters[chid].data?.extensions?.world || '');
@ -6924,7 +6948,7 @@ function select_rm_create() {
//create text poles //create text poles
$('#rm_button_back').css('display', ''); $('#rm_button_back').css('display', '');
$('#character_import_button').css('display', ''); $('#character_import_button').css('display', '');
$('#character_popup_text_h3').text('Create character'); $('#character_popup-button-h3').text('Create character');
$('#character_name_pole').val(create_save.name); $('#character_name_pole').val(create_save.name);
$('#description_textarea').val(create_save.description); $('#description_textarea').val(create_save.description);
$('#character_world').val(create_save.world); $('#character_world').val(create_save.world);
@ -7070,10 +7094,10 @@ function onScenarioOverrideRemoveClick() {
* @param {string} type * @param {string} type
* @param {string} inputValue - Value to set the input to. * @param {string} inputValue - Value to set the input to.
* @param {PopupOptions} options - Options for the popup. * @param {PopupOptions} options - Options for the popup.
* @typedef {{okButton?: string, rows?: number, wide?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean, cropAspect?: number }} PopupOptions - Options for the popup. * @typedef {{okButton?: string, rows?: number, wide?: boolean, wider?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean, cropAspect?: number }} PopupOptions - Options for the popup.
* @returns * @returns
*/ */
export function callPopup(text, type, inputValue = '', { okButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) { export function callPopup(text, type, inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) {
function getOkButtonText() { function getOkButtonText() {
if (['avatarToCrop'].includes(popup_type)) { if (['avatarToCrop'].includes(popup_type)) {
return okButton ?? 'Accept'; return okButton ?? 'Accept';
@ -7103,6 +7127,7 @@ export function callPopup(text, type, inputValue = '', { okButton, rows, wide, l
const $shadowPopup = $('#shadow_popup'); const $shadowPopup = $('#shadow_popup');
$dialoguePopup.toggleClass('wide_dialogue_popup', !!wide) $dialoguePopup.toggleClass('wide_dialogue_popup', !!wide)
.toggleClass('wider_dialogue_popup', !!wider)
.toggleClass('large_dialogue_popup', !!large) .toggleClass('large_dialogue_popup', !!large)
.toggleClass('horizontal_scrolling_dialogue_popup', !!allowHorizontalScrolling) .toggleClass('horizontal_scrolling_dialogue_popup', !!allowHorizontalScrolling)
.toggleClass('vertical_scrolling_dialogue_popup', !!allowVerticalScrolling); .toggleClass('vertical_scrolling_dialogue_popup', !!allowVerticalScrolling);
@ -7215,7 +7240,7 @@ export async function saveMetadata() {
export async function saveChatConditional() { export async function saveChatConditional() {
try { try {
await waitUntilCondition(() => !isChatSaving, durationSaveEdit, 100); await waitUntilCondition(() => !isChatSaving, DEFAULT_SAVE_EDIT_TIMEOUT, 100);
} catch { } catch {
console.warn('Timeout waiting for chat to save'); console.warn('Timeout waiting for chat to save');
return; return;
@ -7592,7 +7617,7 @@ async function createOrEditCharacter(e) {
field.callback && field.callback(fieldValue); field.callback && field.callback(fieldValue);
}); });
$('#character_popup_text_h3').text('Create character'); $('#character_popup-button-h3').text('Create character');
create_save.avatar = ''; create_save.avatar = '';
@ -8943,7 +8968,7 @@ jQuery(async function () {
$('#send_textarea').on('focusin focus click', () => { $('#send_textarea').on('focusin focus click', () => {
S_TAPreviouslyFocused = true; S_TAPreviouslyFocused = true;
}); });
$('#options_button, #send_but, #option_regenerate, #option_continue, #mes_continue').on('click', () => { $('#send_but, #option_regenerate, #option_continue, #mes_continue').on('click', () => {
if (S_TAPreviouslyFocused) { if (S_TAPreviouslyFocused) {
$('#send_textarea').focus(); $('#send_textarea').focus();
} }
@ -9452,7 +9477,7 @@ jQuery(async function () {
} }
function isMouseOverButtonOrMenu() { function isMouseOverButtonOrMenu() {
return menu.is(':hover') || button.is(':hover'); return menu.is(':hover, :focus-within') || button.is(':hover, :focus');
} }
button.on('click', function () { button.on('click', function () {
@ -10352,7 +10377,7 @@ jQuery(async function () {
'#character_cross', '#character_cross',
'#avatar-and-name-block', '#avatar-and-name-block',
'#shadow_popup', '#shadow_popup',
'.shadow_popup', '.popup',
'#world_popup', '#world_popup',
'.ui-widget', '.ui-widget',
'.text_pole', '.text_pole',
@ -10657,7 +10682,7 @@ jQuery(async function () {
const html = await renderTemplateAsync('importCharacters'); const html = await renderTemplateAsync('importCharacters');
/** @type {string?} */ /** @type {string?} */
const input = await callGenericPopup(html, POPUP_TYPE.INPUT, '', { wider: true, okButton: $('#shadow_popup_template').attr('popup_text_import'), rows: 4 }); const input = await callGenericPopup(html, POPUP_TYPE.INPUT, '', { wider: true, okButton: $('#popup_template').attr('popup-button-import'), rows: 4 });
if (!input) { if (!input) {
console.debug('Custom content import cancelled'); console.debug('Custom content import cancelled');
@ -10712,32 +10737,12 @@ jQuery(async function () {
} }
}); });
const $dropzone = $(document.body); charDragDropHandler = new DragAndDropHandler('body', async (files, event) => {
$dropzone.on('dragover', (event) => {
event.preventDefault();
event.stopPropagation();
$dropzone.addClass('dragover');
});
$dropzone.on('dragleave', (event) => {
event.preventDefault();
event.stopPropagation();
$dropzone.removeClass('dragover');
});
$dropzone.on('drop', async (event) => {
event.preventDefault();
event.stopPropagation();
$dropzone.removeClass('dragover');
const files = Array.from(event.originalEvent.dataTransfer.files);
if (!files.length) { if (!files.length) {
await importFromURL(event.originalEvent.dataTransfer.items, files); await importFromURL(event.originalEvent.dataTransfer.items, files);
} }
await processDroppedFiles(files); await processDroppedFiles(files);
}); }, { noAnimation: true });
$('#charListGridToggle').on('click', async () => { $('#charListGridToggle').on('click', async () => {
doCharListDisplaySwitch(); doCharListDisplaySwitch();

View File

@ -303,7 +303,7 @@ export async function favsToHotswap() {
return; return;
} }
buildAvatarList(container, favs, { selectable: true, highlightFavs: false }); buildAvatarList(container, favs, { interactable: true, highlightFavs: false });
} }
//changes input bar and send button display depending on connection status //changes input bar and send button display depending on connection status

View File

@ -6,6 +6,7 @@ import { BlankAutoCompleteOption } from './BlankAutoCompleteOption.js';
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import { AutoCompleteNameResult } from './AutoCompleteNameResult.js'; import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';
import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js'; import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
import { Popup, getTopmostModalLayer } from '../popup.js';
/**@readonly*/ /**@readonly*/
/**@enum {Number}*/ /**@enum {Number}*/
@ -442,7 +443,7 @@ export class AutoComplete {
} }
this.dom.append(frag); this.dom.append(frag);
this.updatePosition(); this.updatePosition();
document.body.append(this.domWrap); getTopmostModalLayer().append(this.domWrap);
} else { } else {
this.domWrap.remove(); this.domWrap.remove();
} }
@ -457,7 +458,7 @@ export class AutoComplete {
if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove(); if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove();
this.detailsDom.innerHTML = ''; this.detailsDom.innerHTML = '';
this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM'); this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM');
document.body.append(this.detailsWrap); getTopmostModalLayer().append(this.detailsWrap);
this.updateDetailsPositionDebounced(); this.updateDetailsPositionDebounced();
} }
@ -473,7 +474,7 @@ export class AutoComplete {
const rect = {}; const rect = {};
rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect(); rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect(); rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.FULL] = document.body.getBoundingClientRect(); rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayer().getBoundingClientRect();
this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`); this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`); this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`; this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`;
@ -500,7 +501,7 @@ export class AutoComplete {
const rect = {}; const rect = {};
rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect(); rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect(); rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.FULL] = document.body.getBoundingClientRect(); rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayer().getBoundingClientRect();
if (this.isReplaceable) { if (this.isReplaceable) {
this.detailsWrap.classList.remove('full'); this.detailsWrap.classList.remove('full');
const selRect = this.selectedItem.dom.children[0].getBoundingClientRect(); const selRect = this.selectedItem.dom.children[0].getBoundingClientRect();
@ -596,7 +597,7 @@ export class AutoComplete {
} }
this.clone.style.position = 'fixed'; this.clone.style.position = 'fixed';
this.clone.style.visibility = 'hidden'; this.clone.style.visibility = 'hidden';
document.body.append(this.clone); getTopmostModalLayer().append(this.clone);
const mo = new MutationObserver(muts=>{ const mo = new MutationObserver(muts=>{
if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) { if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {
this.clone.remove(); this.clone.remove();

View File

@ -94,6 +94,9 @@ function enableBulkSelect() {
}); });
$(el).prepend(checkbox); $(el).prepend(checkbox);
}); });
$('#rm_print_characters_block.group_overlay_mode_select .bogus_folder_select, #rm_print_characters_block.group_overlay_mode_select .group_select')
.addClass('disabled');
$('#rm_print_characters_block').addClass('bulk_select'); $('#rm_print_characters_block').addClass('bulk_select');
// We also need to disable the default click event for the character_select divs // We also need to disable the default click event for the character_select divs
$(document).on('click', '.bulk_select_checkbox', function (event) { $(document).on('click', '.bulk_select_checkbox', function (event) {
@ -106,6 +109,8 @@ function enableBulkSelect() {
*/ */
function disableBulkSelect() { function disableBulkSelect() {
$('.bulk_select_checkbox').remove(); $('.bulk_select_checkbox').remove();
$('#rm_print_characters_block.group_overlay_mode_select .bogus_folder_select, #rm_print_characters_block.group_overlay_mode_select .group_select')
.removeClass('disabled');
$('#rm_print_characters_block').removeClass('bulk_select'); $('#rm_print_characters_block').removeClass('bulk_select');
} }

View File

@ -37,6 +37,7 @@ import {
import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js'; import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js'; import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
import { ScraperManager } from './scrapers.js'; import { ScraperManager } from './scrapers.js';
import { DragAndDropHandler } from './dragdrop.js';
/** /**
* @typedef {Object} FileAttachment * @typedef {Object} FileAttachment
@ -991,25 +992,7 @@ async function openAttachmentManager() {
template.find('.chatAttachmentsName').text(chatName); template.find('.chatAttachmentsName').text(chatName);
} }
function addDragAndDrop() { const dragDropHandler = new DragAndDropHandler('.popup', async (files, event) => {
$(document.body).on('dragover', '.dialogue_popup', (event) => {
event.preventDefault();
event.stopPropagation();
$(event.target).closest('.dialogue_popup').addClass('dragover');
});
$(document.body).on('dragleave', '.dialogue_popup', (event) => {
event.preventDefault();
event.stopPropagation();
$(event.target).closest('.dialogue_popup').removeClass('dragover');
});
$(document.body).on('drop', '.dialogue_popup', async (event) => {
event.preventDefault();
event.stopPropagation();
$(event.target).closest('.dialogue_popup').removeClass('dragover');
const files = Array.from(event.originalEvent.dataTransfer.files);
let selectedTarget = ATTACHMENT_SOURCE.GLOBAL; let selectedTarget = ATTACHMENT_SOURCE.GLOBAL;
const targets = getAvailableTargets(); const targets = getAvailableTargets();
@ -1027,13 +1010,6 @@ async function openAttachmentManager() {
} }
renderAttachments(); renderAttachments();
}); });
}
function removeDragAndDrop() {
$(document.body).off('dragover', '.shadow_popup');
$(document.body).off('dragleave', '.shadow_popup');
$(document.body).off('drop', '.shadow_popup');
}
let sortField = localStorage.getItem('DataBank_sortField') || 'created'; let sortField = localStorage.getItem('DataBank_sortField') || 'created';
let sortOrder = localStorage.getItem('DataBank_sortOrder') || 'desc'; let sortOrder = localStorage.getItem('DataBank_sortOrder') || 'desc';
@ -1129,11 +1105,10 @@ async function openAttachmentManager() {
const cleanupFn = await renderButtons(); const cleanupFn = await renderButtons();
await verifyAttachments(); await verifyAttachments();
await renderAttachments(); await renderAttachments();
addDragAndDrop();
await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close' }); await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close' });
cleanupFn(); cleanupFn();
removeDragAndDrop(); dragDropHandler.destroy();
} }
/** /**

107
public/scripts/dragdrop.js vendored Normal file
View File

@ -0,0 +1,107 @@
import { debounce_timeout } from './constants.js';
/**
* Drag and drop handler
*
* Can be used on any element, enabling drag&drop styling and callback on drop.
*/
export class DragAndDropHandler {
/** @private @type {JQuery.Selector} */ selector;
/** @private @type {(files: File[], event:JQuery.DropEvent<HTMLElement, undefined, any, any>) => void} */ onDropCallback;
/** @private @type {NodeJS.Timeout} Remark: Not actually NodeJS timeout, but it's close */ dragLeaveTimeout;
/** @private @type {boolean} */ noAnimation;
/**
* Create a DragAndDropHandler
* @param {JQuery.Selector} selector - The CSS selector for the elements to enable drag and drop
* @param {(files: File[], event:JQuery.DropEvent<HTMLElement, undefined, any, any>) => void} onDropCallback - The callback function to handle the drop event
*/
constructor(selector, onDropCallback, { noAnimation = false } = {}) {
this.selector = selector;
this.onDropCallback = onDropCallback;
this.dragLeaveTimeout = null;
this.noAnimation = noAnimation;
this.init();
}
/**
* Destroy the drag and drop functionality
*/
destroy() {
if (this.selector === 'body') {
$(document.body).off('dragover', this.handleDragOver.bind(this));
$(document.body).off('dragleave', this.handleDragLeave.bind(this));
$(document.body).off('drop', this.handleDrop.bind(this));
} else {
$(document.body).off('dragover', this.selector, this.handleDragOver.bind(this));
$(document.body).off('dragleave', this.selector, this.handleDragLeave.bind(this));
$(document.body).off('drop', this.selector, this.handleDrop.bind(this));
}
$(this.selector).remove('drop_target no_animation');
}
/**
* Initialize the drag and drop functionality
* Automatically called on construction
* @private
*/
init() {
if (this.selector === 'body') {
$(document.body).on('dragover', this.handleDragOver.bind(this));
$(document.body).on('dragleave', this.handleDragLeave.bind(this));
$(document.body).on('drop', this.handleDrop.bind(this));
} else {
$(document.body).on('dragover', this.selector, this.handleDragOver.bind(this));
$(document.body).on('dragleave', this.selector, this.handleDragLeave.bind(this));
$(document.body).on('drop', this.selector, this.handleDrop.bind(this));
}
$(this.selector).addClass('drop_target');
if (this.noAnimation) $(this.selector).addClass('no_animation');
}
/**
* @param {JQuery.DragOverEvent<HTMLElement, undefined, any, any>} event - The dragover event
* @private
*/
handleDragOver(event) {
event.preventDefault();
event.stopPropagation();
clearTimeout(this.dragLeaveTimeout);
$(this.selector).addClass('drop_target dragover');
if (this.noAnimation) $(this.selector).addClass('no_animation');
}
/**
* @param {JQuery.DragLeaveEvent<HTMLElement, undefined, any, any>} event - The dragleave event
* @private
*/
handleDragLeave(event) {
event.preventDefault();
event.stopPropagation();
// Debounce the removal of the class, so it doesn't "flicker" on dragging over
clearTimeout(this.dragLeaveTimeout);
this.dragLeaveTimeout = setTimeout(() => {
$(this.selector).removeClass('dragover');
}, debounce_timeout.quick);
}
/**
* @param {JQuery.DropEvent<HTMLElement, undefined, any, any>} event - The drop event
* @private
*/
handleDrop(event) {
event.preventDefault();
event.stopPropagation();
clearTimeout(this.dragLeaveTimeout);
$(this.selector).removeClass('dragover');
const files = Array.from(event.originalEvent.dataTransfer.files);
this.onDropCallback(files, event);
}
}

View File

@ -0,0 +1,162 @@
/** @type {CSSStyleSheet} */
let dynamicStyleSheet = null;
/** @type {CSSStyleSheet} */
let dynamicExtensionStyleSheet = null;
/**
* An observer that will check if any new stylesheets are added to the head
* @type {MutationObserver}
*/
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type !== 'childList') return;
mutation.addedNodes.forEach(node => {
if (node instanceof HTMLLinkElement && node.tagName === 'LINK' && node.rel === 'stylesheet') {
node.addEventListener('load', () => {
try {
applyDynamicFocusStyles(node.sheet);
} catch (e) {
console.warn('Failed to process new stylesheet:', e);
}
});
}
});
});
});
/**
* Generates dynamic focus styles based on the given stylesheet, taking its hover styles as reference
*
* @param {CSSStyleSheet} styleSheet - The stylesheet to process
* @param {object} [options] - Optional configuration options
* @param {boolean} [options.fromExtension=false] - Indicates if the styles are from an extension
*/
function applyDynamicFocusStyles(styleSheet, { fromExtension = false } = {}) {
/** @type {{baseSelector: string, rule: CSSStyleRule}[]} */
const hoverRules = [];
/** @type {Set<string>} */
const focusRules = new Set();
const PLACEHOLDER = ':__PLACEHOLDER__';
/**
* Processes the CSS rules and separates selectors for hover and focus
* @param {CSSRuleList} rules - The CSS rules to process
*/
function processRules(rules) {
Array.from(rules).forEach(rule => {
if (rule instanceof CSSImportRule) {
// Make sure that @import rules are processed recursively
processImportedStylesheet(rule.styleSheet);
} else if (rule instanceof CSSStyleRule) {
// Separate multiple selectors on a rule
const selectors = rule.selectorText.split(',').map(s => s.trim());
// We collect all hover and focus rules to be able to later decide which hover rules don't have a matching focus rule
selectors.forEach(selector => {
const isHover = selector.includes(':hover'), isFocus = selector.includes(':focus');
if (isHover && isFocus) {
// We currently do nothing here. Rules containing both hover and focus are very specific and should never be automatically touched
}
else if (isHover) {
const baseSelector = selector.replace(':hover', PLACEHOLDER).trim();
hoverRules.push({ baseSelector, rule });
} else if (isFocus) {
// We need to make sure that we remember all existing :focus, :focus-within and :focus-visible rules
const baseSelector = selector.replace(':focus-within', PLACEHOLDER).replace(':focus-visible', PLACEHOLDER).replace(':focus', PLACEHOLDER).trim();
focusRules.add(baseSelector);
}
});
} else if (rule instanceof CSSMediaRule || rule instanceof CSSSupportsRule) {
// Recursively process nested rules
processRules(rule.cssRules);
}
});
}
/**
* Processes the CSS rules of an imported stylesheet recursively
* @param {CSSStyleSheet} sheet - The imported stylesheet to process
*/
function processImportedStylesheet(sheet) {
if (sheet && sheet.cssRules) {
processRules(sheet.cssRules);
}
}
processRules(styleSheet.cssRules);
/** @type {CSSStyleSheet} */
let targetStyleSheet = null;
// Now finally create the dynamic focus rules
hoverRules.forEach(({ baseSelector, rule }) => {
if (!focusRules.has(baseSelector)) {
// Only initialize the dynamic stylesheet if needed
targetStyleSheet ??= getDynamicStyleSheet({ fromExtension });
// The closest keyboard-equivalent to :hover styling is utilizing the :focus-visible rule from modern browsers.
// It let's the browser decide whether a focus highlighting is expected and makes sense.
// So we take all :hover rules that don't have a manually defined focus rule yet, and create their
// :focus-visible counterpart, which will make the styling work the same for keyboard and mouse.
// If something like :focus-within or a more specific selector like `.blah:has(:focus-visible)` for elements inside,
// it should be manually defined in CSS.
const focusSelector = rule.selectorText.replace(/:hover/g, ':focus-visible');
const focusRule = `${focusSelector} { ${rule.style.cssText} }`;
try {
targetStyleSheet.insertRule(focusRule, targetStyleSheet.cssRules.length);
} catch (e) {
console.warn('Failed to insert focus rule:', e);
}
}
});
}
/**
* Retrieves the stylesheet that should be used for dynamic rules
*
* @param {object} options - The options object
* @param {boolean} [options.fromExtension=false] - Indicates whether the rules are coming from extensions
* @return {CSSStyleSheet} The dynamic stylesheet
*/
function getDynamicStyleSheet({ fromExtension = false } = {}) {
if (fromExtension) {
if (!dynamicExtensionStyleSheet) {
const styleSheetElement = document.createElement('style');
styleSheetElement.setAttribute('id', 'dynamic-extension-styles');
document.head.appendChild(styleSheetElement);
dynamicExtensionStyleSheet = styleSheetElement.sheet;
}
return dynamicExtensionStyleSheet;
} else {
if (!dynamicStyleSheet) {
const styleSheetElement = document.createElement('style');
styleSheetElement.setAttribute('id', 'dynamic-styles');
document.head.appendChild(styleSheetElement);
dynamicStyleSheet = styleSheetElement.sheet;
}
return dynamicStyleSheet;
}
}
/**
* Initializes dynamic styles for ST
*/
export function initDynamicStyles() {
// Start observing the head for any new added stylesheets
observer.observe(document.head, {
childList: true,
subtree: true
});
// Process all stylesheets on initial load
Array.from(document.styleSheets).forEach(sheet => {
try {
applyDynamicFocusStyles(sheet, { fromExtension: sheet.href.toLowerCase().includes('scripts/extensions') });
} catch (e) {
console.warn('Failed to process stylesheet on initial load:', e);
}
});
}

View File

@ -349,12 +349,12 @@ function autoConnectInputHandler() {
function addExtensionsButtonAndMenu() { function addExtensionsButtonAndMenu() {
const buttonHTML = const buttonHTML =
'<div id="extensionsMenuButton" style="display: none;" class="fa-solid fa-magic-wand-sparkles" title="Extras Extensions" /></div>'; '<div id="extensionsMenuButton" style="display: none;" class="fa-solid fa-magic-wand-sparkles interactable" title="Extras Extensions" /></div>';
const extensionsMenuHTML = '<div id="extensionsMenu" class="options-content" style="display: none;"></div>'; const extensionsMenuHTML = '<div id="extensionsMenu" class="options-content" style="display: none;"></div>';
$(document.body).append(extensionsMenuHTML); $(document.body).append(extensionsMenuHTML);
$('#leftSendForm').prepend(buttonHTML); $('#leftSendForm').append(buttonHTML);
const button = $('#extensionsMenuButton'); const button = $('#extensionsMenuButton');
const dropdown = $('#extensionsMenu'); const dropdown = $('#extensionsMenu');

View File

@ -11,6 +11,7 @@ import { dragElement } from '../../RossAscends-mods.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, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { DragAndDropHandler } from '../../dragdrop.js';
const extensionName = 'gallery'; const extensionName = 'gallery';
const extensionFolderPath = `scripts/extensions/${extensionName}/`; const extensionFolderPath = `scripts/extensions/${extensionName}/`;
@ -56,7 +57,8 @@ async function getGalleryItems(url) {
* @returns {Promise<void>} - Promise representing the completion of the gallery initialization. * @returns {Promise<void>} - Promise representing the completion of the gallery initialization.
*/ */
async function initGallery(items, url) { async function initGallery(items, url) {
$('#dragGallery').nanogallery2({ const gallery = $('#dragGallery');
gallery.nanogallery2({
'items': items, 'items': items,
thumbnailWidth: 'auto', thumbnailWidth: 'auto',
thumbnailHeight: thumbnailHeight, thumbnailHeight: thumbnailHeight,
@ -80,44 +82,24 @@ async function initGallery(items, url) {
eventSource.on('resizeUI', function (elmntName) { eventSource.on('resizeUI', function (elmntName) {
jQuery('#dragGallery').nanogallery2('resize'); gallery.nanogallery2('resize');
}); });
const dropZone = $('#dragGallery'); const dragDropHandler = new DragAndDropHandler('#dragGallery', async (files, event) => {
//remove any existing handlers let file = files[0];
dropZone.off('dragover');
dropZone.off('dragleave');
dropZone.off('drop');
// Set dropzone height to be the same as the parent
dropZone.css('height', dropZone.parent().css('height'));
// Initialize dropzone handlers
dropZone.on('dragover', function (e) {
e.stopPropagation(); // Ensure this event doesn't propagate
e.preventDefault();
$(this).addClass('dragging'); // Add a CSS class to change appearance during drag-over
});
dropZone.on('dragleave', function (e) {
e.stopPropagation(); // Ensure this event doesn't propagate
$(this).removeClass('dragging');
});
dropZone.on('drop', function (e) {
e.stopPropagation(); // Ensure this event doesn't propagate
e.preventDefault();
$(this).removeClass('dragging');
let file = e.originalEvent.dataTransfer.files[0];
uploadFile(file, url); // Added url parameter to know where to upload uploadFile(file, url); // Added url parameter to know where to upload
}); });
// Set dropzone height to be the same as the parent
gallery.css('height', gallery.parent().css('height'));
//let images populate first //let images populate first
await delay(100); await delay(100);
//unset the height (which must be getting set by the gallery library at some point) //unset the height (which must be getting set by the gallery library at some point)
$('#dragGallery').css('height', 'unset'); gallery.css('height', 'unset');
//force a resize to make images display correctly //force a resize to make images display correctly
jQuery('#dragGallery').nanogallery2('resize'); gallery.nanogallery2('resize');
} }
/** /**

View File

@ -220,68 +220,68 @@
align-items: baseline; align-items: baseline;
} }
@media screen and (max-width: 750px) { @media screen and (max-width: 750px) {
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor { body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor {
flex-direction: column; flex-direction: column;
overflow: auto; overflow: auto;
} }
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main { body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main {
flex: 0 0 auto; flex: 0 0 auto;
} }
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels { body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels {
flex-direction: column; flex-direction: column;
} }
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder { body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
min-height: 50svh; min-height: 50svh;
height: 50svh; height: 50svh;
} }
} }
.dialogue_popup:has(#qr--modalEditor) { .popup:has(#qr--modalEditor) {
aspect-ratio: unset; aspect-ratio: unset;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text { .popup:has(#qr--modalEditor) .popup-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor {
flex: 1 1 auto; flex: 1 1 auto;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 1em; gap: 1em;
overflow: hidden; overflow: hidden;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main {
flex: 1 1 auto; flex: 1 1 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels {
flex: 0 0 auto; flex: 0 0 auto;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 0.5em; gap: 0.5em;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label {
flex: 1 1 1px; flex: 1 1 1px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText {
flex: 1 1 auto; flex: 1 1 auto;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint {
flex: 1 1 auto; flex: 1 1 auto;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > input { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > input {
flex: 0 0 auto; flex: 0 0 auto;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer {
flex: 1 1 auto; flex: 1 1 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 1em; gap: 1em;
@ -289,35 +289,35 @@
font-size: smaller; font-size: smaller;
align-items: baseline; align-items: baseline;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label {
white-space: nowrap; white-space: nowrap;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label > input { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label > input {
font-size: inherit; font-size: inherit;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
flex: 1 1 auto; flex: 1 1 auto;
display: grid; display: grid;
text-align: left; text-align: left;
overflow: hidden; overflow: hidden;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-messageSyntax { .dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-messageSyntax {
display: none; display: none;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message { .dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message {
background-color: var(--ac-style-color-background); background-color: var(--ac-style-color-background);
color: var(--ac-style-color-text); color: var(--ac-style-color-text);
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection { .dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection {
color: unset; color: unset;
background-color: rgba(108 171 251 / 0.25); background-color: rgba(108 171 251 / 0.25);
} }
@supports (color: rgb(from white r g b / 0.25)) { @supports (color: rgb(from white r g b / 0.25)) {
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection { .dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection {
background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25); background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
} }
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-messageSyntax { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-messageSyntax {
grid-column: 1; grid-column: 1;
grid-row: 1; grid-row: 1;
padding: 0; padding: 0;
@ -327,10 +327,10 @@
min-width: 100%; min-width: 100%;
width: 0; width: 0;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-messageSyntax > #qr--modal-messageSyntaxInner { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-messageSyntax > #qr--modal-messageSyntaxInner {
height: 100%; height: 100%;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message {
background-color: transparent; background-color: transparent;
color: transparent; color: transparent;
grid-column: 1; grid-column: 1;
@ -338,22 +338,22 @@
caret-color: var(--ac-style-color-text); caret-color: var(--ac-style-color-text);
overflow: auto; overflow: auto;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::-webkit-scrollbar, .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::-webkit-scrollbar,
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::-webkit-scrollbar-thumb { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::-webkit-scrollbar-thumb {
visibility: hidden; visibility: hidden;
cursor: default; cursor: default;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::selection { .dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::selection {
color: transparent; color: transparent;
background-color: rgba(108 171 251 / 0.25); background-color: rgba(108 171 251 / 0.25);
} }
@supports (color: rgb(from white r g b / 0.25)) { @supports (color: rgb(from white r g b / 0.25)) {
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::selection { .dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::selection {
background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25); background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
} }
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder #qr--modal-message, .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder #qr--modal-message,
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder #qr--modal-messageSyntaxInner { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder #qr--modal-messageSyntaxInner {
font-family: var(--monoFontFamily); font-family: var(--monoFontFamily);
padding: 0.75em; padding: 0.75em;
margin: 0; margin: 0;
@ -363,11 +363,11 @@
border: 1px solid var(--SmartThemeBorderColor); border: 1px solid var(--SmartThemeBorderColor);
border-radius: 5px; border-radius: 5px;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons {
display: flex; display: flex;
gap: 1em; gap: 1em;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--modal-executeButton { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons .qr--modal-executeButton {
border-width: 2px; border-width: 2px;
border-style: solid; border-style: solid;
display: flex; display: flex;
@ -375,42 +375,42 @@
gap: 0.5em; gap: 0.5em;
padding: 0.5em 0.75em; padding: 0.5em 0.75em;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--modal-executeButton .qr--modal-executeComboIcon { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons .qr--modal-executeButton .qr--modal-executeComboIcon {
display: flex; display: flex;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute {
transition: 200ms; transition: 200ms;
filter: grayscale(0); filter: grayscale(0);
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute.qr--busy { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute.qr--busy {
cursor: wait; cursor: wait;
opacity: 0.5; opacity: 0.5;
filter: grayscale(1); filter: grayscale(1);
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute {
border-color: #51a351; border-color: #51a351;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-pause, .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-pause,
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {
cursor: default; cursor: default;
opacity: 0.5; opacity: 0.5;
filter: grayscale(1); filter: grayscale(1);
pointer-events: none; pointer-events: none;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--busy ~ #qr--modal-pause, .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons .qr--busy ~ #qr--modal-pause,
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--busy ~ #qr--modal-stop { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons .qr--busy ~ #qr--modal-stop {
cursor: pointer; cursor: pointer;
opacity: 1; opacity: 1;
filter: grayscale(0); filter: grayscale(0);
pointer-events: all; pointer-events: all;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-pause { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-pause {
border-color: #92befc; border-color: #92befc;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {
border-color: #d78872; border-color: #d78872;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress {
--prog: 0; --prog: 0;
--progColor: #92befc; --progColor: #92befc;
--progFlashColor: #d78872; --progFlashColor: #d78872;
@ -421,7 +421,7 @@
background-color: var(--black50a); background-color: var(--black50a);
position: relative; position: relative;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress:after { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress:after {
content: ''; content: '';
background-color: var(--progColor); background-color: var(--progColor);
position: absolute; position: absolute;
@ -429,23 +429,23 @@
right: calc(100% - var(--prog) * 1%); right: calc(100% - var(--prog) * 1%);
transition: 200ms; transition: 200ms;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--paused:after { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress.qr--paused:after {
animation-name: qr--progressPulse; animation-name: qr--progressPulse;
animation-duration: 1500ms; animation-duration: 1500ms;
animation-timing-function: ease-in-out; animation-timing-function: ease-in-out;
animation-delay: 0s; animation-delay: 0s;
animation-iteration-count: infinite; animation-iteration-count: infinite;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--aborted:after { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress.qr--aborted:after {
background-color: var(--progAbortedColor); background-color: var(--progAbortedColor);
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--success:after { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress.qr--success:after {
background-color: var(--progSuccessColor); background-color: var(--progSuccessColor);
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--error:after { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress.qr--error:after {
background-color: var(--progErrorColor); background-color: var(--progErrorColor);
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeErrors { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeErrors {
display: none; display: none;
text-align: left; text-align: left;
font-size: smaller; font-size: smaller;
@ -456,10 +456,10 @@
min-width: 100%; min-width: 100%;
width: 0; width: 0;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeErrors.qr--hasErrors { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeErrors.qr--hasErrors {
display: block; display: block;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult {
display: none; display: none;
text-align: left; text-align: left;
font-size: smaller; font-size: smaller;
@ -470,10 +470,10 @@
min-width: 100%; min-width: 100%;
width: 0; width: 0;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult.qr--hasResult { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult.qr--hasResult {
display: block; display: block;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult:before { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult:before {
content: 'Result: '; content: 'Result: ';
} }
@keyframes qr--progressPulse { @keyframes qr--progressPulse {
@ -485,6 +485,6 @@
background-color: var(--progFlashColor); background-color: var(--progFlashColor);
} }
} }
.shadow_popup.qr--hide { .popup.qr--hide {
opacity: 0 !important; opacity: 0 !important;
} }

View File

@ -244,7 +244,7 @@
@media screen and (max-width: 750px) { @media screen and (max-width: 750px) {
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor { body .popup:has(#qr--modalEditor) .popup-content>#qr--modalEditor {
flex-direction: column; flex-direction: column;
overflow: auto; overflow: auto;
> #qr--main { > #qr--main {
@ -259,10 +259,10 @@
} }
} }
} }
.dialogue_popup:has(#qr--modalEditor) { .popup:has(#qr--modalEditor) {
aspect-ratio: unset; aspect-ratio: unset;
.dialogue_popup_text { .popup-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -507,6 +507,6 @@
} }
} }
.shadow_popup.qr--hide { .popup.qr--hide {
opacity: 0 !important; opacity: 0 !important;
} }

View File

@ -73,7 +73,7 @@ import {
depth_prompt_role_default, depth_prompt_role_default,
shouldAutoContinue, shouldAutoContinue,
} from '../script.js'; } from '../script.js';
import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map } from './tags.js'; import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js';
import { FILTER_TYPES, FilterHelper } from './filters.js'; import { FILTER_TYPES, FilterHelper } from './filters.js';
import { isExternalMediaAllowed } from './chats.js'; import { isExternalMediaAllowed } from './chats.js';
@ -1356,7 +1356,7 @@ function select_group_chats(groupId, skipAnimation) {
} }
// render tags // render tags
printTagList($('#groupTagList'), { forEntityOrKey: groupId, tagOptions: { removable: true } }); applyTagsOnGroupSelect(groupId);
// render characters list // render characters list
printGroupCandidates(); printGroupCandidates();

243
public/scripts/keyboard.js Normal file
View File

@ -0,0 +1,243 @@
/* All selectors that should act as interactables / keyboard buttons by default */
const interactableSelectors = [
'.interactable', // Main interactable class for ALL interactable controls (can also be manually added in code, so that's why its listed here)
'.custom_interactable', // Manually made interactable controls via code (see 'makeKeyboardInteractable()')
'.menu_button', // General menu button in ST
'.right_menu_button', // Button-likes in many menus
'.drawer-icon', // Main "menu bar" icons
'.inline-drawer-icon', // Buttons/icons inside the drawer menus
'.paginationjs-pages li a', // Pagination buttons
'.group_select, .character_select, .bogus_folder_select', // Cards to select char, group or folder in character list and other places
'.avatar-container', // Persona list blocks
'.tag .tag_remove', // Remove button in removable tags
'.bg_example', // Background elements in the background menu
'.bg_example .bg_button', // The inline buttons on the backgrounds
'#options a', // Option entries in the popup options menu
'#extensionsMenu div:has(.extensionsMenuExtensionButton)', // Option entries in the extension menu popup that are coming from extensions
'.mes_buttons .mes_button', // Small inline buttons on the chat messages
'.extraMesButtons>div:not(.mes_button)', // The extra/extension buttons inline on the chat messages
'.swipe_left, .swipe_right', // Swipe buttons on the last message
'.stscript_btn', // STscript buttons in the chat bar
'.select2_choice_clickable+span.select2-container .select2-selection__choice__display', // select2 control elements if they are meant to be clickable
'.avatar_load_preview' // Char display avatar selection
];
export const INTERACTABLE_CONTROL_CLASS = 'interactable';
export const CUSTOM_INTERACTABLE_CONTROL_CLASS = 'custom_interactable';
export const NOT_FOCUSABLE_CONTROL_CLASS = 'not_focusable';
export const DISABLED_CONTROL_CLASS = 'disabled';
/**
* An observer that will check if any new interactables or scroll reset containers are added to the body
* @type {MutationObserver}
*/
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(handleNodeChange);
}
if (mutation.type === 'attributes') {
const target = mutation.target;
if (mutation.attributeName === 'class' && target instanceof Element) {
handleNodeChange(target);
}
}
});
});
/**
* Function to handle node changes (added or modified nodes)
* @param {Element} node
*/
function handleNodeChange(node) {
if (node.nodeType === Node.ELEMENT_NODE && node instanceof Element) {
// Handle keyboard interactables
if (isKeyboardInteractable(node)) {
makeKeyboardInteractable(node);
}
initializeInteractables(node);
// Handle scroll reset containers
if (node.classList.contains('scroll-reset-container')) {
applyScrollResetBehavior(node);
}
initializeScrollResetBehaviors(node);
}
}
/**
* Registers an interactable class (for example for an extension) and makes it keyboard interactable.
* Optionally apply the 'not_focusable' and 'disabled' classes if needed.
*
* @param {string} interactableSelector - The CSS selector for the interactable (Supports class combinations, chained via dots like <c>tag.actionable</c>, and sub selectors)
* @param {object} [options={}] - Optional settings for the interactable
* @param {boolean} [options.disabledByDefault=false] - Whether interactables of this class should be disabled by default
* @param {boolean} [options.notFocusableByDefault=false] - Whether interactables of this class should not be focusable by default
*/
export function registerInteractableType(interactableSelector, { disabledByDefault = false, notFocusableByDefault = false } = {}) {
interactableSelectors.push(interactableSelector);
const interactables = document.querySelectorAll(interactableSelector);
if (disabledByDefault || notFocusableByDefault) {
interactables.forEach(interactable => {
if (disabledByDefault) interactable.classList.add(DISABLED_CONTROL_CLASS);
if (notFocusableByDefault) interactable.classList.add(NOT_FOCUSABLE_CONTROL_CLASS);
});
}
makeKeyboardInteractable(...interactables);
}
/**
* Checks if the given control is a keyboard-enabled interactable.
*
* @param {Element} control - The control element to check
* @returns {boolean} Returns true if the control is a keyboard interactable, false otherwise
*/
export function isKeyboardInteractable(control) {
// Check if this control matches any of the selectors
return interactableSelectors.some(selector => control.matches(selector));
}
/**
* Makes all the given controls keyboard interactable and sets their state.
* If the control doesn't have any of the classes, it will be set to a custom-enabled keyboard interactable.
*
* @param {Element[]} interactables - The controls to make interactable and set their state
*/
export function makeKeyboardInteractable(...interactables) {
interactables.forEach(interactable => {
// If this control doesn't have any of the classes, lets say the caller knows this and wants this to be a custom-enabled keyboard control.
if (!isKeyboardInteractable(interactable)) {
interactable.classList.add(CUSTOM_INTERACTABLE_CONTROL_CLASS);
}
// Just for CSS styling and future reference, every keyboard interactable control should have a common class
if (!interactable.classList.contains(INTERACTABLE_CONTROL_CLASS)) {
interactable.classList.add(INTERACTABLE_CONTROL_CLASS);
}
/**
* Check if the element or any parent element has 'disabled' or 'not_focusable' class
* @param {Element} el
* @returns {boolean}
*/
const hasDisabledOrNotFocusableAncestor = (el) => {
while (el) {
if (el.classList.contains(NOT_FOCUSABLE_CONTROL_CLASS) || el.classList.contains(DISABLED_CONTROL_CLASS)) {
return true;
}
el = el.parentElement;
}
return false;
};
// Set/remove the tabindex accordingly to the classes. Remembering if it had a custom value.
if (!hasDisabledOrNotFocusableAncestor(interactable)) {
if (!interactable.hasAttribute('tabindex')) {
const tabIndex = interactable.getAttribute('data-original-tabindex') ?? '0';
interactable.setAttribute('tabindex', tabIndex);
}
} else {
interactable.setAttribute('data-original-tabindex', interactable.getAttribute('tabindex'));
interactable.removeAttribute('tabindex');
}
});
}
/**
* Initializes the focusability of controls on the given element or the document
*
* @param {Element|Document} [element=document] - The element on which to initialize the interactable state. Defaults to the document.
*/
function initializeInteractables(element = document) {
const interactables = getAllInteractables(element);
makeKeyboardInteractable(...interactables);
}
/**
* Queries all interactables within the given element based on the given selectors and returns them as an array
*
* @param {Element|Document} element - The element within which to query the interactables
* @returns {HTMLElement[]} An array containing all the interactables that match the given selectors
*/
function getAllInteractables(element) {
// Query each selector individually and combine all to a big array to return
return [].concat(...interactableSelectors.map(selector => Array.from(element.querySelectorAll(`${selector}`))));
}
/**
* Function to apply scroll reset behavior to a container
* @param {Element} container - The container
*/
const applyScrollResetBehavior = (container) => {
container.addEventListener('focusout', (e) => {
setTimeout(() => {
const focusedElement = document.activeElement;
if (!container.contains(focusedElement)) {
container.scrollTop = 0;
container.scrollLeft = 0;
}
}, 0);
});
};
/**
* Initializes the scroll reset behavior on the given element or the document
*
* @param {Element|Document} [element=document] - The element on which to initialize the scroll reset behavior. Defaults to the document.
*/
function initializeScrollResetBehaviors(element = document) {
const scrollResetContainers = element.querySelectorAll('.scroll-reset-container');
scrollResetContainers.forEach(container => applyScrollResetBehavior(container));
}
/**
* Handles keydown events on the document to trigger click on Enter key press for interactables
*
* @param {KeyboardEvent} event - The keyboard event
*/
function handleGlobalKeyDown(event) {
if (event.key === 'Enter') {
if (!(event.target instanceof HTMLElement))
return;
// Only count enter on this interactable if no modifier key is pressed
if (event.altKey || event.ctrlKey || event.shiftKey)
return;
// Traverse up the DOM tree to find the actual interactable element
let target = event.target;
while (target && !isKeyboardInteractable(target)) {
target = target.parentElement;
}
// Trigger click if a valid interactable is found and it's not disabled
if (target && !target.classList.contains(DISABLED_CONTROL_CLASS)) {
console.debug('Triggering click on keyboard-focused interactable control via Enter', target);
target.click();
}
}
}
/**
* Initializes several keyboard functionalities for ST
*/
export function initKeyboard() {
// Start observing the body for added elements and attribute changes
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class']
});
// Initialize already existing controls
initializeInteractables();
initializeScrollResetBehaviors();
// Add a global keydown listener
document.addEventListener('keydown', handleGlobalKeyDown);
}

View File

@ -1,156 +1,262 @@
import { animation_duration, animation_easing } from '../script.js'; import { removeFromArray, runAfterAnimation, uuidv4 } from './utils.js';
import { delay } from './utils.js';
/** @readonly */
/** @enum {Number} */
/**@readonly*/
/**@enum {Number}*/
export const POPUP_TYPE = { export const POPUP_TYPE = {
'TEXT': 1, 'TEXT': 1,
'CONFIRM': 2, 'CONFIRM': 2,
'INPUT': 3, 'INPUT': 3,
}; };
/**@readonly*/ /** @readonly */
/**@enum {Boolean}*/ /** @enum {number?} */
export const POPUP_RESULT = { export const POPUP_RESULT = {
'AFFIRMATIVE': true, 'AFFIRMATIVE': 1,
'NEGATIVE': false, 'NEGATIVE': 0,
'CANCELLED': undefined, 'CANCELLED': null,
}; };
/**
* @typedef {object} PopupOptions
* @property {string|boolean?} [okButton] - Custom text for the OK button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup)
* @property {string|boolean?} [cancelButton] - Custom text for the Cancel button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup)
* @property {number?} [rows] - The number of rows for the input field
* @property {boolean?} [wide] - Whether to display the popup in wide mode (wide screen, 1/1 aspect ratio)
* @property {boolean?} [wider] - Whether to display the popup in wider mode (just wider, no height scaling)
* @property {boolean?} [large] - Whether to display the popup in large mode (90% of screen)
* @property {boolean?} [allowHorizontalScrolling] - Whether to allow horizontal scrolling in the popup
* @property {boolean?} [allowVerticalScrolling] - Whether to allow vertical scrolling in the popup
* @property {POPUP_RESULT|number?} [defaultResult] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`.
* @property {CustomPopupButton[]|string[]?} [customButtons] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward.
*/
/**
* @typedef {object} CustomPopupButton
* @property {string} text - The text of the button
* @property {POPUP_RESULT|number?} result - The result of the button - can also be a custom result value to make be able to find out that this button was clicked. If no result is specified, this button will **not** close the popup.
* @property {string[]|string?} [classes] - Optional custom CSS classes applied to the button
* @property {()=>void?} [action] - Optional action to perform when the button is clicked
* @property {boolean?} [appendAtEnd] - Whether to append the button to the end of the popup - by default it will be prepended
*/
/**
* @typedef {object} ShowPopupHelper
* Local implementation of the helper functionality to show several popups.
*
* Should be called via `Popup.show.xxxx()`.
*/
const showPopupHelper = {
/**
* Asynchronously displays an input popup with the given header and text, and returns the user's input.
*
* @param {string} header - The header text for the popup.
* @param {string} text - The main text for the popup.
* @param {string} [defaultValue=''] - The default value for the input field.
* @param {PopupOptions} [popupOptions={}] - Options for the popup.
* @return {Promise<string?>} A Promise that resolves with the user's input.
*/
input: async (header, text, defaultValue = '', popupOptions = {}) => {
const content = PopupUtils.BuildTextWithHeader(header, text);
const popup = new Popup(content, POPUP_TYPE.INPUT, defaultValue, popupOptions);
const value = await popup.show();
return value ? String(value) : null;
},
}
export class Popup { export class Popup {
/**@type {POPUP_TYPE}*/ type; /** @type {POPUP_TYPE} */ type;
/**@type {HTMLElement}*/ dom; /** @type {string} */ id;
/**@type {HTMLElement}*/ dlg;
/**@type {HTMLElement}*/ text;
/**@type {HTMLTextAreaElement}*/ input;
/**@type {HTMLElement}*/ ok;
/**@type {HTMLElement}*/ cancel;
/**@type {POPUP_RESULT}*/ result; /** @type {HTMLDialogElement} */ dlg;
/**@type {any}*/ value; /** @type {HTMLElement} */ body;
/** @type {HTMLElement} */ content;
/** @type {HTMLTextAreaElement} */ input;
/** @type {HTMLElement} */ controls;
/** @type {HTMLElement} */ ok;
/** @type {HTMLElement} */ cancel;
/** @type {POPUP_RESULT|number?} */ defaultResult;
/** @type {CustomPopupButton[]|string[]?} */ customButtons;
/**@type {Promise}*/ promise; /** @type {POPUP_RESULT|number} */ result;
/**@type {Function}*/ resolver; /** @type {any} */ value;
/**@type {Function}*/ keyListenerBound;
/** @type {HTMLElement} */ lastFocus;
/** @type {Promise<any>} */ promise;
/** @type {(result: any) => any} */ resolver;
/** /**
* @typedef {{okButton?: string, cancelButton?: string, rows?: number, wide?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean }} PopupOptions - Options for the popup. * Constructs a new Popup object with the given text content, type, inputValue, and options
* @param {JQuery<HTMLElement>|string|Element} text - Text to display in the popup. *
* @param {POPUP_TYPE} type - One of Popup.TYPE * @param {JQuery<HTMLElement>|string|Element} content - Text content to display in the popup
* @param {string} inputValue - Value to set the input to. * @param {POPUP_TYPE} type - The type of the popup
* @param {PopupOptions} options - Options for the popup. * @param {string} [inputValue=''] - The initial value of the input field
* @param {PopupOptions} [options={}] - Additional options for the popup
*/ */
constructor(text, type, inputValue = '', { okButton, cancelButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling } = {}) { constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null } = {}) {
Popup.util.popups.push(this);
// Make this popup uniquely identifiable
this.id = uuidv4();
this.type = type; this.type = type;
/**@type {HTMLTemplateElement}*/ /**@type {HTMLTemplateElement}*/
const template = document.querySelector('#shadow_popup_template'); const template = document.querySelector('#popup_template');
// @ts-ignore // @ts-ignore
this.dom = template.content.cloneNode(true).querySelector('.shadow_popup'); this.dlg = template.content.cloneNode(true).querySelector('.popup');
const dlg = this.dom.querySelector('.dialogue_popup'); this.body = this.dlg.querySelector('.popup-body');
// @ts-ignore this.content = this.dlg.querySelector('.popup-content');
this.dlg = dlg; this.input = this.dlg.querySelector('.popup-input');
this.text = this.dom.querySelector('.dialogue_popup_text'); this.controls = this.dlg.querySelector('.popup-controls');
this.input = this.dom.querySelector('.dialogue_popup_input'); this.ok = this.dlg.querySelector('.popup-button-ok');
this.ok = this.dom.querySelector('.dialogue_popup_ok'); this.cancel = this.dlg.querySelector('.popup-button-cancel');
this.cancel = this.dom.querySelector('.dialogue_popup_cancel');
if (wide) dlg.classList.add('wide_dialogue_popup'); this.dlg.setAttribute('data-id', this.id);
if (wider) dlg.classList.add('wider_dialogue_popup'); if (wide) this.dlg.classList.add('wide_dialogue_popup');
if (large) dlg.classList.add('large_dialogue_popup'); if (wider) this.dlg.classList.add('wider_dialogue_popup');
if (allowHorizontalScrolling) dlg.classList.add('horizontal_scrolling_dialogue_popup'); if (large) this.dlg.classList.add('large_dialogue_popup');
if (allowVerticalScrolling) dlg.classList.add('vertical_scrolling_dialogue_popup'); if (allowHorizontalScrolling) this.dlg.classList.add('horizontal_scrolling_dialogue_popup');
if (allowVerticalScrolling) this.dlg.classList.add('vertical_scrolling_dialogue_popup');
this.ok.textContent = okButton ?? 'OK'; // If custom button captions are provided, we set them beforehand
this.cancel.textContent = cancelButton ?? template.getAttribute('popup_text_cancel'); this.ok.textContent = typeof okButton === 'string' ? okButton : 'OK';
this.cancel.textContent = typeof cancelButton === 'string' ? cancelButton : template.getAttribute('popup-button-cancel');
this.defaultResult = defaultResult;
this.customButtons = customButtons;
this.customButtons?.forEach((x, index) => {
/** @type {CustomPopupButton} */
const button = typeof x === 'string' ? { text: x, result: index + 2 } : x;
const buttonElement = document.createElement('div');
buttonElement.classList.add('menu_button', 'popup-button-custom', 'result-control');
buttonElement.classList.add(...(button.classes ?? []));
buttonElement.setAttribute('data-result', String(button.result ?? undefined));
buttonElement.textContent = button.text;
buttonElement.tabIndex = 0;
if (button.action) buttonElement.addEventListener('click', button.action);
if (button.result) buttonElement.addEventListener('click', () => this.complete(button.result));
if (button.appendAtEnd) {
this.controls.appendChild(buttonElement);
} else {
this.controls.insertBefore(buttonElement, this.ok);
}
});
// Set the default button class
const defaultButton = this.controls.querySelector(`[data-result="${this.defaultResult}"]`);
if (defaultButton) defaultButton.classList.add('menu_button_default');
switch (type) { switch (type) {
case POPUP_TYPE.TEXT: { case POPUP_TYPE.TEXT: {
this.input.style.display = 'none'; this.input.style.display = 'none';
this.cancel.style.display = 'none'; if (!cancelButton) this.cancel.style.display = 'none';
break; break;
} }
case POPUP_TYPE.CONFIRM: { case POPUP_TYPE.CONFIRM: {
this.input.style.display = 'none'; this.input.style.display = 'none';
this.ok.textContent = okButton ?? template.getAttribute('popup_text_yes'); if (!okButton) this.ok.textContent = template.getAttribute('popup-button-yes');
this.cancel.textContent = cancelButton ?? template.getAttribute('popup_text_no'); if (!cancelButton) this.cancel.textContent = template.getAttribute('popup-button-no');
break; break;
} }
case POPUP_TYPE.INPUT: { case POPUP_TYPE.INPUT: {
this.input.style.display = 'block'; this.input.style.display = 'block';
this.ok.textContent = okButton ?? template.getAttribute('popup_text_save'); if (!okButton) this.ok.textContent = template.getAttribute('popup-button-save');
break; break;
} }
default: { default: {
// illegal argument console.warn('Unknown popup type.', type);
break;
} }
} }
this.input.value = inputValue; this.input.value = inputValue;
this.input.rows = rows ?? 1; this.input.rows = rows ?? 1;
this.text.innerHTML = ''; this.content.innerHTML = '';
if (text instanceof jQuery) { if (content instanceof jQuery) {
$(this.text).append(text); $(this.content).append(content);
} else if (text instanceof HTMLElement) { } else if (content instanceof HTMLElement) {
this.text.append(text); this.content.append(content);
} else if (typeof text == 'string') { } else if (typeof content == 'string') {
this.text.innerHTML = text; this.content.innerHTML = content;
} else { } else {
// illegal argument console.warn('Unknown popup text type. Should be jQuery, HTMLElement or string.', content);
} }
this.input.addEventListener('keydown', (evt) => { // Already prepare the auto-focus control by adding the "autofocus" attribute, this should be respected by showModal()
if (evt.key != 'Enter' || evt.altKey || evt.ctrlKey || evt.shiftKey) return; this.setAutoFocus({ applyAutoFocus: true });
evt.preventDefault();
evt.stopPropagation();
this.completeAffirmative();
});
this.ok.addEventListener('click', () => this.completeAffirmative()); // Set focus event that remembers the focused element
this.cancel.addEventListener('click', () => this.completeNegative()); this.dlg.addEventListener('focusin', (evt) => { if (evt.target instanceof HTMLElement && evt.target != this.dlg) this.lastFocus = evt.target; });
this.ok.addEventListener('click', () => this.complete(POPUP_RESULT.AFFIRMATIVE));
this.cancel.addEventListener('click', () => this.complete(POPUP_RESULT.NEGATIVE));
const keyListener = (evt) => { const keyListener = (evt) => {
switch (evt.key) { switch (evt.key) {
case 'Escape': { case 'Escape': {
// does it really matter where we check? // Check if we are the currently active popup
const topModal = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2)?.closest('.shadow_popup'); if (this.dlg != document.activeElement?.closest('.popup'))
if (topModal == this.dom) { return;
this.complete(POPUP_RESULT.CANCELLED);
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
this.completeCancelled();
window.removeEventListener('keydown', keyListenerBound); window.removeEventListener('keydown', keyListenerBound);
break; break;
} }
} case 'Enter': {
} // CTRL+Enter counts as a closing action, but all other modifiers (ALT, SHIFT) should not trigger this
}; if (evt.altKey || evt.shiftKey)
const keyListenerBound = keyListener.bind(this); return;
window.addEventListener('keydown', keyListenerBound);
} // Check if we are the currently active popup
if (this.dlg != document.activeElement?.closest('.popup'))
return;
// Check if the current focus is a result control. Only should we apply the compelete action
const resultControl = document.activeElement?.closest('.result-control');
if (!resultControl)
return;
const result = Number(document.activeElement.getAttribute('data-result') ?? this.defaultResult);
this.complete(result);
evt.preventDefault();
evt.stopPropagation();
window.removeEventListener('keydown', keyListenerBound);
async show() {
document.body.append(this.dom);
this.dom.style.display = 'block';
switch (this.type) {
case POPUP_TYPE.INPUT: {
this.input.focus();
break; break;
} }
} }
$(this.dom).transition({ };
opacity: 1, const keyListenerBound = keyListener.bind(this);
duration: animation_duration, this.dlg.addEventListener('keydown', keyListenerBound);
easing: animation_easing, }
});
/**
* Asynchronously shows the popup element by appending it to the document body,
* setting its display to 'block' and focusing on the input if the popup type is INPUT.
*
* @returns {Promise<string|number|boolean?>} A promise that resolves with the value of the popup when it is completed.
*/
async show() {
document.body.append(this.dlg);
// Run opening animation
this.dlg.setAttribute('opening', '');
this.dlg.showModal();
// We need to fix the toastr to be present inside this dialog
fixToastrForDialogs();
runAfterAnimation(this.dlg, () => {
this.dlg.removeAttribute('opening');
})
this.promise = new Promise((resolve) => { this.promise = new Promise((resolve) => {
this.resolver = resolve; this.resolver = resolve;
@ -158,80 +264,201 @@ export class Popup {
return this.promise; return this.promise;
} }
completeAffirmative() { setAutoFocus({ applyAutoFocus = false } = {}) {
/** @type {HTMLElement} */
let control;
// Try to find if we have an autofocus control already present
control = this.dlg.querySelector('[autofocus]');
// If not, find the default control for this popup type
if (!control) {
switch (this.type) { switch (this.type) {
case POPUP_TYPE.TEXT:
case POPUP_TYPE.CONFIRM: {
this.value = true;
break;
}
case POPUP_TYPE.INPUT: { case POPUP_TYPE.INPUT: {
this.value = this.input.value; control = this.input;
break;
}
default:
// Select default button
control = this.controls.querySelector(`[data-result="${this.defaultResult}"]`);
break; break;
} }
} }
this.result = POPUP_RESULT.AFFIRMATIVE;
if (applyAutoFocus) {
control.setAttribute('autofocus', '');
} else {
control.focus();
}
}
/**
* Completes the popup and sets its result and value
*
* The completion handling will make the popup return the result to the original show promise.
*
* There will be two different types of result values:
* - popup with `POPUP_TYPE.INPUT` will return the input value - or `false` on negative and `null` on cancelled
* - All other will return the result value as provided as `POPUP_RESULT` or a custom number value
*
* @param {POPUP_RESULT|number} result - The result of the popup (either an existing `POPUP_RESULT` or a custom result value)
*/
complete(result) {
// In all cases besides INPUT the popup value should be the result
/** @type {POPUP_RESULT|number|boolean|string?} */
let value = result;
// Input type have special results, so the input can be accessed directly without the need to save the popup and access both result and value
if (this.type === POPUP_TYPE.INPUT) {
if (result >= POPUP_RESULT.AFFIRMATIVE) value = this.input.value;
else if (result === POPUP_RESULT.NEGATIVE) value = false;
else if (result === POPUP_RESULT.CANCELLED) value = null;
else value = false; // Might a custom negative value?
}
this.value = value;
this.result = result;
Popup.util.lastResult = { value, result };
this.hide(); this.hide();
} }
completeNegative() { /**
switch (this.type) { * Hides the popup, using the internal resolver to return the value to the original show promise
case POPUP_TYPE.TEXT: * @private
case POPUP_TYPE.CONFIRM: */
case POPUP_TYPE.INPUT: {
this.value = false;
break;
}
}
this.result = POPUP_RESULT.NEGATIVE;
this.hide();
}
completeCancelled() {
switch (this.type) {
case POPUP_TYPE.TEXT:
case POPUP_TYPE.CONFIRM:
case POPUP_TYPE.INPUT: {
this.value = null;
break;
}
}
this.result = POPUP_RESULT.CANCELLED;
this.hide();
}
hide() { hide() {
$(this.dom).transition({ // We close the dialog, first running the animation
opacity: 0, this.dlg.setAttribute('closing', '');
duration: animation_duration,
easing: animation_easing, // Once the hiding starts, we need to fix the toastr to the layer below
}); fixToastrForDialogs();
delay(animation_duration).then(() => {
this.dom.remove(); // After the dialog is actually completely closed, remove it from the DOM
runAfterAnimation(this.dlg, () => {
// Call the close on the dialog
this.dlg.close();
// Remove it from the dom
this.dlg.remove();
// Remove it from the popup references
removeFromArray(Popup.util.popups, this);
// If there is any popup below this one, see if we can set the focus
if (Popup.util.popups.length > 0) {
const activeDialog = document.activeElement?.closest('.popup');
const id = activeDialog?.getAttribute('data-id');
const popup = Popup.util.popups.find(x => x.id == id);
if (popup) {
if (popup.lastFocus) popup.lastFocus.focus();
else popup.setAutoFocus();
}
}
}); });
this.resolver(this.value); this.resolver(this.value);
} }
/**
* Show a popup with any of the given helper methods. Use `await` to make them blocking.
*/
static show = showPopupHelper;
/**
* Utility for popup and popup management.
*
* Contains the list of all currently open popups, and it'll remember the result of the last closed popup.
*/
static util = {
/** @type {Popup[]} Remember all popups */
popups: [],
/** @type {{value: any, result: POPUP_RESULT|number?}?} Last popup result */
lastResult: null,
/** @returns {boolean} Checks if any modal popup dialog is open */
isPopupOpen() {
return Popup.util.popups.length > 0;
},
/**
* Returns the topmost modal layer in the document. If there is an open dialog popup,
* it returns the dialog element. Otherwise, it returns the document body.
*
* @return {HTMLElement} The topmost modal layer element
*/
getTopmostModalLayer() {
return getTopmostModalLayer();
},
}
} }
class PopupUtils {
static BuildTextWithHeader(header, text) {
return `
<h3>${header}</h1>
${text}`;
}
}
/** /**
* Displays a blocking popup with a given text and type. * Displays a blocking popup with a given content and type
* @param {JQuery<HTMLElement>|string|Element} text - Text to display in the popup. *
* @param {JQuery<HTMLElement>|string|Element} content - Content or text to display in the popup
* @param {POPUP_TYPE} type * @param {POPUP_TYPE} type
* @param {string} inputValue - Value to set the input to. * @param {string} inputValue - Value to set the input to
* @param {PopupOptions} options - Options for the popup. * @param {PopupOptions} [popupOptions={}] - Options for the popup
* @returns * @returns {Promise<POPUP_RESULT|string|boolean?>} The value for this popup, which can either be the popup retult or the input value if chosen
*/ */
export function callGenericPopup(text, type, inputValue = '', { okButton, cancelButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling } = {}) { export function callGenericPopup(content, type, inputValue = '', popupOptions = {}) {
const popup = new Popup( const popup = new Popup(
text, content,
type, type,
inputValue, inputValue,
{ okButton, cancelButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling }, popupOptions,
); );
return popup.show(); return popup.show();
} }
/**
* Returns the topmost modal layer in the document. If there is an open dialog,
* it returns the dialog element. Otherwise, it returns the document body.
*
* @return {HTMLElement} The topmost modal layer element
*/
export function getTopmostModalLayer() {
const dlg = Array.from(document.querySelectorAll('dialog[open]:not([closing])')).pop();
if (dlg instanceof HTMLElement) return dlg;
return document.body;
}
/**
* Fixes the issue with toastr not displaying on top of the dialog by moving the toastr container inside the dialog or back to the main body
*/
export function fixToastrForDialogs() {
// Hacky way of getting toastr to actually display on top of the popup...
const dlg = Array.from(document.querySelectorAll('dialog[open]:not([closing])')).pop();
let toastContainer = document.getElementById('toast-container');
const isAlreadyPresent = !!toastContainer;
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.setAttribute('id', 'toast-container');
if (toastr.options.positionClass) toastContainer.classList.add(toastr.options.positionClass);
}
// Check if toastr is already a child. If not, we need to move it inside this dialog.
// This is either the existing toastr container or the newly created one.
if (dlg && !dlg.contains(toastContainer)) {
dlg?.appendChild(toastContainer);
return;
}
// Now another case is if we only have one popup and that is currently closing. In that case the toastr container exists,
// but we don't have an open dialog to move it into. It's just inside the existing one that will be gone in milliseconds.
// To prevent new toasts from being showing up in there and then vanish in an instant,
// we move the toastr back to the main body
if (!dlg && isAlreadyPresent) {
document.body.appendChild(toastContainer);
}
}

View File

@ -4,22 +4,26 @@ import {
this_chid, this_chid,
callPopup, callPopup,
menu_type, menu_type,
getCharacters,
entitiesFilter, entitiesFilter,
printCharactersDebounced, printCharactersDebounced,
buildAvatarList, buildAvatarList,
eventSource, eventSource,
event_types, event_types,
DEFAULT_PRINT_TIMEOUT,
} from '../script.js'; } from '../script.js';
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
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 } from './utils.js'; import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray, getFreeName, debounce } 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 { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
import { debounce_timeout } from './constants.js';
import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js';
import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
import { SlashCommandExecutor } from './slash-commands/SlashCommandExecutor.js'; import { SlashCommandExecutor } from './slash-commands/SlashCommandExecutor.js';
@ -46,6 +50,8 @@ export {
removeTagFromMap, removeTagFromMap,
}; };
/** @typedef {import('../script.js').Character} Character */
const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter'; const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
const GROUP_FILTER_SELECTOR = '#rm_group_chats_block .rm_tag_filter'; const GROUP_FILTER_SELECTOR = '#rm_group_chats_block .rm_tag_filter';
const TAG_TEMPLATE = $('#tag_template .tag'); const TAG_TEMPLATE = $('#tag_template .tag');
@ -329,7 +335,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;
} }
@ -468,29 +474,34 @@ export function getTagKeyForEntityElement(element) {
} }
/** /**
* Adds a tag to a given entity * Adds one or more tags to a given entity
* @param {Tag} tag - The tag to add *
* @param {string|string[]} entityId - The entity to add this tag to. Has to be the entity key (e.g. `addTagToEntity`). (Also allows multiple entities to be passed in) * @param {Tag|Tag[]} tag - The tag or tags to add
* @param {string|string[]} entityId - The entity or entities to add this tag to. Has to be the entity key (e.g. `addTagToEntity`).
* @param {object} [options={}] - Optional arguments * @param {object} [options={}] - Optional arguments
* @param {JQuery<HTMLElement>|string?} [options.tagListSelector=null] - An optional selector if a specific list should be updated with the new tag too (for example because the add was triggered for that function) * @param {JQuery<HTMLElement>|string?} [options.tagListSelector=null] - An optional selector if a specific list should be updated with the new tag too (for example because the add was triggered for that function)
* @param {PrintTagListOptions} [options.tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before. * @param {PrintTagListOptions} [options.tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before.
* @returns {boolean} Whether at least one tag was added * @returns {boolean} Whether at least one tag was added
*/ */
export function addTagToEntity(tag, entityId, { tagListSelector = null, tagListOptions = {} } = {}) { export function addTagsToEntity(tag, entityId, { tagListSelector = null, tagListOptions = {} } = {}) {
const tags = Array.isArray(tag) ? tag : [tag];
const entityIds = Array.isArray(entityId) ? entityId : [entityId];
let result = false; let result = false;
// Add tags to the map // Add tags to the map
if (Array.isArray(entityId)) { entityIds.forEach((id) => {
entityId.forEach((id) => result = addTagToMap(tag.id, id) || result); tags.forEach((tag) => {
} else { result = addTagToMap(tag.id, id) || result;
result = addTagToMap(tag.id, entityId); });
} });
// Save and redraw // Save and redraw
printCharactersDebounced(); printCharactersDebounced();
saveSettingsDebounced(); saveSettingsDebounced();
// We should manually add the selected tag to the print tag function, so we cover places where the tag list did not automatically include it // We should manually add the selected tag to the print tag function, so we cover places where the tag list did not automatically include it
tagListOptions.addTag = tag; tagListOptions.addTag = tags;
// add tag to the UI and internal map - we reprint so sorting and new markup is done correctly // add tag to the UI and internal map - we reprint so sorting and new markup is done correctly
if (tagListSelector) printTagList(tagListSelector, tagListOptions); if (tagListSelector) printTagList(tagListSelector, tagListOptions);
@ -588,10 +599,10 @@ function removeTagFromMap(tagId, characterId = null) {
function findTag(request, resolve, listSelector) { function findTag(request, resolve, listSelector) {
const skipIds = [...($(listSelector).find('.tag').map((_, el) => $(el).attr('id')))]; const skipIds = [...($(listSelector).find('.tag').map((_, el) => $(el).attr('id')))];
const haystack = tags.filter(t => !skipIds.includes(t.id)).map(t => t.name).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); const haystack = tags.filter(t => !skipIds.includes(t.id)).sort(compareTagsForSort).map(t => t.name);
const needle = request.term.toLowerCase(); const needle = request.term;
const hasExactMatch = haystack.findIndex(x => x.toLowerCase() == needle) !== -1; const hasExactMatch = haystack.findIndex(x => equalsIgnoreCaseAndAccents(x, needle)) !== -1;
const result = haystack.filter(x => x.toLowerCase().includes(needle)); const result = haystack.filter(x => includesIgnoreCaseAndAccents(x, needle));
if (request.term && !hasExactMatch) { if (request.term && !hasExactMatch) {
result.unshift(request.term); result.unshift(request.term);
@ -612,7 +623,7 @@ function findTag(request, resolve, listSelector) {
*/ */
function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) { function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) {
let tagName = ui.item.value; let tagName = ui.item.value;
let tag = tags.find(t => t.name === tagName); let tag = getTag(tagName);
// create new tag if it doesn't exist // create new tag if it doesn't exist
if (!tag) { if (!tag) {
@ -626,7 +637,7 @@ function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) {
const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters; const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters;
const characterIds = characterData ? JSON.parse(characterData).characterIds : null; const characterIds = characterData ? JSON.parse(characterData).characterIds : null;
addTagToEntity(tag, characterIds, { tagListSelector: listSelector, tagListOptions: tagListOptions }); addTagsToEntity(tag, characterIds, { tagListSelector: listSelector, tagListOptions: tagListOptions });
// need to return false to keep the input clear // need to return false to keep the input clear
return false; return false;
@ -635,70 +646,200 @@ function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) {
/** /**
* Get a list of existing tags matching a list of provided new tag names * Get a list of existing tags matching a list of provided new tag names
* *
* @param {string[]} new_tags - A list of strings representing tag names * @param {string[]} newTags - A list of strings representing tag names
* @returns List of existing tags * @returns {Tag[]} List of existing tags
*/ */
function getExistingTags(new_tags) { function getExistingTags(newTags) {
let existing_tags = []; let existingTags = [];
for (let tag of new_tags) { for (let tagName of newTags) {
let foundTag = tags.find(t => t.name.toLowerCase() === tag.toLowerCase()); let foundTag = getTag(tagName);
if (foundTag) { if (foundTag) {
existing_tags.push(foundTag.name); existingTags.push(foundTag);
} }
} }
return existing_tags; return existingTags;
} }
async function importTags(imported_char) { const tagImportSettings = {
let imported_tags = imported_char.tags.filter(t => t !== 'ROOT' && t !== 'TAVERN'); ALWAYS_IMPORT_ALL: 1,
let existingTags = await getExistingTags(imported_tags); ONLY_IMPORT_EXISTING: 2,
//make this case insensitive IMPORT_NONE: 3,
let newTags = imported_tags.filter(t => !existingTags.some(existingTag => existingTag.toLowerCase() === t.toLowerCase())); ASK: 4,
let selected_tags = ''; };
const existingTagsString = existingTags.length ? (': ' + existingTags.join(', ')) : '';
if (newTags.length === 0) {
await callPopup(`<h3>Importing Tags For ${imported_char.name}</h3><p>${existingTags.length} existing tags have been found${existingTagsString}.</p>`, 'text');
} 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(', '));
}
// @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) {
selected_tags = selected_tags.slice(0, 15);
}
for (let tagName of selected_tags) {
let tag = tags.find(t => t.name === tagName);
if (!tag) { let globalTagImportSetting = tagImportSettings.ASK; // Default setting
const IMPORT_EXLCUDED_TAGS = ['ROOT', 'TAVERN'];
const ANTI_TROLL_MAX_TAGS = 15;
/**
* Imports tags for a given character
*
* @param {Character} character - The character
* @returns {Promise<boolean>} Boolean indicating whether any tag was imported
*/
async function importTags(character) {
// Gather the tags to import based on the selected setting
const tagNamesToImport = await handleTagImport(character);
if (!tagNamesToImport?.length) {
toastr.info('No tags imported', 'Importing Tags');
return;
}
const tagsToImport = tagNamesToImport.map(tag => getTag(tag, { createNew: true }));
const added = addTagsToEntity(tagsToImport, character.avatar);
toastr.success(`Imported tags:<br />${tagsToImport.map(x => x.name).join(', ')}`, 'Importing Tags', { escapeHtml: false });
return added;
}
/**
* Handles the import of tags for a given character and returns the resulting list of tags to add
*
* @param {Character} character - The character
* @returns {Promise<string[]>} Array of strings representing the tags to import
*/
async function handleTagImport(character) {
/** @type {string[]} */
const importTags = character.tags.map(t => t.trim()).filter(t => t)
.filter(t => !IMPORT_EXLCUDED_TAGS.includes(t))
.slice(0, ANTI_TROLL_MAX_TAGS);
const existingTags = getExistingTags(importTags);
const newTags = importTags.filter(t => !existingTags.some(existingTag => existingTag.name.toLowerCase() === t.toLowerCase()))
.map(newTag);
switch (globalTagImportSetting) {
case tagImportSettings.ALWAYS_IMPORT_ALL:
return existingTags.concat(newTags).map(t => t.name);
case tagImportSettings.ONLY_IMPORT_EXISTING:
return existingTags.map(t => t.name);
case tagImportSettings.ASK:
return await showTagImportPopup(character, existingTags, newTags);
case tagImportSettings.IMPORT_NONE:
default:
return [];
}
}
/**
* Shows a popup to import tags for a given character and returns the resulting list of tags to add
*
* @param {Character} character - The character
* @param {Tag[]} existingTags - List of existing tags
* @param {Tag[]} newTags - List of new tags
* @returns {Promise<string[]>} Array of strings representing the tags to import
*/
async function showTagImportPopup(character, existingTags, newTags) {
/** @type {{[key: string]: import('./popup.js').CustomPopupButton}} */
const importButtons = {
EXISTING: { result: 2, text: 'Import Existing' },
ALL: { result: 3, text: 'Import All' },
NONE: { result: 4, text: 'Import None' },
};
const customButtonsCaptions = Object.values(importButtons).map(button => `&quot;${button.text}&quot;`);
const customButtonsString = customButtonsCaptions.slice(0, -1).join(', ') + ' or ' + customButtonsCaptions.slice(-1);
const popupContent = $(`
<h3>Import Tags For ${character.name}</h3>
<div class="import_avatar_placeholder"></div>
<div class="import_tags_content justifyLeft">
<small>
Click remove on any tag to remove it from this import.<br />
Select one of the import options to finish importing the tags.
</small>
<h4 class="m-t-1">Existing Tags</h4>
<div id="import_existing_tags_list" class="tags"></div>
<h4 class="m-t-1">New Tags</h4>
<div id="import_new_tags_list" class="tags"></div>
<small>
<label class="checkbox flex-container alignitemscenter flexNoGap m-t-3" for="import_remember_option">
<input type="checkbox" id="import_remember_option" name="import_remember_option" />
<span data-i18n="Remember my choice">
Remember my choice
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Remember the chosen import option\nIf ${customButtonsString} is selected, this dialog will not show up anymore.\nTo change this, go to the settings and modify &quot;Tag Import Option&quot;.\n\nIf the &quot;Import&quot; option is chosen, the global setting will stay on &quot;Ask&quot;."
title="Remember the chosen import option\nIf ${customButtonsString} is selected, this dialog will not show up anymore.\nTo change this, go to the settings and modify &quot;Tag Import Option&quot;.\n\nIf the &quot;Import&quot; option is chosen, the global setting will stay on &quot;Ask&quot;.">
</div>
</span>
</label>
</small>
</div>`);
// Print tags after popup is shown, so that events can be added
printTagList(popupContent.find('#import_existing_tags_list'), { tags: existingTags, tagOptions: { removable: true, removeAction: tag => removeFromArray(existingTags, tag) } });
printTagList(popupContent.find('#import_new_tags_list'), { tags: newTags, tagOptions: { removable: true, removeAction: tag => removeFromArray(newTags, tag) } });
const result = await callGenericPopup(popupContent, POPUP_TYPE.TEXT, null, { wider: true, okButton: 'Import', cancelButton: true, customButtons: Object.values(importButtons) });
if (!result) {
return [];
}
switch (result) {
case 1:
case true:
case importButtons.ALL.result: // Default 'Import' option where it imports all selected
return existingTags.concat(newTags).map(t => t.name);
case importButtons.EXISTING.result:
return existingTags.map(t => t.name);
case importButtons.NONE.result:
default:
return [];
}
}
/**
* Gets a tag from the tags array based on the provided tag name (insensitive soft matching)
* Optionally creates the tag if it doesn't exist
*
* @param {string} tagName - The name of the tag to search for
* @param {object} [options={}] - Optional parameters
* @param {boolean} [options.createNew=false] - Whether to create the tag if it doesn't exist
* @returns {Tag?} The tag object that matches the provided tag name, or undefined if no match is found
*/
function getTag(tagName, { createNew = false } = {}) {
let tag = tags.find(t => equalsIgnoreCaseAndAccents(t.name, tagName));
if (!tag && createNew) {
tag = createNewTag(tagName); tag = createNewTag(tagName);
} }
return tag;
if (!tag_map[imported_char.avatar].includes(tag.id)) {
tag_map[imported_char.avatar].push(tag.id);
console.debug('added tag to map', tag, imported_char.name);
}
}
saveSettingsDebounced();
// Await the character list, which will automatically reprint it and all tag filters
await getCharacters();
// need to return false to keep the input clear
return false;
} }
/** /**
* Creates a new tag with default properties and a randomly generated id * Creates a new tag with default properties and a randomly generated id
* *
* Does **not** trigger a save, so it's up to the caller to do that
*
* @param {string} tagName - name of the tag * @param {string} tagName - name of the tag
* @returns {Tag} * @returns {Tag} the newly created tag, or the existing tag if it already exists (with a logged warning)
*/ */
function createNewTag(tagName) { function createNewTag(tagName) {
const tag = { const existing = getTag(tagName);
if (existing) {
toastr.warning(`Cannot create new tag. A tag with the name already exists:<br />${existing.name}`, 'Creating Tag', { escapeHtml: false });
return existing;
}
const tag = newTag(tagName);
tags.push(tag);
console.debug('Created new tag', tag.name, 'with id', tag.id);
return tag;
}
/**
* Creates a new tag object with the given tag name and default properties
*
* Not to be confused with `createNewTag`, which actually creates the tag and adds it to the existing list of tags.
* Use this one to create temporary tag objects, for example for drawing.
*
* @param {string} tagName - The name of the tag
* @return {Tag} The newly created tag object
*/
function newTag(tagName) {
return {
id: uuidv4(), id: uuidv4(),
name: tagName, name: tagName,
folder_type: TAG_FOLDER_DEFAULT_TYPE, folder_type: TAG_FOLDER_DEFAULT_TYPE,
@ -708,16 +849,14 @@ function createNewTag(tagName) {
color2: '', color2: '',
create_date: Date.now(), create_date: Date.now(),
}; };
tags.push(tag);
console.debug('Created new tag', tag.name, 'with id', tag.id);
return tag;
} }
/** /**
* @typedef {object} TagOptions - Options for tag behavior. (Same object will be passed into "appendTagToList") * @typedef {object} TagOptions - Options for tag behavior. (Same object will be passed into "appendTagToList")
* @property {boolean} [removable=false] - Whether tags can be removed. * @property {boolean} [removable=false] - Whether tags can be removed.
* @property {boolean} [selectable=false] - Whether tags can be selected. * @property {boolean} [isFilter=false] - Whether tags can be selected as a filter.
* @property {function} [action=undefined] - Action to perform on tag interaction. * @property {function} [action=undefined] - Action to perform on tag interaction.
* @property {(tag: Tag)=>boolean} [removeAction=undefined] - Action to perform on tag removal instead of the default remove action. If the action returns false, the tag will not be removed.
* @property {boolean} [isGeneralList=false] - If true, indicates that this is the general list of tags. * @property {boolean} [isGeneralList=false] - If true, indicates that this is the general list of tags.
* @property {boolean} [skipExistsCheck=false] - If true, the tag gets added even if a tag with the same id already exists. * @property {boolean} [skipExistsCheck=false] - If true, the tag gets added even if a tag with the same id already exists.
*/ */
@ -725,7 +864,7 @@ function createNewTag(tagName) {
/** /**
* @typedef {object} PrintTagListOptions - Optional parameters for printing the tag list. * @typedef {object} PrintTagListOptions - Optional parameters for printing the tag list.
* @property {Tag[]|function(): Tag[]} [tags=undefined] - Optional override of tags that should be printed. Those will not be sorted. If no supplied, tags for the relevant character are printed. Can also be a function that returns the tags. * @property {Tag[]|function(): Tag[]} [tags=undefined] - Optional override of tags that should be printed. Those will not be sorted. If no supplied, tags for the relevant character are printed. Can also be a function that returns the tags.
* @property {Tag} [addTag=undefined] - Optionally provide a tag that should be manually added to this print. Either to the overriden tag list or the found tags based on the entity/key. Will respect the tag exists check. * @property {Tag|Tag[]} [addTag=undefined] - Optionally provide one or multiple tags that should be manually added to this print. Either to the overriden tag list or the found tags based on the entity/key. Will respect the tag exists check.
* @property {object|number|string} [forEntityOrKey=undefined] - Optional override for the chosen entity, otherwise the currently selected is chosen. Can be an entity with id property (character, group, tag), or directly an id or tag key. * @property {object|number|string} [forEntityOrKey=undefined] - Optional override for the chosen entity, otherwise the currently selected is chosen. Can be an entity with id property (character, group, tag), or directly an id or tag key.
* @property {boolean|string} [empty=true] - Whether the list should be initially empty. If a string string is provided, 'always' will always empty the list, otherwise it'll evaluate to a boolean. * @property {boolean|string} [empty=true] - Whether the list should be initially empty. If a string string is provided, 'always' will always empty the list, otherwise it'll evaluate to a boolean.
* @property {boolean} [sort=true] - Whether the tags should be sorted via the sort function, or kept as is. * @property {boolean} [sort=true] - Whether the tags should be sorted via the sort function, or kept as is.
@ -749,8 +888,9 @@ function printTagList(element, { tags = undefined, addTag = undefined, forEntity
$element.empty(); $element.empty();
} }
if (addTag && (tagOptions.skipExistsCheck || !printableTags.some(x => x.id === addTag.id))) { if (addTag) {
printableTags = [...printableTags, addTag]; const addTags = Array.isArray(addTag) ? addTag : [addTag];
printableTags = printableTags.concat(addTags.filter(tag => tagOptions.skipExistsCheck || !printableTags.some(t => t.id === tag.id)));
} }
// one last sort, because we might have modified the tag list or manually retrieved it from a function // one last sort, because we might have modified the tag list or manually retrieved it from a function
@ -834,7 +974,7 @@ function printTagList(element, { tags = undefined, addTag = undefined, forEntity
* @param {TagOptions} [options={}] - Options for tag behavior * @param {TagOptions} [options={}] - Options for tag behavior
* @returns {void} * @returns {void}
*/ */
function appendTagToList(listElement, tag, { removable = false, selectable = false, action = undefined, isGeneralList = false, skipExistsCheck = false } = {}) { function appendTagToList(listElement, tag, { removable = false, isFilter = false, action = undefined, removeAction = undefined, isGeneralList = false, skipExistsCheck = false } = {}) {
if (!listElement) { if (!listElement) {
return; return;
} }
@ -852,6 +992,13 @@ function appendTagToList(listElement, tag, { removable = false, selectable = fal
tagElement.find('.tag_name').text(tag.name); tagElement.find('.tag_name').text(tag.name);
const removeButton = tagElement.find('.tag_remove'); const removeButton = tagElement.find('.tag_remove');
removable ? removeButton.show() : removeButton.hide(); removable ? removeButton.show() : removeButton.hide();
if (removable && removeAction) {
tagElement.attr('custom-remove-action', String(true));
removeButton.on('click', () => {
const result = removeAction(tag);
if (result !== false) tagElement.remove();
});
}
if (tag.class) { if (tag.class) {
tagElement.addClass(tag.class); tagElement.addClass(tag.class);
@ -867,19 +1014,20 @@ function appendTagToList(listElement, tag, { removable = false, selectable = fal
// We could have multiple ways of actions passed in. The manual arguments have precendence in front of a specified tag action // We could have multiple ways of actions passed in. The manual arguments have precendence in front of a specified tag action
const clickableAction = action ?? tag.action; const clickableAction = action ?? tag.action;
// If this is a tag for a general list and its either selectable or actionable, lets mark its current state // If this is a tag for a general list and its either a filter or actionable, lets mark its current state
if ((selectable || clickableAction) && isGeneralList) { if ((isFilter || clickableAction) && isGeneralList) {
toggleTagThreeState(tagElement, { stateOverride: tag.filter_state ?? DEFAULT_FILTER_STATE }); toggleTagThreeState(tagElement, { stateOverride: tag.filter_state ?? DEFAULT_FILTER_STATE });
} }
if (selectable) { if (isFilter) {
tagElement.on('click', () => onTagFilterClick.bind(tagElement)(listElement)); tagElement.on('click', () => onTagFilterClick.bind(tagElement)(listElement));
tagElement.addClass(INTERACTABLE_CONTROL_CLASS);
} }
if (clickableAction) { if (clickableAction) {
const filter = getFilterHelper($(listElement)); const filter = getFilterHelper($(listElement));
tagElement.on('click', (e) => clickableAction.bind(tagElement)(filter, e)); tagElement.on('click', (e) => clickableAction.bind(tagElement)(filter, e));
tagElement.addClass('clickable-action'); tagElement.addClass('clickable-action').addClass(INTERACTABLE_CONTROL_CLASS);
} }
$(listElement).append(tagElement); $(listElement).append(tagElement);
@ -888,6 +1036,7 @@ function appendTagToList(listElement, tag, { removable = false, selectable = fal
function onTagFilterClick(listElement) { function onTagFilterClick(listElement) {
const tagId = $(this).attr('id'); const tagId = $(this).attr('id');
const existingTag = tags.find((tag) => tag.id === tagId); const existingTag = tags.find((tag) => tag.id === tagId);
const parent = $(this).parents('.tags');
let state = toggleTagThreeState($(this)); let state = toggleTagThreeState($(this));
@ -898,6 +1047,9 @@ function onTagFilterClick(listElement) {
// We don't print anything manually, updating the filter will automatically trigger a redraw of all relevant stuff // We don't print anything manually, updating the filter will automatically trigger a redraw of all relevant stuff
runTagFilters(listElement); runTagFilters(listElement);
// Focus the tag again we were at, if possible. To improve keyboard navigation
setTimeout(() => parent.find(`.tag[id="${tagId}"]`).trigger('focus'), DEFAULT_PRINT_TIMEOUT + 1);
} }
/** /**
@ -975,7 +1127,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.filter(x => characterTagIds.includes(x.id)).sort(compareTagsForSort); const tagsToDisplay = tags.filter(x => characterTagIds.includes(x.id)).sort(compareTagsForSort);
printTagList($(FILTER_SELECTOR), { empty: false, tags: tagsToDisplay, tagOptions: { selectable: true, isGeneralList: true } }); printTagList($(FILTER_SELECTOR), { empty: false, tags: tagsToDisplay, tagOptions: { isFilter: true, isGeneralList: true } });
// Print bogus folder navigation // Print bogus folder navigation
const bogusDrilldown = $(FILTER_SELECTOR).siblings('.rm_tag_bogus_drilldown'); const bogusDrilldown = $(FILTER_SELECTOR).siblings('.rm_tag_bogus_drilldown');
@ -1010,6 +1162,12 @@ function onTagRemoveClick(event) {
const tagElement = $(this).closest('.tag'); const tagElement = $(this).closest('.tag');
const tagId = tagElement.attr('id'); const tagId = tagElement.attr('id');
// If we have a custom remove action, we are not executing anything here in the default handler
if (tagElement.attr('custom-remove-action')) {
console.debug('Custom remove action', tagId);
return;
}
// Check if we are inside the drilldown. If so, we call remove on the bogus folder // Check if we are inside the drilldown. If so, we call remove on the bogus folder
if ($(this).closest('.rm_tag_bogus_drilldown').length > 0) { if ($(this).closest('.rm_tag_bogus_drilldown').length > 0) {
console.debug('Bogus drilldown remove', tagId); console.debug('Bogus drilldown remove', tagId);
@ -1029,7 +1187,7 @@ function onTagRemoveClick(event) {
// @ts-ignore // @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 (getTag(String(val))) return;
// @ts-ignore // @ts-ignore
$(this).autocomplete('search', val); $(this).autocomplete('search', val);
} }
@ -1047,7 +1205,7 @@ function onGroupCreateClick() {
// Nothing to do here at the moment. Tags in group interface get automatically redrawn. // Nothing to do here at the moment. Tags in group interface get automatically redrawn.
} }
export function applyTagsOnCharacterSelect() { export function applyTagsOnCharacterSelect(chid = null) {
// If we are in create window, we cannot simply redraw, as there are no real persisted tags. Grab them, and pass them in // If we are in create window, we cannot simply redraw, as there are no real persisted tags. Grab them, and pass them in
if (menu_type === 'create') { if (menu_type === 'create') {
const currentTagIds = $('#tagList').find('.tag').map((_, el) => $(el).attr('id')).get(); const currentTagIds = $('#tagList').find('.tag').map((_, el) => $(el).attr('id')).get();
@ -1056,11 +1214,11 @@ export function applyTagsOnCharacterSelect() {
return; return;
} }
const chid = this_chid ? Number(this_chid) : null; chid = chid ?? Number(this_chid);
printTagList($('#tagList'), { forEntityOrKey: chid, tagOptions: { removable: true } }); printTagList($('#tagList'), { forEntityOrKey: chid, tagOptions: { removable: true } });
} }
export function applyTagsOnGroupSelect() { export function applyTagsOnGroupSelect(groupId = null) {
// If we are in create window, we explicitly have to tell the system to print for the new group, not the one selected in the background // If we are in create window, we explicitly have to tell the system to print for the new group, not the one selected in the background
if (menu_type === 'group_create') { if (menu_type === 'group_create') {
const currentTagIds = $('#groupTagList').find('.tag').map((_, el) => $(el).attr('id')).get(); const currentTagIds = $('#groupTagList').find('.tag').map((_, el) => $(el).attr('id')).get();
@ -1069,7 +1227,7 @@ export function applyTagsOnGroupSelect() {
return; return;
} }
const groupId = selected_group; groupId = groupId ?? Number(selected_group);
printTagList($('#groupTagList'), { forEntityOrKey: groupId, tagOptions: { removable: true } }); printTagList($('#groupTagList'), { forEntityOrKey: groupId, tagOptions: { removable: true } });
} }
@ -1091,9 +1249,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(`
@ -1134,13 +1290,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);
callPopup(html, 'text', null, { allowVerticalScrolling: true }); printViewTagList(tagContainer);
printViewTagList();
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));
} }
/** /**
@ -1202,7 +1355,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.');
} }
@ -1329,7 +1482,7 @@ async function onTagRestoreFileSelect(e) {
printCharactersDebounced(); printCharactersDebounced();
saveSettingsDebounced(); saveSettingsDebounced();
onViewTagsListClick(); await onViewTagsListClick();
} }
function onBackupRestoreClick() { function onBackupRestoreClick() {
@ -1351,14 +1504,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');
} }
function appendViewTagToList(list, tag, everything) { function appendViewTagToList(list, tag, everything) {
@ -1382,25 +1539,47 @@ 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('.tag_view_color_picker[data-value="color"]').append(primaryColorPicker)
template.find('.tagColorPicker2Holder').append(secondaryColorPicker); .append($('<div class="fas fa-link fa-xs link_icon right_menu_button" title="Link to theme color"></div>'));
template.find('.tag_view_color_picker[data-value="color2"]').append(secondaryColorPicker)
.append($('<div class="fas fa-link fa-xs link_icon right_menu_button" title="Link to theme color"></div>'));
template.find('.tag_as_folder').attr('id', tagAsFolderId); template.find('.tag_as_folder').attr('id', tagAsFolderId);
primaryColorPicker.on('change', (evt) => onTagColorize(evt, (tag, color) => tag.color = color, 'background-color'));
secondaryColorPicker.on('change', (evt) => onTagColorize(evt, (tag, color) => tag.color2 = color, 'color'));
template.find('.tag_view_color_picker .link_icon').on('click', (evt) => {
const colorPicker = $(evt.target).closest('.tag_view_color_picker').find('toolcool-color-picker');
const defaultColor = colorPicker.attr('data-default-color');
// @ts-ignore
colorPicker[0].color = defaultColor;
});
list.append(template); list.append(template);
updateDrawTagFolder(template, tag); // We prevent the popup from auto-close on Escape press on the color pickups. If the user really wants to, he can hit it again
// Not the "cleanest" way, that would be actually using and observer, remembering whether the popup was open just before, but eh
// Not gonna invest too much time into this small control here
let lastHit = 0;
template.on('keydown', (evt) => {
if (evt.key === 'Escape') {
if (evt.target === primaryColorPicker[0] || evt.target === secondaryColorPicker[0]) {
if (Date.now() - lastHit < 5000) // If user hits it twice in five seconds
return;
lastHit = Date.now();
evt.stopPropagation();
evt.preventDefault();
}
}
});
// @ts-ignore updateDrawTagFolder(template, tag);
$(colorPickerId).color = tag.color;
// @ts-ignore
$(colorPicker2Id).color = tag.color2;
} }
function onTagAsFolderClick() { function onTagAsFolderClick() {
@ -1438,20 +1617,56 @@ function updateDrawTagFolder(element, tag) {
indicator.css('font-size', `calc(var(--mainFontSize) * ${tagFolder.size})`); indicator.css('font-size', `calc(var(--mainFontSize) * ${tagFolder.size})`);
} }
function onTagDeleteClick() { async function onTagDeleteClick() {
if (!confirm('Are you sure?')) { const id = $(this).closest('.tag_view_item').attr('id');
const tag = tags.find(x => x.id === id);
const otherTags = sortTags(tags.filter(x => x.id !== id).map(x => ({ id: x.id, name: x.name })));
const popupContent = $(`
<h3>Delete Tag</h3>
<div>Do you want to delete the tag <div id="tag_to_delete" class="tags_inline inline-flex margin-r2"></div>?</div>
<div class="m-t-2 marginBot5">If you want to merge all references to this tag into another tag, select it below:</div>
<select id="merge_tag_select">
<option value="">--- None ---</option>
${otherTags.map(x => `<option value="${x.id}">${x.name}</option>`).join('')}
</select>`);
appendTagToList(popupContent.find('#tag_to_delete'), tag);
// Make the select control more fancy on not mobile
if (!isMobile()) {
// Delete the empty option in the dropdown, and make the select2 be empty by default
popupContent.find('#merge_tag_select option[value=""]').remove();
popupContent.find('#merge_tag_select').select2({
width: '50%',
placeholder: 'Select tag to merge into',
allowClear: true,
}).val(null).trigger('change');
}
const result = await callGenericPopup(popupContent, POPUP_TYPE.CONFIRM);
if (result !== POPUP_RESULT.AFFIRMATIVE) {
return; return;
} }
const id = $(this).closest('.tag_view_item').attr('id'); const mergeTagId = $('#merge_tag_select').val() ? String($('#merge_tag_select').val()) : null;
// Remove the tag from all entities that use it
// If we have a replacement tag, add that one instead
for (const key of Object.keys(tag_map)) { for (const key of Object.keys(tag_map)) {
if (tag_map[key].includes(id)) {
tag_map[key] = tag_map[key].filter(x => x !== id); tag_map[key] = tag_map[key].filter(x => x !== id);
if (mergeTagId) tag_map[key].push(mergeTagId);
} }
}
const index = tags.findIndex(x => x.id === id); const index = tags.findIndex(x => x.id === id);
tags.splice(index, 1); tags.splice(index, 1);
$(`.tag[id="${id}"]`).remove(); $(`.tag[id="${id}"]`).remove();
$(`.tag_view_item[id="${id}"]`).remove(); $(`.tag_view_item[id="${id}"]`).remove();
toastr.success(`'${tag.name}' deleted${mergeTagId ? ` and merged into '${tags.find(x => x.id === mergeTagId).name}'` : ''}`, 'Delete Tag');
printCharactersDebounced(); printCharactersDebounced();
saveSettingsDebounced(); saveSettingsDebounced();
} }
@ -1461,35 +1676,41 @@ 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) { /**
* Handles the colorization of a tag when the user interacts with the color picker
*
* @param {*} evt - The custom colorize event object
* @param {(tag: Tag, val: string) => void} setColor - A function that sets the color of the tag
* @param {string} cssProperty - The CSS property to apply the color to
*/
function onTagColorize(evt, setColor, cssProperty) {
console.debug(evt); console.debug(evt);
const isDefaultColor = $(evt.target).data('default-color') === evt.detail.rgba;
$(evt.target).closest('.tag_view_color_picker').find('.link_icon').toggle(!isDefaultColor);
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;
$(evt.target).parent().parent().find('.tag_view_name').css('background-color', newColor); if (isDefaultColor) newColor = '';
$(`.tag[id="${id}"]`).css('background-color', newColor);
$(`.bogus_folder_select[tagid="${id}"] .avatar`).css('background-color', newColor); $(evt.target).closest('.tag_view_item').find('.tag_view_name').css(cssProperty, newColor);
const tag = tags.find(x => x.id === id); const tag = tags.find(x => x.id === id);
tag.color = newColor; setColor(tag, newColor);
console.debug(tag); console.debug(tag);
saveSettingsDebounced(); saveSettingsDebounced();
// Debounce redrawing color of the tag in other elements
debouncedTagColoring(tag.id, cssProperty, newColor);
} }
function onTagColorize2(evt) { const debouncedTagColoring = debounce((tagId, cssProperty, newColor) => {
console.debug(evt); $(`.tag[id="${tagId}"]`).css(cssProperty, newColor);
const id = $(evt.target).closest('.tag_view_item').attr('id'); $(`.bogus_folder_select[tagid="${tagId}"] .avatar`).css(cssProperty, newColor);
const newColor = evt.detail.rgba; }, debounce_timeout.quick);
$(evt.target).parent().parent().find('.tag_view_name').css('color', newColor);
$(`.tag[id="${id}"]`).css('color', newColor);
$(`.bogus_folder_select[tagid="${id}"] .avatar`).css('color', newColor);
const tag = tags.find(x => x.id === id);
tag.color2 = newColor;
console.debug(tag);
saveSettingsDebounced();
}
function onTagListHintClick() { function onTagListHintClick() {
$(this).toggleClass('selected'); $(this).toggleClass('selected');
@ -1529,9 +1750,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(empty = true) { function printViewTagList(tagContainer, empty = true) {
const tagContainer = $('#dialogue_popup .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);
@ -1569,7 +1788,7 @@ function registerTagsSlashCommands() {
toastr.warning('Tag name must be provided.'); toastr.warning('Tag name must be provided.');
return null; return null;
} }
let tag = tags.find(t => t.name === tagName); let tag = getTag(tagName);
if (allowCreate && !tag) { if (allowCreate && !tag) {
tag = createNewTag(tagName); tag = createNewTag(tagName);
} }
@ -1580,21 +1799,6 @@ function registerTagsSlashCommands() {
return tag; return tag;
} }
function updateTagsList() {
switch (menu_type) {
case 'characters':
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
break;
case 'character_edit':
applyTagsOnCharacterSelect();
break;
case 'group_edit':
select_group_chats(selected_group, true);
break;
}
}
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'tag-add', name: 'tag-add',
returns: 'true/false - Whether the tag was added or was assigned already', returns: 'true/false - Whether the tag was added or was assigned already',
@ -1604,8 +1808,7 @@ function registerTagsSlashCommands() {
if (!key) return 'false'; if (!key) return 'false';
const tag = paraGetTag(tagName, { allowCreate: true }); const tag = paraGetTag(tagName, { allowCreate: true });
if (!tag) return 'false'; if (!tag) return 'false';
const result = addTagToEntity(tag, key); const result = addTagsToEntity(tag, key);
updateTagsList();
return String(result); return String(result);
}, },
namedArgumentList: [ namedArgumentList: [
@ -1658,7 +1861,6 @@ function registerTagsSlashCommands() {
const tag = paraGetTag(tagName); const tag = paraGetTag(tagName);
if (!tag) return 'false'; if (!tag) return 'false';
const result = removeTagFromEntity(tag, key); const result = removeTagFromEntity(tag, key);
updateTagsList();
return String(result); return String(result);
}, },
namedArgumentList: [ namedArgumentList: [
@ -1781,22 +1983,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}"]`));
} }
}); });

View File

@ -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.
@ -1518,6 +1532,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.
* *
@ -1774,3 +1817,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)}`;
}

View File

@ -1,11 +1,14 @@
@charset "UTF-8"; @charset "UTF-8";
@import url(css/animations.css);
@import url(css/popup.css);
@import url(css/promptmanager.css); @import url(css/promptmanager.css);
@import url(css/loader.css); @import url(css/loader.css);
@import url(css/character-group-overlay.css); @import url(css/character-group-overlay.css);
@import url(css/file-form.css); @import url(css/file-form.css);
@import url(css/logprobs.css); @import url(css/logprobs.css);
@import url(css/accounts.css); @import url(css/accounts.css);
@import url(css/tags.css);
:root { :root {
--doc-height: 100%; --doc-height: 100%;
@ -48,6 +51,9 @@
--active: rgb(88, 182, 0); --active: rgb(88, 182, 0);
--preferred: rgb(244, 67, 54); --preferred: rgb(244, 67, 54);
--interactable-outline-color: var(--white100);
--interactable-outline-color-faint: var(--white50a);
/*Default Theme, will be changed by ToolCool Color Picker*/ /*Default Theme, will be changed by ToolCool Color Picker*/
--SmartThemeBodyColor: rgb(220, 220, 210); --SmartThemeBodyColor: rgb(220, 220, 210);
@ -106,6 +112,9 @@
--avatar-base-border-radius: 2px; --avatar-base-border-radius: 2px;
--avatar-base-border-radius-round: 50%; --avatar-base-border-radius-round: 50%;
--inline-avatar-small-factor: 0.6; --inline-avatar-small-factor: 0.6;
--animation-duration: 125ms;
--animation-duration-slow: calc(var(--animation-duration) * 3);
} }
* { * {
@ -205,25 +214,43 @@ table.responsiveTable {
box-shadow: inset 0 0 5px var(--SmartThemeQuoteColor); box-shadow: inset 0 0 5px var(--SmartThemeQuoteColor);
} }
@keyframes flash {
20%,
60%,
100% {
opacity: 1;
}
0%,
40%,
80% {
opacity: 0.2;
}
}
.flash { .flash {
animation-name: flash; animation-name: flash;
} }
/* Keyboard/focus navigation styling */
/* Mimic the outline of keyboard navigation for most most focusable controls */
.interactable {
border-radius: 5px;
}
/* Outline for "normal" buttons should only be when actual keyboard navigation is used */
.interactable:focus-visible {
outline: 1px solid var(--interactable-outline-color);
}
/* The specific input controls can always have a faint outline, even on mouse interaction, to give a better hint */
select:focus-visible,
input:focus-visible,
textarea:focus-visible {
outline: 1px solid var(--interactable-outline-color-faint);
}
input[type='checkbox']:focus-visible {
outline: 1px solid var(--interactable-outline-color);
}
/* General dragover styling */
.dragover {
filter: brightness(1.1) saturate(1.0);
outline: 3px dashed var(--SmartThemeBorderColor);
animation: pulse 0.5s infinite alternate;
}
.dragover.no_animation {
animation: none;
}
.tokenItemizingSubclass { .tokenItemizingSubclass {
font-size: calc(var(--mainFontSize) * 0.8); font-size: calc(var(--mainFontSize) * 0.8);
color: var(--SmartThemeEmColor); color: var(--SmartThemeEmColor);
@ -246,6 +273,11 @@ table.responsiveTable {
text-align: center; text-align: center;
} }
.fa-lock.right_menu_button,
.fa-unlock.right_menu_button {
padding: 2px 4px;
}
.text_muted { .text_muted {
font-size: calc(var(--mainFontSize) - 0.2rem); font-size: calc(var(--mainFontSize) - 0.2rem);
color: var(--white50a); color: var(--white50a);
@ -670,7 +702,6 @@ body .panelControlBar {
width: var(--bottomFormBlockSize); width: var(--bottomFormBlockSize);
height: var(--bottomFormBlockSize); height: var(--bottomFormBlockSize);
margin: 0; margin: 0;
outline: none;
border: none; border: none;
cursor: pointer; cursor: pointer;
opacity: 0.7; opacity: 0.7;
@ -787,7 +818,6 @@ body .panelControlBar {
height: var(--bottomFormBlockSize); height: var(--bottomFormBlockSize);
font-size: var(--bottomFormIconSize); font-size: var(--bottomFormIconSize);
margin: 0; margin: 0;
outline: none;
border: none; border: none;
position: relative; position: relative;
opacity: 0.7; opacity: 0.7;
@ -809,7 +839,7 @@ body .panelControlBar {
#options, #options,
#extensionsMenu, #extensionsMenu,
.shadow_popup .popper-modal { .popup .popper-modal {
display: flex; display: flex;
z-index: 29999; z-index: 29999;
background-color: var(--SmartThemeBlurTintColor); background-color: var(--SmartThemeBlurTintColor);
@ -818,6 +848,12 @@ body .panelControlBar {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
flex-flow: column; flex-flow: column;
border-radius: 10px; border-radius: 10px;
padding: 2px;
}
#extensionsMenu,
.options-content {
padding: 2px;
} }
.options-content, .options-content,
@ -853,11 +889,6 @@ body .panelControlBar {
padding: 1px; padding: 1px;
} }
#extensionsMenuButton:hover {
opacity: 1;
filter: brightness(1.2);
}
.options-content a, .options-content a,
#extensionsMenu>div, #extensionsMenu>div,
.list-group>div, .list-group>div,
@ -1026,11 +1057,11 @@ body .panelControlBar {
justify-content: space-evenly; justify-content: space-evenly;
} }
.avatar.selectable { .avatar.interactable {
opacity: 0.6; opacity: 0.6;
} }
.avatar.selectable:hover { .avatar.interactable:hover {
opacity: 1; opacity: 1;
background-color: transparent !important; background-color: transparent !important;
cursor: pointer; cursor: pointer;
@ -1186,16 +1217,14 @@ select {
transition: clip-path 200ms; transition: clip-path 200ms;
} }
@keyframes script_progress_pulse { #send_textarea:focus-visible {
/* Disable outline for the chat bar itself, we add it to the outer div */
outline: none;
}
0%, #send_form:has(#send_textarea:focus-visible) {
100% { border-color: var(--interactable-outline-color-faint);
border-top-color: var(--progColor); outline: 1px solid var(--interactable-outline-color-faint);
}
50% {
border-top-color: var(--progFlashColor);
}
} }
#form_sheld.isExecutingCommandsFromChatInput.script_paused #send_textarea { #form_sheld.isExecutingCommandsFromChatInput.script_paused #send_textarea {
@ -2028,9 +2057,13 @@ body[data-stscript-style] .hljs.language-stscript {
} }
} }
.editor_maximize {
padding: 2px;
}
#character_popup .editor_maximize { #character_popup .editor_maximize {
cursor: pointer; cursor: pointer;
margin: 5px; margin: 3px;
opacity: 0.75; opacity: 0.75;
filter: grayscale(1); filter: grayscale(1);
-webkit-transition: all 250ms ease-in-out; -webkit-transition: all 250ms ease-in-out;
@ -2147,12 +2180,6 @@ h3 {
padding: 5px 0; padding: 5px 0;
} }
input:focus,
textarea:focus,
select:focus {
outline: none;
}
input[type="file"] { input[type="file"] {
display: none; display: none;
} }
@ -2173,12 +2200,6 @@ input[type="file"] {
padding: 0px 10px 0px 5px; padding: 0px 10px 0px 5px;
} }
#right-nav-panel-tabs .right_menu_button,
#CharListButtonAndHotSwaps .right_menu_button {
padding-right: 0;
color: unset;
}
#chartokenwarning.menu_button { #chartokenwarning.menu_button {
font-size: unset; font-size: unset;
height: fit-content; height: fit-content;
@ -2194,13 +2215,14 @@ input[type="file"] {
cursor: pointer; cursor: pointer;
text-align: center; text-align: center;
margin-top: 0; margin-top: 0;
padding: 1px;
filter: grayscale(1) brightness(75%); filter: grayscale(1) brightness(75%);
-webkit-transition: all 0.5s ease-in-out; -webkit-transition: all 0.5s ease-in-out;
transition: all 0.5s ease-in-out; transition: all 0.5s ease-in-out;
} }
.right_menu_button:hover { .right_menu_button:hover {
filter: brightness(150%) grayscale(1); filter: brightness(150%);
} }
#rm_button_characters, #rm_button_characters,
@ -2209,6 +2231,7 @@ input[type="file"] {
#WI_button_panel_pin_div { #WI_button_panel_pin_div {
font-size: 24px; font-size: 24px;
display: inline; display: inline;
padding: 1px;
} }
#rm_button_panel_pin_div, #rm_button_panel_pin_div,
@ -2219,8 +2242,11 @@ input[type="file"] {
} }
#rm_button_panel_pin_div:hover, #rm_button_panel_pin_div:hover,
#rm_button_panel_pin_div:has(:focus-visible),
#lm_button_panel_pin_div:hover, #lm_button_panel_pin_div:hover,
#WI_button_panel_pin_div:hover { #lm_button_panel_pin_div:has(:focus-visible),
#WI_button_panel_pin_div:hover,
#WI_button_panel_pin_div:has(:focus-visible) {
opacity: 1; opacity: 1;
} }
@ -2268,6 +2294,7 @@ input[type="file"] {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
opacity: 0.5; opacity: 0.5;
padding: 1px;
} }
#rm_button_selected_ch:hover { #rm_button_selected_ch:hover {
@ -2295,6 +2322,7 @@ input[type="file"] {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
padding: 1px;
} }
.bulk_select_checkbox { .bulk_select_checkbox {
@ -2332,12 +2360,13 @@ input[type="file"] {
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
height: 100%; height: 100%;
padding: 1px;
} }
#rm_ch_create_block { #rm_ch_create_block {
display: none; display: none;
overflow-y: auto; overflow-y: auto;
padding: 5px; padding: 4px;
height: 100%; height: 100%;
} }
@ -2747,19 +2776,23 @@ input[type=search]:focus::-webkit-search-cancel-button {
outline: 2px solid var(--golden); outline: 2px solid var(--golden);
} }
.bg_example:hover.locked .bg_example_lock { .bg_example:hover.locked .bg_example_lock,
.bg_example:focus-within.locked .bg_example_lock {
display: none; display: none;
} }
.bg_example:hover:not(.locked) .bg_example_unlock { .bg_example:hover:not(.locked) .bg_example_unlock,
.bg_example:focus-within:not(.locked) .bg_example_unlock {
display: none; display: none;
} }
.bg_example:hover[custom="true"] .bg_example_edit { .bg_example:hover[custom="true"] .bg_example_edit,
.bg_example:focus-within[custom="true"] .bg_example_edit {
display: none; display: none;
} }
.bg_example:hover[custom="false"] .bg_example_copy { .bg_example:hover[custom="false"] .bg_example_copy,
.bg_example:focus-within[custom="false"] .bg_example_copy {
display: none; display: none;
} }
@ -2784,24 +2817,23 @@ input[type=search]:focus::-webkit-search-cancel-button {
} }
.bg_button { .bg_button {
width: 15px; padding: 4px;
height: 15px;
position: absolute; position: absolute;
top: 5px; top: 5px;
cursor: pointer; cursor: pointer;
opacity: 0.8; opacity: 0.8;
border-radius: 50%; border-radius: 3px;
font-size: 20px; font-size: 20px;
color: var(--black70a); color: var(--black70a);
text-shadow: none; text-shadow: none;
padding: 0;
margin: 0; margin: 0;
filter: drop-shadow(0px 0px 3px white); filter: drop-shadow(0px 0px 3px white);
transition: opacity 0.2s ease-in-out; transition: opacity 0.2s ease-in-out;
display: none; display: none;
} }
.bg_example:hover .bg_button { .bg_example:hover .bg_button,
.bg_example:focus-within .bg_button {
display: block; display: block;
} }
@ -2810,15 +2842,15 @@ input[type=search]:focus::-webkit-search-cancel-button {
} }
.bg_example_cross { .bg_example_cross {
right: 10px; right: 6px;
} }
.bg_example_edit { .bg_example_edit {
left: 10px; left: 6px;
} }
.bg_example_copy { .bg_example_copy {
left: 10px; left: 6px;
} }
.bg_example_lock, .bg_example_lock,
@ -2847,6 +2879,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
padding: 1px;
} }
.avatar_div { .avatar_div {
@ -2936,6 +2969,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
flex-direction: row; flex-direction: row;
align-items: flex-start; align-items: flex-start;
gap: 5px; gap: 5px;
margin: 1px;
padding: 5px; padding: 5px;
border-radius: 10px; border-radius: 10px;
cursor: pointer; cursor: pointer;
@ -3035,7 +3069,8 @@ grammarly-extension {
font-size: calc(var(--mainFontSize) * 0.9); font-size: calc(var(--mainFontSize) * 0.9);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 6px;
padding: 1px 3px;
} }
#result_info_text { #result_info_text {
@ -3045,15 +3080,14 @@ grammarly-extension {
text-align: right; text-align: right;
} }
.rm_stats_button { #result_info .right_menu_button {
cursor: pointer; padding: 4px;
} }
/* Focus */ /* Focus */
#bulk_tag_popup, #bulk_tag_popup,
#dialogue_popup, #dialogue_popup {
.dialogue_popup {
width: 500px; width: 500px;
max-width: 90vw; max-width: 90vw;
max-width: 90svw; max-width: 90svw;
@ -3101,17 +3135,16 @@ grammarly-extension {
min-width: 750px; min-width: 750px;
} }
.horizontal_scrolling_dialogue_popup { #dialogue_popup .horizontal_scrolling_dialogue_popup {
overflow-x: unset !important; overflow-x: unset !important;
} }
.vertical_scrolling_dialogue_popup { #dialogue_popup .vertical_scrolling_dialogue_popup {
overflow-y: unset !important; overflow-y: unset !important;
} }
#bulk_tag_popup_holder, #bulk_tag_popup_holder,
#dialogue_popup_holder, #dialogue_popup_holder {
.dialogue_popup_holder {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
@ -3119,15 +3152,14 @@ grammarly-extension {
padding: 0 10px; padding: 0 10px;
} }
#dialogue_popup_text, #dialogue_popup_text {
.dialogue_popup_text {
flex-grow: 1; flex-grow: 1;
overflow-y: auto; overflow-y: auto;
height: 100%; height: 100%;
} }
#dialogue_popup_controls, #dialogue_popup_controls {
.dialogue_popup_controls { margin-top: 10px;
display: flex; display: flex;
align-self: center; align-self: center;
gap: 20px; gap: 20px;
@ -3135,16 +3167,14 @@ grammarly-extension {
#bulk_tag_popup_reset, #bulk_tag_popup_reset,
#bulk_tag_popup_remove_mutual, #bulk_tag_popup_remove_mutual,
#dialogue_popup_ok, #dialogue_popup_ok {
.dialogue_popup_ok {
background-color: var(--crimson70a); background-color: var(--crimson70a);
cursor: pointer; cursor: pointer;
} }
#bulk_tag_popup_reset:hover, #bulk_tag_popup_reset:hover,
#bulk_tag_popup_remove_mutual:hover, #bulk_tag_popup_remove_mutual:hover,
#dialogue_popup_ok:hover, #dialogue_popup_ok:hover {
.dialogue_popup_ok:hover {
background-color: var(--crimson-hover); background-color: var(--crimson-hover);
} }
@ -3152,15 +3182,13 @@ grammarly-extension {
max-height: 70vh; max-height: 70vh;
} }
#dialogue_popup_input, #dialogue_popup_input {
.dialogue_popup_input { margin: 10px 0 0 0;
margin: 10px 0;
width: 100%; width: 100%;
} }
#bulk_tag_popup_cancel, #bulk_tag_popup_cancel,
#dialogue_popup_cancel, #dialogue_popup_cancel {
.dialogue_popup_cancel {
cursor: pointer; cursor: pointer;
} }
@ -3215,13 +3243,11 @@ grammarly-extension {
} }
#dialogue_del_mes .menu_button { #dialogue_del_mes .menu_button {
margin-left: 25px; margin-left: 25px;
margin-right: 25px; margin-right: 25px;
} }
#shadow_popup, #shadow_popup {
.shadow_popup {
backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2)); backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
-webkit-backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2)); -webkit-backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
background-color: var(--black30a); background-color: var(--black30a);
@ -3233,15 +3259,6 @@ grammarly-extension {
height: 100svh; height: 100svh;
z-index: 9999; z-index: 9999;
top: 0; top: 0;
&.shadow_popup {
z-index: 9998;
}
}
.dialogue_popup.dragover {
filter: brightness(1.1) saturate(1.1);
outline: 3px dashed var(--SmartThemeBorderColor);
} }
#bgtest { #bgtest {
@ -3386,7 +3403,7 @@ body #toast-container>div {
display: block; display: block;
} }
input[type='checkbox']:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin):not(#WI_panel_pin) { input[type='checkbox'] {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
appearance: none; appearance: none;
@ -3404,13 +3421,10 @@ input[type='checkbox']:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button
flex-shrink: 0; flex-shrink: 0;
place-content: center; place-content: center;
filter: brightness(1.2); filter: brightness(1.2);
}
input[type='checkbox']:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin):not(#WI_panel_pin):not(.del_checkbox) {
display: grid; display: grid;
} }
input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin):not(#WI_panel_pin)::before { input[type="checkbox"]::before {
content: ""; content: "";
width: 0.65em; width: 0.65em;
height: 0.65em; height: 0.65em;
@ -3421,16 +3435,16 @@ input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
} }
input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin):not(#WI_panel_pin):checked::before { input[type="checkbox"]:checked::before {
transform: scale(1); transform: scale(1);
} }
input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin):not(#WI_panel_pin):disabled { input[type="checkbox"]:disabled {
color: grey; color: grey;
cursor: not-allowed; cursor: not-allowed;
} }
.del_checkbox { input[type='checkbox'].del_checkbox {
display: none; display: none;
opacity: 0.7; opacity: 0.7;
margin-top: 12px; margin-top: 12px;
@ -3442,6 +3456,7 @@ input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-around; justify-content: space-around;
padding: 1px;
} }
.avatar-container .avatar { .avatar-container .avatar {
@ -3559,6 +3574,16 @@ input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button
z-index: 1; z-index: 1;
} }
.neo-range-slider:hover,
input[type="range"]:hover {
filter: brightness(1.25);
}
.neo-range-slider:focus-visible,
input[type="range"]:focus-visible {
outline: 1px solid var(--interactable-outline-color);
}
.range-block-range { .range-block-range {
margin: 0; margin: 0;
flex: 5; flex: 5;
@ -3758,35 +3783,29 @@ input[type="range"]::-webkit-slider-thumb {
/* height: 20px; */ /* height: 20px; */
position: relative; position: relative;
display: flex; display: flex;
gap: 10px; gap: 4px;
flex-wrap: nowrap; flex-wrap: nowrap;
justify-content: flex-end; justify-content: flex-end;
transition: all 200ms; transition: all 200ms;
overflow-x: hidden; overflow-x: hidden;
padding: 1px;
} }
.extraMesButtons { .extraMesButtons {
display: none; display: none;
} }
.mes_buttons .mes_edit, .mes_button,
.mes_buttons .mes_bookmark, .extraMesButtons>div {
.mes_buttons .mes_create_bookmark,
.extraMesButtonsHint,
.tagListHint,
.extraMesButtons div {
cursor: pointer; cursor: pointer;
transition: 0.3s ease-in-out; transition: 0.3s ease-in-out;
filter: drop-shadow(0px 0px 2px black); filter: drop-shadow(0px 0px 2px black);
opacity: 0.3; opacity: 0.3;
padding: 1px 3px;
} }
.mes_buttons .mes_edit:hover, .mes_button:hover,
.mes_buttons .mes_bookmark:hover, .extraMesButtons>div:hover {
.mes_buttons .mes_create_bookmark:hover,
.extraMesButtonsHint:hover,
.tagListHint:hover,
.extraMesButtons div:hover {
opacity: 1; opacity: 1;
} }
@ -4093,17 +4112,6 @@ h5 {
animation: infinite-spinning 1s ease-out 0s infinite normal; animation: infinite-spinning 1s ease-out 0s infinite normal;
} }
/* HEINOUS */
@keyframes infinite-spinning {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
#export_character_div { #export_character_div {
display: grid; display: grid;
grid-template-columns: 340px auto; grid-template-columns: 340px auto;
@ -4257,24 +4265,6 @@ body.big-avatars .missing-avatar {
margin: 5px 0; margin: 5px 0;
} }
@keyframes ellipsis {
0% {
content: ""
}
25% {
content: "."
}
50% {
content: ".."
}
75% {
content: "..."
}
}
.warning { .warning {
color: var(--warning); color: var(--warning);
font-weight: bolder; font-weight: bolder;
@ -4588,6 +4578,7 @@ body:has(#character_popup.open) #top-settings-holder:has(.drawer-content.openDra
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
font-size: var(--topBarIconSize); font-size: var(--topBarIconSize);
padding: 1px 3px;
} }
.drawer-icon.openIcon { .drawer-icon.openIcon {
@ -5177,5 +5168,3 @@ body:not(.movingUI) .drawer-content.maximized {
.regex-highlight { .regex-highlight {
color: #FAF8F6; color: #FAF8F6;
} }
/* Pastel White */