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.0Milkdown 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.
npm install @milkdown/plugin-tooltipimport { tooltipFactory, TooltipProvider, type TooltipProviderOptions } from "@milkdown/plugin-tooltip";
import { $ctx, $prose } from "@milkdown/utils";
import type { SliceType } from "@milkdown/ctx";For CommonJS:
const { tooltipFactory, TooltipProvider } = require("@milkdown/plugin-tooltip");import { tooltipFactory, TooltipProvider } from "@milkdown/plugin-tooltip";
import { Editor } from "@milkdown/core";
// Create a tooltip plugin with unique identifier
const [tooltipSpec, tooltipPlugin] = tooltipFactory("myTooltip");
// Create tooltip content element
const tooltipElement = document.createElement("div");
tooltipElement.innerHTML = "Custom tooltip content";
tooltipElement.className = "my-tooltip";
// Configure tooltip provider
const tooltipProvider = new TooltipProvider({
content: tooltipElement,
debounce: 300,
shouldShow: (view) => !view.state.selection.empty,
offset: { mainAxis: 10, crossAxis: 0 },
});
// Set up tooltip plugin with provider
tooltipSpec(ctx => ({
view: (view) => ({
update: (view, prevState) => {
tooltipProvider.update(view, prevState);
},
destroy: () => {
tooltipProvider.destroy();
}
})
}));
// Use in editor
const editor = Editor.make()
.config((ctx) => {
ctx.set(tooltipSpec.key, tooltipSpec);
})
.use([tooltipPlugin]);The plugin follows Milkdown's plugin architecture with two core components:
Creates a tooltip plugin with a unique identifier for integration with Milkdown's plugin system.
/**
* Create a tooltip plugin with a unique id.
* @param id - Unique string identifier for the tooltip plugin
* @returns Tuple containing context specification and prose plugin with additional properties
*/
function tooltipFactory<Id extends string, State = any>(id: Id): TooltipPlugin<Id, State>;
type TooltipSpecId<Id extends string> = `${Id}_TOOLTIP_SPEC`;
type TooltipPlugin<Id extends string, State = any> = [
$Ctx<PluginSpec<State>, TooltipSpecId<Id>>,
$Prose,
] & {
key: SliceType<PluginSpec<State>, TooltipSpecId<Id>>;
pluginKey: $Prose['key'];
};Manages tooltip positioning, display logic, and lifecycle using floating-ui for intelligent positioning.
/**
* A provider for creating and managing tooltips with intelligent positioning
*/
class TooltipProvider {
/** The root element of the tooltip */
element: HTMLElement;
/** Callback executed when tooltip is shown */
onShow: () => void;
/** Callback executed when tooltip is hidden */
onHide: () => void;
constructor(options: TooltipProviderOptions);
/**
* Update provider state by editor view
* @param view - Current editor view
* @param prevState - Previous editor state for comparison
*/
update(view: EditorView, prevState?: EditorState): void;
/**
* Show the tooltip, optionally positioned relative to a virtual element
* @param virtualElement - Optional element to position tooltip relative to
*/
show(virtualElement?: VirtualElement): void;
/**
* Hide the tooltip
*/
hide(): void;
/**
* Destroy the tooltip and cancel pending updates
*/
destroy(): void;
}Configuration options for customizing tooltip behavior and positioning.
interface TooltipProviderOptions {
/** The tooltip content */
content: HTMLElement;
/** The debounce time for updating tooltip, 200ms by default */
debounce?: number;
/** Function to determine whether the tooltip should be shown */
shouldShow?: (view: EditorView, prevState?: EditorState) => boolean;
/** The offset to get the block. Default is 0 */
offset?: OffsetOptions;
/** The amount to shift options the block by */
shift?: ShiftOptions;
/** Other middlewares for floating ui. This will be added after the internal middlewares */
middleware?: Middleware[];
/** Options for floating ui. If you pass `middleware` or `placement`, it will override the internal settings */
floatingUIOptions?: Partial<ComputePositionConfig>;
/** The root element that the tooltip will be appended to */
root?: HTMLElement;
}// Milkdown core types used in API
interface $Ctx<T, N extends string> {
key: SliceType<T, N>;
meta: { package: string; displayName: string };
}
interface $Prose {
key: Symbol;
meta: { package: string; displayName: string };
}
interface SliceType<T, N extends string> {
id: symbol;
name: N;
}
interface PluginSpec<State = any> {
props?: any;
state?: any;
key?: any;
view?: (view: EditorView) => {
update?: (view: EditorView, prevState: EditorState) => void;
destroy?: () => void;
};
}
// Floating-ui types used in configuration
interface OffsetOptions {
mainAxis?: number;
crossAxis?: number;
alignmentAxis?: number | null;
}
interface ShiftOptions {
mainAxis?: boolean;
crossAxis?: boolean;
limiter?: {
fn: (state: MiddlewareState) => Coords;
options?: any;
};
}
interface VirtualElement {
getBoundingClientRect(): ClientRect | DOMRect;
contextElement?: Element;
}
interface Middleware {
name: string;
options?: any;
fn: (state: MiddlewareState) => Coords | Promise<Coords>;
}
interface ComputePositionConfig {
placement?: Placement;
strategy?: Strategy;
middleware?: Array<Middleware | null | undefined | false>;
platform?: Platform;
}
// ProseMirror types used in API
interface EditorView {
state: EditorState;
dom: HTMLElement;
hasFocus(): boolean;
editable: boolean;
composing: boolean;
}
interface EditorState {
doc: Node;
selection: Selection;
}import { tooltipFactory, TooltipProvider } from "@milkdown/plugin-tooltip";
// Create tooltip plugin
const [tooltipSpec, tooltipPlugin] = tooltipFactory("selectionTooltip");
// Create tooltip content
const content = document.createElement("div");
content.innerHTML = "<button>Bold</button><button>Italic</button>";
content.className = "selection-tooltip";
// Configure provider to show on text selection
const provider = new TooltipProvider({
content,
shouldShow: (view) => !view.state.selection.empty, // Show when text is selected
offset: { mainAxis: 8 },
shift: { crossAxis: true },
});
// Set up plugin specification
tooltipSpec(ctx => ({
view: () => ({
update: provider.update,
destroy: provider.destroy,
})
}));import { flip, hide } from "@floating-ui/dom";
const provider = new TooltipProvider({
content: tooltipElement,
middleware: [
hide(), // Hide tooltip when reference is not visible
],
floatingUIOptions: {
placement: "bottom-start",
strategy: "fixed",
},
debounce: 100, // Faster updates
});const provider = new TooltipProvider({
content: tooltipElement,
shouldShow: (view, prevState) => {
const { selection } = view.state;
// Only show for text selections longer than 5 characters
if (selection.empty) return false;
const selectedText = view.state.doc.textBetween(
selection.from,
selection.to
);
return selectedText.length > 5;
},
});
// Add event handlers
provider.onShow = () => {
console.log("Tooltip shown");
provider.element.style.opacity = "1";
};
provider.onHide = () => {
console.log("Tooltip hidden");
provider.element.style.opacity = "0";
};// Create multiple tooltip plugins with unique identifiers
const [formatTooltipSpec, formatTooltipPlugin] = tooltipFactory("formatting");
const [linkTooltipSpec, linkTooltipPlugin] = tooltipFactory("linkPreview");
// Configure different providers
const formatProvider = new TooltipProvider({
content: formatToolbarElement,
shouldShow: (view) => !view.state.selection.empty,
offset: { mainAxis: 10 },
});
const linkProvider = new TooltipProvider({
content: linkPreviewElement,
shouldShow: (view) => {
// Show when hovering over links
const { $head } = view.state.selection;
return $head.parent.type.name === "link";
},
debounce: 500,
});