mirror of https://github.com/go-gitea/gitea.git
Improve markdown textarea for indentation and lists (#31406)
Almost works like GitHub * use Tab/Shift-Tab to indent/unindent the selected lines * use Enter to insert a new line with the same indentation and prefix
This commit is contained in:
parent
06782872c4
commit
621e1ff9c9
|
@ -10,6 +10,7 @@ import {easyMDEToolbarActions} from './EasyMDEToolbarActions.js';
|
||||||
import {initTextExpander} from './TextExpander.js';
|
import {initTextExpander} from './TextExpander.js';
|
||||||
import {showErrorToast} from '../../modules/toast.js';
|
import {showErrorToast} from '../../modules/toast.js';
|
||||||
import {POST} from '../../modules/fetch.js';
|
import {POST} from '../../modules/fetch.js';
|
||||||
|
import {initTextareaMarkdown} from './EditorMarkdown.js';
|
||||||
|
|
||||||
let elementIdCounter = 0;
|
let elementIdCounter = 0;
|
||||||
|
|
||||||
|
@ -84,17 +85,6 @@ class ComboMarkdownEditor {
|
||||||
if (el.nodeName === 'BUTTON' && !el.getAttribute('type')) el.setAttribute('type', 'button');
|
if (el.nodeName === 'BUTTON' && !el.getAttribute('type')) el.setAttribute('type', 'button');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.textarea.addEventListener('keydown', (e) => {
|
|
||||||
if (e.shiftKey) {
|
|
||||||
e.target._shiftDown = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.textarea.addEventListener('keyup', (e) => {
|
|
||||||
if (!e.shiftKey) {
|
|
||||||
e.target._shiftDown = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const monospaceButton = this.container.querySelector('.markdown-switch-monospace');
|
const monospaceButton = this.container.querySelector('.markdown-switch-monospace');
|
||||||
const monospaceEnabled = localStorage?.getItem('markdown-editor-monospace') === 'true';
|
const monospaceEnabled = localStorage?.getItem('markdown-editor-monospace') === 'true';
|
||||||
const monospaceText = monospaceButton.getAttribute(monospaceEnabled ? 'data-disable-text' : 'data-enable-text');
|
const monospaceText = monospaceButton.getAttribute(monospaceEnabled ? 'data-disable-text' : 'data-enable-text');
|
||||||
|
@ -118,6 +108,7 @@ class ComboMarkdownEditor {
|
||||||
await this.switchToEasyMDE();
|
await this.switchToEasyMDE();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
initTextareaMarkdown(this.textarea);
|
||||||
if (this.dropzone) {
|
if (this.dropzone) {
|
||||||
initTextareaPaste(this.textarea, this.dropzone);
|
initTextareaPaste(this.textarea, this.dropzone);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
import {triggerEditorContentChanged} from './Paste.js';
|
||||||
|
|
||||||
|
function handleIndentSelection(textarea, e) {
|
||||||
|
const selStart = textarea.selectionStart;
|
||||||
|
const selEnd = textarea.selectionEnd;
|
||||||
|
if (selEnd === selStart) return; // do not process when no selection
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
const lines = textarea.value.split('\n');
|
||||||
|
const selectedLines = [];
|
||||||
|
|
||||||
|
let pos = 0;
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (pos > selEnd) break;
|
||||||
|
if (pos >= selStart) selectedLines.push(i);
|
||||||
|
pos += lines[i].length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const i of selectedLines) {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
lines[i] = lines[i].replace(/^(\t| {1,2})/, '');
|
||||||
|
} else {
|
||||||
|
lines[i] = ` ${lines[i]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-calculating the selection range
|
||||||
|
let newSelStart, newSelEnd;
|
||||||
|
pos = 0;
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (i === selectedLines[0]) {
|
||||||
|
newSelStart = pos;
|
||||||
|
}
|
||||||
|
if (i === selectedLines[selectedLines.length - 1]) {
|
||||||
|
newSelEnd = pos + lines[i].length;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pos += lines[i].length + 1;
|
||||||
|
}
|
||||||
|
textarea.value = lines.join('\n');
|
||||||
|
textarea.setSelectionRange(newSelStart, newSelEnd);
|
||||||
|
triggerEditorContentChanged(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNewline(textarea, e) {
|
||||||
|
const selStart = textarea.selectionStart;
|
||||||
|
const selEnd = textarea.selectionEnd;
|
||||||
|
if (selEnd !== selStart) return; // do not process when there is a selection
|
||||||
|
|
||||||
|
const value = textarea.value;
|
||||||
|
|
||||||
|
// find the current line
|
||||||
|
// * if selStart is 0, lastIndexOf(..., -1) is the same as lastIndexOf(..., 0)
|
||||||
|
// * if lastIndexOf reruns -1, lineStart is 0 and it is still correct.
|
||||||
|
const lineStart = value.lastIndexOf('\n', selStart - 1) + 1;
|
||||||
|
let lineEnd = value.indexOf('\n', selStart);
|
||||||
|
lineEnd = lineEnd < 0 ? value.length : lineEnd;
|
||||||
|
let line = value.slice(lineStart, lineEnd);
|
||||||
|
if (!line) return; // if the line is empty, do nothing, let the browser handle it
|
||||||
|
|
||||||
|
// parse the indention
|
||||||
|
const indention = /^\s*/.exec(line)[0];
|
||||||
|
line = line.slice(indention.length);
|
||||||
|
|
||||||
|
// parse the prefixes: "1. ", "- ", "* ", "[ ] ", "[x] "
|
||||||
|
// there must be a space after the prefix because none of "1.foo" / "-foo" is a list item
|
||||||
|
const prefixMatch = /^([0-9]+\.|[-*]|\[ \]|\[x\])\s/.exec(line);
|
||||||
|
let prefix = '';
|
||||||
|
if (prefixMatch) {
|
||||||
|
prefix = prefixMatch[0];
|
||||||
|
if (lineStart + prefix.length > selStart) prefix = ''; // do not add new line if cursor is at prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
line = line.slice(prefix.length);
|
||||||
|
if (!indention && !prefix) return; // if no indention and no prefix, do nothing, let the browser handle it
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
if (!line) {
|
||||||
|
// clear current line if we only have i.e. '1. ' and the user presses enter again to finish creating a list
|
||||||
|
textarea.value = value.slice(0, lineStart) + value.slice(lineEnd);
|
||||||
|
} else {
|
||||||
|
// start a new line with the same indention and prefix
|
||||||
|
let newPrefix = prefix;
|
||||||
|
if (newPrefix === '[x]') newPrefix = '[ ]';
|
||||||
|
if (/^\d+\./.test(newPrefix)) newPrefix = `1. `; // a simple approach, otherwise it needs to parse the lines after the current line
|
||||||
|
const newLine = `\n${indention}${newPrefix}`;
|
||||||
|
textarea.value = value.slice(0, selStart) + newLine + value.slice(selEnd);
|
||||||
|
textarea.setSelectionRange(selStart + newLine.length, selStart + newLine.length);
|
||||||
|
}
|
||||||
|
triggerEditorContentChanged(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initTextareaMarkdown(textarea) {
|
||||||
|
textarea.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Tab' && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||||
|
// use Tab/Shift-Tab to indent/unindent the selected lines
|
||||||
|
handleIndentSelection(textarea, e);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ async function uploadFile(file, uploadUrl) {
|
||||||
return await res.json();
|
return await res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerEditorContentChanged(target) {
|
export function triggerEditorContentChanged(target) {
|
||||||
target.dispatchEvent(new CustomEvent('ce-editor-content-changed', {bubbles: true}));
|
target.dispatchEvent(new CustomEvent('ce-editor-content-changed', {bubbles: true}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,17 +124,19 @@ async function handleClipboardImages(editor, dropzone, images, e) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClipboardText(textarea, text, e) {
|
function handleClipboardText(textarea, e, {text, isShiftDown}) {
|
||||||
// when pasting links over selected text, turn it into [text](link), except when shift key is held
|
// pasting with "shift" means "paste as original content" in most applications
|
||||||
const {value, selectionStart, selectionEnd, _shiftDown} = textarea;
|
if (isShiftDown) return; // let the browser handle it
|
||||||
if (_shiftDown) return;
|
|
||||||
|
// when pasting links over selected text, turn it into [text](link)
|
||||||
|
const {value, selectionStart, selectionEnd} = textarea;
|
||||||
const selectedText = value.substring(selectionStart, selectionEnd);
|
const selectedText = value.substring(selectionStart, selectionEnd);
|
||||||
const trimmedText = text.trim();
|
const trimmedText = text.trim();
|
||||||
if (selectedText && isUrl(trimmedText)) {
|
if (selectedText && isUrl(trimmedText)) {
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
replaceTextareaSelection(textarea, `[${selectedText}](${trimmedText})`);
|
replaceTextareaSelection(textarea, `[${selectedText}](${trimmedText})`);
|
||||||
}
|
}
|
||||||
|
// else, let the browser handle it
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initEasyMDEPaste(easyMDE, dropzone) {
|
export function initEasyMDEPaste(easyMDE, dropzone) {
|
||||||
|
@ -147,12 +149,19 @@ export function initEasyMDEPaste(easyMDE, dropzone) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initTextareaPaste(textarea, dropzone) {
|
export function initTextareaPaste(textarea, dropzone) {
|
||||||
|
let isShiftDown = false;
|
||||||
|
textarea.addEventListener('keydown', (e) => {
|
||||||
|
if (e.shiftKey) isShiftDown = true;
|
||||||
|
});
|
||||||
|
textarea.addEventListener('keyup', (e) => {
|
||||||
|
if (!e.shiftKey) isShiftDown = false;
|
||||||
|
});
|
||||||
textarea.addEventListener('paste', (e) => {
|
textarea.addEventListener('paste', (e) => {
|
||||||
const {images, text} = getPastedContent(e);
|
const {images, text} = getPastedContent(e);
|
||||||
if (images.length) {
|
if (images.length) {
|
||||||
handleClipboardImages(new TextareaEditor(textarea), dropzone, images, e);
|
handleClipboardImages(new TextareaEditor(textarea), dropzone, images, e);
|
||||||
} else if (text) {
|
} else if (text) {
|
||||||
handleClipboardText(textarea, text, e);
|
handleClipboardText(textarea, e, {text, isShiftDown});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue