@tiptap/vue-2 3.29.2 → 3.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiptap/vue-2",
3
- "version": "3.29.2",
3
+ "version": "3.30.1",
4
4
  "description": "Vue components for tiptap",
5
5
  "keywords": [
6
6
  "tiptap",
@@ -52,17 +52,17 @@
52
52
  },
53
53
  "devDependencies": {
54
54
  "vue": "^2.7.16",
55
- "@tiptap/core": "^3.29.2",
56
- "@tiptap/pm": "^3.29.2"
55
+ "@tiptap/core": "^3.30.1",
56
+ "@tiptap/pm": "^3.30.1"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "vue": "^2.6.0",
60
- "@tiptap/core": "3.29.2",
61
- "@tiptap/pm": "3.29.2"
60
+ "@tiptap/core": "3.30.1",
61
+ "@tiptap/pm": "3.30.1"
62
62
  },
63
63
  "optionalDependencies": {
64
- "@tiptap/extension-bubble-menu": "^3.29.2",
65
- "@tiptap/extension-floating-menu": "^3.29.2"
64
+ "@tiptap/extension-bubble-menu": "^3.30.1",
65
+ "@tiptap/extension-floating-menu": "^3.30.1"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "tsup"
@@ -0,0 +1,135 @@
1
+ import { createWidgetDecoration } from '@tiptap/core'
2
+ import type {
3
+ Editor as CoreEditor,
4
+ WidgetDecoration,
5
+ WidgetDecorationOptions,
6
+ WidgetRenderer,
7
+ } from '@tiptap/core'
8
+ import type { Component, VueConstructor } from 'vue'
9
+
10
+ import type { Editor } from './Editor.js'
11
+ import { createWidgetConstructor } from './utils/createWidgetConstructor.js'
12
+ import { VueRenderer } from './VueRenderer.js'
13
+
14
+ /**
15
+ * Props every widget-decoration component receives in addition to the props you
16
+ * pass through `VueWidgetRenderer`. Declare the ones you use on your component.
17
+ */
18
+ export interface VueWidgetDecorationProps {
19
+ editor: CoreEditor
20
+ getPos: () => number | undefined
21
+ }
22
+
23
+ export interface VueWidgetRendererOptions<
24
+ P extends Record<string, any> = object,
25
+ > extends WidgetDecorationOptions {
26
+ /**
27
+ * The editor instance.
28
+ */
29
+ editor: Editor
30
+ /**
31
+ * The document position the widget is rendered at.
32
+ */
33
+ pos: number
34
+ /**
35
+ * A stable, position-independent identifier for the widget.
36
+ * Reusing the same key keeps the component mounted across re-renders.
37
+ * Good: `comment-${id}`. Bad: paragraph index or document position.
38
+ */
39
+ key: string
40
+ /**
41
+ * Props passed to the component (merged with {@link VueWidgetDecorationProps}).
42
+ * The component must render a single root element.
43
+ */
44
+ props?: P
45
+ }
46
+
47
+ const WIDGET_CACHE = Symbol('tiptapVue2WidgetCache')
48
+
49
+ // @ts-ignore
50
+ const isDev = process.env.NODE_ENV !== 'production'
51
+
52
+ /** The `VueRenderer` plus the warning wrapper the widget cache drives. */
53
+ interface WidgetRendererEntry extends WidgetRenderer {
54
+ renderer: VueRenderer
55
+ }
56
+
57
+ // Vue 2 fixes the prop list at construction. Keys absent on first mount are
58
+ // never declared, so updateProps silently drops them on later updates.
59
+ function warnUndeclaredProps(renderer: VueRenderer, props: Record<string, any>, key: string): void {
60
+ if (!isDev) {
61
+ return
62
+ }
63
+
64
+ const declared = renderer.ref.$props ?? {}
65
+
66
+ for (const name of Object.keys(props)) {
67
+ if (!(name in declared)) {
68
+ console.warn(
69
+ `[tiptap warn]: VueWidgetRenderer prop "${name}" was not passed on first mount for widget "${key}".`,
70
+ 'Vue 2 cannot declare new props after mount, so this value is ignored.',
71
+ )
72
+ }
73
+ }
74
+ }
75
+
76
+ // Wrap the renderer so every prop push warns, whether it comes from the first
77
+ // render or from the deferred flush.
78
+ function withPropWarnings(renderer: VueRenderer, key: string): WidgetRendererEntry {
79
+ return {
80
+ renderer,
81
+ updateProps: props => {
82
+ warnUndeclaredProps(renderer, props, key)
83
+ renderer.updateProps(props)
84
+ },
85
+ destroy: () => renderer.destroy(),
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Renders a Vue 2 component into a ProseMirror widget decoration.
91
+ * Reuses Tiptap's `VueRenderer` so the component is mounted under the editor's
92
+ * content component (inject/provide works as usual). Use a stable `key` for
93
+ * stateful widgets. The component must render a single root element.
94
+ * @example
95
+ * addDecorations() {
96
+ * return {
97
+ * create: ({ editor, state }) =>
98
+ * findMatches(state.doc).map(match =>
99
+ * VueWidgetRenderer(MyWidget, {
100
+ * editor, pos: match.pos, key: `match-${match.id}`,
101
+ * props: { label: match.label },
102
+ * }),
103
+ * ),
104
+ * }
105
+ * }
106
+ */
107
+ export function VueWidgetRenderer<P extends Record<string, any> = object>(
108
+ component: Component,
109
+ options: VueWidgetRendererOptions<P>,
110
+ ): WidgetDecoration {
111
+ const { editor, key, props = {} as P } = options
112
+
113
+ return createWidgetDecoration<WidgetRendererEntry>({
114
+ // Forwards editor, pos, key and the ProseMirror widget options unchanged.
115
+ ...options,
116
+ props,
117
+ cacheKey: WIDGET_CACHE,
118
+ // `view` is not passed on purpose: Vue 2 deeply observes props and
119
+ // observing a ProseMirror view corrupts its internals.
120
+ context: getPos => ({ editor, getPos }),
121
+ create: renderProps => {
122
+ const base = (editor.contentComponent?.$options as any)?._base as VueConstructor | undefined
123
+ const Constructor = createWidgetConstructor(base, component, renderProps)
124
+
125
+ return withPropWarnings(
126
+ new VueRenderer(Constructor, {
127
+ parent: editor.contentComponent,
128
+ propsData: renderProps,
129
+ }),
130
+ key,
131
+ )
132
+ },
133
+ materialize: entry => entry.renderer.element as HTMLElement,
134
+ })
135
+ }
package/src/index.ts CHANGED
@@ -4,4 +4,5 @@ export * from './NodeViewContent.js'
4
4
  export * from './NodeViewWrapper.js'
5
5
  export * from './VueNodeViewRenderer.js'
6
6
  export * from './VueRenderer.js'
7
+ export * from './VueWidgetRenderer.js'
7
8
  export * from '@tiptap/core'
@@ -16,6 +16,7 @@ export interface FloatingMenuInterface extends Vue {
16
16
  appendTo: FloatingMenuPluginProps['appendTo']
17
17
  shouldShow: FloatingMenuPluginProps['shouldShow']
18
18
  getPluginKey: () => FloatingMenuPluginProps['pluginKey']
19
+ isDestroyed: boolean
19
20
  }
20
21
 
21
22
  export const FloatingMenu: Component = {
@@ -58,39 +59,38 @@ export const FloatingMenu: Component = {
58
59
  },
59
60
  },
60
61
 
61
- watch: {
62
- editor: {
63
- immediate: true,
64
- handler(this: FloatingMenuInterface, editor: FloatingMenuPluginProps['editor']) {
65
- if (!editor) {
66
- return
67
- }
68
-
69
- if (!this.$el) {
70
- return
71
- }
72
-
73
- ;(this.$el as HTMLElement).style.visibility = 'hidden'
74
- ;(this.$el as HTMLElement).style.position = 'absolute'
75
-
76
- this.$el.remove()
77
-
78
- this.$nextTick(() => {
79
- editor.registerPlugin(
80
- FloatingMenuPlugin({
81
- pluginKey: this.getPluginKey(),
82
- editor,
83
- element: this.$el as HTMLElement,
84
- updateDelay: this.updateDelay,
85
- resizeDelay: this.resizeDelay,
86
- options: this.options,
87
- appendTo: this.appendTo,
88
- shouldShow: this.shouldShow,
89
- }),
90
- )
91
- })
92
- },
93
- },
62
+ mounted(this: FloatingMenuInterface) {
63
+ const editor = this.editor
64
+ const el = this.$el as HTMLElement
65
+
66
+ if (!editor || !el) {
67
+ return
68
+ }
69
+
70
+ el.style.visibility = 'hidden'
71
+ el.style.position = 'absolute'
72
+
73
+ // Remove element from DOM; plugin will re-parent it when shown
74
+ el.remove()
75
+
76
+ this.$nextTick(() => {
77
+ if (this.isDestroyed) {
78
+ return
79
+ }
80
+
81
+ editor.registerPlugin(
82
+ FloatingMenuPlugin({
83
+ pluginKey: this.getPluginKey(),
84
+ editor,
85
+ element: el,
86
+ updateDelay: this.updateDelay,
87
+ resizeDelay: this.resizeDelay,
88
+ options: this.options,
89
+ appendTo: this.appendTo,
90
+ shouldShow: this.shouldShow,
91
+ }),
92
+ )
93
+ })
94
94
  },
95
95
 
96
96
  render(this: FloatingMenuInterface, createElement: CreateElement) {
@@ -109,7 +109,15 @@ export const FloatingMenu: Component = {
109
109
  },
110
110
 
111
111
  beforeDestroy(this: FloatingMenuInterface) {
112
- this.editor.unregisterPlugin(this.getPluginKey())
112
+ this.isDestroyed = true
113
+
114
+ const editor = this.editor
115
+
116
+ if (!editor) {
117
+ return
118
+ }
119
+
120
+ editor.unregisterPlugin(this.getPluginKey())
113
121
  },
114
122
 
115
123
  methods: {
@@ -0,0 +1,38 @@
1
+ import type { Component, VueConstructor } from 'vue'
2
+
3
+ import { Vue } from '../Vue.js'
4
+
5
+ /**
6
+ * Builds the Vue 2 constructor for a widget component.
7
+ *
8
+ * Extends the editor's own Vue constructor so the widget shares its context,
9
+ * then auto-declares every passed prop the component does not declare itself.
10
+ * Vue 2 fixes the prop list at construction, so undeclared keys would be
11
+ * dropped. Existing declarations keep their type, default and validator.
12
+ *
13
+ * @param base The editor's Vue constructor, or undefined to fall back to `Vue`.
14
+ * @param component The widget component.
15
+ * @param props The props the widget is mounted with.
16
+ * @returns A constructor that accepts every key in `props`.
17
+ * @example
18
+ * createWidgetConstructor(undefined, MyWidget, { editor, getPos, label: 'a' })
19
+ */
20
+ export function createWidgetConstructor(
21
+ base: VueConstructor | undefined,
22
+ component: Component,
23
+ props: Record<string, any>,
24
+ ): VueConstructor {
25
+ const VueBase = base ?? Vue
26
+ const Extended = VueBase.extend(component as any)
27
+ const declaredProps = {
28
+ ...(Extended as unknown as { options: { props?: Record<string, any> } }).options.props,
29
+ }
30
+
31
+ for (const name of Object.keys(props)) {
32
+ if (!Object.prototype.hasOwnProperty.call(declaredProps, name)) {
33
+ declaredProps[name] = null
34
+ }
35
+ }
36
+
37
+ return Extended.extend({ props: declaredProps })
38
+ }