mirror of
https://github.com/go-gitea/gitea.git
synced 2025-12-12 16:13:56 +01:00
Add matching pair insertion to markdown textarea (#36121)
1. Our textarea already has some editor-like feature like tab indentation, so I thought why not also add insertion of matching closing quotes/brackets over selected text. This does that. 2. `textareaInsertText` is replaced with `replaceTextareaSelection` which does the same but create a new edit history entry in the textarea so CTRL-Z works. The button that inserts tables into the textarea can now also be reverted via CTRL-Z, which was not possible before.
This commit is contained in:
parent
d83a071db9
commit
ed698d1a61
@ -17,7 +17,7 @@ import {POST} from '../../modules/fetch.ts';
|
||||
import {
|
||||
EventEditorContentChanged,
|
||||
initTextareaMarkdown,
|
||||
textareaInsertText,
|
||||
replaceTextareaSelection,
|
||||
triggerEditorContentChanged,
|
||||
} from './EditorMarkdown.ts';
|
||||
import {DropzoneCustomEventReloadFiles, initDropzone} from '../dropzone.ts';
|
||||
@ -273,7 +273,7 @@ export class ComboMarkdownEditor {
|
||||
let cols = parseInt(addTablePanel.querySelector<HTMLInputElement>('[name=cols]')!.value);
|
||||
rows = Math.max(1, Math.min(100, rows));
|
||||
cols = Math.max(1, Math.min(100, cols));
|
||||
textareaInsertText(this.textarea, `\n${this.generateMarkdownTable(rows, cols)}\n\n`);
|
||||
replaceTextareaSelection(this.textarea, `\n${this.generateMarkdownTable(rows, cols)}\n\n`);
|
||||
addTablePanelTippy.hide();
|
||||
});
|
||||
}
|
||||
|
||||
@ -4,14 +4,23 @@ export function triggerEditorContentChanged(target: HTMLElement) {
|
||||
target.dispatchEvent(new CustomEvent(EventEditorContentChanged, {bubbles: true}));
|
||||
}
|
||||
|
||||
export function textareaInsertText(textarea: HTMLTextAreaElement, value: string) {
|
||||
const startPos = textarea.selectionStart;
|
||||
const endPos = textarea.selectionEnd;
|
||||
textarea.value = textarea.value.substring(0, startPos) + value + textarea.value.substring(endPos);
|
||||
textarea.selectionStart = startPos;
|
||||
textarea.selectionEnd = startPos + value.length;
|
||||
/** replace selected text or insert text by creating a new edit history entry,
|
||||
* e.g. CTRL-Z works after this */
|
||||
export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: string) {
|
||||
const before = textarea.value.slice(0, textarea.selectionStart);
|
||||
const after = textarea.value.slice(textarea.selectionEnd);
|
||||
|
||||
textarea.focus();
|
||||
triggerEditorContentChanged(textarea);
|
||||
let success = false;
|
||||
try {
|
||||
success = document.execCommand('insertText', false, text); // eslint-disable-line @typescript-eslint/no-deprecated
|
||||
} catch {}
|
||||
|
||||
// fall back to regular replacement
|
||||
if (!success) {
|
||||
textarea.value = `${before}${text}${after}`;
|
||||
triggerEditorContentChanged(textarea);
|
||||
}
|
||||
}
|
||||
|
||||
type TextareaValueSelection = {
|
||||
@ -176,7 +185,7 @@ export function markdownHandleIndention(tvs: TextareaValueSelection): MarkdownHa
|
||||
return {handled: true, valueSelection: {value: linesBuf.lines.join('\n'), selStart: newPos, selEnd: newPos}};
|
||||
}
|
||||
|
||||
function handleNewline(textarea: HTMLTextAreaElement, e: Event) {
|
||||
function handleNewline(textarea: HTMLTextAreaElement, e: KeyboardEvent) {
|
||||
const ret = markdownHandleIndention({value: textarea.value, selStart: textarea.selectionStart, selEnd: textarea.selectionEnd});
|
||||
if (!ret.handled || !ret.valueSelection) return; // FIXME: the "handled" seems redundant, only valueSelection is enough (null for unhandled)
|
||||
e.preventDefault();
|
||||
@ -185,6 +194,28 @@ function handleNewline(textarea: HTMLTextAreaElement, e: Event) {
|
||||
triggerEditorContentChanged(textarea);
|
||||
}
|
||||
|
||||
// Keys that act as dead keys will not work because the spec dictates that such keys are
|
||||
// emitted as `Dead` in e.key instead of the actual key.
|
||||
const pairs = new Map<string, string>([
|
||||
["'", "'"],
|
||||
['"', '"'],
|
||||
['`', '`'],
|
||||
['(', ')'],
|
||||
['[', ']'],
|
||||
['{', '}'],
|
||||
['<', '>'],
|
||||
]);
|
||||
|
||||
function handlePairCharacter(textarea: HTMLTextAreaElement, e: KeyboardEvent): void {
|
||||
const selStart = textarea.selectionStart;
|
||||
const selEnd = textarea.selectionEnd;
|
||||
if (selEnd === selStart) return; // do not process when no selection
|
||||
e.preventDefault();
|
||||
const inner = textarea.value.substring(selStart, selEnd);
|
||||
replaceTextareaSelection(textarea, `${e.key}${inner}${pairs.get(e.key)}`);
|
||||
textarea.setSelectionRange(selStart + 1, selEnd + 1);
|
||||
}
|
||||
|
||||
function isTextExpanderShown(textarea: HTMLElement): boolean {
|
||||
return Boolean(textarea.closest('text-expander')?.querySelector('.suggestions'));
|
||||
}
|
||||
@ -198,6 +229,8 @@ export function initTextareaMarkdown(textarea: HTMLTextAreaElement) {
|
||||
} else if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
// use Enter to insert a new line with the same indention and prefix
|
||||
handleNewline(textarea, e);
|
||||
} else if (pairs.has(e.key)) {
|
||||
handlePairCharacter(textarea, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {imageInfo} from '../../utils/image.ts';
|
||||
import {textareaInsertText, triggerEditorContentChanged} from './EditorMarkdown.ts';
|
||||
import {replaceTextareaSelection, triggerEditorContentChanged} from './EditorMarkdown.ts';
|
||||
import {
|
||||
DropzoneCustomEventRemovedFile,
|
||||
DropzoneCustomEventUploadDone,
|
||||
@ -43,7 +43,7 @@ class TextareaEditor {
|
||||
}
|
||||
|
||||
insertPlaceholder(value: string) {
|
||||
textareaInsertText(this.editor, value);
|
||||
replaceTextareaSelection(this.editor, value);
|
||||
}
|
||||
|
||||
replacePlaceholder(oldVal: string, newVal: string) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user