Files
plainleaf/client/components/plainleaf_markdown_toolbar.ts
T
wushenghuaandCodex 645c04ed01 feat: 优化移动端笔记与编辑体验
手机端改用抽屉侧栏并移除底部导航,补充 Markdown 编辑工具栏,收拢开发者选项并改善社区扩展下载错误反馈。

Co-Authored-By: Codex <noreply@anthropic.com>
2026-08-13 06:42:18 +08:00

131 lines
3.7 KiB
TypeScript

import type { EditorView } from "@codemirror/view";
export type PlainleafEditorAction =
| "undo"
| "redo"
| "heading-1"
| "heading-2"
| "bold"
| "italic"
| "bullet-list"
| "numbered-list"
| "task-list"
| "quote"
| "code"
| "link"
| "image";
const blockPrefix =
/^\s*(?:#{1,6}\s+|[-*+]\s+(?:\[[ xX]\]\s+)?|\d+[.)]\s+|>\s+)?/;
function replaceSelectedLines(
view: EditorView,
makePrefix: (index: number) => string,
activePrefix: RegExp,
): void {
const selection = view.state.selection.main;
const firstLine = view.state.doc.lineAt(selection.from);
const lastPosition = Math.max(selection.from, selection.to - 1);
const lastLine = view.state.doc.lineAt(lastPosition);
const source = view.state.doc.sliceString(firstLine.from, lastLine.to);
const lines = source.split("\n");
const remove = lines.every((line) => !line.trim() || activePrefix.test(line));
const replacement = lines
.map((line, index) => {
if (!line.trim()) return line;
const content = line.replace(blockPrefix, "");
return remove ? content : `${makePrefix(index)}${content}`;
})
.join("\n");
view.dispatch({
changes: { from: firstLine.from, to: lastLine.to, insert: replacement },
selection: {
anchor: firstLine.from,
head: firstLine.from + replacement.length,
},
});
}
function wrapSelection(
view: EditorView,
before: string,
after = before,
placeholder = "文字",
): void {
const selection = view.state.selection.main;
const selected = view.state.doc.sliceString(selection.from, selection.to);
const text = selected || placeholder;
const insert = `${before}${text}${after}`;
view.dispatch({
changes: { from: selection.from, to: selection.to, insert },
selection: selected
? {
anchor: selection.from + before.length,
head: selection.to + before.length,
}
: {
anchor: selection.from + before.length,
head: selection.from + before.length + placeholder.length,
},
});
}
export function applyPlainleafEditorAction(
view: EditorView,
action: PlainleafEditorAction,
): boolean {
switch (action) {
case "heading-1":
replaceSelectedLines(view, () => "# ", /^\s*#\s+/);
break;
case "heading-2":
replaceSelectedLines(view, () => "## ", /^\s*##\s+/);
break;
case "bullet-list":
replaceSelectedLines(view, () => "- ", /^\s*[-*+]\s+/);
break;
case "numbered-list":
replaceSelectedLines(view, (index) => `${index + 1}. `, /^\s*\d+[.)]\s+/);
break;
case "task-list":
replaceSelectedLines(view, () => "- [ ] ", /^\s*[-*+]\s+\[[ xX]\]\s+/);
break;
case "quote":
replaceSelectedLines(view, () => "> ", /^\s*>\s+/);
break;
case "bold":
wrapSelection(view, "**");
break;
case "italic":
wrapSelection(view, "_");
break;
case "code": {
const selection = view.state.selection.main;
const selected = view.state.doc.sliceString(selection.from, selection.to);
if (selected.includes("\n"))
wrapSelection(view, "```\n", "\n```", "代码");
else wrapSelection(view, "`", "`", "代码");
break;
}
case "link": {
const selection = view.state.selection.main;
const selected = view.state.doc.sliceString(selection.from, selection.to);
const label = selected || "链接文字";
const insert = `[${label}](https://)`;
const urlFrom = selection.from + label.length + 3;
view.dispatch({
changes: { from: selection.from, to: selection.to, insert },
selection: { anchor: urlFrom, head: urlFrom + 8 },
});
break;
}
case "undo":
case "redo":
case "image":
return false;
}
view.focus();
return true;
}