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>
This commit is contained in:
RossAscends
2024-09-14 20:16:26 +09:00
committed by GitHub
parent bbb0391459
commit b376ea884b
6 changed files with 229 additions and 26 deletions

View File

@@ -0,0 +1,152 @@
import { power_user } from './power-user.js';
export function initInputMarkdown() {
$(document).on('keydown', 'textarea.mdHotkeys', function (e) {
if (!power_user.enable_md_hotkeys) { return; }
// Ensure that the element is a textarea
let textarea = this;
if (!(textarea instanceof HTMLTextAreaElement)) {
return;
}
// Early return on only control or no control, alt key, and win/cmd key
if (e.key === 'Control' || !e.ctrlKey || e.altKey || e.metaKey || (e.shiftKey && !(e.ctrlKey && e.shiftKey && e.code === 'Backquote'))) {
return;
}
let charsToAdd = '';
let possiblePreviousFormattingMargin = 1;
switch (true) {
case e.ctrlKey && e.shiftKey && e.code === 'Backquote':
e.preventDefault();
e.stopPropagation();
charsToAdd = '~~';
possiblePreviousFormattingMargin = 2;
break;
case e.ctrlKey && e.code === 'KeyB':
e.preventDefault();
e.stopPropagation();
charsToAdd = '**';
possiblePreviousFormattingMargin = 2;
break;
case e.ctrlKey && e.code === 'KeyI':
e.preventDefault();
e.stopPropagation();
charsToAdd = '*';
break;
case e.ctrlKey && e.code === 'KeyU':
e.preventDefault();
e.stopPropagation();
charsToAdd = '__';
possiblePreviousFormattingMargin = 2;
break;
case e.ctrlKey && e.code === 'KeyK':
e.preventDefault();
e.stopPropagation();
charsToAdd = '`';
break;
default:
return; // Early return if no key matches
}
let selectedText = '';
let start = textarea.selectionStart;
let end = textarea.selectionEnd;
let beforeCaret = textarea.value.substring(start - 1, start);
let afterCaret = textarea.value.substring(end, end + 1);
let isTextSelected = (start !== end);
let cursorShift = charsToAdd.length;
let selectedTextandPossibleFormatting = textarea.value.substring(start - possiblePreviousFormattingMargin, end + possiblePreviousFormattingMargin).trim();
if (isTextSelected) { //if text is selected
selectedText = textarea.value.substring(start, end);
if (selectedTextandPossibleFormatting === charsToAdd + selectedText + charsToAdd) {
// If the selected text is already formatted, remove the formatting
let expandedStart = start - charsToAdd.length;
let expandedEnd = end + charsToAdd.length;
// Ensure expanded range is within the bounds of the text
if (expandedStart < 0) expandedStart = 0;
if (expandedEnd > textarea.value.length) expandedEnd = textarea.value.length;
// Select the expanded range
textarea.setSelectionRange(expandedStart, expandedEnd);
// Replace the expanded selection with the original selected text
document.execCommand('insertText', false, selectedText);
// Adjust cursor position
cursorShift = -charsToAdd.length;
} else {
// Add formatting to the selected text
let possibleAddedSpace = '';
if (selectedText.endsWith(' ')) {
possibleAddedSpace = ' ';
selectedText = selectedText.substring(0, selectedText.length - 1);
end--; // Adjust the end index since we removed the space
}
// To add the formatting, we need to select the text first
textarea.focus();
document.execCommand('insertText', false, charsToAdd + selectedText + charsToAdd + possibleAddedSpace);
}
} else {// No text is selected
//check 1 character before and after the cursor for non-space characters
if (beforeCaret !== ' ' && afterCaret !== ' ' && afterCaret !== '' && beforeCaret !== '') { //look for caret in the middle of a word
//expand the selection range until the next space on both sides
let midCaretExpandedStart = start - 1;
let midCaretExpandedEnd = end + 1;
while (midCaretExpandedStart > 0 && textarea.value.substring(midCaretExpandedStart - 1, midCaretExpandedStart) !== ' ') {
midCaretExpandedStart--;
}
while (midCaretExpandedEnd < textarea.value.length && textarea.value.substring(midCaretExpandedEnd, midCaretExpandedEnd + 1) !== ' ') {
midCaretExpandedEnd++;
}
//make a selection of the discovered word
textarea.setSelectionRange(midCaretExpandedStart, midCaretExpandedEnd);
//set variables for comparison
let discoveredWordWithPossibleFormatting = textarea.value.substring(midCaretExpandedStart, midCaretExpandedEnd).trim();
let discoveredWord = '';
if (discoveredWordWithPossibleFormatting.endsWith(charsToAdd) && discoveredWordWithPossibleFormatting.startsWith(charsToAdd)) {
discoveredWord = textarea.value.substring(midCaretExpandedStart + charsToAdd.length, midCaretExpandedEnd - charsToAdd.length).trim();
} else {
discoveredWord = textarea.value.substring(midCaretExpandedStart, midCaretExpandedEnd).trim();
}
if (charsToAdd + discoveredWord + charsToAdd === discoveredWordWithPossibleFormatting) {
// Replace the expanded selection with the original discovered word
textarea.focus();
document.execCommand('insertText', false, discoveredWord);
// Adjust cursor position
cursorShift = -charsToAdd.length;
} else { //format did not previously exist, so add it
textarea.focus();
document.execCommand('insertText', false, charsToAdd + discoveredWord + charsToAdd);
}
} else { //caret is not inside a word, so just add the formatting
textarea.focus();
textarea.setSelectionRange(start, end);
selectedText = textarea.value.substring(start, end);
document.execCommand('insertText', false, charsToAdd + selectedText + charsToAdd);
}
}
// Manually trigger the 'input' event to make undo/redo work
let event = new Event('input', { bubbles: true });
textarea.dispatchEvent(event); // This notifies the browser of a change, allowing undo/redo to function.
// Update the cursor position
if (isTextSelected) {
textarea.selectionStart = start + cursorShift;
textarea.selectionEnd = start + cursorShift + selectedText.length;
} else {
textarea.selectionStart = start + cursorShift;
textarea.selectionEnd = start + cursorShift;
}
});
}

View File

@@ -200,6 +200,7 @@ let power_user = {
relaxed_api_urls: false,
world_import_dialog: true,
enable_auto_select_input: false,
enable_md_hotkeys: false,
tag_import_setting: tag_import_setting.ASK,
disable_group_trimming: false,
single_line: false,
@@ -1452,6 +1453,7 @@ async function loadPowerUserSettings(settings, data) {
$('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls);
$('#world_import_dialog').prop('checked', power_user.world_import_dialog);
$('#enable_auto_select_input').prop('checked', power_user.enable_auto_select_input);
$('#enable_md_hotkeys').prop('checked', power_user.enable_md_hotkeys);
$('#trim_spaces').prop('checked', power_user.trim_spaces);
$('#continue_on_send').prop('checked', power_user.continue_on_send);
$('#quick_continue').prop('checked', power_user.quick_continue);
@@ -1601,6 +1603,17 @@ async function loadPowerUserSettings(settings, data) {
switchSpoilerMode();
loadMovingUIState();
loadCharListState();
toggleMDHotkeyIconDisplay();
}
function toggleMDHotkeyIconDisplay() {
if (power_user.enable_md_hotkeys) {
$('.mdhotkey_location').each(function () {
$(this).parent().append('<i class="fa-brands fa-markdown mdhotkey_icon"></i>');
});
} else {
$('.mdhotkey_icon').remove();
}
}
function loadCharListState() {
@@ -3610,6 +3623,13 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#enable_md_hotkeys').on('input', function () {
const value = !!$(this).prop('checked');
power_user.enable_md_hotkeys = value;
toggleMDHotkeyIconDisplay();
saveSettingsDebounced();
});
$('#spoiler_free_mode').on('input', function () {
power_user.spoiler_free_mode = !!$(this).prop('checked');
switchSpoilerMode();

View File

@@ -1,13 +1,31 @@
<span data-i18n="help_hotkeys_0">Hotkeys/Keybinds</span>:
<div>
<strong data-i18n="help_hotkeys_0">Chat Hotkeys</strong>
</div>
<ul>
<li><tt data-i18n="help_hotkeys_1">Up</tt> = <span data-i18n="help_hotkeys_2">Edit last message in chat</span></li>
<li><tt data-i18n="help_hotkeys_3">Ctrl+Up</tt> = <span data-i18n="help_hotkeys_4">Edit last USER message in chat</span></li>
<li><tt data-i18n="help_hotkeys_5">Left</tt> = <span data-i18n="help_hotkeys_6">swipe left</span></li>
<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>
<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>
<li><tt data-i18n="help_hotkeys_11">Ctrl+Enter</tt> = <span data-i18n="help_hotkeys_12">Regenerate the last AI response</span></li>
<li><tt data-i18n="help_hotkeys_13">Alt+Enter</tt> = <span data-i18n="help_hotkeys_14">Continue the last AI response</span></li>
<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>
<li><tt data-i18n="help_hotkeys_17">Ctrl+Shift+Up</tt> = <span data-i18n="help_hotkeys_18">Scroll to context line</span></li>
<li><tt data-i18n="help_hotkeys_19">Ctrl+Shift+Down</tt> = <span data-i18n="help_hotkeys_20">Scroll chat to bottom</span></li>
<li><kbd data-i18n="help_hotkeys_1">Up</kbd> = <span data-i18n="help_hotkeys_2">Edit last message in chat</span></li>
<li><kbd data-i18n="help_hotkeys_3">Ctrl+Up</kbd> = <span data-i18n="help_hotkeys_4">Edit last USER message in chat</span></li>
<li><kbd data-i18n="help_hotkeys_5">Left</kbd> = <span data-i18n="help_hotkeys_6">swipe left</span></li>
<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>
<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>
<li><kbd data-i18n="help_hotkeys_11">Ctrl+Enter</kbd> = <span data-i18n="help_hotkeys_12">Regenerate the last AI response</span></li>
<li><kbd data-i18n="help_hotkeys_13">Alt+Enter</kbd> = <span data-i18n="help_hotkeys_14">Continue the last AI response</span></li>
<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>
<li><kbd data-i18n="help_hotkeys_17">Ctrl+Shift+Up</kbd> = <span data-i18n="help_hotkeys_18">Scroll to context line</span></li>
<li><kbd data-i18n="help_hotkeys_19">Ctrl+Shift+Down</kbd> = <span data-i18n="help_hotkeys_20">Scroll chat to bottom</span></li>
</ul>
<div>
<strong>Markdown Hotkeys</strong>
</div>
<div>
<small>
<span>Works in the chatbar and textareas marked with this icon:</span>
<code><i class="fa-brands fa-markdown"></i></code>
</small>
</div>
<ul>
<li><kbd>Ctrl+B</kbd> = <span>**bold**</span></li>
<li><kbd>Ctrl+I</kbd> = <span>*italic*</span></li>
<li><kbd>Ctrl+U</kbd> = <span>__underline__</span></li>
<li><kbd>Ctrl+K</kbd> = <span>`inline code`</span></li>
<li><kbd>Ctrl+Shift+~</kbd> = <span>~~strikethrough~~</span></li>
</ul>