Markdown hotkeys for textareas (#2800) * initial commit * dont hijack all ctrl keybinds * change strikethrough bind, convert to subscribable class, target key textareas * better early return, hotkey reversiblility * possible undo solution, key checks to switch * execCommand alternate, perfect Undo * format full word when caret is in the middle of a word * double backticks do nothing, dummy. * ctrl+K for ....'K'ode snippet... * remove console logs * Add new hotkeys to help * Allow hotkeys in message edit textarea * add markdown hotkey help text * help text addition to mention hotkeys work in message edits * add markdown hotkeys to WI entry content box * disengage if alt/win pressed, universal prevent default * disengage if shiftKey pressed * re-allow shift for one special case * add MD hotkeys toggle in user settings * add markdown enabled icon on relevant textareas when appropriate * Add icon to help * Uniform formatting * Add opacity to icon --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

b376ea884b56f310f6cb6382f8aa7883fc81801e

RossAscends <124905043+RossAscends@users.noreply.github.com>

Signed
6 files changed, +229 -26Ignore whitespace
public/css/toggle-dependent.css+4 -0
@@ -455,3 +455,7 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint {
455#smooth_streaming:checked~#smooth_streaming_speed_control {455#smooth_streaming:checked~#smooth_streaming_speed_control {
456 display: block;456 display: block;
457}457}
458
459.mdhotkey_icon {
460 opacity: 0.6;
461}
public/index.html+21 -14
@@ -4033,10 +4033,17 @@
4033 <input id="world_import_dialog" type="checkbox" />4033 <input id="world_import_dialog" type="checkbox" />
4034 <small data-i18n="Lorebook Import Dialog">Lorebook Import Dialog</small>4034 <small data-i18n="Lorebook Import Dialog">Lorebook Import Dialog</small>
4035 </label>4035 </label>
4036 <label class="checkbox_label" for="enable_auto_select_input" title="Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields." data-i18n="[title]Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields.">4036 <label class="checkbox_label" for=" enable_auto_select_input" title="Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields." data-i18n="[title]Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields.">
4037 <input id="enable_auto_select_input" type="checkbox" />4037 <input id="enable_auto_select_input" type="checkbox" />
4038 <small data-i18n="Auto-select Input Text">Auto-select Input Text</small>4038 <small data-i18n="Auto-select Input Text">Auto-select Input Text</small>
4039 </label>4039 </label>
4040 <label class="checkbox_label alignItemsCenter" for="enable_md_hotkeys" title="Enable hotkeys for inserting markdown format characters in certain text input boxes. See '/help hotkeys'.">
4041 <input id="enable_md_hotkeys" type="checkbox" />
4042 <small>
4043 <span data-i18n="Markdown Hotkeys">Markdown Hotkeys</span>
4044 <i class="fa-brands fa-markdown"></i>
4045 </small>
4046 </label>
4040 <label class="checkbox_label" for="restore_user_input" title="Restore unsaved user input on page refresh." data-i18n="[title]Restore unsaved user input on page refresh">4047 <label class="checkbox_label" for="restore_user_input" title="Restore unsaved user input on page refresh." data-i18n="[title]Restore unsaved user input on page refresh">
4041 <input id="restore_user_input" type="checkbox" />4048 <input id="restore_user_input" type="checkbox" />
4042 <small data-i18n="Restore User Input">Restore User Input</small>4049 <small data-i18n="Restore User Input">Restore User Input</small>
@@ -4720,7 +4727,7 @@
4720 <hr>4727 <hr>
4721 <div id="description_div" class="title_restorable">4728 <div id="description_div" class="title_restorable">
4722 <div class="flex-container alignitemscenter">4729 <div class="flex-container alignitemscenter">
4723 <span data-i18n="Character Description">Description</span>4730 <span data-i18n="Character Description" class="mdhotkey_location">Description</span>
4724 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="description_textarea" title="Expand the editor" data-i18n="[title]Expand the editor"></i>4731 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="description_textarea" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
4725 <a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#character-description" class="notes-link" target="_blank">4732 <a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#character-description" class="notes-link" target="_blank">
4726 <span class="fa-solid fa-circle-question note-link-span"></span>4733 <span class="fa-solid fa-circle-question note-link-span"></span>
@@ -4734,7 +4741,7 @@
4734 </span>4741 </span>
4735 </div>4742 </div>
4736 </div>4743 </div>
4737 <textarea id="description_textarea" data-i18n="[placeholder]Describe your character's physical and mental traits here." placeholder="Describe your character's physical and mental traits here." name="description" placeholder=""></textarea>4744 <textarea id="description_textarea" class="mdHotkeys" data-i18n="[placeholder]Describe your character's physical and mental traits here." placeholder="Describe your character's physical and mental traits here." name="description" placeholder=""></textarea>
4738 <div class="extension_token_counter">4745 <div class="extension_token_counter">
4739 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="description_textarea" data-token-permanent="true">counting...</span>4746 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="description_textarea" data-token-permanent="true">counting...</span>
4740 </div>4747 </div>
@@ -4742,7 +4749,7 @@
4742 <div id="firstMessageWrapper" class="flex-container flexFlowColumn flex1">4749 <div id="firstMessageWrapper" class="flex-container flexFlowColumn flex1">
4743 <div id="first_message_div" class="title_restorable">4750 <div id="first_message_div" class="title_restorable">
4744 <div class="flex-container alignitemscenter flex1">4751 <div class="flex-container alignitemscenter flex1">
4745 <span data-i18n="First message">First message</span>4752 <span data-i18n="First message" class="mdhotkey_location">First message</span>
4746 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="firstmessage_textarea" title="Expand the editor" data-i18n="[title]Expand the editor"></i>4753 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="firstmessage_textarea" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
4747 <a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#first-message" class="notes-link" target="_blank">4754 <a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#first-message" class="notes-link" target="_blank">
4748 <span class="fa-solid fa-circle-question note-link-span"></span>4755 <span class="fa-solid fa-circle-question note-link-span"></span>
@@ -4754,7 +4761,7 @@
4754 </span>4761 </span>
4755 </div>4762 </div>
4756 </div>4763 </div>
4757 <textarea id="firstmessage_textarea" data-i18n="[placeholder]This will be the first message from the character that starts every chat." placeholder="This will be the first message from the character that starts every chat." name="first_mes" placeholder=""></textarea>4764 <textarea class="mdHotkeys" id="firstmessage_textarea" data-i18n="[placeholder]This will be the first message from the character that starts every chat." placeholder="This will be the first message from the character that starts every chat." name="first_mes" placeholder=""></textarea>
4758 <div class="extension_token_counter">4765 <div class="extension_token_counter">
4759 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="firstmessage_textarea">counting...</span>4766 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="firstmessage_textarea">counting...</span>
4760 </div>4767 </div>
@@ -5112,7 +5119,7 @@
5112 </div>5119 </div>
5113 <hr>5120 <hr>
5114 <div id="personality_div">5121 <div id="personality_div">
5115 <h4>5122 <h4 class="flex-container alignItemsBaseline">
5116 <span data-i18n="Personality summary">Personality summary</span>5123 <span data-i18n="Personality summary">Personality summary</span>
5117 <a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#personality-summary" class="notes-link" target="_blank"><span class="fa-solid fa-circle-question note-link-span"></span></a>5124 <a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#personality-summary" class="notes-link" target="_blank"><span class="fa-solid fa-circle-question note-link-span"></span></a>
5118 </h4>5125 </h4>
@@ -5122,7 +5129,7 @@
5122 </div>5129 </div>
5123 </div>5130 </div>
5124 <div id="scenario_div">5131 <div id="scenario_div">
5125 <h4>5132 <h4 class="flex-container alignItemsBaseline">
5126 <span data-i18n="Scenario">Scenario</span>5133 <span data-i18n="Scenario">Scenario</span>
5127 <a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#scenario" class="notes-link" target="_blank">5134 <a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#scenario" class="notes-link" target="_blank">
5128 <span class="fa-solid fa-circle-question note-link-span"></span>5135 <span class="fa-solid fa-circle-question note-link-span"></span>
@@ -5179,10 +5186,10 @@
5179 <hr>5186 <hr>
5180 <div id="mes_example_div" class="flex-container flexFlowColumn">5187 <div id="mes_example_div" class="flex-container flexFlowColumn">
5181 <div>5188 <div>
5182 <h4><span data-i18n="Examples of dialogue">Examples of dialogue</span></h4>5189 <h4 class="flex-container gap5px alignItemsBaseline"><span data-i18n="Examples of dialogue" class="mdhotkey_location">Examples of dialogue</span></h4>
5183 <h5 data-i18n="Important to set the character's writing style.">Important to set the character's writing style. <a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#examples-of-dialogue" class="notes-link" target="_blank"><span class="fa-solid fa-circle-question note-link-span"></span></a></h5>5190 <h5 data-i18n="Important to set the character's writing style.">Important to set the character's writing style. <a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#examples-of-dialogue" class="notes-link" target="_blank"><span class="fa-solid fa-circle-question note-link-span"></span></a></h5>
5184 </div>5191 </div>
5185 <textarea id="mes_example_textarea" class="flexGrow" name="mes_example" data-i18n="[placeholder](Examples of chat dialog. Begin each example with START on a new line.)" placeholder="(Examples of chat dialog. Begin each example with &lt;START&gt; on a new line.)" form="form_create" rows="6"></textarea>5192 <textarea id="mes_example_textarea" class="flexGrow mdHotkeys" name="mes_example" data-i18n="[placeholder](Examples of chat dialog. Begin each example with START on a new line.)" placeholder="(Examples of chat dialog. Begin each example with &lt;START&gt; on a new line.)" form="form_create" rows="6"></textarea>
5186 <div class="extension_token_counter">5193 <div class="extension_token_counter">
5187 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="mes_example_textarea">counting...</span>5194 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="mes_example_textarea">counting...</span>
5188 </div>5195 </div>
@@ -5477,7 +5484,7 @@
5477 <label for="content ">5484 <label for="content ">
5478 <small>5485 <small>
5479 <span class="alignitemscenter flex-container flexnowrap wide100p justifySpaceBetween">5486 <span class="alignitemscenter flex-container flexnowrap wide100p justifySpaceBetween">
5480 <span data-i18n="Content" class="alignitemscenter flex-container flexNoGap">5487 <span data-i18n="Content" class="alignitemscenter flex-container flexNoGap mdhotkey_location">
5481 Content5488 Content
5482 </span>5489 </span>
5483 <span>5490 <span>
@@ -5512,7 +5519,7 @@
5512 </span>5519 </span>
5513 </small>5520 </small>
5514 </label>5521 </label>
5515 <textarea class="text_pole" name="content" rows="8" data-i18n="[placeholder]What this keyword should mean to the AI, sent verbatim" placeholder="What this keyword should mean to the AI, sent verbatim"></textarea>5522 <textarea class="text_pole mdHotkeys" name="content" rows="8" data-i18n="[placeholder]What this keyword should mean to the AI, sent verbatim" placeholder="What this keyword should mean to the AI, sent verbatim"></textarea>
5516 </div>5523 </div>
5517 </div>5524 </div>
5518 <div class="world_entry_thin_controls commentContainer">5525 <div class="world_entry_thin_controls commentContainer">
@@ -5987,7 +5994,7 @@
5987 <div id="alternate_greetings_template" class="template_element">5994 <div id="alternate_greetings_template" class="template_element">
5988 <div class="alternate_grettings flexFlowColumn flex-container">5995 <div class="alternate_grettings flexFlowColumn flex-container">
5989 <div class="title_restorable">5996 <div class="title_restorable">
5990 <h3><span data-i18n="Alternate Greetings">Alternate Greetings</span></h3>5997 <h3><span data-i18n="Alternate Greetings" class="mdhotkey_location">Alternate Greetings</span></h3>
5991 <div title="Add" class="menu_button fa-solid fa-plus add_alternate_greeting" data-i18n="[title]Add"></div>5998 <div title="Add" class="menu_button fa-solid fa-plus add_alternate_greeting" data-i18n="[title]Add"></div>
5992 </div>5999 </div>
5993 <small class="justifyLeft" data-i18n="Alternate_Greetings_desc">6000 <small class="justifyLeft" data-i18n="Alternate_Greetings_desc">
@@ -6008,7 +6015,7 @@
6008 <strong><span data-i18n="Alternate Greeting #">Alternate Greeting #</span><span class="greeting_index"></span></strong>6015 <strong><span data-i18n="Alternate Greeting #">Alternate Greeting #</span><span class="greeting_index"></span></strong>
6009 <div class="menu_button fa-solid fa-trash-alt delete_alternate_greeting"></div>6016 <div class="menu_button fa-solid fa-trash-alt delete_alternate_greeting"></div>
6010 </div>6017 </div>
6011 <textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text" value="" autocomplete="off" rows="16"></textarea>6018 <textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text mdHotkeys" value="" autocomplete="off" rows="16"></textarea>
6012 </div>6019 </div>
6013 </div>6020 </div>
60146021
@@ -6392,7 +6399,7 @@
6392 <div id="leftSendForm" class="alignContentCenter">6399 <div id="leftSendForm" class="alignContentCenter">
6393 <div id="options_button" class="fa-solid fa-bars interactable"></div>6400 <div id="options_button" class="fa-solid fa-bars interactable"></div>
6394 </div>6401 </div>
6395 <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>6402 <textarea id="send_textarea" name="text" class="mdHotkeys" 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>
6396 <div id="rightSendForm" class="alignContentCenter">6403 <div id="rightSendForm" class="alignContentCenter">
6397 <div id="stscript_continue" title="Continue script execution" class="stscript_btn stscript_continue" data-i18n="[title]Continue script execution">6404 <div id="stscript_continue" title="Continue script execution" class="stscript_btn stscript_continue" data-i18n="[title]Continue script execution">
6398 <i class="fa-solid fa-play"></i>6405 <i class="fa-solid fa-play"></i>
public/script.js+3 -1
@@ -242,6 +242,7 @@ import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js'
242import { initDynamicStyles } from './scripts/dynamic-styles.js';242import { initDynamicStyles } from './scripts/dynamic-styles.js';
243import { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/SlashCommandEnumValue.js';243import { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/SlashCommandEnumValue.js';
244import { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js';244import { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js';
245import { initInputMarkdown } from './scripts/input-md-formatting.js';
245import { AbortReason } from './scripts/util/AbortReason.js';246import { AbortReason } from './scripts/util/AbortReason.js';
246247
247//exporting functions and vars for mods248//exporting functions and vars for mods
@@ -951,6 +952,7 @@ async function firstLoadInit() {
951 initStats();952 initStats();
952 initCfg();953 initCfg();
953 initLogprobs();954 initLogprobs();
955 initInputMarkdown();
954 doDailyExtensionUpdatesCheck();956 doDailyExtensionUpdatesCheck();
955 await hideLoader();957 await hideLoader();
956 await fixViewport();958 await fixViewport();
@@ -10200,7 +10202,7 @@ jQuery(async function () {
10200 .closest('.mes_block')10202 .closest('.mes_block')
10201 .find('.mes_text')10203 .find('.mes_text')
10202 .append(10204 .append(
10203 '<textarea id=\'curEditTextarea\' class=\'edit_textarea\' style=\'max-width:auto;\'></textarea>',10205 '<textarea id=\'curEditTextarea\' class=\'edit_textarea mdHotkeys\' style=\'max-width:auto;\'></textarea>',
10204 );10206 );
10205 $('#curEditTextarea').val(text);10207 $('#curEditTextarea').val(text);
10206 let edit_textarea = $(this)10208 let edit_textarea = $(this)
public/scripts/input-md-formatting.js+152 -0
@@ -0,0 +1,152 @@
1import { power_user } from './power-user.js';
2
3export function initInputMarkdown() {
4 $(document).on('keydown', 'textarea.mdHotkeys', function (e) {
5 if (!power_user.enable_md_hotkeys) { return; }
6
7 // Ensure that the element is a textarea
8 let textarea = this;
9 if (!(textarea instanceof HTMLTextAreaElement)) {
10 return;
11 }
12
13 // Early return on only control or no control, alt key, and win/cmd key
14 if (e.key === 'Control' || !e.ctrlKey || e.altKey || e.metaKey || (e.shiftKey && !(e.ctrlKey && e.shiftKey && e.code === 'Backquote'))) {
15 return;
16 }
17 let charsToAdd = '';
18 let possiblePreviousFormattingMargin = 1;
19
20 switch (true) {
21 case e.ctrlKey && e.shiftKey && e.code === 'Backquote':
22 e.preventDefault();
23 e.stopPropagation();
24 charsToAdd = '~~';
25 possiblePreviousFormattingMargin = 2;
26 break;
27 case e.ctrlKey && e.code === 'KeyB':
28 e.preventDefault();
29 e.stopPropagation();
30 charsToAdd = '**';
31 possiblePreviousFormattingMargin = 2;
32 break;
33 case e.ctrlKey && e.code === 'KeyI':
34 e.preventDefault();
35 e.stopPropagation();
36 charsToAdd = '*';
37 break;
38 case e.ctrlKey && e.code === 'KeyU':
39 e.preventDefault();
40 e.stopPropagation();
41 charsToAdd = '__';
42 possiblePreviousFormattingMargin = 2;
43 break;
44 case e.ctrlKey && e.code === 'KeyK':
45 e.preventDefault();
46 e.stopPropagation();
47 charsToAdd = '`';
48 break;
49 default:
50 return; // Early return if no key matches
51 }
52
53 let selectedText = '';
54 let start = textarea.selectionStart;
55 let end = textarea.selectionEnd;
56 let beforeCaret = textarea.value.substring(start - 1, start);
57 let afterCaret = textarea.value.substring(end, end + 1);
58 let isTextSelected = (start !== end);
59 let cursorShift = charsToAdd.length;
60 let selectedTextandPossibleFormatting = textarea.value.substring(start - possiblePreviousFormattingMargin, end + possiblePreviousFormattingMargin).trim();
61
62 if (isTextSelected) { //if text is selected
63 selectedText = textarea.value.substring(start, end);
64 if (selectedTextandPossibleFormatting === charsToAdd + selectedText + charsToAdd) {
65 // If the selected text is already formatted, remove the formatting
66
67 let expandedStart = start - charsToAdd.length;
68 let expandedEnd = end + charsToAdd.length;
69
70 // Ensure expanded range is within the bounds of the text
71 if (expandedStart < 0) expandedStart = 0;
72 if (expandedEnd > textarea.value.length) expandedEnd = textarea.value.length;
73
74 // Select the expanded range
75 textarea.setSelectionRange(expandedStart, expandedEnd);
76
77 // Replace the expanded selection with the original selected text
78 document.execCommand('insertText', false, selectedText);
79 // Adjust cursor position
80 cursorShift = -charsToAdd.length;
81 } else {
82 // Add formatting to the selected text
83 let possibleAddedSpace = '';
84 if (selectedText.endsWith(' ')) {
85 possibleAddedSpace = ' ';
86 selectedText = selectedText.substring(0, selectedText.length - 1);
87 end--; // Adjust the end index since we removed the space
88 }
89 // To add the formatting, we need to select the text first
90 textarea.focus();
91 document.execCommand('insertText', false, charsToAdd + selectedText + charsToAdd + possibleAddedSpace);
92 }
93 } else {// No text is selected
94 //check 1 character before and after the cursor for non-space characters
95
96 if (beforeCaret !== ' ' && afterCaret !== ' ' && afterCaret !== '' && beforeCaret !== '') { //look for caret in the middle of a word
97 //expand the selection range until the next space on both sides
98 let midCaretExpandedStart = start - 1;
99 let midCaretExpandedEnd = end + 1;
100 while (midCaretExpandedStart > 0 && textarea.value.substring(midCaretExpandedStart - 1, midCaretExpandedStart) !== ' ') {
101 midCaretExpandedStart--;
102 }
103 while (midCaretExpandedEnd < textarea.value.length && textarea.value.substring(midCaretExpandedEnd, midCaretExpandedEnd + 1) !== ' ') {
104 midCaretExpandedEnd++;
105 }
106 //make a selection of the discovered word
107 textarea.setSelectionRange(midCaretExpandedStart, midCaretExpandedEnd);
108 //set variables for comparison
109 let discoveredWordWithPossibleFormatting = textarea.value.substring(midCaretExpandedStart, midCaretExpandedEnd).trim();
110 let discoveredWord = '';
111
112 if (discoveredWordWithPossibleFormatting.endsWith(charsToAdd) && discoveredWordWithPossibleFormatting.startsWith(charsToAdd)) {
113 discoveredWord = textarea.value.substring(midCaretExpandedStart + charsToAdd.length, midCaretExpandedEnd - charsToAdd.length).trim();
114 } else {
115 discoveredWord = textarea.value.substring(midCaretExpandedStart, midCaretExpandedEnd).trim();
116 }
117
118 if (charsToAdd + discoveredWord + charsToAdd === discoveredWordWithPossibleFormatting) {
119
120 // Replace the expanded selection with the original discovered word
121 textarea.focus();
122 document.execCommand('insertText', false, discoveredWord);
123 // Adjust cursor position
124 cursorShift = -charsToAdd.length;
125 } else { //format did not previously exist, so add it
126 textarea.focus();
127 document.execCommand('insertText', false, charsToAdd + discoveredWord + charsToAdd);
128 }
129
130
131 } else { //caret is not inside a word, so just add the formatting
132 textarea.focus();
133 textarea.setSelectionRange(start, end);
134 selectedText = textarea.value.substring(start, end);
135 document.execCommand('insertText', false, charsToAdd + selectedText + charsToAdd);
136 }
137 }
138
139 // Manually trigger the 'input' event to make undo/redo work
140 let event = new Event('input', { bubbles: true });
141 textarea.dispatchEvent(event); // This notifies the browser of a change, allowing undo/redo to function.
142
143 // Update the cursor position
144 if (isTextSelected) {
145 textarea.selectionStart = start + cursorShift;
146 textarea.selectionEnd = start + cursorShift + selectedText.length;
147 } else {
148 textarea.selectionStart = start + cursorShift;
149 textarea.selectionEnd = start + cursorShift;
150 }
151 });
152}
public/scripts/power-user.js+20 -0
@@ -200,6 +200,7 @@ let power_user = {
200 relaxed_api_urls: false,200 relaxed_api_urls: false,
201 world_import_dialog: true,201 world_import_dialog: true,
202 enable_auto_select_input: false,202 enable_auto_select_input: false,
203 enable_md_hotkeys: false,
203 tag_import_setting: tag_import_setting.ASK,204 tag_import_setting: tag_import_setting.ASK,
204 disable_group_trimming: false,205 disable_group_trimming: false,
205 single_line: false,206 single_line: false,
@@ -1452,6 +1453,7 @@ async function loadPowerUserSettings(settings, data) {
1452 $('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls);1453 $('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls);
1453 $('#world_import_dialog').prop('checked', power_user.world_import_dialog);1454 $('#world_import_dialog').prop('checked', power_user.world_import_dialog);
1454 $('#enable_auto_select_input').prop('checked', power_user.enable_auto_select_input);1455 $('#enable_auto_select_input').prop('checked', power_user.enable_auto_select_input);
1456 $('#enable_md_hotkeys').prop('checked', power_user.enable_md_hotkeys);
1455 $('#trim_spaces').prop('checked', power_user.trim_spaces);1457 $('#trim_spaces').prop('checked', power_user.trim_spaces);
1456 $('#continue_on_send').prop('checked', power_user.continue_on_send);1458 $('#continue_on_send').prop('checked', power_user.continue_on_send);
1457 $('#quick_continue').prop('checked', power_user.quick_continue);1459 $('#quick_continue').prop('checked', power_user.quick_continue);
@@ -1601,6 +1603,17 @@ async function loadPowerUserSettings(settings, data) {
1601 switchSpoilerMode();1603 switchSpoilerMode();
1602 loadMovingUIState();1604 loadMovingUIState();
1603 loadCharListState();1605 loadCharListState();
1606 toggleMDHotkeyIconDisplay();
1607}
1608
1609function toggleMDHotkeyIconDisplay() {
1610 if (power_user.enable_md_hotkeys) {
1611 $('.mdhotkey_location').each(function () {
1612 $(this).parent().append('<i class="fa-brands fa-markdown mdhotkey_icon"></i>');
1613 });
1614 } else {
1615 $('.mdhotkey_icon').remove();
1616 }
1604}1617}
16051618
1606function loadCharListState() {1619function loadCharListState() {
@@ -3610,6 +3623,13 @@ $(document).ready(() => {
3610 saveSettingsDebounced();3623 saveSettingsDebounced();
3611 });3624 });
36123625
3626 $('#enable_md_hotkeys').on('input', function () {
3627 const value = !!$(this).prop('checked');
3628 power_user.enable_md_hotkeys = value;
3629 toggleMDHotkeyIconDisplay();
3630 saveSettingsDebounced();
3631 });
3632
3613 $('#spoiler_free_mode').on('input', function () {3633 $('#spoiler_free_mode').on('input', function () {
3614 power_user.spoiler_free_mode = !!$(this).prop('checked');3634 power_user.spoiler_free_mode = !!$(this).prop('checked');
3615 switchSpoilerMode();3635 switchSpoilerMode();
public/scripts/templates/hotkeys.html+29 -11
@@ -1,13 +1,31 @@
1<span data-i18n="help_hotkeys_0">Hotkeys/Keybinds</span>:1<div>
2 <strong data-i18n="help_hotkeys_0">Chat Hotkeys</strong>
3</div>
2<ul>4<ul>
3 <li><tt data-i18n="help_hotkeys_1">Up</tt> = <span data-i18n="help_hotkeys_2">Edit last message in chat</span></li>5 <li><kbd data-i18n="help_hotkeys_1">Up</kbd> = <span data-i18n="help_hotkeys_2">Edit last message in chat</span></li>
4 <li><tt data-i18n="help_hotkeys_3">Ctrl+Up</tt> = <span data-i18n="help_hotkeys_4">Edit last USER message in chat</span></li>6 <li><kbd data-i18n="help_hotkeys_3">Ctrl+Up</kbd> = <span data-i18n="help_hotkeys_4">Edit last USER message in chat</span></li>
5 <li><tt data-i18n="help_hotkeys_5">Left</tt> = <span data-i18n="help_hotkeys_6">swipe left</span></li>7 <li><kbd data-i18n="help_hotkeys_5">Left</kbd> = <span data-i18n="help_hotkeys_6">swipe left</span></li>
6 <li><tt data-i18n="help_hotkeys_7">Right</tt> = <span data-i18n="help_hotkeys_8">swipe right (NOTE: swipe hotkeys are disabled when chatbar has something typed into it)</span></li>8 <li><kbd data-i18n="help_hotkeys_7">Right</kbd> = <span data-i18n="help_hotkeys_8">swipe right (NOTE: swipe hotkeys are disabled when chatbar has something typed into it)</span></li>
7 <li><tt data-i18n="help_hotkeys_9">Enter</tt> <span data-i18n="help_hotkeys_10">(with chat bar selected)</span> = <span data-i18n="help_hotkeys_10_1">send your message to AI</span></li>9 <li><kbd data-i18n="help_hotkeys_9">Enter</kbd> <span data-i18n="help_hotkeys_10">(with chat bar selected)</span> = <span data-i18n="help_hotkeys_10_1">send your message to AI</span></li>
8 <li><tt data-i18n="help_hotkeys_11">Ctrl+Enter</tt> = <span data-i18n="help_hotkeys_12">Regenerate the last AI response</span></li>10 <li><kbd data-i18n="help_hotkeys_11">Ctrl+Enter</kbd> = <span data-i18n="help_hotkeys_12">Regenerate the last AI response</span></li>
9 <li><tt data-i18n="help_hotkeys_13">Alt+Enter</tt> = <span data-i18n="help_hotkeys_14">Continue the last AI response</span></li>11 <li><kbd data-i18n="help_hotkeys_13">Alt+Enter</kbd> = <span data-i18n="help_hotkeys_14">Continue the last AI response</span></li>
10 <li><tt data-i18n="help_hotkeys_15">Escape</tt> = <span data-i18n="help_hotkeys_16">stop AI response generation, close UI panels, cancel message edit</span></li>12 <li><kbd data-i18n="help_hotkeys_15">Escape</kbd> = <span data-i18n="help_hotkeys_16">stop AI response generation, close UI panels, cancel message edit</span></li>
11 <li><tt data-i18n="help_hotkeys_17">Ctrl+Shift+Up</tt> = <span data-i18n="help_hotkeys_18">Scroll to context line</span></li>13 <li><kbd data-i18n="help_hotkeys_17">Ctrl+Shift+Up</kbd> = <span data-i18n="help_hotkeys_18">Scroll to context line</span></li>
12 <li><tt data-i18n="help_hotkeys_19">Ctrl+Shift+Down</tt> = <span data-i18n="help_hotkeys_20">Scroll chat to bottom</span></li>14 <li><kbd data-i18n="help_hotkeys_19">Ctrl+Shift+Down</kbd> = <span data-i18n="help_hotkeys_20">Scroll chat to bottom</span></li>
15</ul>
16<div>
17 <strong>Markdown Hotkeys</strong>
18</div>
19<div>
20 <small>
21 <span>Works in the chatbar and textareas marked with this icon:</span>
22 <code><i class="fa-brands fa-markdown"></i></code>
23 </small>
24</div>
25<ul>
26 <li><kbd>Ctrl+B</kbd> = <span>**bold**</span></li>
27 <li><kbd>Ctrl+I</kbd> = <span>*italic*</span></li>
28 <li><kbd>Ctrl+U</kbd> = <span>__underline__</span></li>
29 <li><kbd>Ctrl+K</kbd> = <span>`inline code`</span></li>
30 <li><kbd>Ctrl+Shift+~</kbd> = <span>~~strikethrough~~</span></li>
13</ul>31</ul>