| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- export const EventEditorContentChanged = 'ce-editor-content-changed';
-
- 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;
- textarea.focus();
- triggerEditorContentChanged(textarea);
- }
-
- type TextareaValueSelection = {
- value: string;
- selStart: number;
- selEnd: number;
- };
-
- function handleIndentSelection(textarea: HTMLTextAreaElement, e: KeyboardEvent) {
- 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);
- }
-
- function isTextExpanderShown(textarea: HTMLElement): boolean {
- return Boolean(textarea.closest('text-expander')?.querySelector('.suggestions'));
- }
-
- export function initTextareaMarkdown(textarea: HTMLTextAreaElement) {
- textarea.addEventListener('keydown', (e) => {
- if (isTextExpanderShown(textarea)) return;
- 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);
- }
- });
- }
|