Files
upstream-tiptap/packages/vue/src/VueWidgetRenderer.ts
upstream-mirror 083a46e93b
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:11:02 +05:00

121 lines
4.6 KiB
TypeScript

import { createWidgetDecoration } from '@tiptap/core'
import type { Editor, WidgetDecoration, WidgetDecorationOptions } from '@tiptap/core'
import type { Component, FunctionalComponent, SetupContext } from 'vue'
import { defineComponent, markRaw } from 'vue'
import { undeclaredWidgetProps } from './utils/undeclaredWidgetProps.js'
import { wrapFunctionalWidget } from './utils/wrapFunctionalWidget.js'
import { VueRenderer } from './VueRenderer.js'
/**
* Props every widget-decoration component receives in addition to the props you
* pass through `VueWidgetRenderer`.
*/
export interface VueWidgetDecorationProps {
editor: Editor
getPos: () => number | undefined
}
export interface VueWidgetRendererOptions<
P extends Record<string, any> = object,
> extends WidgetDecorationOptions {
/**
* The editor instance.
*/
editor: Editor
/**
* The document position the widget is rendered at.
*/
pos: number
/**
* A stable, position-independent identifier for the widget.
* Reusing the same key keeps the component mounted across re-renders.
* Good: `comment-${id}`. Bad: paragraph index or document position.
*/
key: string
/**
* Props passed to the component (merged with {@link VueWidgetDecorationProps}).
* The component must have a single root element.
*
* @remarks Props are deeply wrapped in `reactive()` by `VueRenderer`. The
* `editor` is protected with `markRaw`; any other non-reactive objects you
* pass (ProseMirror nodes, views, editor refs, ...) are not. Mark them raw
* yourself when reactivity would cause recursion or unwanted tracking.
*/
props?: P
}
const WIDGET_CACHE = Symbol('tiptapVueWidgetCache')
/**
* Renders a Vue component into a ProseMirror widget decoration.
* Reuses Tiptap's `VueRenderer` so the component shares the editor's app
* context (provide/inject works as usual). Use a stable `key` for stateful
* widgets. The component must render a single root element.
* @example
* addDecorations() {
* return {
* create: ({ editor, state }) =>
* findMatches(state.doc).map(match =>
* VueWidgetRenderer(MyWidget, {
* editor, pos: match.pos, key: `match-${match.id}`,
* props: { label: match.label },
* }),
* ),
* }
* }
*/
export function VueWidgetRenderer<P extends Record<string, any> = object>(
component: Component,
options: VueWidgetRendererOptions<P>,
): WidgetDecoration {
const { editor, props = {} as P } = options
// A functional component is its own render function, so it cannot go through
// the options wrapper below.
const wrappedComponent = isFunctionalComponent(component)
? wrapFunctionalWidget(component, props)
: buildOptionsWidget(component, props)
return createWidgetDecoration<VueRenderer>({
// Forwards editor, pos, key and the ProseMirror widget options unchanged.
...options,
props,
cacheKey: WIDGET_CACHE,
// VueRenderer wraps props in reactive(), deeply proxying every object.
// `editor` is markRaw so proxying its view/DOM graph does not crash.
// User props are NOT auto-protected: markRaw any ProseMirror nodes,
// views, editor refs, or other objects reactivity would harm.
context: getPos => ({ editor: markRaw(editor), getPos }),
create: renderProps => new VueRenderer(wrappedComponent, { editor, props: renderProps }),
materialize: renderer => renderer.element as HTMLElement,
})
}
function isFunctionalComponent(component: Component): component is FunctionalComponent {
return typeof component === 'function'
}
/**
* Wraps an object component so the widget props are declared on it. Everything
* Vue reads off the original options has to be carried over by hand, because
* `extends` alone does not apply to `template`, `setup` or the compiler keys.
*/
function buildOptionsWidget(component: Component, props: Record<string, any>) {
return defineComponent({
extends: { ...(component as any) },
// Only declare what the component does not declare itself. Redeclaring a
// prop here would replace its type, default and validator with an empty one.
props: undeclaredWidgetProps(component, props),
template: (component as any).template,
// Vue only calls the outermost `setup`, so run the component's own setup and
// forward the full context, otherwise `emit`, `slots` and `attrs` are missing.
setup: (reactiveProps: any, context: SetupContext) =>
(component as any).setup?.(reactiveProps, context),
__scopeId: (component as any).__scopeId,
__cssModules: (component as any).__cssModules,
__name: (component as any).__name,
__file: (component as any).__file,
})
}