最近因为写ToDoList软件的时候需要一款MarkDown编辑器,尝试过很多种。都有些不尽人意的地方,后来热心群友推荐了Milkdown这款编辑器,当时一看到界面就被它的颜值吸引到了,在体验到他便捷的语法后当即决定使用这款编辑器。
但是使用过程中,并不是很顺畅,文档有些感觉就像是话说了一半的样子,网上的文档也不是很多。所以这里将进行一些使用过程中遇见的问题记录。方便自己帮助他人。
官方示例:
import {
createCmdKey,
MilkdownPlugin,
CommandsReady,
commandsCtx,
schemaCtx,
} from "@milkdown/core";
import { wrapIn } from "prosemirror-commands";
export const WrapInBlockquote = createCmdKey();
const plugin: MilkdownPlugin = () => async (ctx) => {
// wait for command manager ready
await ctx.wait(CommandsReady);
const commandManager = ctx.get(commandsCtx);
const schema = ctx.get(schemaCtx);
commandManager.create(WrapInBlockquote, () =>
wrapIn(schema.nodes.blockquote)
);
};
// call command
commandManager.call(WrapInBlockquote);
我的示例:
import {
createCmdKey,
MilkdownPlugin,
CommandsReady,
commandsCtx,
schemaCtx,
} from "@milkdown/core";
import { wrapIn } from "prosemirror-commands";
export const taskList = createCmdKey("TaskList");
export const taskListPlugin: MilkdownPlugin = () => async (ctx) => {
// 等待命令管理器初始化完成
await ctx.wait(CommandsReady);
const commandManager = ctx.get(commandsCtx);
const schema = ctx.get(schemaCtx);
// 下方wrapIn(schema.nodes.blockquote)可以修改为自己所期望的行为
commandManager.create(taskList, () => wrapIn(schema.nodes.blockquote));
};
使用方式:
const { editor } = useEditor(
(root) =>
Editor.make()
.config((ctx) => {
ctx.set(rootCtx, root);
})
.use(taskListPlugin) // 你导出的插件名
);
该功能主要是自定义 markdown 中元素的样式,比如超链接的样子。唯一的坑点就是你引入@milkdown/preset-gfm这个包后,会和官方示例冲突,所以使用官方示例的时候需要注释掉@milkdown/preset-gfm这个插件的使用。即可解决冲突问题。
官方的解释:https://github.com/Saul-Mirone/milkdown/issues/294
官方示例:
import {
slashPlugin,
slash,
createDropdownItem,
defaultActions,
} from "@milkdown/plugin-slash";
import { themeManagerCtx, commandsCtx } from "@milkdown/core";
Editor.make().use(
slash.configure(slashPlugin, {
config: (ctx) => {
// Get default slash plugin items
const actions = defaultActions(ctx);
// Define a status builder
return ({ isTopLevel, content, parentNode }) => {
// You can only show something at root level
if (!isTopLevel) return null;
// Empty content ? Set your custom empty placeholder !
if (!content) {
return { placeholder: "Type / to use the slash commands..." };
}
// Define the placeholder & actions (dropdown items) you want to display depending on content
if (content.startsWith("/")) {
// Add some actions depending on your content's parent node
if (parentNode.type.name === "customNode") {
actions.push({
id: "custom",
dom: createDropdownItem(ctx.get(themeManagerCtx), "Custom", "h1"),
command: () =>
ctx.get(commandsCtx).call(/* Add custom command here */),
keyword: ["custom"],
typeName: "heading",
});
}
return content === "/"
? {
placeholder: "Type to filter...",
actions,
}
: {
actions: actions.filter(({ keyword }) =>
keyword.some((key) =>
key.includes(content.slice(1).toLocaleLowerCase())
)
),
};
}
};
},
})
);
我的示例:
import { commandsCtx, Ctx, schemaCtx, themeManagerCtx } from "@milkdown/core";
import {
createDropdownItem,
slash,
slashPlugin,
WrappedAction,
} from "@milkdown/plugin-slash";
// 自定义下拉菜单
const diyActions = (ctx: Ctx, input = "/"): WrappedAction[] => {
const { nodes } = ctx.get(schemaCtx);
const actions: Array<
WrappedAction & { keyword: string[]; typeName: string }
> = [
{
id: "h1",
dom: createDropdownItem(ctx.get(themeManagerCtx), "标签一", "h1"),
command: () => ctx.get(commandsCtx).call("TurnIntoHeading", 1),
keyword: ["h1", "large heading"],
typeName: "heading",
},
{
id: "h2",
dom: createDropdownItem(ctx.get(themeManagerCtx), "标签二", "h2"),
command: () => ctx.get(commandsCtx).call("TurnIntoHeading", 2),
keyword: ["h2", "medium heading"],
typeName: "heading",
},
{
id: "h3",
dom: createDropdownItem(ctx.get(themeManagerCtx), "标签三", "h3"),
command: () => ctx.get(commandsCtx).call("TurnIntoHeading", 3),
keyword: ["h3", "small heading"],
typeName: "heading",
},
{
id: "bulletList",
dom: createDropdownItem(
ctx.get(themeManagerCtx),
"无序列表",
"bulletList"
),
command: () => ctx.get(commandsCtx).call("WrapInBulletList"),
keyword: ["bullet list", "ul"],
typeName: "bullet_list",
},
{
id: "orderedList",
dom: createDropdownItem(
ctx.get(themeManagerCtx),
"有序列表",
"orderedList"
),
command: () => ctx.get(commandsCtx).call("WrapInOrderedList"),
keyword: ["ordered list", "ol"],
typeName: "ordered_list",
},
{
id: "taskList",
dom: createDropdownItem(ctx.get(themeManagerCtx), "任务列表", "taskList"),
command: () => ctx.get(commandsCtx).call("TurnIntoTaskList"),
keyword: ["task list", "task"],
typeName: "task_list_item",
},
{
id: "image",
dom: createDropdownItem(ctx.get(themeManagerCtx), "图片", "image"),
command: () => ctx.get(commandsCtx).call("InsertImage"),
keyword: ["image"],
typeName: "image",
},
{
id: "blockquote",
dom: createDropdownItem(ctx.get(themeManagerCtx), "引用", "quote"),
command: () => ctx.get(commandsCtx).call("WrapInBlockquote"),
keyword: ["quote", "blockquote"],
typeName: "blockquote",
},
{
id: "table",
dom: createDropdownItem(ctx.get(themeManagerCtx), "表格", "table"),
command: () => ctx.get(commandsCtx).call("InsertTable"),
keyword: ["table"],
typeName: "table",
},
{
id: "code",
dom: createDropdownItem(ctx.get(themeManagerCtx), "代码块", "code"),
command: () => ctx.get(commandsCtx).call("TurnIntoCodeFence"),
keyword: ["code"],
typeName: "fence",
},
{
id: "divider",
dom: createDropdownItem(ctx.get(themeManagerCtx), "分隔符", "divider"),
command: () => ctx.get(commandsCtx).call("InsertHr"),
keyword: ["divider", "hr"],
typeName: "hr",
},
{
id: "h4",
dom: createDropdownItem(ctx.get(themeManagerCtx), "测试", "h3"),
command: () => {
ctx.get(commandsCtx).call("TaskList");
},
keyword: ["h4", "Test"],
typeName: "heading",
},
];
const userInput = input.slice(1).toLocaleLowerCase();
return actions
.filter(
(action) =>
!!nodes[action.typeName] &&
action.keyword.some((keyword) => keyword.includes(userInput))
)
.map(({ keyword, typeName, ...action }) => action);
};
export const diySlash = slash.configure(slashPlugin, {
config: (ctx) => {
// 获取自定义的斜线命令
const actions: any = diyActions(ctx);
// Define a status builder
return ({ isTopLevel, content, parentNode }) => {
if (!isTopLevel) return null;
if (!content) {
return { placeholder: "输入/来使用斜杠命令..." };
}
if (content.startsWith("/")) {
return content === "/"
? {
placeholder: "请选择类型...",
actions: diyActions(ctx),
}
: {
actions: diyActions(ctx, content),
};
}
};
},
});
使用方式:
const { editor } = useEditor((root) => {
const editor: Editor undefined = Editor.make()
.config((ctx) => {
ctx.set(rootCtx, root);
})
// 自定义斜线命令
.use(diySlash)
// 自定义命令,这斜线命令使用了自定义的命令,所以这里也一定要加上。如何自定义指令
.use(taskListPlugin);
return editor;
});
我这样写的几个好处是:
- 可以自定义列表的排序
- 可以添加自定义指令,比如我的最后一个就是自定义的指令
- 通俗易懂,比官方的看的更明白
Comments