-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
0678287
commit 621e1ff
Showing
3 changed files
with
121 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters