手机端改用抽屉侧栏并移除底部导航,补充 Markdown 编辑工具栏,收拢开发者选项并改善社区扩展下载错误反馈。 Co-Authored-By: Codex <noreply@anthropic.com>
131 lines
3.7 KiB
TypeScript
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;
|
|
}
|