try to improve react support

This commit is contained in:
Philipp Kühn
2021-03-08 13:19:05 +01:00
parent 48c747be02
commit c20b43aa6c
14 changed files with 797 additions and 197 deletions

View File

@@ -23,9 +23,13 @@
],
"peerDependencies": {
"@tiptap/core": "^2.0.0-beta.1",
"react": "^17.0.1"
"react": "^17.0.1",
"react-dom": "^17.0.1"
},
"dependencies": {
"prosemirror-view": "^1.17.8"
},
"devDependencies": {
"@types/react-dom": "^17.0.1"
}
}

View File

@@ -0,0 +1,5 @@
import { Editor as CoreEditor } from '@tiptap/core'
export class Editor extends CoreEditor {
public contentComponent: any | null = null
}

View File

@@ -0,0 +1,71 @@
import React, { useState, useRef, useEffect } from 'react'
import ReactDOM from 'react-dom'
import { Editor } from './Editor'
// export const EditorContent = ({ editor }) => {
// const editorContentRef = useRef(null)
// useEffect(() => {
// if (editor && editor.options.element) {
// console.log('set editorContent element')
// const element = editorContentRef.current
// element.appendChild(editor.options.element.firstChild)
// editor.setOptions({
// element,
// })
// console.log({instance: this})
// // TODO: why setTimeout?
// setTimeout(() => {
// editor.createNodeViews()
// }, 0)
// }
// })
// return (
// <div ref={editorContentRef} />
// )
// }
export class EditorContent extends React.Component {
constructor(props) {
super(props)
this.editorContentRef = React.createRef()
this.editorPortalRef = React.createRef()
this.state = {
editor: this.props.editor
}
}
componentDidUpdate() {
const { editor } = this.props
if (editor && editor.options.element) {
const element = this.editorContentRef.current
element.appendChild(editor.options.element.firstChild)
editor.setOptions({
element,
})
editor.contentComponent = this
// TODO: why setTimeout?
setTimeout(() => {
editor.createNodeViews()
}, 0)
}
}
render() {
return (
<div ref={this.editorContentRef} />
)
}
}

View File

@@ -1 +1,209 @@
export default class ReactNodeViewRenderer {}
// @ts-nocheck
import { Node, NodeViewRenderer, NodeViewRendererProps } from '@tiptap/core'
import { Decoration, NodeView } from 'prosemirror-view'
import { NodeSelection } from 'prosemirror-state'
import { Node as ProseMirrorNode } from 'prosemirror-model'
import React from 'react'
import ReactDOM from 'react-dom'
import { Editor } from './Editor'
import { ReactRenderer } from './ReactRenderer'
import test from './test'
interface ReactNodeViewRendererOptions {
stopEvent: ((event: Event) => boolean) | null,
update: ((node: ProseMirrorNode, decorations: Decoration[]) => boolean) | null,
}
class ReactNodeView implements NodeView {
renderer!: ReactRenderer
editor: Editor
extension!: Node
node!: ProseMirrorNode
decorations!: Decoration[]
getPos!: any
isDragging = false
options: ReactNodeViewRendererOptions = {
stopEvent: null,
update: null,
}
constructor(component: any, props: NodeViewRendererProps, options?: Partial<ReactNodeViewRendererOptions>) {
this.options = { ...this.options, ...options }
this.editor = props.editor as Editor
this.extension = props.extension
this.node = props.node
this.getPos = props.getPos
this.mount(component)
}
mount(component: any) {
const props = {}
if (!component.displayName) {
component.displayName = this.extension.config.name
}
this.renderer = new ReactRenderer(component, {
editor: this.editor,
props,
})
}
get dom() {
// if (!this.renderer.element) {
// return null
// }
// if (!this.renderer.element.hasAttribute('data-node-view-wrapper')) {
// throw Error('Please use the NodeViewWrapper component for your node view.')
// }
return this.renderer.element
}
get contentDOM() {
// console.log(this.dom)
// return null
// if (!this.renderer.element) {
// return null
// }
const hasContent = !this.node.type.isAtom
if (!hasContent) {
return null
}
const contentElement = this.dom.querySelector('[data-node-view-content]')
return contentElement || this.dom
}
stopEvent(event: Event) {
if (typeof this.options.stopEvent === 'function') {
return this.options.stopEvent(event)
}
const target = (event.target as HTMLElement)
const isInElement = this.dom.contains(target) && !this.contentDOM?.contains(target)
// ignore all events from child nodes
if (!isInElement) {
return false
}
const { isEditable } = this.editor
const { isDragging } = this
const isDraggable = !!this.node.type.spec.draggable
const isSelectable = NodeSelection.isSelectable(this.node)
const isCopyEvent = event.type === 'copy'
const isPasteEvent = event.type === 'paste'
const isCutEvent = event.type === 'cut'
const isClickEvent = event.type === 'mousedown'
const isDragEvent = event.type.startsWith('drag') || event.type === 'drop'
// ProseMirror tries to drag selectable nodes
// even if `draggable` is set to `false`
// this fix prevents that
if (!isDraggable && isSelectable && isDragEvent) {
event.preventDefault()
}
if (isDraggable && isDragEvent && !isDragging) {
event.preventDefault()
return false
}
// we have to store that dragging started
if (isDraggable && isEditable && !isDragging && isClickEvent) {
const dragHandle = target.closest('[data-drag-handle]')
const isValidDragHandle = dragHandle
&& (this.dom === dragHandle || (this.dom.contains(dragHandle)))
if (isValidDragHandle) {
this.isDragging = true
document.addEventListener('dragend', () => {
this.isDragging = false
}, { once: true })
}
}
// these events are handled by prosemirror
if (
isDragging
|| isCopyEvent
|| isPasteEvent
|| isCutEvent
|| (isClickEvent && isSelectable)
) {
return false
}
return true
}
ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) {
if (mutation.type === 'selection') {
if (this.node.isLeaf) {
return true
}
return false
}
if (!this.contentDOM) {
return true
}
const contentDOMHasChanged = !this.contentDOM.contains(mutation.target)
|| this.contentDOM === mutation.target
return contentDOMHasChanged
}
destroy() {
this.renderer.destroy()
}
update(node: ProseMirrorNode, decorations: Decoration[]) {
if (typeof this.options.update === 'function') {
return this.options.update(node, decorations)
}
if (node.type !== this.node.type) {
return false
}
if (node === this.node && this.decorations === decorations) {
return true
}
this.node = node
this.decorations = decorations
// this.renderer.updateProps({ node, decorations })
this.renderer.render()
return true
}
}
export function ReactNodeViewRenderer(component: any, options?: Partial<ReactNodeViewRendererOptions>): NodeViewRenderer {
return (props: NodeViewRendererProps) => {
// try to get the parent component
// this is important for vue devtools to show the component hierarchy correctly
// maybe its `undefined` because <editor-content> isnt rendered yet
if (!(props.editor as Editor).contentComponent) {
return {}
}
return new ReactNodeView(component, props, options) as NodeView
}
}

View File

@@ -1 +1,58 @@
export default class ReactRenderer {}
// @ts-nocheck
import React from 'react'
import { render, unmountComponentAtNode } from 'react-dom'
import { Editor } from './Editor'
export interface VueRendererOptions {
as?: string;
editor: Editor;
props?: { [key: string]: any };
}
export class ReactRenderer {
id: string
editor: Editor
component: any
teleportElement: Element
element: Element
props: { [key: string]: any }
constructor(component: any, { props = {}, editor }: VueRendererOptions) {
this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString()
this.component = component
this.editor = editor
this.props = props
this.teleportElement = document.createElement('div')
// this.teleportElement.setAttribute('data-bla', '')
// render(React.createElement(component), this.teleportElement)
// render(React.createElement(component), this.teleportElement)
this.render()
// this.element = this.teleportElement.firstElementChild as Element
this.element = this.teleportElement
}
render() {
render(React.createElement(this.component), this.teleportElement)
}
updateProps(props: { [key: string]: any } = {}) {
// TODO
}
destroy() {
// TODO
// console.log('DESTROY', { bla: this.teleportElement })
// console.log(document.querySelector('[data-bla]'))
unmountComponentAtNode(this.teleportElement)
// unmountComponentAtNode(document.querySelector('[data-bla]'))
}
}

View File

@@ -1,7 +1,9 @@
// @ts-nocheck
export * from '@tiptap/core'
export { default as ReactRenderer } from './ReactRenderer'
export { default as ReactNodeViewRenderer } from './ReactNodeViewRenderer'
export {
Editor, EditorContext, useEditor,
} from './components/Editor'
export { Editor } from './Editor'
// export {
// Editor, EditorContext, useEditor,
// } from './components/Editor'
export * from './ReactRenderer'
export * from './ReactNodeViewRenderer'
export * from './EditorContent'

View File

@@ -0,0 +1,7 @@
import React from 'react'
export default () => {
return (
<div className="jooo" data-node-view-wrapper></div>
)
}