Вставка Markdown в редактор: ссылки [текст](/p/<id>), форматирование, выделение + URL → ссылка
This commit is contained in:
@@ -40,6 +40,27 @@ const AuthImage = Image.extend({
|
||||
|
||||
const isInternal = (href: string) => /^\/p\/[A-Za-z0-9_-]+$/.test(href);
|
||||
|
||||
/** Адрес, который можно повесить на выделение: http(s), mailto или внутренняя ссылка /p/<id>. */
|
||||
const isLinkTarget = (s: string) => /^(https?:\/\/[^\s]+|mailto:[^\s]+|\/p\/[A-Za-z0-9_-]+)$/i.test(s);
|
||||
|
||||
/** Похоже ли на Markdown: ссылки, картинки, заголовки, списки, цитаты, код, таблицы, жирный/курсив. */
|
||||
const looksLikeMarkdown = (s: string) =>
|
||||
/!?\[[^\]\n]+\]\([^)\s]+\)|\*\*[^*\n]+\*\*|__[^_\n]+__|~~[^~\n]+~~|`[^`\n]+`/.test(s) ||
|
||||
/^(#{1,6}\s|\s*[-*+]\s|\s*\d+[.)]\s|>\s?|```|\|.*\|\s*$|\s*[-*_]{3,}\s*$)/m.test(s);
|
||||
|
||||
/**
|
||||
* Вставка Markdown тем же парсером, которым загружается страница.
|
||||
* Один абзац (например, одна ссылка [Назва](/p/<id>) или фраза с ней) вставляется в строку, без нового абзаца.
|
||||
*/
|
||||
function pasteMarkdown(ed: Editor, md: string): boolean {
|
||||
const doc = ed.markdown?.parse(md);
|
||||
const blocks = doc?.content ?? [];
|
||||
if (!blocks.length) return false;
|
||||
const content = blocks.length === 1 && blocks[0].type === "paragraph" ? blocks[0].content ?? [] : blocks;
|
||||
if (!content.length) return false;
|
||||
return ed.chain().focus().insertContent(content).run();
|
||||
}
|
||||
|
||||
export function WikiEditor(props: {
|
||||
markdown: string; pageId: string; nodes: Node[];
|
||||
onChange: (getMarkdown: () => string) => void;
|
||||
@@ -88,11 +109,24 @@ export function WikiEditor(props: {
|
||||
else if (/^(https?:|mailto:)/i.test(href)) window.open(href, "_blank", "noopener,noreferrer");
|
||||
return true;
|
||||
},
|
||||
handlePaste: (_view, event) => {
|
||||
const files = [...(event.clipboardData?.files ?? [])].filter((f) => f.type.startsWith("image/"));
|
||||
if (!files.length || !editor) return false;
|
||||
void upload(editor, files);
|
||||
return true;
|
||||
handlePaste: (view, event): boolean => {
|
||||
const cd = event.clipboardData;
|
||||
if (!cd || !editor) return false;
|
||||
const files = [...cd.files].filter((f) => f.type.startsWith("image/"));
|
||||
if (files.length) { void upload(editor, files); return true; }
|
||||
const plain = cd.getData("text/plain");
|
||||
const target = plain.trim();
|
||||
// Выделили текст и вставили адрес — выделение становится ссылкой.
|
||||
if (!view.state.selection.empty && isLinkTarget(target) && !editor.isActive("codeBlock")) {
|
||||
// Курсор — в конец ссылки: следующий ввод не заменит только что оформленный текст.
|
||||
const end = view.state.selection.to;
|
||||
editor.chain().focus().setLink({ href: target }).setTextSelection(end).run();
|
||||
return true;
|
||||
}
|
||||
// HTML (из браузера, из другой страницы вики) — как по умолчанию в TipTap.
|
||||
if (cd.types.includes("text/html")) return false;
|
||||
if (!plain || editor.isActive("codeBlock") || editor.isActive("code") || !looksLikeMarkdown(plain)) return false;
|
||||
return pasteMarkdown(editor, plain);
|
||||
},
|
||||
handleDrop: (view, event) => {
|
||||
const files = [...(event.dataTransfer?.files ?? [])].filter((f) => f.type.startsWith("image/"));
|
||||
|
||||
Reference in New Issue
Block a user