-
-
Notifications
You must be signed in to change notification settings - Fork 5.8k
Improve markdown textarea for indentation and lists #31406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
7e305dd
2772e6f
358cd19
1c8c42d
8024d10
5c3a077
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
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 | ||
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] " | ||
wxiaoguang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const prefixMatch = /^([0-9]+\.|[-*]|\[ \]|\[x\])\s/.exec(line); | ||
wxiaoguang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
wxiaoguang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) => { | ||
silverwind marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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); | ||
} | ||
wxiaoguang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -12,7 +12,7 @@ async function uploadFile(file, uploadUrl) { | |||||
return await res.json(); | ||||||
} | ||||||
|
||||||
function triggerEditorContentChanged(target) { | ||||||
export function triggerEditorContentChanged(target) { | ||||||
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) { | ||||||
// when pasting links over selected text, turn it into [text](link), except when shift key is held | ||||||
const {value, selectionStart, selectionEnd, _shiftDown} = textarea; | ||||||
if (_shiftDown) return; | ||||||
function handleClipboardText(textarea, e, text, isShiftDown) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Better to have options argument, also easier to read at caller site. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not think it's worth to have an option here. It is not optional. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could remove the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yes, if you still have the variable name at caller site, that's fine. |
||||||
// pasting with "shift" means "paste as original content" in most applications | ||||||
if (isShiftDown) return; // let the browser handle it | ||||||
|
||||||
// when pasting links over selected text, turn it into [text](link) | ||||||
const {value, selectionStart, selectionEnd} = textarea; | ||||||
const selectedText = value.substring(selectionStart, selectionEnd); | ||||||
const trimmedText = text.trim(); | ||||||
if (selectedText && isUrl(trimmedText)) { | ||||||
e.stopPropagation(); | ||||||
e.preventDefault(); | ||||||
replaceTextareaSelection(textarea, `[${selectedText}](${trimmedText})`); | ||||||
} | ||||||
// else, let the browser handle it | ||||||
} | ||||||
|
||||||
export function initEasyMDEPaste(easyMDE, dropzone) { | ||||||
|
@@ -147,12 +149,19 @@ export function initEasyMDEPaste(easyMDE, 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) => { | ||||||
const {images, text} = getPastedContent(e); | ||||||
if (images.length) { | ||||||
handleClipboardImages(new TextareaEditor(textarea), dropzone, images, e); | ||||||
} else if (text) { | ||||||
handleClipboardText(textarea, text, e); | ||||||
handleClipboardText(textarea, e, text, isShiftDown); | ||||||
} | ||||||
}); | ||||||
} |
Uh oh!
There was an error while loading. Please reload this page.