@octanejs/tiptap 0.0.1 → 0.0.3
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/README.md +28 -16
- package/package.json +13 -6
- package/src/Editor.ts +27 -0
- package/src/EditorContent.tsrx +96 -6
- package/src/EditorContent.tsrx.d.ts +3 -0
- package/src/NodeViewContent.ts +30 -0
- package/src/NodeViewWrapper.ts +27 -0
- package/src/ReactMarkViewRenderer.ts +113 -0
- package/src/ReactNodeViewRenderer.ts +353 -0
- package/src/ReactRenderer.ts +128 -0
- package/src/index.ts +6 -0
- package/src/menus/BubbleMenu.ts +207 -0
- package/src/menus/FloatingMenu.ts +205 -0
- package/src/menus/getAutoPluginKey.ts +8 -0
- package/src/menus/index.ts +6 -0
- package/src/menus/useMenuElementProps.ts +441 -0
- package/src/useEditor.ts +13 -0
- package/src/useReactNodeView.ts +32 -0
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ export function RichTextEditor() @{
|
|
|
28
28
|
|
|
29
29
|
## Current scope
|
|
30
30
|
|
|
31
|
-
The
|
|
31
|
+
The binding includes:
|
|
32
32
|
|
|
33
33
|
- `useEditor`, including create/update/recreate and deferred-destroy lifecycle
|
|
34
34
|
behavior;
|
|
@@ -36,21 +36,33 @@ The initial binding includes:
|
|
|
36
36
|
bailout behavior;
|
|
37
37
|
- `EditorContext`, `EditorProvider`, `EditorConsumer`, and `useCurrentEditor`;
|
|
38
38
|
- `EditorContent` / `PureEditorContent` for adopting, switching, resetting, and
|
|
39
|
-
exposing
|
|
40
|
-
- `Tiptap`, `Tiptap.Content`, `useTiptap`, and `useTiptapState
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
`
|
|
45
|
-
|
|
46
|
-
`@
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
39
|
+
exposing an editor DOM while owning its portal registry;
|
|
40
|
+
- `Tiptap`, `Tiptap.Content`, `useTiptap`, and `useTiptapState`;
|
|
41
|
+
- `ReactRenderer`, `ReactNodeViewRenderer`, `ReactMarkViewRenderer`,
|
|
42
|
+
`useReactNodeView`, and the NodeView/MarkView wrapper and content components
|
|
43
|
+
for custom Octane editor views; and
|
|
44
|
+
- `BubbleMenu` and `FloatingMenu` from the `@octanejs/tiptap/menus` subpath.
|
|
45
|
+
|
|
46
|
+
The public names follow `@tiptap/react` so existing TipTap extensions can keep
|
|
47
|
+
their renderer declarations while their components move to Octane. Despite the
|
|
48
|
+
compatibility names, the implementation does not install or render through
|
|
49
|
+
React.
|
|
50
|
+
|
|
51
|
+
For the pinned 3.28.0 release, the root and `./menus` runtime export surfaces
|
|
52
|
+
match `@tiptap/react`. Package-boundary tests lock those exports and the client
|
|
53
|
+
module directives, while shared-fixture differential tests exercise the same
|
|
54
|
+
editor and custom-view components through both bindings.
|
|
55
|
+
|
|
56
|
+
On the server, editor construction is suppressed, hook snapshots use a `null`
|
|
57
|
+
editor, and menu portals emit no detached target. Set `immediatelyRender: false`
|
|
58
|
+
when the editor should remain nullable through the initial client render as
|
|
59
|
+
well. The binding's SSR and hydration suite verifies deferred server output,
|
|
60
|
+
adoption of the existing host DOM, and creation of the live editor after
|
|
61
|
+
hydration.
|
|
62
|
+
|
|
63
|
+
Real-browser coverage complements the jsdom suite for contracts that require a
|
|
64
|
+
layout engine: caret-preserving input, text selection, NodeView dragging, and
|
|
65
|
+
BubbleMenu/FloatingMenu visibility and positioning.
|
|
54
66
|
|
|
55
67
|
## Status
|
|
56
68
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/tiptap",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
|
-
"description": "TipTap bindings for
|
|
24
|
+
"description": "TipTap bindings for Octane, including editor hooks and contexts, EditorContent, custom node and mark views, and floating menus without a React runtime dependency.",
|
|
25
25
|
"author": {
|
|
26
26
|
"name": "Dominic Gannaway",
|
|
27
27
|
"email": "dg@domgan.com"
|
|
@@ -42,15 +42,20 @@
|
|
|
42
42
|
"README.md"
|
|
43
43
|
],
|
|
44
44
|
"exports": {
|
|
45
|
-
".": "./src/index.ts"
|
|
45
|
+
".": "./src/index.ts",
|
|
46
|
+
"./menus": "./src/menus/index.ts"
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
49
|
"@tiptap/core": "3.28.0",
|
|
49
50
|
"@tiptap/pm": "3.28.0",
|
|
50
51
|
"fast-equals": "^5.3.3"
|
|
51
52
|
},
|
|
53
|
+
"optionalDependencies": {
|
|
54
|
+
"@tiptap/extension-bubble-menu": "3.28.0",
|
|
55
|
+
"@tiptap/extension-floating-menu": "3.28.0"
|
|
56
|
+
},
|
|
52
57
|
"peerDependencies": {
|
|
53
|
-
"octane": "0.1.
|
|
58
|
+
"octane": "0.1.9"
|
|
54
59
|
},
|
|
55
60
|
"devDependencies": {
|
|
56
61
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -58,11 +63,13 @@
|
|
|
58
63
|
"@tiptap/starter-kit": "3.28.0",
|
|
59
64
|
"@tsrx/react": "^0.2.37",
|
|
60
65
|
"esbuild": "^0.28.1",
|
|
66
|
+
"playwright": "^1.61.0",
|
|
61
67
|
"react": "^19.2.0",
|
|
62
68
|
"react-dom": "^19.2.0",
|
|
69
|
+
"vite": "^8.0.16",
|
|
63
70
|
"vitest": "^4.1.9",
|
|
64
|
-
"@octanejs/testing-library": "0.1.
|
|
65
|
-
"octane": "0.1.
|
|
71
|
+
"@octanejs/testing-library": "0.1.6",
|
|
72
|
+
"octane": "0.1.9"
|
|
66
73
|
},
|
|
67
74
|
"scripts": {
|
|
68
75
|
"test": "vitest run"
|
package/src/Editor.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Editor } from '@tiptap/core';
|
|
2
|
+
import type { PortalDescriptor } from 'octane';
|
|
3
|
+
|
|
4
|
+
import type { ReactRenderer } from './ReactRenderer';
|
|
5
|
+
|
|
6
|
+
/** One immutable renderer entry consumed by EditorContent's keyed portal list. */
|
|
7
|
+
export type RendererPortalEntry = {
|
|
8
|
+
id: string;
|
|
9
|
+
portal: PortalDescriptor;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type RendererPortalSnapshot = Readonly<Record<string, RendererPortalEntry>>;
|
|
13
|
+
|
|
14
|
+
/** External-store registry shared by EditorContent and custom view renderers. */
|
|
15
|
+
export type ContentComponent = {
|
|
16
|
+
setRenderer(id: string, renderer: ReactRenderer<any, any>): void;
|
|
17
|
+
removeRenderer(id: string): void;
|
|
18
|
+
subscribe(callback: () => void): () => void;
|
|
19
|
+
getSnapshot(): RendererPortalSnapshot;
|
|
20
|
+
getServerSnapshot(): RendererPortalSnapshot;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** TipTap's Editor augmented with the framework binding's mounted content state. */
|
|
24
|
+
export type EditorWithContentComponent = Editor & {
|
|
25
|
+
contentComponent?: ContentComponent | null;
|
|
26
|
+
isEditorContentInitialized?: boolean;
|
|
27
|
+
};
|
package/src/EditorContent.tsrx
CHANGED
|
@@ -1,11 +1,86 @@
|
|
|
1
|
-
// DOM lifecycle bridge ported from @tiptap/react 3.28.0
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// DOM lifecycle and portal-registry bridge ported from @tiptap/react 3.28.0.
|
|
2
|
+
import {
|
|
3
|
+
createPortal,
|
|
4
|
+
memo,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
useSyncExternalStore,
|
|
10
|
+
} from 'octane';
|
|
11
|
+
|
|
12
|
+
function PortalEntry({ entry }) {
|
|
13
|
+
return entry.portal;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Render every custom view in this EditorContent's logical Octane tree. */
|
|
17
|
+
function Portals({ contentComponent }) @{
|
|
18
|
+
const renderers = useSyncExternalStore(
|
|
19
|
+
contentComponent.subscribe,
|
|
20
|
+
contentComponent.getSnapshot,
|
|
21
|
+
contentComponent.getServerSnapshot,
|
|
22
|
+
);
|
|
23
|
+
const entries = Object.values(renderers);
|
|
24
|
+
|
|
25
|
+
<>
|
|
26
|
+
@for (const entry of entries; key entry.id) {
|
|
27
|
+
<PortalEntry entry={entry} />
|
|
28
|
+
}
|
|
29
|
+
</>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Create the immutable external-store registry consumed by Portals. */
|
|
33
|
+
export function createContentComponent() {
|
|
34
|
+
const subscribers = new Set();
|
|
35
|
+
let renderers = {};
|
|
36
|
+
let isNotificationQueued = false;
|
|
37
|
+
|
|
38
|
+
const notifySubscribers = () => {
|
|
39
|
+
if (isNotificationQueued || !subscribers.size) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
isNotificationQueued = true;
|
|
44
|
+
queueMicrotask(() => {
|
|
45
|
+
isNotificationQueued = false;
|
|
46
|
+
subscribers.forEach((subscriber) => subscriber());
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
subscribe(callback) {
|
|
52
|
+
subscribers.add(callback);
|
|
53
|
+
return () => subscribers.delete(callback);
|
|
54
|
+
},
|
|
55
|
+
getSnapshot() {
|
|
56
|
+
return renderers;
|
|
57
|
+
},
|
|
58
|
+
getServerSnapshot() {
|
|
59
|
+
return renderers;
|
|
60
|
+
},
|
|
61
|
+
setRenderer(id, renderer) {
|
|
62
|
+
renderers = {
|
|
63
|
+
...renderers,
|
|
64
|
+
[id]: {
|
|
65
|
+
id,
|
|
66
|
+
portal: createPortal(renderer.reactElement, renderer.element),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
notifySubscribers();
|
|
70
|
+
},
|
|
71
|
+
removeRenderer(id) {
|
|
72
|
+
const nextRenderers = { ...renderers };
|
|
73
|
+
delete nextRenderers[id];
|
|
74
|
+
renderers = nextRenderers;
|
|
75
|
+
notifySubscribers();
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
5
79
|
|
|
6
80
|
export function PureEditorContent(props) @{
|
|
7
81
|
const { editor, innerRef, ref, ...rest } = props;
|
|
8
82
|
const editorContentRef = useRef(null);
|
|
83
|
+
const [contentComponent, setContentComponent] = useState(null);
|
|
9
84
|
|
|
10
85
|
useLayoutEffect(() => {
|
|
11
86
|
if (!editor || editor.isDestroyed || editor.isEditorContentInitialized) {
|
|
@@ -24,16 +99,26 @@ export function PureEditorContent(props) @{
|
|
|
24
99
|
// moving nodes preserves the existing EditorView and DOM identity.
|
|
25
100
|
element.append(...editorParent.childNodes);
|
|
26
101
|
editor.setOptions({ element });
|
|
102
|
+
|
|
103
|
+
// The registry must exist before createNodeViews: renderer factories publish
|
|
104
|
+
// into it while ProseMirror builds the custom view instances.
|
|
105
|
+
const nextContentComponent = createContentComponent();
|
|
106
|
+
editor.contentComponent = nextContentComponent;
|
|
27
107
|
editor.createNodeViews();
|
|
28
108
|
editor.isEditorContentInitialized = true;
|
|
109
|
+
setContentComponent(nextContentComponent);
|
|
29
110
|
|
|
30
111
|
return () => {
|
|
31
112
|
editor.isEditorContentInitialized = false;
|
|
32
113
|
|
|
33
114
|
if (!editor.isDestroyed) {
|
|
34
|
-
|
|
115
|
+
// ProseMirror destroys the live view instances here. Keep the registry
|
|
116
|
+
// installed until their destroy methods have unregistered every portal.
|
|
117
|
+
editor.view.setProps({ nodeViews: {}, markViews: {} });
|
|
35
118
|
}
|
|
36
119
|
|
|
120
|
+
editor.contentComponent = null;
|
|
121
|
+
|
|
37
122
|
// Reset the editor to a detached element so the still-live instance can
|
|
38
123
|
// safely be mounted by a later EditorContent. A view that has already
|
|
39
124
|
// been destroyed or never mounted has nothing left to transfer.
|
|
@@ -53,7 +138,12 @@ export function PureEditorContent(props) @{
|
|
|
53
138
|
};
|
|
54
139
|
}, [editor]);
|
|
55
140
|
|
|
56
|
-
|
|
141
|
+
<>
|
|
142
|
+
<div {...rest} ref={[ref, innerRef, editorContentRef]} />
|
|
143
|
+
@if (contentComponent) {
|
|
144
|
+
<Portals contentComponent={contentComponent} />
|
|
145
|
+
}
|
|
146
|
+
</>
|
|
57
147
|
}
|
|
58
148
|
|
|
59
149
|
function EditorContentWithKey(props) @{
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Editor } from '@tiptap/core';
|
|
2
2
|
|
|
3
|
+
import type { ContentComponent } from './Editor';
|
|
4
|
+
|
|
3
5
|
export type EditorContentRef<T> =
|
|
4
6
|
| { current: T | null }
|
|
5
7
|
| ((value: T | null) => void | (() => void))
|
|
@@ -17,5 +19,6 @@ export interface EditorContentProps {
|
|
|
17
19
|
[key: string]: unknown;
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
export declare function createContentComponent(): ContentComponent;
|
|
20
23
|
export declare function PureEditorContent(props: EditorContentProps): unknown;
|
|
21
24
|
export declare const EditorContent: (props: EditorContentProps) => unknown;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createElement, type ComponentBody } from 'octane';
|
|
2
|
+
|
|
3
|
+
import { useReactNodeView } from './useReactNodeView';
|
|
4
|
+
|
|
5
|
+
type HostComponent = string | ComponentBody<any>;
|
|
6
|
+
|
|
7
|
+
export interface NodeViewContentProps {
|
|
8
|
+
as?: HostComponent;
|
|
9
|
+
style?: Record<string, unknown>;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function NodeViewContent({ as: Tag = 'div', ...props }: NodeViewContentProps): unknown {
|
|
14
|
+
const { nodeViewContentRef, nodeViewContentChildren } = useReactNodeView();
|
|
15
|
+
// The editable content node is owned by ProseMirror rather than Octane. Keep a
|
|
16
|
+
// fresh adapter so a host-prop update re-attaches that node after Octane has
|
|
17
|
+
// reconciled the wrapper's authored children.
|
|
18
|
+
const contentRef = (element: HTMLElement | null) => nodeViewContentRef?.(element);
|
|
19
|
+
|
|
20
|
+
return createElement(Tag, {
|
|
21
|
+
...props,
|
|
22
|
+
ref: contentRef,
|
|
23
|
+
'data-node-view-content': '',
|
|
24
|
+
style: {
|
|
25
|
+
whiteSpace: 'pre-wrap',
|
|
26
|
+
...props.style,
|
|
27
|
+
},
|
|
28
|
+
children: nodeViewContentChildren,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createElement, type ComponentBody } from 'octane';
|
|
2
|
+
|
|
3
|
+
import { useReactNodeView } from './useReactNodeView';
|
|
4
|
+
|
|
5
|
+
type HostComponent = string | ComponentBody<any>;
|
|
6
|
+
|
|
7
|
+
export interface NodeViewWrapperProps {
|
|
8
|
+
as?: HostComponent;
|
|
9
|
+
ref?: unknown;
|
|
10
|
+
style?: Record<string, unknown>;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function NodeViewWrapper({ as: Tag = 'div', ...props }: NodeViewWrapperProps): unknown {
|
|
15
|
+
const { onDragStart } = useReactNodeView();
|
|
16
|
+
|
|
17
|
+
return createElement(Tag, {
|
|
18
|
+
...props,
|
|
19
|
+
ref: props.ref,
|
|
20
|
+
'data-node-view-wrapper': '',
|
|
21
|
+
onDragStart,
|
|
22
|
+
style: {
|
|
23
|
+
whiteSpace: 'normal',
|
|
24
|
+
...props.style,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { MarkViewProps, MarkViewRenderer, MarkViewRendererOptions } from '@tiptap/core';
|
|
2
|
+
import { MarkView } from '@tiptap/core';
|
|
3
|
+
import { createContext, createElement, memo, useContext, type ComponentBody } from 'octane';
|
|
4
|
+
|
|
5
|
+
import { ReactRenderer } from './ReactRenderer';
|
|
6
|
+
|
|
7
|
+
export interface MarkViewContextProps {
|
|
8
|
+
markViewContentRef: (element: HTMLElement | null) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ReactMarkViewContext = createContext<MarkViewContextProps>({
|
|
12
|
+
markViewContentRef: () => {},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
type HostComponent = string | ComponentBody<any>;
|
|
16
|
+
|
|
17
|
+
export interface MarkViewContentProps {
|
|
18
|
+
as?: HostComponent;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Host the ProseMirror-managed content of a custom mark view. */
|
|
23
|
+
export function MarkViewContent({ as: Tag = 'span', ...props }: MarkViewContentProps): unknown {
|
|
24
|
+
const { markViewContentRef } = useContext(ReactMarkViewContext);
|
|
25
|
+
// Mark content is mutated by ProseMirror. Re-attach it after any Octane host
|
|
26
|
+
// reconciliation instead of treating the unmanaged child as authored output.
|
|
27
|
+
const contentRef = (element: HTMLElement | null) => markViewContentRef(element);
|
|
28
|
+
|
|
29
|
+
return createElement(Tag, {
|
|
30
|
+
...props,
|
|
31
|
+
ref: contentRef,
|
|
32
|
+
'data-mark-view-content': '',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ReactMarkViewRendererOptions extends MarkViewRendererOptions {
|
|
37
|
+
/** Wrapper element tag. */
|
|
38
|
+
as?: string;
|
|
39
|
+
/** Additional wrapper classes. */
|
|
40
|
+
className?: string;
|
|
41
|
+
/** Static wrapper attributes. */
|
|
42
|
+
attrs?: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class ReactMarkView extends MarkView<
|
|
46
|
+
ComponentBody<MarkViewProps>,
|
|
47
|
+
ReactMarkViewRendererOptions
|
|
48
|
+
> {
|
|
49
|
+
renderer: ReactRenderer<unknown, MarkViewProps>;
|
|
50
|
+
contentDOMElement: HTMLElement;
|
|
51
|
+
|
|
52
|
+
constructor(
|
|
53
|
+
component: ComponentBody<MarkViewProps>,
|
|
54
|
+
props: MarkViewProps,
|
|
55
|
+
options?: Partial<ReactMarkViewRendererOptions>,
|
|
56
|
+
) {
|
|
57
|
+
super(component, props, options);
|
|
58
|
+
|
|
59
|
+
const { as = 'span', attrs, className = '' } = options || {};
|
|
60
|
+
const componentProps = {
|
|
61
|
+
...props,
|
|
62
|
+
updateAttributes: this.updateAttributes.bind(this),
|
|
63
|
+
} satisfies MarkViewProps;
|
|
64
|
+
|
|
65
|
+
this.contentDOMElement = document.createElement('span');
|
|
66
|
+
const context: MarkViewContextProps = {
|
|
67
|
+
markViewContentRef: (element) => {
|
|
68
|
+
if (element && !element.contains(this.contentDOMElement)) {
|
|
69
|
+
element.appendChild(this.contentDOMElement);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
const providerBody: ComponentBody<MarkViewProps> = (providerProps) =>
|
|
74
|
+
createElement(ReactMarkViewContext.Provider, {
|
|
75
|
+
value: context,
|
|
76
|
+
children: createElement(component, providerProps),
|
|
77
|
+
});
|
|
78
|
+
const ReactMarkViewProvider = memo(providerBody);
|
|
79
|
+
(ReactMarkViewProvider as any).displayName = 'ReactMarkView';
|
|
80
|
+
|
|
81
|
+
this.renderer = new ReactRenderer(ReactMarkViewProvider, {
|
|
82
|
+
editor: props.editor,
|
|
83
|
+
props: componentProps,
|
|
84
|
+
as,
|
|
85
|
+
className: `mark-${props.mark.type.name} ${className}`.trim(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (attrs) {
|
|
89
|
+
this.renderer.updateAttributes(attrs);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get dom(): HTMLElement {
|
|
94
|
+
return this.renderer.element;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get contentDOM(): HTMLElement {
|
|
98
|
+
return this.contentDOMElement;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** ProseMirror calls this when the mark view leaves the document. */
|
|
102
|
+
destroy(): void {
|
|
103
|
+
this.renderer.destroy();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Create a TipTap mark-view renderer backed by an Octane portal. */
|
|
108
|
+
export function ReactMarkViewRenderer(
|
|
109
|
+
component: ComponentBody<MarkViewProps>,
|
|
110
|
+
options: Partial<ReactMarkViewRendererOptions> = {},
|
|
111
|
+
): MarkViewRenderer {
|
|
112
|
+
return (props) => new ReactMarkView(component, props, options);
|
|
113
|
+
}
|