Tooltip plugin system for the Milkdown markdown editor with configurable positioning and display logic
npx @tessl/cli install tessl/npm-milkdown--plugin-tooltip@7.15.00
# Milkdown Plugin Tooltip
1
2
Milkdown Plugin Tooltip provides a tooltip plugin system for the Milkdown markdown editor framework. It offers a factory function for creating customizable tooltips with identifiers, and a provider class that handles intelligent positioning and display logic using floating-ui.
3
4
## Package Information
5
6
- **Package Name**: @milkdown/plugin-tooltip
7
- **Package Type**: npm
8
- **Language**: TypeScript
9
- **Installation**: `npm install @milkdown/plugin-tooltip`
10
11
## Core Imports
12
13
```typescript
14
import { tooltipFactory, TooltipProvider, type TooltipProviderOptions } from "@milkdown/plugin-tooltip";
15
import { $ctx, $prose } from "@milkdown/utils";
16
import type { SliceType } from "@milkdown/ctx";
17
```
18
19
For CommonJS:
20
21
```javascript
22
const { tooltipFactory, TooltipProvider } = require("@milkdown/plugin-tooltip");
23
```
24
25
## Basic Usage
26
27
```typescript
28
import { tooltipFactory, TooltipProvider } from "@milkdown/plugin-tooltip";
29
import { Editor } from "@milkdown/core";
30
31
// Create a tooltip plugin with unique identifier
32
const [tooltipSpec, tooltipPlugin] = tooltipFactory("myTooltip");
33
34
// Create tooltip content element
35
const tooltipElement = document.createElement("div");
36
tooltipElement.innerHTML = "Custom tooltip content";
37
tooltipElement.className = "my-tooltip";
38
39
// Configure tooltip provider
40
const tooltipProvider = new TooltipProvider({
41
content: tooltipElement,
42
debounce: 300,
43
shouldShow: (view) => !view.state.selection.empty,
44
offset: { mainAxis: 10, crossAxis: 0 },
45
});
46
47
// Set up tooltip plugin with provider
48
tooltipSpec(ctx => ({
49
view: (view) => ({
50
update: (view, prevState) => {
51
tooltipProvider.update(view, prevState);
52
},
53
destroy: () => {
54
tooltipProvider.destroy();
55
}
56
})
57
}));
58
59
// Use in editor
60
const editor = Editor.make()
61
.config((ctx) => {
62
ctx.set(tooltipSpec.key, tooltipSpec);
63
})
64
.use([tooltipPlugin]);
65
```
66
67
## Architecture
68
69
The plugin follows Milkdown's plugin architecture with two core components:
70
71
- **Tooltip Factory**: Creates plugin instances with unique identifiers for integration with ProseMirror's plugin system
72
- **Tooltip Provider**: Manages tooltip lifecycle, positioning, and display logic using floating-ui
73
- **Positioning System**: Intelligent positioning with flip, offset, and shift middleware for optimal placement
74
- **Debounced Updates**: Performance optimization through configurable update throttling
75
76
## Capabilities
77
78
### Tooltip Plugin Factory
79
80
Creates a tooltip plugin with a unique identifier for integration with Milkdown's plugin system.
81
82
```typescript { .api }
83
/**
84
* Create a tooltip plugin with a unique id.
85
* @param id - Unique string identifier for the tooltip plugin
86
* @returns Tuple containing context specification and prose plugin with additional properties
87
*/
88
function tooltipFactory<Id extends string, State = any>(id: Id): TooltipPlugin<Id, State>;
89
90
type TooltipSpecId<Id extends string> = `${Id}_TOOLTIP_SPEC`;
91
92
type TooltipPlugin<Id extends string, State = any> = [
93
$Ctx<PluginSpec<State>, TooltipSpecId<Id>>,
94
$Prose,
95
] & {
96
key: SliceType<PluginSpec<State>, TooltipSpecId<Id>>;
97
pluginKey: $Prose['key'];
98
};
99
```
100
101
### Tooltip Provider
102
103
Manages tooltip positioning, display logic, and lifecycle using floating-ui for intelligent positioning.
104
105
```typescript { .api }
106
/**
107
* A provider for creating and managing tooltips with intelligent positioning
108
*/
109
class TooltipProvider {
110
/** The root element of the tooltip */
111
element: HTMLElement;
112
113
/** Callback executed when tooltip is shown */
114
onShow: () => void;
115
116
/** Callback executed when tooltip is hidden */
117
onHide: () => void;
118
119
constructor(options: TooltipProviderOptions);
120
121
/**
122
* Update provider state by editor view
123
* @param view - Current editor view
124
* @param prevState - Previous editor state for comparison
125
*/
126
update(view: EditorView, prevState?: EditorState): void;
127
128
/**
129
* Show the tooltip, optionally positioned relative to a virtual element
130
* @param virtualElement - Optional element to position tooltip relative to
131
*/
132
show(virtualElement?: VirtualElement): void;
133
134
/**
135
* Hide the tooltip
136
*/
137
hide(): void;
138
139
/**
140
* Destroy the tooltip and cancel pending updates
141
*/
142
destroy(): void;
143
}
144
```
145
146
### Tooltip Provider Configuration
147
148
Configuration options for customizing tooltip behavior and positioning.
149
150
```typescript { .api }
151
interface TooltipProviderOptions {
152
/** The tooltip content */
153
content: HTMLElement;
154
155
/** The debounce time for updating tooltip, 200ms by default */
156
debounce?: number;
157
158
/** Function to determine whether the tooltip should be shown */
159
shouldShow?: (view: EditorView, prevState?: EditorState) => boolean;
160
161
/** The offset to get the block. Default is 0 */
162
offset?: OffsetOptions;
163
164
/** The amount to shift options the block by */
165
shift?: ShiftOptions;
166
167
/** Other middlewares for floating ui. This will be added after the internal middlewares */
168
middleware?: Middleware[];
169
170
/** Options for floating ui. If you pass `middleware` or `placement`, it will override the internal settings */
171
floatingUIOptions?: Partial<ComputePositionConfig>;
172
173
/** The root element that the tooltip will be appended to */
174
root?: HTMLElement;
175
}
176
```
177
178
## Types
179
180
```typescript { .api }
181
// Milkdown core types used in API
182
interface $Ctx<T, N extends string> {
183
key: SliceType<T, N>;
184
meta: { package: string; displayName: string };
185
}
186
187
interface $Prose {
188
key: Symbol;
189
meta: { package: string; displayName: string };
190
}
191
192
interface SliceType<T, N extends string> {
193
id: symbol;
194
name: N;
195
}
196
197
interface PluginSpec<State = any> {
198
props?: any;
199
state?: any;
200
key?: any;
201
view?: (view: EditorView) => {
202
update?: (view: EditorView, prevState: EditorState) => void;
203
destroy?: () => void;
204
};
205
}
206
207
// Floating-ui types used in configuration
208
interface OffsetOptions {
209
mainAxis?: number;
210
crossAxis?: number;
211
alignmentAxis?: number | null;
212
}
213
214
interface ShiftOptions {
215
mainAxis?: boolean;
216
crossAxis?: boolean;
217
limiter?: {
218
fn: (state: MiddlewareState) => Coords;
219
options?: any;
220
};
221
}
222
223
interface VirtualElement {
224
getBoundingClientRect(): ClientRect | DOMRect;
225
contextElement?: Element;
226
}
227
228
interface Middleware {
229
name: string;
230
options?: any;
231
fn: (state: MiddlewareState) => Coords | Promise<Coords>;
232
}
233
234
interface ComputePositionConfig {
235
placement?: Placement;
236
strategy?: Strategy;
237
middleware?: Array<Middleware | null | undefined | false>;
238
platform?: Platform;
239
}
240
241
// ProseMirror types used in API
242
interface EditorView {
243
state: EditorState;
244
dom: HTMLElement;
245
hasFocus(): boolean;
246
editable: boolean;
247
composing: boolean;
248
}
249
250
interface EditorState {
251
doc: Node;
252
selection: Selection;
253
}
254
```
255
256
## Usage Examples
257
258
### Basic Tooltip with Text Selection
259
260
```typescript
261
import { tooltipFactory, TooltipProvider } from "@milkdown/plugin-tooltip";
262
263
// Create tooltip plugin
264
const [tooltipSpec, tooltipPlugin] = tooltipFactory("selectionTooltip");
265
266
// Create tooltip content
267
const content = document.createElement("div");
268
content.innerHTML = "<button>Bold</button><button>Italic</button>";
269
content.className = "selection-tooltip";
270
271
// Configure provider to show on text selection
272
const provider = new TooltipProvider({
273
content,
274
shouldShow: (view) => !view.state.selection.empty, // Show when text is selected
275
offset: { mainAxis: 8 },
276
shift: { crossAxis: true },
277
});
278
279
// Set up plugin specification
280
tooltipSpec(ctx => ({
281
view: () => ({
282
update: provider.update,
283
destroy: provider.destroy,
284
})
285
}));
286
```
287
288
### Custom Positioning with Middleware
289
290
```typescript
291
import { flip, hide } from "@floating-ui/dom";
292
293
const provider = new TooltipProvider({
294
content: tooltipElement,
295
middleware: [
296
hide(), // Hide tooltip when reference is not visible
297
],
298
floatingUIOptions: {
299
placement: "bottom-start",
300
strategy: "fixed",
301
},
302
debounce: 100, // Faster updates
303
});
304
```
305
306
### Tooltip with Custom Show/Hide Logic
307
308
```typescript
309
const provider = new TooltipProvider({
310
content: tooltipElement,
311
shouldShow: (view, prevState) => {
312
const { selection } = view.state;
313
314
// Only show for text selections longer than 5 characters
315
if (selection.empty) return false;
316
317
const selectedText = view.state.doc.textBetween(
318
selection.from,
319
selection.to
320
);
321
322
return selectedText.length > 5;
323
},
324
});
325
326
// Add event handlers
327
provider.onShow = () => {
328
console.log("Tooltip shown");
329
provider.element.style.opacity = "1";
330
};
331
332
provider.onHide = () => {
333
console.log("Tooltip hidden");
334
provider.element.style.opacity = "0";
335
};
336
```
337
338
### Multiple Tooltips with Different Behaviors
339
340
```typescript
341
// Create multiple tooltip plugins with unique identifiers
342
const [formatTooltipSpec, formatTooltipPlugin] = tooltipFactory("formatting");
343
const [linkTooltipSpec, linkTooltipPlugin] = tooltipFactory("linkPreview");
344
345
// Configure different providers
346
const formatProvider = new TooltipProvider({
347
content: formatToolbarElement,
348
shouldShow: (view) => !view.state.selection.empty,
349
offset: { mainAxis: 10 },
350
});
351
352
const linkProvider = new TooltipProvider({
353
content: linkPreviewElement,
354
shouldShow: (view) => {
355
// Show when hovering over links
356
const { $head } = view.state.selection;
357
return $head.parent.type.name === "link";
358
},
359
debounce: 500,
360
});
361
```