141 lines
4.8 KiB
Markdown
141 lines
4.8 KiB
Markdown
|
|
# Example: Tooltip Plugin
|
|||
|
|
|
|||
|
|
This guide walks you through creating and using **tooltip-based plugins** in Milkdown.
|
|||
|
|
You will learn how the low-level `@milkdown/plugin-tooltip` works and how to build richer experiences on top of it in **vanilla TypeScript**, **React**, and **Vue**.
|
|||
|
|
|
|||
|
|
> **TL;DR** – A tooltip in Milkdown is nothing more than a ProseMirror plugin created by `tooltipFactory(id)`.
|
|||
|
|
> It receives position information from the editor and renders any DOM of your choice.
|
|||
|
|
> Everything else (buttons, inputs, styling, framework bindings) can be composed on top of that.
|
|||
|
|
|
|||
|
|
## 1. Anatomy of a Tooltip
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
At its core the tooltip plugin exported from `@milkdown/plugin-tooltip` contains two helpers:
|
|||
|
|
|
|||
|
|
1. **`TooltipProvider`** – An utility class powered by [floating-ui](https://floating-ui.com/) to calculate the tooltip position.
|
|||
|
|
2. **`tooltipFactory(id)`** – A factory that returns a pair of Milkdown plugin slices which wire the provider into the editor.
|
|||
|
|
|
|||
|
|
The factory is extremely small (≈40 lines):
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
import { tooltipFactory } from "@milkdown/plugin-tooltip";
|
|||
|
|
|
|||
|
|
// Create a tooltip identified by the string "my".
|
|||
|
|
export const [myTooltipSpec, myTooltipPlugin] = tooltipFactory("my");
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The first element (`myTooltipSpec`) is a **ctx slice** that stores a `PluginSpec`, while the second one (`myTooltipPlugin`) is the real ProseMirror plugin which consumes that spec.
|
|||
|
|
|
|||
|
|
## 2. A Minimal Vanilla Tooltip
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
Below is the complete code for a tooltip that shows the **length of the current selection**.
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
import { Editor } from "@milkdown/kit/core";
|
|||
|
|
import { commonmark } from "@milkdown/kit/preset/commonmark";
|
|||
|
|
import { TooltipProvider, tooltipFactory } from "@milkdown/plugin-tooltip";
|
|||
|
|
|
|||
|
|
// 1) Prepare DOM that we will mount into the page.
|
|||
|
|
const el = document.createElement("div");
|
|||
|
|
el.className = "selection-length";
|
|||
|
|
el.style.cssText = `
|
|||
|
|
pointer-events:none;
|
|||
|
|
background:#333;color:#fff;padding:2px 6px;border-radius:4px;font-size:12px;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
// 2) Build a provider which updates the content.
|
|||
|
|
const provider = new TooltipProvider({
|
|||
|
|
content: el,
|
|||
|
|
shouldShow: (view) => !!view.state.selection.content().size,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 3) Bridge provider & editor.
|
|||
|
|
const tooltip = tooltipFactory("sel-length");
|
|||
|
|
const tooltipConfig = (ctx: Ctx) => {
|
|||
|
|
ctx.set(selectionTooltipSpec.key, {
|
|||
|
|
view: () => ({
|
|||
|
|
update: provider.update,
|
|||
|
|
destroy: provider.destroy,
|
|||
|
|
}),
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
Editor.make().config(tooltipConfig).use(commonmark).use(tooltip).create();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Key points:
|
|||
|
|
|
|||
|
|
- We **create** any DOM element we like (`el`).
|
|||
|
|
- `TooltipProvider` tracks the editor position and moves the element.
|
|||
|
|
- `tooltipFactory` wraps the provider into a pluggable slice.
|
|||
|
|
|
|||
|
|
## 3. Framework Examples
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
Sometimes building UI is easier in your favourite framework.
|
|||
|
|
Because the tooltip provider only deals with **DOM elements**, you can freely render React, Vue or Svelte components and pass their root node to the provider.
|
|||
|
|
|
|||
|
|
### React
|
|||
|
|
|
|||
|
|
::iframe{src="https://stackblitz.com/github/Milkdown/examples/tree/main/react-tooltip"}
|
|||
|
|
|
|||
|
|
The React example shows how to:
|
|||
|
|
|
|||
|
|
1. Create a React component (`<SelectionTooltip/>`).
|
|||
|
|
2. Render it into a portal and give the root HTML element to `TooltipProvider`.
|
|||
|
|
3. Re-use React state/hooks while Milkdown takes care of positioning.
|
|||
|
|
|
|||
|
|
### Vue
|
|||
|
|
|
|||
|
|
::iframe{src="https://stackblitz.com/github/Milkdown/examples/tree/main/vue-tooltip"}
|
|||
|
|
|
|||
|
|
The Vue example follows the same pattern with `defineComponent` and `teleport`.
|
|||
|
|
|
|||
|
|
## 4. Real-world Examples
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 4-1. Link Tooltip (_@milkdown/component/link-tooltip_)
|
|||
|
|
|
|||
|
|
The [link tooltip](https://github.com/Milkdown/milkdown/tree/main/packages/components/src/link-tooltip) demonstrates how to:
|
|||
|
|
|
|||
|
|
- Maintain UI **state** (`preview` vs `edit`) in ctx slices.
|
|||
|
|
- Communicate with the editor through an **API slice** (add / edit / remove links).
|
|||
|
|
- Render framework-agnostic UI inside a tooltip provider.
|
|||
|
|
|
|||
|
|
Have a look at the files below to see those techniques in action:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
packages/components/src/link-tooltip/
|
|||
|
|
├── slices.ts # state & API slices
|
|||
|
|
├── tooltips.ts # preview & edit providers
|
|||
|
|
└── component.tsx # (framework examples)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4-2. Toolbar Feature (_@milkdown/crepe/feature/toolbar_)
|
|||
|
|
|
|||
|
|
The toolbar in the [crepe](https://github.com/Milkdown/milkdown/tree/main/packages/crepe) package pushes the idea further by:
|
|||
|
|
|
|||
|
|
- Using multiple tooltip instances (one per button group).
|
|||
|
|
- Rendering the UI with Vue _inside_ the provider.
|
|||
|
|
- Sharing configuration via ctx slices so that every button is extensible by third-party plugins.
|
|||
|
|
|
|||
|
|
You can browse the implementation starting from
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
packages/crepe/src/feature/toolbar/component.tsx
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 5. Summary & Next Steps
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
- `@milkdown/plugin-tooltip` offers **just enough** abstraction: positioning & lifecycle.
|
|||
|
|
- Everything else – **state, styling, framework integration** – is totally up to you.
|
|||
|
|
|
|||
|
|
Try to customise one of the examples above, then ship your own tooltip-powered features 🤟.
|