Add documentation for using Milkdown with various frameworks
- Created a new document for using components in Milkdown. - Added a guide for using plugins in Milkdown, including toggling plugins programmatically and listing official plugins. - Introduced a recipe for integrating Milkdown with Angular, including installation steps and component creation. - Added a recipe for using Milkdown with Next.js, detailing installation and component setup. - Created a guide for integrating Milkdown with NuxtJS, including installation and component creation. - Added a comprehensive guide for using Milkdown with React, covering both Crepe and core Milkdown usage. - Introduced a recipe for SolidJS integration with Milkdown, including installation and component creation. - Added a guide for using Milkdown with Svelte, detailing installation and component setup. - Created a comprehensive guide for integrating Milkdown with Vue, covering both Crepe and core Milkdown usage. - Added a recipe for using Milkdown with Vue2, including installation and component creation.
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# Announcing Telemetry Inspector
|
||||
|
||||
There's a lot of questions from community asking that how can they know what plugins are enabled.
|
||||
From Milkdown@7.2, we've added telemetries for milkdown, it can be available by inspectors.
|
||||
|
||||
With this API, you can inspect editor inner status.
|
||||
You can even use visualizer to visualize the data. We create a simple example on [our playground](/playground).
|
||||
|
||||

|
||||
|
||||
## Get Started
|
||||
|
||||
Inspector will be a top-level API in Milkdown. You can use it like this:
|
||||
|
||||
```ts
|
||||
import { Editor } from "@milkdown/core";
|
||||
import { Telemetry } from "@milkdown/ctx";
|
||||
|
||||
const editor = await Editor.make()
|
||||
// Inspector is disabled by default considering performance. You need to enable it manually.
|
||||
.enableInspector()
|
||||
// ...
|
||||
.create();
|
||||
|
||||
const telemetry: Telemetry[] = editor.inspect();
|
||||
```
|
||||
|
||||
The `Telemetry` interface will have the following fields:
|
||||
|
||||
```ts
|
||||
interface Telemetry {
|
||||
// User defined information for the plugin.
|
||||
metadata: Meta;
|
||||
|
||||
// The slices and their current value defined by the plugin.
|
||||
injectedSlices: { name: string; value: unknown }[];
|
||||
|
||||
// The slices and their current value consumed by the plugin.
|
||||
consumedSlices: { name: string; value: unknown }[];
|
||||
|
||||
// The timers and their duration defined by the plugin.
|
||||
recordedTimers: { name: string; duration: number; status: TimerStatus }[];
|
||||
|
||||
// The timers and their duration consumed by the plugin.
|
||||
// Generally, the plugin will wait for them.
|
||||
waitTimers: { name: string; duration: number; status: TimerStatus }[];
|
||||
}
|
||||
|
||||
type TimerStatus = "pending" | "resolved" | "rejected";
|
||||
|
||||
interface Meta {
|
||||
displayName: string;
|
||||
description?: string;
|
||||
package: string;
|
||||
group?: string;
|
||||
additional?: Record<string, any>;
|
||||
}
|
||||
```
|
||||
|
||||
For every plugin, it'll have a telemetry if it has metadata declared.
|
||||
With the data, you'll know the sequence of the plugins loaded, the slices and timers they defined and consumed.
|
||||
For example:
|
||||
|
||||
```ts
|
||||
[
|
||||
{
|
||||
metadata: {
|
||||
displayName: "Config",
|
||||
package: "@milkdown/core",
|
||||
group: "System",
|
||||
},
|
||||
injectedSlices: [],
|
||||
consumedSlices: [
|
||||
/* ... */
|
||||
],
|
||||
recordedTimers: [
|
||||
{
|
||||
name: "ConfigReady",
|
||||
duration: 3,
|
||||
status: "resolved",
|
||||
},
|
||||
],
|
||||
waitTimers: [],
|
||||
},
|
||||
{
|
||||
metadata: {
|
||||
displayName: "Init",
|
||||
package: "@milkdown/core",
|
||||
group: "System",
|
||||
},
|
||||
injectedSlices: [],
|
||||
consumedSlices: [
|
||||
/* ... */
|
||||
],
|
||||
recordedTimers: [
|
||||
{
|
||||
name: "InitReady",
|
||||
duration: 5,
|
||||
status: "resolved",
|
||||
},
|
||||
],
|
||||
waitTimers: [
|
||||
{
|
||||
name: "ConfigReady",
|
||||
duration: 5,
|
||||
status: "resolved",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
From above information, we can know that the `Init` plugin wait for `Config` plugin to be ready.
|
||||
We can build a sequence diagram from the data.
|
||||
|
||||

|
||||
|
||||
## Add Metadata for Plugin
|
||||
|
||||
For plugin maintainers, you can add metadata to your plugin to make it more friendly to the inspector.
|
||||
|
||||
```ts
|
||||
import { MilkdownPlugin } from "@milkdown/ctx";
|
||||
|
||||
const yourMilkdownPlugin: MilkdownPlugin = () => {
|
||||
/* your implementation */
|
||||
};
|
||||
|
||||
yourMilkdownPlugin.metadata = {
|
||||
displayName: "Your Plugin",
|
||||
package: "your-plugin-package",
|
||||
description: "Your plugin description",
|
||||
group: "If you have a lot of plugins in your package, you can group them.",
|
||||
addtitional: {
|
||||
/* You can add any additional information here. */
|
||||
version: "1.0.0",
|
||||
authror: "Mike",
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
With metadata, your plugin will report telemetry correctly to the inspector.
|
||||
@@ -0,0 +1,247 @@
|
||||
# Build Your Own Milkdown Copilot
|
||||
|
||||
OpenAI introduced ChatGPT in 2020, which is a chatbot that can generate natural language responses to user input.
|
||||
Which brings us a new way to interact with devices and applications.
|
||||
Nowadays, there are more and more tools that are powered by AI. Such as Notion, GitHub and even Microsoft 365.
|
||||
|
||||
Since OpenAI also released the [API](https://openai.com/blog/openai-api) of it. And Milkdown is composed by plugins.
|
||||
I think it's possible to build a Milkdown Copilot Plugin that can help you write documents. So I did it.
|
||||
Let's see the result.
|
||||
|
||||

|
||||
|
||||
Looks cool, right? But how does it work? I'll explain it in the following sections.
|
||||
|
||||
## Prepare a Backend
|
||||
|
||||
**Before we start, you need to have a OpenAI API Key.** You'll need to get one [here](https://platform.openai.com/account/api-keys).
|
||||
I'll not explain how to get it. You can find the details in their [official docs](https://platform.openai.com/).
|
||||
|
||||
I'll use Node.js to build the backend. You can use any language you like.
|
||||
The backend is very simple. It just calls the OpenAI API and returns the result.
|
||||
|
||||
```ts
|
||||
import { Configuration, OpenAIApi } from "openai";
|
||||
|
||||
const configuration = new Configuration({
|
||||
// Get your API key from env variable
|
||||
apiKey: process.env.OPENAPI_KEY,
|
||||
});
|
||||
const openai = new OpenAIApi(configuration);
|
||||
|
||||
export const handler = async (req, res, next) => {
|
||||
if (req.path === "/api/copilot" && req.method === "POST") {
|
||||
const buffers = [];
|
||||
|
||||
// Get the body of the request.
|
||||
const body = JSON.parse(req.body);
|
||||
|
||||
// Get prompt from the body.
|
||||
const { prompt } = body;
|
||||
const completion = await openai.createCompletion({
|
||||
// Pick a model you like
|
||||
model: "text-davinci-003",
|
||||
prompt,
|
||||
});
|
||||
const hint = completion.data.choices[0].text;
|
||||
return res.end(JSON.stringify({ hint }));
|
||||
}
|
||||
next();
|
||||
return;
|
||||
};
|
||||
```
|
||||
|
||||
We watch the `/api/copilot` route and call the OpenAI API when we receive a POST request.
|
||||
The post request should contain a `prompt` field which is the text that we want to complete.
|
||||
|
||||
To call our API, we just need one single helper in browser environment:
|
||||
|
||||
```ts
|
||||
async function fetchAIHint(prompt: string) {
|
||||
const data: Record<string, string> = { prompt };
|
||||
const response = await fetch("/api/copilot", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const res = (await response.json()) as { hint: string };
|
||||
return res.hint;
|
||||
}
|
||||
```
|
||||
|
||||
## Build a Milkdown Plugin
|
||||
|
||||
Now let's focus on the Milkdown Copilot Plugin.
|
||||
|
||||
Basically I want to implement two things:
|
||||
|
||||
1. When the user types `<Enter>` or `<Space>`, they will get a hint from the copilot.
|
||||
2. When the user types `<Tab>`, they will apply the content from the hint to the editor.
|
||||
|
||||
### Overview
|
||||
|
||||
To build a bridge between the copilot and the editor,
|
||||
we can build a prosemirror plugin and use the `onKeyDown` hook to listen to the keydown event.
|
||||
|
||||
```ts
|
||||
function keyDownHandler(ctx: Ctx, event: Event) {
|
||||
if (event.key === "Enter" || event.code === "Space") {
|
||||
getHint(ctx);
|
||||
return;
|
||||
}
|
||||
if (event.key === "Tab") {
|
||||
// prevent the browser from focusing on the next element.
|
||||
event.preventDefault();
|
||||
|
||||
applyHint(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
hideHint(ctx);
|
||||
}
|
||||
```
|
||||
|
||||
When the user types `<Enter>` or `<Space>`, we will call the `getHint` function to get a hint from the copilot.
|
||||
And when the user types `<Tab>`, we will call the `applyHint` function to apply the hint to the editor.
|
||||
If user types other keys, we will hide the hint.
|
||||
|
||||
And we also need a component to render the hint. Here I choose to use a simple [widget decoration in prosemirror](https://prosemirror.net/docs/ref/#view.Decoration^widget).
|
||||
|
||||
```ts
|
||||
function renderHint(message: string) {
|
||||
const dom = document.createElement("pre");
|
||||
dom.className = "copilot-hint";
|
||||
dom.innerHTML = message;
|
||||
return dom;
|
||||
}
|
||||
```
|
||||
|
||||
So our component looks like:
|
||||
|
||||
```ts
|
||||
import { Plugin, PluginKey } from "@milkdown/prose/state";
|
||||
import { Decoration, DecorationSet } from "@milkdown/prose/view";
|
||||
import { $prose } from "@milkdown/utils";
|
||||
|
||||
const initialState = {
|
||||
deco: DecorationSet.empty,
|
||||
message: "",
|
||||
};
|
||||
|
||||
export const copilotPluginKey = new PluginKey("milkdown-copilot");
|
||||
export const copilotPlugin = $prose(
|
||||
(ctx) =>
|
||||
new Plugin({
|
||||
key: copilotPluginKey,
|
||||
props: {
|
||||
handleKeyDwon(view, event) {
|
||||
keydownHandler(ctx, event);
|
||||
},
|
||||
decorations(state) {
|
||||
return copilotPluginKey.getState(state).deco;
|
||||
},
|
||||
},
|
||||
state: {
|
||||
init() {
|
||||
return { ...initialState };
|
||||
},
|
||||
apply(tr, value, _prevState, state) {
|
||||
const message = tr.getMeta(copilotPluginKey);
|
||||
if (typeof message !== "string") return value;
|
||||
|
||||
if (message.length === 0) {
|
||||
return { ...initialState };
|
||||
}
|
||||
|
||||
const { to } = tr.selection;
|
||||
const widget = Decoration.widget(to + 1, () => renderHint(message));
|
||||
return {
|
||||
deco: DecorationSet.create(state.doc, [widget]),
|
||||
message,
|
||||
};
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
### Get Hint
|
||||
|
||||
To get a hint from the copilot, we need to get the text before the cursor.
|
||||
|
||||
```ts
|
||||
function getHint(ctx: Ctx) {
|
||||
const view = ctx.get(editorViewCtx);
|
||||
const { state } = view;
|
||||
const { tr, schema } = state;
|
||||
const { from } = tr.selection;
|
||||
|
||||
const slice = tr.doc.slice(0, from);
|
||||
const serializer = ctx.get(serializerCtx);
|
||||
const doc = schema.topNodeType.createAndFill(undefined, slice.content);
|
||||
if (!doc) return;
|
||||
|
||||
const markdown = serializer(doc);
|
||||
fetchAIHint(markdown).then((hint) => {
|
||||
const tr = view.state.tr;
|
||||
view.dispatch(tr.setMeta(copilotPluginKey, hint));
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
1. First of all, we get the `selection` from the `state` of the editor.
|
||||
2. Then we get a `slice` of the document from the start to the cursor.
|
||||
3. Then we use the `serializer` to convert the slice to markdown.
|
||||
4. After that, we call the `fetchAIHint` function to get a hint from the copilot.
|
||||
5. Finally, we dispatch a transaction with the hint message we get to update the state of the editor.
|
||||
|
||||
### Hide Hint
|
||||
|
||||
To hide the hint, we just need to dispatch a transaction with an empty message.
|
||||
|
||||
```ts
|
||||
function hideHint(ctx: Ctx) {
|
||||
const view = ctx.get(editorViewCtx);
|
||||
const { state } = view;
|
||||
const { tr } = state;
|
||||
view.dispatch(tr.setMeta(copilotPluginKey, ""));
|
||||
}
|
||||
```
|
||||
|
||||
### Apply Hint
|
||||
|
||||
Since we pass markdown to the OpenAI API. It may return a markdown snippet.
|
||||
|
||||
So, before we apply the hint to the editor, we need to convert the markdown snippet to prosemirror node.
|
||||
|
||||
```ts
|
||||
function applyHint(ctx: Ctx) {
|
||||
const view = ctx.get(editorViewCtx);
|
||||
const { state } = view;
|
||||
const { tr, schema } = state;
|
||||
|
||||
const { message } = copilotPluginKey.getState(state);
|
||||
const parser = ctx.get(parserCtx);
|
||||
const slice = parser(message);
|
||||
const dom = DOMSerializer.fromSchema(schema).serializeFragment(slice.content);
|
||||
const node = DOMParser.fromSchema(schema).parseSlice(dom);
|
||||
|
||||
// Reset the hint since it's applied
|
||||
tr.setMeta(copilotPluginKey, "")
|
||||
// Replace the selection with the hint
|
||||
.replaceSelection(node);
|
||||
|
||||
view.dispatch(tr);
|
||||
}
|
||||
```
|
||||
|
||||
1. First of all, we get the hint message from the state of the editor.
|
||||
2. Then we use the `parser` to convert the markdown snippet to prosemirror node.
|
||||
3. Finally, we dispatch a transaction to replace the selection with the hint.
|
||||
|
||||
## Conclusion
|
||||
|
||||
In this article, we have built a really simple Copilot plugin for Milkdown.
|
||||
The plugin is not perfect, but it's a good start to help you build your own.
|
||||
|
||||
The source code is available on [Milkdown/examples/vanilla-openapi](https://github.com/Milkdown/examples/tree/main/vanilla-openai).
|
||||
I hope it can give you some inspiration.
|
||||
@@ -0,0 +1,151 @@
|
||||
# Introducing Milkdown@7
|
||||
|
||||
It's been almost one year since the release of [milkdown](https://milkdown.dev) V6.
|
||||
It helped a lot of users to build their own markdown based applications.
|
||||
It has 13k downloads per month and I feel so grateful that users like that.
|
||||
|
||||
However, we noticed that there're some problems cannot be resolved if we don't make a new major version.
|
||||
What big changes did we made? I'll introduce them to you in this blog.
|
||||
|
||||
## TL;DR
|
||||
|
||||
- The editor becomes a first-class headless component.
|
||||
|
||||
- Factory plugins are fully replaced by **composable plugins**.
|
||||
|
||||
- Runtime plugin toggling is supported.
|
||||
|
||||
- Universal widget plugins.
|
||||
|
||||
- Better Vue and React support.
|
||||
|
||||
- API documentation is provided.
|
||||
|
||||
## Why Headless?
|
||||
|
||||
In the past, milkdown had a lot of internal styles to make sure the editor can work out of box and the themes are easy to create.
|
||||
However, I found it limits the users to design their own editor.
|
||||
Even worse, if you have an well designed application,
|
||||
it is really hard to keep the style of the milkdown editor same with the rest of the application.
|
||||
You'll need to override lots of styles everywhere.
|
||||
It stops a log of users from using milkdown.
|
||||
|
||||
If we think about why users need an editor,
|
||||
the most important thing is always the functionality of the editor.
|
||||
Users just want a component that can provide smooth editing experience.
|
||||
Style will always be the second thing.
|
||||
|
||||
So, why not remove all the internal styles and make the editor a headless component?
|
||||
The users can easily integrate the editor into their own application.
|
||||
They can use their own styles and even use their own components to render the editor.
|
||||
We just care about the functionality of the editor. Make sure it works well.
|
||||
|
||||
## Composable Plugins
|
||||
|
||||
Although the composable plugins have been existed in milkdown for a long time,
|
||||
we use factory plugins to create most of the official plugins in V6.
|
||||
|
||||
But, the problem is that factory plugins limit the possibility of the plugins.
|
||||
The factory plugins handle a bunch of complex logic and it is hard to extend.
|
||||
So for users who want to create a plugin in a easy way, they must follow the factory plugin's way.
|
||||
|
||||
```ts
|
||||
const nodePlugin = createPlugin(() => ({
|
||||
id: 'node',
|
||||
schema: someSchema,
|
||||
inputRules: someInputRules
|
||||
commands: someCommands
|
||||
}))
|
||||
```
|
||||
|
||||
See? You can define a lot of things inside the factory plugin.
|
||||
But if you want to use some part of them in another plugin, it's really hard to do that.
|
||||
|
||||
However, the milkdown's plugin system is designed to be flexible and composable.
|
||||
We want to let users to control the data flow entirely.
|
||||
|
||||
So, we decided to remove all the factory plugins and use composable plugins to replace them.
|
||||
The composable plugins can keep the atomicity of the plugins and make the plugin system more flexible.
|
||||
They also make the plugin system easier to maintain.
|
||||
|
||||
```ts
|
||||
const nodeSchema = $node("node", someSchema);
|
||||
const nodeInputRules = $inputRules(someInputRules);
|
||||
const nodeCommands = $commands(someCommands);
|
||||
```
|
||||
|
||||
If you want to reuse them, it also will be very easy.
|
||||
|
||||
```ts
|
||||
const anotherCommand = $commands(() => {
|
||||
return setBlockType(nodeSchema.type());
|
||||
});
|
||||
```
|
||||
|
||||
## Runtime Plugin Toggling
|
||||
|
||||
In the past, once you register a plugin, you cannot remove it.
|
||||
In V7, we support runtime plugin toggling by providing two new API: `editor.remove` and `editor.removeConfig`.
|
||||
They can let users remove the plugins and configs at runtime.
|
||||
|
||||
```ts
|
||||
import { Editor } from "@milkdown/core";
|
||||
import { someMilkdownPlugin } from "some-milkdown-plugin";
|
||||
|
||||
const editor = await Editor.config(configForPlugin)
|
||||
.use(someMilkdownPlugin)
|
||||
.create();
|
||||
|
||||
// remove plugin
|
||||
await editor.remove(someMilkdownPlugin);
|
||||
|
||||
// remove config
|
||||
editor.removeConfig(configForPlugin);
|
||||
|
||||
// add another plugin
|
||||
editor.use(anotherMilkdownPlugin);
|
||||
|
||||
// Recreate the editor to apply changes.
|
||||
await editor.create();
|
||||
```
|
||||
|
||||
Also, if you call the `editor.create` method after the editor is created,
|
||||
it will recreate the editor and apply all the changes.
|
||||
|
||||
## Universal Widget Plugins
|
||||
|
||||
We have 4 official widget plugins in V6: _slash_, _tooltip_, _block_ and _menu_.
|
||||
|
||||
They are all well designed and easy to use.
|
||||
But if you want to customize them, what you can do is really limited.
|
||||
Also, it's hard to reuse their logic even if you want to create something similar to them.
|
||||
For example, if you want to create a mention plugin which will show a list of users when you type `@`,
|
||||
you need to create a new plugin from scratch.
|
||||
|
||||
So, in V7, we make _slash_, _tooltip_ and _block_ plugins universal.
|
||||
You can use them to build you features easily.
|
||||
For example, if you want to create a mention plugin, you can use the new slash plugin to do that.
|
||||
Another example is that you can also create tooltips for different types of nodes.
|
||||
Display a tooltip with input when you focus on an image node, or display a tooltip with buttons when you select some text.
|
||||
|
||||
What about the _menu_ plugin? We removed it because we think it's easy to create a menu plugin by yourself.
|
||||
We've already done that in the [official playground](https://milkdown.dev/playground).
|
||||
And, trust me, [it won't need much code](https://github.com/milkdown/website/blob/main/src/component/Playground/Milkdown/index.tsx#L57).
|
||||
|
||||
## Better Vue and React Support
|
||||
|
||||
Thanks to the [Saul-Mirone/prosemirror-adapter project](https://github.com/Saul-Mirone/prosemirror-adapter).
|
||||
In milkdown V7. We allow users to use vue and react to render lots of parts of the editor.
|
||||
For example, you can use them to render your own code block, drag handle or even small icons.
|
||||
|
||||
- React Example: [](https://stackblitz.com/github/Milkdown/examples/tree/main/react-custom-component)
|
||||
- Vue Example: [](https://stackblitz.com/github/Milkdown/examples/tree/main/vue-custom-component)
|
||||
|
||||
## API Documentation
|
||||
|
||||
What's the hardest thing to do when maintaining an open source project?
|
||||
Keep the documentation up to date.
|
||||
Thanks to the [marijnh/builddocs project](https://github.com/marijnh/builddocs),
|
||||
we can generate the API documentation automatically from the source code.
|
||||
|
||||
We also redesigned the documentation website, provide a more powerful playground and lots of examples.
|
||||
@@ -0,0 +1,172 @@
|
||||
# Understanding Headless Slash Plugin
|
||||
|
||||
In the old Milkdown versions. The slash plugin can be used to display a list of commands when users type `/` in the editor.
|
||||
It provides a way to insert nodes and commands into the editor, and it's really easy to use.
|
||||
|
||||

|
||||
|
||||
However, it's hard to extend the slash plugin to support more commands, or if you want to change the UI of the slash plugin, you have to rewrite the whole plugin.
|
||||
But, write a new plugin is always a hard work. You have to understand a lot of context and APIs of both ProseMirror and Milkdown.
|
||||
|
||||
## User Story
|
||||
|
||||
So, why don't we provide the slash plugin as a headless plugin?
|
||||
In most cases, developers just want to make sure that when users type a special character, a dropdown menu will be displayed.
|
||||
But the trigger character and the UI of the dropdown menu are different in different cases.
|
||||
|
||||
For example:
|
||||
|
||||
- When user type `/`, the menu contains a list of **commands**.
|
||||
- When user type `:`, the menu contains a list of **emoji**.
|
||||
- When user type `@`, the menu contains a list of **users**.
|
||||
|
||||
That's the story behind the headless slash plugin. We provide the plugin to solve a single problem: **display a dropdown menu when users input satisfy a condition**.
|
||||
|
||||
## How to use
|
||||
|
||||
In the new slash plugin, you'll need to control when to display the dropdown menu by yourself.
|
||||
And you'll also need to provide the UI of the dropdown menu.
|
||||
So, you'll need to create a `SlashProvider` instance.
|
||||
|
||||
```ts
|
||||
import { slashPlugin, SlashProvider } from "@milkdown/plugin-slash";
|
||||
|
||||
const slashProvider = new SlashProvider({
|
||||
content: YourDropdownUI,
|
||||
shouldShow(this: SlashProvider, view: EditorView) {
|
||||
const currentText = this.getContent(view);
|
||||
|
||||
if (currentText === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Display the menu if the last character is `/`.
|
||||
if (currentText.endsWith("/")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Then, you can use the slash provider in your plugin view.
|
||||
|
||||
```ts
|
||||
import { EditorState } from "@milkdown/prose/state";
|
||||
import { EditorView, PluginView } from "@milkdown/prose/view";
|
||||
|
||||
function yourSlashView(): PluginView {
|
||||
return {
|
||||
update: (view: EditorView, prevState: EditorState) => {
|
||||
slashProvider.update(view, prevState);
|
||||
},
|
||||
destroy: () => {
|
||||
slashProvider.destroy();
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Last, you'll need to add the slash plugin to your editor.
|
||||
|
||||
```ts
|
||||
import { Editor } from "@milkdown/core";
|
||||
import { slashFactory } from "@milkdown/plugin-slash";
|
||||
|
||||
const slash = slashFactory("my-slash");
|
||||
|
||||
Editor.make()
|
||||
.config((ctx) => {
|
||||
ctx.set(slash.key, {
|
||||
view: slashPluginView,
|
||||
});
|
||||
})
|
||||
.use(slash)
|
||||
.create();
|
||||
```
|
||||
|
||||
## Use with Prosemirror Adapter
|
||||
|
||||
If you're using milkdown with UI frameworks like React,
|
||||
I recommend you to use the [Prosemirror Adapter](https://github.com/Saul-Mirone/prosemirror-adapter).
|
||||
It can help you build prosemirror UI components with your favorite UI framework.
|
||||
|
||||
For example, if you're using React:
|
||||
|
||||
```tsx
|
||||
import { SlashProvider } from "@milkdown/plugin-slash";
|
||||
import { useInstance } from "@milkdown/react";
|
||||
import { usePluginViewContext } from "@prosemirror-adapter/react";
|
||||
|
||||
export const DropdownMenu = () => {
|
||||
const { view, prevState } = usePluginViewContext();
|
||||
const slashProvider = useRef<SlashProvider>();
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
const [loading] = useInstance();
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current || loading) return;
|
||||
|
||||
slashProvider.current ??= new SlashProvider({
|
||||
content: divRef.current,
|
||||
// ...
|
||||
});
|
||||
|
||||
return () => {
|
||||
slashProvider.current?.destroy();
|
||||
slashProvider.current = undefined;
|
||||
};
|
||||
}, [loading, root, setOpened, setSearch, setSelected]);
|
||||
|
||||
useEffect(() => {
|
||||
slashProvider.current?.update(view, prevState);
|
||||
});
|
||||
|
||||
// Add a wrapper `div` to hide the dropdown menu when initializing.
|
||||
return (
|
||||
<div className="hidden">
|
||||
<div role="tooltip" ref={divRef}>
|
||||
<h1>Hi! I'm a dropdown menu.</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
And in your editor component:
|
||||
|
||||
```ts
|
||||
import { usePluginViewFactory } from "@prosemirror-adapter/react";
|
||||
|
||||
export const YourEditor = () => {
|
||||
const pluginViewFactory = usePluginViewFactory();
|
||||
|
||||
useEditor((editor) => {
|
||||
return Editor.make()
|
||||
.config((ctx) => {
|
||||
ctx.set(slash.key, {
|
||||
view: pluginViewFactory({
|
||||
component: DopdownMenu,
|
||||
}),
|
||||
});
|
||||
})
|
||||
.use(slash);
|
||||
});
|
||||
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
## Real World Example
|
||||
|
||||
In [milkdown playground](/playground), you can type `/` to display a dropdown menu.
|
||||
|
||||

|
||||
|
||||
You can also type `:(\S)+` (for example: `:mil`) to display a list of emojis.
|
||||
|
||||

|
||||
|
||||
You can find the source code of them in [Milkdown website](https://github.com/Milkdown/website).
|
||||
I hope you enjoy the new slash plugin.
|
||||
Reference in New Issue
Block a user