diff --git a/docs/src/demos/Examples/Default/React/index.jsx b/docs/src/demos/Examples/Default/React/index.jsx index 3f8d89df..b54f9e66 100644 --- a/docs/src/demos/Examples/Default/React/index.jsx +++ b/docs/src/demos/Examples/Default/React/index.jsx @@ -1,6 +1,7 @@ import React from 'react' -import { useEditor, EditorContent } from '@tiptap/react' +import { useEditor, EditorContent, ReactNodeViewRenderer } from '@tiptap/react' import { defaultExtensions } from '@tiptap/starter-kit' +import Paragraph from '@tiptap/extension-paragraph' import './styles.scss' const MenuBar = ({ editor }) => { @@ -125,38 +126,49 @@ const MenuBar = ({ editor }) => { export default () => { const editor = useEditor({ extensions: [ - ...defaultExtensions(), + Paragraph.extend({ + addNodeView() { + return ReactNodeViewRenderer(({ selected }) => { + console.log({selected}) + return ( +
noooode view {selected}
+ ) + }) + } + }), + ...defaultExtensions().filter(item => item.config.name !== 'paragraph'), ], - content: ` -

- Hi there, -

-

- this is a basic basic example of tiptap. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists: -

- -

- Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block: -

-
body {
-  display: none;
-}
-

- I know, I know, this is impressive. It’s only the tip of the iceberg though. Give it a try and click a little bit around. Don’t forget to check the other examples too. -

-
- Wow, that’s amazing. Good work, boy! 👏 -
- — Mom -
- `, + content: `

test

`, +// content: ` +//

+// Hi there, +//

+//

+// this is a basic basic example of tiptap. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists: +//

+// +//

+// Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block: +//

+//
body {
+//   display: none;
+// }
+//

+// I know, I know, this is impressive. It’s only the tip of the iceberg though. Give it a try and click a little bit around. Don’t forget to check the other examples too. +//

+//
+// Wow, that’s amazing. Good work, boy! 👏 +//
+// — Mom +//
+// `, }) return ( diff --git a/packages/react/src/EditorContent.tsx b/packages/react/src/EditorContent.tsx index eb14d226..cf86bd0b 100644 --- a/packages/react/src/EditorContent.tsx +++ b/packages/react/src/EditorContent.tsx @@ -1,11 +1,67 @@ import React from 'react' +import ReactDOM from 'react-dom' import { Editor } from './Editor' type EditorContentProps = { editor: Editor | null } -export class PureEditorContent extends React.Component { +// const Portals = ({ editor }: { editor: Editor | null }) => { +// if (!editor?.contentComponent) { +// return null +// } + +// console.log('render portals') + +// return ( +//
portaaals
+// ) +// } + +const Portals = ({ renderers }: { renderers: Map }) => { + return ( +
+ {Array.from(renderers).map(([key, renderer]) => { + + // console.log({renderer}) + // return ( + //
{value}
+ // ) + + // return React.createElement(renderer.component) + + + // return ( + // + // {ReactDOM.createPortal( + // React.createElement(renderer.component), + // renderer.teleportElement, + // )} + // + // ) + + return ( + + {renderer.bla} + + ) + + // return ReactDOM.createPortal( + // React.createElement(renderer.component), + // renderer.teleportElement, + // ) + })} +
+ ) +} + +// const Content = React.memo(({ reference }: { reference: React.RefObject }) => { +// return ( +//
+// ) +// }) + +export class PureEditorContent extends React.Component { editorContentRef: React.RefObject constructor(props: EditorContentProps) { @@ -13,14 +69,33 @@ export class PureEditorContent extends React.Component { + // if (this.props?.editor?.contentComponent) { + // this.props.editor.contentComponent.setState({ + // renderers: this.state.renderers.set(Math.random(), Math.random()) + // }) + // } + // }, 1000) } componentDidUpdate() { const { editor } = this.props if (editor && editor.options.element) { + if (editor.contentComponent) { + return + } + + // this.setState({ + // editor, + // }) + + console.log('UPDATE') + const element = this.editorContentRef.current element.appendChild(editor.options.element.firstChild) @@ -39,10 +114,20 @@ export class PureEditorContent extends React.Component + <> +
+ {/* */} + {/* */} + + + ) } } -export const EditorContent = React.memo(PureEditorContent); +export const EditorContent = React.memo(PureEditorContent) + +// export const EditorContent = PureEditorContent diff --git a/packages/react/src/ReactNodeViewRenderer.ts b/packages/react/src/ReactNodeViewRenderer.ts index b735fbc1..f1d6383b 100644 --- a/packages/react/src/ReactNodeViewRenderer.ts +++ b/packages/react/src/ReactNodeViewRenderer.ts @@ -1,208 +1,246 @@ -// // @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' +// @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' -// interface ReactNodeViewRendererOptions { -// stopEvent: ((event: Event) => boolean) | null, -// update: ((node: ProseMirrorNode, decorations: Decoration[]) => boolean) | null, -// } +interface ReactNodeViewRendererOptions { + stopEvent: ((event: Event) => 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 = { -// stopEvent: null, -// update: null, -// } + options: ReactNodeViewRendererOptions = { + stopEvent: null, + update: null, + } -// constructor(component: any, props: NodeViewRendererProps, options?: Partial) { -// 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) -// } + domWrapper: Element -// mount(component: any) { -// const props = {} + contentDOMWrapper: Element -// if (!component.displayName) { -// component.displayName = this.extension.config.name -// } + constructor(component: any, props: NodeViewRendererProps, options?: Partial) { + 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, { -// editor: this.editor, -// props, -// }) -// } + this.domWrapper = document.createElement('div') + this.domWrapper.classList.add('dom-wrapper') + this.contentDOMWrapper = document.createElement('div') + this.contentDOMWrapper.classList.add('content-dom-wrapper') -// get dom() { -// // if (!this.renderer.element) { -// // return null -// // } + this.mount(component) + } -// // if (!this.renderer.element.hasAttribute('data-node-view-wrapper')) { -// // throw Error('Please use the NodeViewWrapper component for your node view.') -// // } + mount(component: any) { + 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() { -// // console.log(this.dom) -// // return null -// // if (!this.renderer.element) { -// // return null -// // } + this.renderer = new ReactRenderer(component, { + editor: this.editor, + props, + }) + } -// const hasContent = !this.node.type.isAtom + get dom() { + return this.renderer.element + // return this.domWrapper -// if (!hasContent) { -// return null -// } + // if (!this.renderer.element) { + // 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) { -// if (typeof this.options.stopEvent === 'function') { -// return this.options.stopEvent(event) -// } + get contentDOM() { + // return this.renderer.element + return undefined + // return this.renderer.element + // return this.contentDOMWrapper -// const target = (event.target as HTMLElement) -// const isInElement = this.dom.contains(target) && !this.contentDOM?.contains(target) + // console.log(this.dom) + // return null + // if (!this.renderer.element) { + // return null + // } -// // ignore all events from child nodes -// if (!isInElement) { -// return false -// } + // const hasContent = !this.node.type.isAtom -// 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' + // if (!hasContent) { + // return null + // } -// // ProseMirror tries to drag selectable nodes -// // even if `draggable` is set to `false` -// // this fix prevents that -// if (!isDraggable && isSelectable && isDragEvent) { -// event.preventDefault() -// } + // const contentElement = this.dom.querySelector('[data-node-view-content]') -// if (isDraggable && isDragEvent && !isDragging) { -// event.preventDefault() -// return false -// } + // return contentElement || this.dom + } -// // 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))) + stopEvent(event: Event) { + if (typeof this.options.stopEvent === 'function') { + return this.options.stopEvent(event) + } -// if (isValidDragHandle) { -// this.isDragging = true -// document.addEventListener('dragend', () => { -// this.isDragging = false -// }, { once: true }) -// } -// } + const target = (event.target as HTMLElement) + const isInElement = this.dom.contains(target) && !this.contentDOM?.contains(target) -// // these events are handled by prosemirror -// if ( -// isDragging -// || isCopyEvent -// || isPasteEvent -// || isCutEvent -// || (isClickEvent && isSelectable) -// ) { -// return false -// } + // ignore all events from child nodes + if (!isInElement) { + 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 }) { -// if (mutation.type === 'selection') { -// if (this.node.isLeaf) { -// return true -// } + // ProseMirror tries to drag selectable nodes + // even if `draggable` is set to `false` + // this fix prevents that + if (!isDraggable && isSelectable && isDragEvent) { + event.preventDefault() + } -// return false -// } + if (isDraggable && isDragEvent && !isDragging) { + event.preventDefault() + return false + } -// if (!this.contentDOM) { -// return true -// } + // 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))) -// const contentDOMHasChanged = !this.contentDOM.contains(mutation.target) -// || this.contentDOM === mutation.target + if (isValidDragHandle) { + 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() { -// this.renderer.destroy() -// } + return true + } -// update(node: ProseMirrorNode, decorations: Decoration[]) { -// if (typeof this.options.update === 'function') { -// return this.options.update(node, decorations) -// } + ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) { + if (mutation.type === 'selection') { + if (this.node.isLeaf) { + return true + } -// if (node.type !== this.node.type) { -// return false -// } + return false + } -// if (node === this.node && this.decorations === decorations) { -// return true -// } + if (!this.contentDOM) { + return true + } -// this.node = node -// this.decorations = decorations -// // this.renderer.updateProps({ node, decorations }) -// this.renderer.render() + const contentDOMHasChanged = !this.contentDOM.contains(mutation.target) + || this.contentDOM === mutation.target -// return true -// } -// } + return contentDOMHasChanged + } -// export function ReactNodeViewRenderer(component: any, options?: Partial): NodeViewRenderer { -// return (props: NodeViewRendererProps) => { -// // try to get the parent component -// // this is important for vue devtools to show the component hierarchy correctly -// // maybe it’s `undefined` because isn’t rendered yet -// if (!(props.editor as Editor).contentComponent) { -// return {} -// } + destroy() { + this.renderer.destroy() + } -// 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): NodeViewRenderer { + return (props: NodeViewRendererProps) => { + // try to get the parent component + // this is important for vue devtools to show the component hierarchy correctly + // maybe it’s `undefined` because isn’t rendered yet + if (!(props.editor as Editor).contentComponent) { + return {} + } + + return new ReactNodeView(component, props, options) as NodeView + } +} diff --git a/packages/react/src/ReactRenderer.ts b/packages/react/src/ReactRenderer.ts index 3bae50d0..98341d71 100644 --- a/packages/react/src/ReactRenderer.ts +++ b/packages/react/src/ReactRenderer.ts @@ -1,58 +1,91 @@ -// // @ts-nocheck +// @ts-nocheck -// import React from 'react' -// import { render, unmountComponentAtNode } from 'react-dom' +import React from 'react' +import ReactDOM, { render, unmountComponentAtNode } from 'react-dom' -// import { Editor } from './Editor' +import { Editor } from './Editor' -// export interface VueRendererOptions { -// as?: string; -// editor: Editor; -// props?: { [key: string]: any }; -// } +export interface VueRendererOptions { + as?: string; + editor: Editor; + props?: { [key: string]: any }; +} -// export class ReactRenderer { -// id: string +export class ReactRenderer { + 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) { -// this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString() -// this.component = component -// this.editor = editor -// this.props = props + 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 -// } + this.teleportElement = document.createElement('div') + this.teleportElement.classList.add('teleport-element') + this.element = this.teleportElement -// render() { -// render(React.createElement(this.component), this.teleportElement) -// } + // 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 -// updateProps(props: { [key: string]: any } = {}) { -// // TODO -// } + console.log({ props }) -// destroy() { -// // TODO -// // console.log('DESTROY', { bla: this.teleportElement }) -// // console.log(document.querySelector('[data-bla]')) -// unmountComponentAtNode(this.teleportElement) -// // unmountComponentAtNode(document.querySelector('[data-bla]')) -// } + // this.bla = ReactDOM.createPortal( + // React.createElement(this.component, props), + // this.teleportElement, + // ) -// } + 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, + }) + } + } + +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 19177902..27e45903 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -2,6 +2,6 @@ export * from '@tiptap/core' export { Editor } from './Editor' export * from './useEditor' -// export * from './ReactRenderer' -// export * from './ReactNodeViewRenderer' +export * from './ReactRenderer' +export * from './ReactNodeViewRenderer' export * from './EditorContent'