-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
/
ReactRenderer.tsx
173 lines (146 loc) · 3.76 KB
/
ReactRenderer.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import { Editor } from '@tiptap/core'
import React from 'react'
import { flushSync } from 'react-dom'
import { EditorWithContentComponent } from './Editor.js'
/**
* Check if a component is a class component.
* @param Component
* @returns {boolean}
*/
function isClassComponent(Component: any) {
return !!(
typeof Component === 'function'
&& Component.prototype
&& Component.prototype.isReactComponent
)
}
/**
* Check if a component is a forward ref component.
* @param Component
* @returns {boolean}
*/
function isForwardRefComponent(Component: any) {
return !!(
typeof Component === 'object'
&& Component.$$typeof?.toString() === 'Symbol(react.forward_ref)'
)
}
export interface ReactRendererOptions {
/**
* The editor instance.
* @type {Editor}
*/
editor: Editor,
/**
* The props for the component.
* @type {Record<string, any>}
* @default {}
*/
props?: Record<string, any>,
/**
* The tag name of the element.
* @type {string}
* @default 'div'
*/
as?: string,
/**
* The class name of the element.
* @type {string}
* @default ''
* @example 'foo bar'
*/
className?: string,
}
type ComponentType<R, P> =
React.ComponentClass<P> |
React.FunctionComponent<P> |
React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<R>>;
/**
* The ReactRenderer class. It's responsible for rendering React components inside the editor.
* @example
* new ReactRenderer(MyComponent, {
* editor,
* props: {
* foo: 'bar',
* },
* as: 'span',
* })
*/
export class ReactRenderer<R = unknown, P extends Record<string, any> = {}> {
id: string
editor: Editor
component: any
element: Element
props: P
reactElement: React.ReactNode
ref: R | null = null
/**
* Immediately creates element and renders the provided React component.
*/
constructor(component: ComponentType<R, P>, {
editor,
props = {},
as = 'div',
className = '',
}: ReactRendererOptions) {
this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString()
this.component = component
this.editor = editor as EditorWithContentComponent
this.props = props as P
this.element = document.createElement(as)
this.element.classList.add('react-renderer')
if (className) {
this.element.classList.add(...className.split(' '))
}
if (this.editor.isInitialized) {
// On first render, we need to flush the render synchronously
// Renders afterwards can be async, but this fixes a cursor positioning issue
flushSync(() => {
this.render()
})
} else {
this.render()
}
}
/**
* Render the React component.
*/
render(): void {
const Component = this.component
const props = this.props
const editor = this.editor as EditorWithContentComponent
if (isClassComponent(Component) || isForwardRefComponent(Component)) {
// @ts-ignore This is a hack to make the ref work
props.ref = (ref: R) => {
this.ref = ref
}
}
this.reactElement = React.createElement(Component, props)
editor?.contentComponent?.setRenderer(this.id, this)
}
/**
* Re-renders the React component with new props.
*/
updateProps(props: Record<string, any> = {}): void {
this.props = {
...this.props,
...props,
}
this.render()
}
/**
* Destroy the React component.
*/
destroy(): void {
const editor = this.editor as EditorWithContentComponent
editor?.contentComponent?.removeRenderer(this.id)
}
/**
* Update the attributes of the element that holds the React component.
*/
updateAttributes(attributes: Record<string, string>): void {
Object.keys(attributes).forEach(key => {
this.element.setAttribute(key, attributes[key])
})
}
}