wip react renderer

This commit is contained in:
Philipp Kühn
2021-03-12 21:21:13 +01:00
parent d9377f9085
commit bc60a91904
5 changed files with 412 additions and 244 deletions

View File

@@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import { useEditor, EditorContent } from '@tiptap/react' import { useEditor, EditorContent, ReactNodeViewRenderer } from '@tiptap/react'
import { defaultExtensions } from '@tiptap/starter-kit' import { defaultExtensions } from '@tiptap/starter-kit'
import Paragraph from '@tiptap/extension-paragraph'
import './styles.scss' import './styles.scss'
const MenuBar = ({ editor }) => { const MenuBar = ({ editor }) => {
@@ -125,38 +126,49 @@ const MenuBar = ({ editor }) => {
export default () => { export default () => {
const editor = useEditor({ const editor = useEditor({
extensions: [ extensions: [
...defaultExtensions(), Paragraph.extend({
addNodeView() {
return ReactNodeViewRenderer(({ selected }) => {
console.log({selected})
return (
<div className="paragraph">noooode view {selected}</div>
)
})
}
}),
...defaultExtensions().filter(item => item.config.name !== 'paragraph'),
], ],
content: ` content: `<p>test</p>`,
<h2> // content: `
Hi there, // <h2>
</h2> // Hi there,
<p> // </h2>
this is a basic <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists: // <p>
</p> // this is a basic <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists:
<ul> // </p>
<li> // <ul>
Thats a bullet list with one … // <li>
</li> // Thats a bullet list with one …
<li> // </li>
… or two list items. // <li>
</li> // … or two list items.
</ul> // </li>
<p> // </ul>
Isnt that great? And all of that is editable. But wait, theres more. Lets try a code block: // <p>
</p> // Isnt that great? And all of that is editable. But wait, theres more. Lets try a code block:
<pre><code class="language-css">body { // </p>
display: none; // <pre><code class="language-css">body {
}</code></pre> // display: none;
<p> // }</code></pre>
I know, I know, this is impressive. Its only the tip of the iceberg though. Give it a try and click a little bit around. Dont forget to check the other examples too. // <p>
</p> // I know, I know, this is impressive. Its only the tip of the iceberg though. Give it a try and click a little bit around. Dont forget to check the other examples too.
<blockquote> // </p>
Wow, thats amazing. Good work, boy! 👏 // <blockquote>
<br /> // Wow, thats amazing. Good work, boy! 👏
— Mom // <br />
</blockquote> // — Mom
`, // </blockquote>
// `,
}) })
return ( return (

View File

@@ -1,11 +1,67 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom'
import { Editor } from './Editor' import { Editor } from './Editor'
type EditorContentProps = { type EditorContentProps = {
editor: Editor | null editor: Editor | null
} }
export class PureEditorContent extends React.Component<EditorContentProps, EditorContentProps> { // const Portals = ({ editor }: { editor: Editor | null }) => {
// if (!editor?.contentComponent) {
// return null
// }
// console.log('render portals')
// return (
// <div>portaaals</div>
// )
// }
const Portals = ({ renderers }: { renderers: Map<any, any> }) => {
return (
<div>
{Array.from(renderers).map(([key, renderer]) => {
// console.log({renderer})
// return (
// <div key={key}>{value}</div>
// )
// return React.createElement(renderer.component)
// return (
// <React.Fragment key={renderer.id}>
// {ReactDOM.createPortal(
// React.createElement(renderer.component),
// renderer.teleportElement,
// )}
// </React.Fragment>
// )
return (
<React.Fragment key={renderer.id}>
{renderer.bla}
</React.Fragment>
)
// return ReactDOM.createPortal(
// React.createElement(renderer.component),
// renderer.teleportElement,
// )
})}
</div>
)
}
// const Content = React.memo(({ reference }: { reference: React.RefObject<any> }) => {
// return (
// <div ref={reference} />
// )
// })
export class PureEditorContent extends React.Component<EditorContentProps, any> {
editorContentRef: React.RefObject<any> editorContentRef: React.RefObject<any>
constructor(props: EditorContentProps) { constructor(props: EditorContentProps) {
@@ -13,14 +69,33 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
this.editorContentRef = React.createRef() this.editorContentRef = React.createRef()
this.state = { this.state = {
editor: this.props.editor editor: this.props.editor,
renderers: new Map(),
} }
// setInterval(() => {
// if (this.props?.editor?.contentComponent) {
// this.props.editor.contentComponent.setState({
// renderers: this.state.renderers.set(Math.random(), Math.random())
// })
// }
// }, 1000)
} }
componentDidUpdate() { componentDidUpdate() {
const { editor } = this.props const { editor } = this.props
if (editor && editor.options.element) { if (editor && editor.options.element) {
if (editor.contentComponent) {
return
}
// this.setState({
// editor,
// })
console.log('UPDATE')
const element = this.editorContentRef.current const element = this.editorContentRef.current
element.appendChild(editor.options.element.firstChild) element.appendChild(editor.options.element.firstChild)
@@ -39,10 +114,20 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
} }
render() { render() {
console.log('render', this.state)
// console.log('render', this.props.editor, this.state.editor)
return ( return (
<>
<div ref={this.editorContentRef} /> <div ref={this.editorContentRef} />
{/* <Content reference={this.editorContentRef} /> */}
{/* <Portals editor={this.props.editor} /> */}
<Portals renderers={this.state.renderers} />
</>
) )
} }
} }
export const EditorContent = React.memo(PureEditorContent); export const EditorContent = React.memo(PureEditorContent)
// export const EditorContent = PureEditorContent

View File

@@ -1,208 +1,246 @@
// // @ts-nocheck // @ts-nocheck
// import { Node, NodeViewRenderer, NodeViewRendererProps } from '@tiptap/core' import { Node, NodeViewRenderer, NodeViewRendererProps } from '@tiptap/core'
// import { Decoration, NodeView } from 'prosemirror-view' import { Decoration, NodeView } from 'prosemirror-view'
// import { NodeSelection } from 'prosemirror-state' import { NodeSelection } from 'prosemirror-state'
// import { Node as ProseMirrorNode } from 'prosemirror-model' import { Node as ProseMirrorNode } from 'prosemirror-model'
// import React from 'react' import React from 'react'
// import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
// import { Editor } from './Editor' import { Editor } from './Editor'
// import { ReactRenderer } from './ReactRenderer' import { ReactRenderer } from './ReactRenderer'
// interface ReactNodeViewRendererOptions { interface ReactNodeViewRendererOptions {
// stopEvent: ((event: Event) => boolean) | null, stopEvent: ((event: Event) => boolean) | null,
// update: ((node: ProseMirrorNode, decorations: Decoration[]) => boolean) | null, update: ((node: ProseMirrorNode, decorations: Decoration[]) => boolean) | null,
// } }
// class ReactNodeView implements NodeView { class ReactNodeView implements NodeView {
// renderer!: ReactRenderer renderer!: ReactRenderer
// editor: Editor editor: Editor
// extension!: Node extension!: Node
// node!: ProseMirrorNode node!: ProseMirrorNode
// decorations!: Decoration[] decorations!: Decoration[]
// getPos!: any getPos!: any
// isDragging = false isDragging = false
// options: ReactNodeViewRendererOptions = { options: ReactNodeViewRendererOptions = {
// stopEvent: null, stopEvent: null,
// update: null, update: null,
// } }
// constructor(component: any, props: NodeViewRendererProps, options?: Partial<ReactNodeViewRendererOptions>) { domWrapper: Element
// 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) { contentDOMWrapper: Element
// const props = {}
// if (!component.displayName) { constructor(component: any, props: NodeViewRendererProps, options?: Partial<ReactNodeViewRendererOptions>) {
// component.displayName = this.extension.config.name this.options = { ...this.options, ...options }
// } this.editor = props.editor as Editor
this.extension = props.extension
this.node = props.node
this.getPos = props.getPos
// this.renderer = new ReactRenderer(component, { this.domWrapper = document.createElement('div')
// editor: this.editor, this.domWrapper.classList.add('dom-wrapper')
// props, this.contentDOMWrapper = document.createElement('div')
// }) this.contentDOMWrapper.classList.add('content-dom-wrapper')
// }
// get dom() { this.mount(component)
// // if (!this.renderer.element) { }
// // return null
// // }
// // if (!this.renderer.element.hasAttribute('data-node-view-wrapper')) { mount(component: any) {
// // throw Error('Please use the NodeViewWrapper component for your node view.') const props = {
// // } editor: this.editor,
node: this.node,
decorations: this.decorations,
selected: false,
extension: this.extension,
getPos: () => this.getPos(),
updateAttributes: (attributes = {}) => this.updateAttributes(attributes),
}
// return this.renderer.element if (!component.displayName) {
// } component.displayName = this.extension.config.name
}
// get contentDOM() { this.renderer = new ReactRenderer(component, {
// // console.log(this.dom) editor: this.editor,
// // return null props,
// // if (!this.renderer.element) { })
// // return null }
// // }
// const hasContent = !this.node.type.isAtom get dom() {
return this.renderer.element
// return this.domWrapper
// if (!hasContent) { // if (!this.renderer.element) {
// return null // return null
// } // }
// const contentElement = this.dom.querySelector('[data-node-view-content]') // if (!this.renderer.element.hasAttribute('data-node-view-wrapper')) {
// throw Error('Please use the NodeViewWrapper component for your node view.')
// }
// return contentElement || this.dom // return this.renderer.element
// } }
// stopEvent(event: Event) { get contentDOM() {
// if (typeof this.options.stopEvent === 'function') { // return this.renderer.element
// return this.options.stopEvent(event) return undefined
// } // return this.renderer.element
// return this.contentDOMWrapper
// const target = (event.target as HTMLElement) // console.log(this.dom)
// const isInElement = this.dom.contains(target) && !this.contentDOM?.contains(target) // return null
// if (!this.renderer.element) {
// return null
// }
// // ignore all events from child nodes // const hasContent = !this.node.type.isAtom
// if (!isInElement) {
// return false
// }
// const { isEditable } = this.editor // if (!hasContent) {
// const { isDragging } = this // return null
// 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 // const contentElement = this.dom.querySelector('[data-node-view-content]')
// // even if `draggable` is set to `false`
// // this fix prevents that
// if (!isDraggable && isSelectable && isDragEvent) {
// event.preventDefault()
// }
// if (isDraggable && isDragEvent && !isDragging) { // return contentElement || this.dom
// event.preventDefault() }
// return false
// }
// // we have to store that dragging started stopEvent(event: Event) {
// if (isDraggable && isEditable && !isDragging && isClickEvent) { if (typeof this.options.stopEvent === 'function') {
// const dragHandle = target.closest('[data-drag-handle]') return this.options.stopEvent(event)
// const isValidDragHandle = dragHandle }
// && (this.dom === dragHandle || (this.dom.contains(dragHandle)))
// if (isValidDragHandle) { const target = (event.target as HTMLElement)
// this.isDragging = true const isInElement = this.dom.contains(target) && !this.contentDOM?.contains(target)
// document.addEventListener('dragend', () => {
// this.isDragging = false
// }, { once: true })
// }
// }
// // these events are handled by prosemirror // ignore all events from child nodes
// if ( if (!isInElement) {
// isDragging return false
// || isCopyEvent }
// || isPasteEvent
// || isCutEvent
// || (isClickEvent && isSelectable)
// ) {
// return false
// }
// return true 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'
// ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) { // ProseMirror tries to drag selectable nodes
// if (mutation.type === 'selection') { // even if `draggable` is set to `false`
// if (this.node.isLeaf) { // this fix prevents that
// return true if (!isDraggable && isSelectable && isDragEvent) {
// } event.preventDefault()
}
// return false if (isDraggable && isDragEvent && !isDragging) {
// } event.preventDefault()
return false
}
// if (!this.contentDOM) { // we have to store that dragging started
// return true if (isDraggable && isEditable && !isDragging && isClickEvent) {
// } const dragHandle = target.closest('[data-drag-handle]')
const isValidDragHandle = dragHandle
&& (this.dom === dragHandle || (this.dom.contains(dragHandle)))
// const contentDOMHasChanged = !this.contentDOM.contains(mutation.target) if (isValidDragHandle) {
// || this.contentDOM === mutation.target this.isDragging = true
document.addEventListener('dragend', () => {
this.isDragging = false
}, { once: true })
}
}
// return contentDOMHasChanged // these events are handled by prosemirror
// } if (
isDragging
|| isCopyEvent
|| isPasteEvent
|| isCutEvent
|| (isClickEvent && isSelectable)
) {
return false
}
// destroy() { return true
// this.renderer.destroy() }
// }
// update(node: ProseMirrorNode, decorations: Decoration[]) { ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) {
// if (typeof this.options.update === 'function') { if (mutation.type === 'selection') {
// return this.options.update(node, decorations) if (this.node.isLeaf) {
// } return true
}
// if (node.type !== this.node.type) { return false
// return false }
// }
// if (node === this.node && this.decorations === decorations) { if (!this.contentDOM) {
// return true return true
// } }
// this.node = node const contentDOMHasChanged = !this.contentDOM.contains(mutation.target)
// this.decorations = decorations || this.contentDOM === mutation.target
// // this.renderer.updateProps({ node, decorations })
// this.renderer.render()
// return true return contentDOMHasChanged
// } }
// }
// export function ReactNodeViewRenderer(component: any, options?: Partial<ReactNodeViewRendererOptions>): NodeViewRenderer { destroy() {
// return (props: NodeViewRendererProps) => { this.renderer.destroy()
// // 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 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
}
selectNode() {
this.renderer.updateProps({
selected: true,
})
}
deselectNode() {
this.renderer.updateProps({
selected: false,
})
}
}
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,58 +1,91 @@
// // @ts-nocheck // @ts-nocheck
// import React from 'react' import React from 'react'
// import { render, unmountComponentAtNode } from 'react-dom' import ReactDOM, { render, unmountComponentAtNode } from 'react-dom'
// import { Editor } from './Editor' import { Editor } from './Editor'
// export interface VueRendererOptions { export interface VueRendererOptions {
// as?: string; as?: string;
// editor: Editor; editor: Editor;
// props?: { [key: string]: any }; props?: { [key: string]: any };
// } }
// export class ReactRenderer { export class ReactRenderer {
// id: string id: string
// editor: Editor editor: Editor
// component: any component: any
// teleportElement: Element teleportElement: Element
// element: Element element: Element
// props: { [key: string]: any } props: { [key: string]: any }
// constructor(component: any, { props = {}, editor }: VueRendererOptions) { constructor(component: any, { props = {}, editor }: VueRendererOptions) {
// this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString() this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString()
// this.component = component this.component = component
// this.editor = editor this.editor = editor
// this.props = props this.props = props
// this.teleportElement = document.createElement('div') this.teleportElement = document.createElement('div')
// // this.teleportElement.setAttribute('data-bla', '') this.teleportElement.classList.add('teleport-element')
// // render(React.createElement(component), this.teleportElement) this.element = this.teleportElement
// // render(React.createElement(component), this.teleportElement)
// this.render()
// // this.element = this.teleportElement.firstElementChild as Element
// this.element = this.teleportElement
// }
// render() { // this.teleportElement.setAttribute('data-bla', '')
// render(React.createElement(this.component), this.teleportElement) // render(React.createElement(component), this.teleportElement)
// } // render(React.createElement(component), this.teleportElement)
// this.render()
// // this.element = this.teleportElement.firstElementChild as Element
// updateProps(props: { [key: string]: any } = {}) { console.log({ props })
// // TODO
// }
// destroy() { // this.bla = ReactDOM.createPortal(
// // TODO // React.createElement(this.component, props),
// // console.log('DESTROY', { bla: this.teleportElement }) // this.teleportElement,
// // console.log(document.querySelector('[data-bla]')) // )
// unmountComponentAtNode(this.teleportElement)
// // unmountComponentAtNode(document.querySelector('[data-bla]'))
// }
// } this.bla = React.createElement(this.component, props)
// console.log({ bla })
if (this.editor?.contentComponent) {
this.editor.contentComponent.setState({
renderers: this.editor.contentComponent.state.renderers.set(
this.id,
this,
),
})
}
}
render() {
render(React.createElement(this.component), this.teleportElement)
}
updateProps(props: { [key: string]: any } = {}) {
// TODO
console.log('update props', { props })
}
destroy() {
// TODO
// console.log('DESTROY', { bla: this.teleportElement })
// console.log(document.querySelector('[data-bla]'))
// unmountComponentAtNode(this.teleportElement)
// unmountComponentAtNode(document.querySelector('[data-bla]'))
if (this.editor?.contentComponent) {
const { renderers } = this.editor.contentComponent.state
renderers.delete(this.id)
this.editor.contentComponent.setState({
renderers,
})
}
}
}

View File

@@ -2,6 +2,6 @@
export * from '@tiptap/core' export * from '@tiptap/core'
export { Editor } from './Editor' export { Editor } from './Editor'
export * from './useEditor' export * from './useEditor'
// export * from './ReactRenderer' export * from './ReactRenderer'
// export * from './ReactNodeViewRenderer' export * from './ReactNodeViewRenderer'
export * from './EditorContent' export * from './EditorContent'