mirror of https://github.com/go-gitea/gitea.git
198 lines
7.3 KiB
TypeScript
198 lines
7.3 KiB
TypeScript
export const EventEditorContentChanged = 'ce-editor-content-changed';
|
|
|
|
export function triggerEditorContentChanged(target) {
|
|
target.dispatchEvent(new CustomEvent(EventEditorContentChanged, {bubbles: true}));
|
|
}
|
|
|
|
export function textareaInsertText(textarea, value) {
|
|
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;
|
|
textarea.focus();
|
|
triggerEditorContentChanged(textarea);
|
|
}
|
|
|
|
type TextareaValueSelection = {
|
|
value: string;
|
|
selStart: number;
|
|
selEnd: number;
|
|
}
|
|
|
|
function handleIndentSelection(textarea: HTMLTextAreaElement, 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);
|
|
}
|
|
|
|
type MarkdownHandleIndentionResult = {
|
|
handled: boolean;
|
|
valueSelection?: TextareaValueSelection;
|
|
}
|
|
|
|
type TextLinesBuffer = {
|
|
lines: string[];
|
|
lengthBeforePosLine: number;
|
|
posLineIndex: number;
|
|
inlinePos: number
|
|
}
|
|
|
|
export function textareaSplitLines(value: string, pos: number): TextLinesBuffer {
|
|
const lines = value.split('\n');
|
|
let lengthBeforePosLine = 0, inlinePos = 0, posLineIndex = 0;
|
|
for (; posLineIndex < lines.length; posLineIndex++) {
|
|
const lineLength = lines[posLineIndex].length + 1;
|
|
if (lengthBeforePosLine + lineLength > pos) {
|
|
inlinePos = pos - lengthBeforePosLine;
|
|
break;
|
|
}
|
|
lengthBeforePosLine += lineLength;
|
|
}
|
|
return {lines, lengthBeforePosLine, posLineIndex, inlinePos};
|
|
}
|
|
|
|
function markdownReformatListNumbers(linesBuf: TextLinesBuffer, indention: string) {
|
|
const reDeeperIndention = new RegExp(`^${indention}\\s+`);
|
|
const reSameLevel = new RegExp(`^${indention}([0-9]+)\\.`);
|
|
let firstLineIdx: number;
|
|
for (firstLineIdx = linesBuf.posLineIndex - 1; firstLineIdx >= 0; firstLineIdx--) {
|
|
const line = linesBuf.lines[firstLineIdx];
|
|
if (!reDeeperIndention.test(line) && !reSameLevel.test(line)) break;
|
|
}
|
|
firstLineIdx++;
|
|
let num = 1;
|
|
for (let i = firstLineIdx; i < linesBuf.lines.length; i++) {
|
|
const oldLine = linesBuf.lines[i];
|
|
const sameLevel = reSameLevel.test(oldLine);
|
|
if (!sameLevel && !reDeeperIndention.test(oldLine)) break;
|
|
if (sameLevel) {
|
|
const newLine = `${indention}${num}.${oldLine.replace(reSameLevel, '')}`;
|
|
linesBuf.lines[i] = newLine;
|
|
num++;
|
|
if (linesBuf.posLineIndex === i) {
|
|
// need to correct the cursor inline position if the line length changes
|
|
linesBuf.inlinePos += newLine.length - oldLine.length;
|
|
linesBuf.inlinePos = Math.max(0, linesBuf.inlinePos);
|
|
linesBuf.inlinePos = Math.min(newLine.length, linesBuf.inlinePos);
|
|
}
|
|
}
|
|
}
|
|
recalculateLengthBeforeLine(linesBuf);
|
|
}
|
|
|
|
function recalculateLengthBeforeLine(linesBuf: TextLinesBuffer) {
|
|
linesBuf.lengthBeforePosLine = 0;
|
|
for (let i = 0; i < linesBuf.posLineIndex; i++) {
|
|
linesBuf.lengthBeforePosLine += linesBuf.lines[i].length + 1;
|
|
}
|
|
}
|
|
|
|
export function markdownHandleIndention(tvs: TextareaValueSelection): MarkdownHandleIndentionResult {
|
|
const unhandled: MarkdownHandleIndentionResult = {handled: false};
|
|
if (tvs.selEnd !== tvs.selStart) return unhandled; // do not process when there is a selection
|
|
|
|
const linesBuf = textareaSplitLines(tvs.value, tvs.selStart);
|
|
const line = linesBuf.lines[linesBuf.posLineIndex] ?? '';
|
|
if (!line) return unhandled; // if the line is empty, do nothing, let the browser handle it
|
|
|
|
// parse the indention
|
|
let lineContent = line;
|
|
const indention = /^\s*/.exec(lineContent)[0];
|
|
lineContent = lineContent.slice(indention.length);
|
|
if (linesBuf.inlinePos <= indention.length) return unhandled; // if cursor is at the indention, do nothing, let the browser handle it
|
|
|
|
// parse the prefixes: "1. ", "- ", "* ", there could also be " [ ] " or " [x] " for task lists
|
|
// there must be a space after the prefix because none of "1.foo" / "-foo" is a list item
|
|
const prefixMatch = /^([0-9]+\.|[-*])(\s\[([ x])\])?\s/.exec(lineContent);
|
|
let prefix = '';
|
|
if (prefixMatch) {
|
|
prefix = prefixMatch[0];
|
|
if (prefix.length > linesBuf.inlinePos) prefix = ''; // do not add new line if cursor is at prefix
|
|
}
|
|
|
|
lineContent = lineContent.slice(prefix.length);
|
|
if (!indention && !prefix) return unhandled; // if no indention and no prefix, do nothing, let the browser handle it
|
|
|
|
if (!lineContent) {
|
|
// clear current line if we only have i.e. '1. ' and the user presses enter again to finish creating a list
|
|
linesBuf.lines[linesBuf.posLineIndex] = '';
|
|
linesBuf.inlinePos = 0;
|
|
} else {
|
|
// start a new line with the same indention
|
|
let newPrefix = prefix;
|
|
if (/^\d+\./.test(prefix)) newPrefix = `1. ${newPrefix.slice(newPrefix.indexOf('.') + 2)}`;
|
|
newPrefix = newPrefix.replace('[x]', '[ ]');
|
|
|
|
const inlinePos = linesBuf.inlinePos;
|
|
linesBuf.lines[linesBuf.posLineIndex] = line.substring(0, inlinePos);
|
|
const newLineLeft = `${indention}${newPrefix}`;
|
|
const newLine = `${newLineLeft}${line.substring(inlinePos)}`;
|
|
linesBuf.lines.splice(linesBuf.posLineIndex + 1, 0, newLine);
|
|
linesBuf.posLineIndex++;
|
|
linesBuf.inlinePos = newLineLeft.length;
|
|
recalculateLengthBeforeLine(linesBuf);
|
|
}
|
|
|
|
markdownReformatListNumbers(linesBuf, indention);
|
|
const newPos = linesBuf.lengthBeforePosLine + linesBuf.inlinePos;
|
|
return {handled: true, valueSelection: {value: linesBuf.lines.join('\n'), selStart: newPos, selEnd: newPos}};
|
|
}
|
|
|
|
function handleNewline(textarea: HTMLTextAreaElement, e: Event) {
|
|
const ret = markdownHandleIndention({value: textarea.value, selStart: textarea.selectionStart, selEnd: textarea.selectionEnd});
|
|
if (!ret.handled) return;
|
|
e.preventDefault();
|
|
textarea.value = ret.valueSelection.value;
|
|
textarea.setSelectionRange(ret.valueSelection.selStart, ret.valueSelection.selEnd);
|
|
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);
|
|
}
|
|
});
|
|
}
|