diff --git a/docs/src/demos/Examples/Community/index.vue b/docs/src/demos/Examples/Community/index.vue
index 9c9730af..7aec67da 100644
--- a/docs/src/demos/Examples/Community/index.vue
+++ b/docs/src/demos/Examples/Community/index.vue
@@ -110,7 +110,7 @@ export default {
})
},
onKeyDown(props) {
- return component.vm.onKeyDown(props)
+ return component.ref.onKeyDown(props)
},
onExit() {
popup[0].destroy()
diff --git a/docs/src/demos/Experiments/Commands/index.vue b/docs/src/demos/Experiments/Commands/index.vue
index e59e8878..6a5b9abc 100644
--- a/docs/src/demos/Experiments/Commands/index.vue
+++ b/docs/src/demos/Experiments/Commands/index.vue
@@ -105,7 +105,7 @@ export default {
})
},
onKeyDown(props) {
- return component.vm.onKeyDown(props)
+ return component.ref.onKeyDown(props)
},
onExit() {
popup[0].destroy()
diff --git a/docs/src/demos/Nodes/Mention/index.vue b/docs/src/demos/Nodes/Mention/index.vue
index 9a5dc8a1..ccc93ecb 100644
--- a/docs/src/demos/Nodes/Mention/index.vue
+++ b/docs/src/demos/Nodes/Mention/index.vue
@@ -69,7 +69,7 @@ export default {
})
},
onKeyDown(props) {
- return component.vm.onKeyDown(props)
+ return component.ref.onKeyDown(props)
},
onExit() {
popup[0].destroy()
diff --git a/docs/src/demos/ReactPlayground/index.jsx b/docs/src/demos/ReactPlayground/index.jsx
new file mode 100644
index 00000000..085713b2
--- /dev/null
+++ b/docs/src/demos/ReactPlayground/index.jsx
@@ -0,0 +1,290 @@
+import React from 'react'
+import tippy from 'tippy.js'
+import { useEditor, EditorContent, ReactRenderer, ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from '@tiptap/react'
+import { defaultExtensions } from '@tiptap/starter-kit'
+import Heading from '@tiptap/extension-heading'
+import Paragraph from '@tiptap/extension-paragraph'
+import Mention from '@tiptap/extension-mention'
+import './styles.scss'
+import { render } from 'react-dom'
+
+const MenuBar = ({ editor }) => {
+ if (!editor) {
+ return null
+ }
+
+ return (
+ <>
+ editor.chain().focus().toggleBold().run()}
+ className={editor.isActive('bold') ? 'is-active' : ''}
+ >
+ bold
+
+ editor.chain().focus().toggleItalic().run()}
+ className={editor.isActive('italic') ? 'is-active' : ''}
+ >
+ italic
+
+ editor.chain().focus().toggleStrike().run()}
+ className={editor.isActive('strike') ? 'is-active' : ''}
+ >
+ strike
+
+ editor.chain().focus().toggleCode().run()}
+ className={editor.isActive('code') ? 'is-active' : ''}
+ >
+ code
+
+ editor.chain().focus().unsetAllMarks().run()}>
+ clear marks
+
+ editor.chain().focus().clearNodes().run()}>
+ clear nodes
+
+ editor.chain().focus().setParagraph().run()}
+ className={editor.isActive('paragraph') ? 'is-active' : ''}
+ >
+ paragraph
+
+ editor.chain().focus().toggleHeading({ level: 1 }).run()}
+ className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
+ >
+ h1
+
+ editor.chain().focus().toggleHeading({ level: 2 }).run()}
+ className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''}
+ >
+ h2
+
+ editor.chain().focus().toggleHeading({ level: 3 }).run()}
+ className={editor.isActive('heading', { level: 3 }) ? 'is-active' : ''}
+ >
+ h3
+
+ editor.chain().focus().toggleHeading({ level: 4 }).run()}
+ className={editor.isActive('heading', { level: 4 }) ? 'is-active' : ''}
+ >
+ h4
+
+ editor.chain().focus().toggleHeading({ level: 5 }).run()}
+ className={editor.isActive('heading', { level: 5 }) ? 'is-active' : ''}
+ >
+ h5
+
+ editor.chain().focus().toggleHeading({ level: 6 }).run()}
+ className={editor.isActive('heading', { level: 6 }) ? 'is-active' : ''}
+ >
+ h6
+
+ editor.chain().focus().toggleBulletList().run()}
+ className={editor.isActive('bulletList') ? 'is-active' : ''}
+ >
+ bullet list
+
+ editor.chain().focus().toggleOrderedList().run()}
+ className={editor.isActive('orderedList') ? 'is-active' : ''}
+ >
+ ordered list
+
+ editor.chain().focus().toggleCodeBlock().run()}
+ className={editor.isActive('codeBlock') ? 'is-active' : ''}
+ >
+ code block
+
+ editor.chain().focus().toggleBlockquote().run()}
+ className={editor.isActive('blockquote') ? 'is-active' : ''}
+ >
+ blockquote
+
+ editor.chain().focus().setHorizontalRule().run()}>
+ horizontal rule
+
+ editor.chain().focus().setHardBreak().run()}>
+ hard break
+
+ editor.chain().focus().undo().run()}>
+ undo
+
+ editor.chain().focus().redo().run()}>
+ redo
+
+ >
+ )
+}
+
+const MentionList = (props) => {
+ console.log({props})
+
+ return (
+
+ mentions
+ {props.items.map((item) => (
+
+ {item}
+
+ ))}
+
+ )
+}
+
+class MentionList2 extends React.Component {
+
+ onKeyDown(props) {
+ console.log('onKeyDown', props)
+ }
+
+ render() {
+ return (
+
+ mentions
+ {this.props.items.map((item, index) => (
+
+ {item}
+
+ ))}
+
+ )
+ }
+}
+
+export default () => {
+ const editor = useEditor({
+ // onTransaction({ editor }) {
+ // console.log('anchor', editor.state.selection.anchor)
+ // },
+ extensions: [
+ ...defaultExtensions().filter(item => item.config.name !== 'heading'),
+ Heading.extend({
+ draggable: true,
+ addNodeView() {
+ return ReactNodeViewRenderer((props) => {
+ return (
+
+
+ ⠿
+ level: {props.node.attrs.level}
+ props.updateAttributes({ level: 1 })}>
+ set level 1
+
+
+
+
+ )
+ })
+ }
+ }),
+ Mention.configure({
+ suggestion: {
+ items: query => {
+ return [
+ 'Lea Thompson', 'Cyndi Lauper', 'Tom Cruise', 'Madonna', 'Jerry Hall', 'Joan Collins', 'Winona Ryder', 'Christina Applegate', 'Alyssa Milano', 'Molly Ringwald', 'Ally Sheedy', 'Debbie Harry', 'Olivia Newton-John', 'Elton John', 'Michael J. Fox', 'Axl Rose', 'Emilio Estevez', 'Ralph Macchio', 'Rob Lowe', 'Jennifer Grey', 'Mickey Rourke', 'John Cusack', 'Matthew Broderick', 'Justine Bateman', 'Lisa Bonet',
+ ].filter(item => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 10)
+ },
+ render: () => {
+ let reactRenderer
+ let popup
+
+ return {
+ onStart: props => {
+ reactRenderer = new ReactRenderer(MentionList2, {
+ props,
+ editor: props.editor,
+ })
+
+ popup = tippy('body', {
+ getReferenceClientRect: props.clientRect,
+ appendTo: () => document.body,
+ content: reactRenderer.element,
+ showOnCreate: true,
+ interactive: true,
+ trigger: 'manual',
+ placement: 'bottom-start',
+ })
+ },
+ onUpdate(props) {
+ reactRenderer.updateProps(props)
+
+ popup[0].setProps({
+ getReferenceClientRect: props.clientRect,
+ })
+ },
+ onKeyDown(props) {
+ return reactRenderer.ref.onKeyDown(props)
+ },
+ onExit() {
+ popup[0].destroy()
+ reactRenderer.destroy()
+ },
+ }
+ }
+ },
+ })
+ ],
+ content: `
+ heading
+ heading
+ 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:
+//
+//
+//
+// That’s a bullet list with one …
+//
+//
+// … or two list items.
+//
+//
+//
+// 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 (
+
+
+ editor.setEditable(true)}>editable
+ editor.setEditable(false)}>readonly
+
+
+
+
+ )
+}
diff --git a/docs/src/demos/ReactPlayground/index.spec.js b/docs/src/demos/ReactPlayground/index.spec.js
new file mode 100644
index 00000000..350c2d41
--- /dev/null
+++ b/docs/src/demos/ReactPlayground/index.spec.js
@@ -0,0 +1,22 @@
+context('/demos/Examples/Default/React', () => {
+ before(() => {
+ cy.visit('/demos/Examples/Default/React')
+ })
+
+ beforeEach(() => {
+ cy.get('.ProseMirror').then(([{ editor }]) => {
+ editor.commands.setContent('Example Text ')
+ cy.get('.ProseMirror').type('{selectall}')
+ })
+ })
+
+ it('should apply the paragraph style when the keyboard shortcut is pressed', () => {
+ cy.get('.ProseMirror h1').should('exist')
+ cy.get('.ProseMirror p').should('not.exist')
+
+ cy.get('.ProseMirror')
+ .trigger('keydown', { modKey: true, altKey: true, key: '0' })
+ .find('p')
+ .should('contain', 'Example Text')
+ })
+})
diff --git a/docs/src/demos/ReactPlayground/styles.scss b/docs/src/demos/ReactPlayground/styles.scss
new file mode 100644
index 00000000..dc996245
--- /dev/null
+++ b/docs/src/demos/ReactPlayground/styles.scss
@@ -0,0 +1,55 @@
+/* Basic editor styles */
+.ProseMirror {
+ > * + * {
+ margin-top: 0.75em;
+ }
+
+ ul,
+ ol {
+ padding: 0 1rem;
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ line-height: 1.1;
+ }
+
+ code {
+ background-color: rgba(#616161, 0.1);
+ color: #616161;
+ }
+
+ pre {
+ background: #0D0D0D;
+ color: #FFF;
+ font-family: 'JetBrainsMono', monospace;
+ padding: 0.75rem 1rem;
+ border-radius: 0.5rem;
+
+ code {
+ color: inherit;
+ background: none;
+ font-size: 0.8rem;
+ }
+ }
+
+ img {
+ max-width: 100%;
+ height: auto;
+ }
+
+ blockquote {
+ padding-left: 1rem;
+ border-left: 2px solid rgba(#0D0D0D, 0.1);
+ }
+
+ hr {
+ border: none;
+ border-top: 2px solid rgba(#0D0D0D, 0.1);
+ margin: 2rem 0;
+ }
+}
diff --git a/packages/react/src/Editor.ts b/packages/react/src/Editor.ts
index b687498e..06614c23 100644
--- a/packages/react/src/Editor.ts
+++ b/packages/react/src/Editor.ts
@@ -1,6 +1,7 @@
import React from 'react'
import { Editor as CoreEditor } from '@tiptap/core'
+import { EditorContentProps, EditorContentState } from './EditorContent'
export class Editor extends CoreEditor {
- public contentComponent: React.Component | null = null
+ public contentComponent: React.Component | null = null
}
diff --git a/packages/react/src/EditorContent.tsx b/packages/react/src/EditorContent.tsx
index eb14d226..0e96b494 100644
--- a/packages/react/src/EditorContent.tsx
+++ b/packages/react/src/EditorContent.tsx
@@ -1,11 +1,32 @@
import React from 'react'
+import ReactDOM from 'react-dom'
import { Editor } from './Editor'
+import { ReactRenderer } from './ReactRenderer'
-type EditorContentProps = {
- editor: Editor | null
+const Portals: React.FC<{ renderers: Map }> = ({ renderers }) => {
+ return (
+ <>
+ {Array.from(renderers).map(([key, renderer]) => {
+ return ReactDOM.createPortal(
+ renderer.reactElement,
+ renderer.element,
+ key,
+ )
+ })}
+ >
+ )
}
-export class PureEditorContent extends React.Component {
+export interface EditorContentProps {
+ editor: Editor | null,
+}
+
+export interface EditorContentState {
+ editor: Editor | null,
+ renderers: Map
+}
+
+export class PureEditorContent extends React.Component {
editorContentRef: React.RefObject
constructor(props: EditorContentProps) {
@@ -13,7 +34,8 @@ export class PureEditorContent extends React.Component
+ <>
+
+
+ >
)
}
}
-export const EditorContent = React.memo(PureEditorContent);
+export const EditorContent = React.memo(PureEditorContent)
diff --git a/packages/react/src/NodeViewContent.tsx b/packages/react/src/NodeViewContent.tsx
new file mode 100644
index 00000000..fbc64d8a
--- /dev/null
+++ b/packages/react/src/NodeViewContent.tsx
@@ -0,0 +1,19 @@
+import React from 'react'
+import { useReactNodeView } from './useReactNodeView'
+
+export interface NodeViewContentProps {
+ as: React.ElementType
+}
+
+export const NodeViewContent: React.FC = props => {
+ const { isEditable } = useReactNodeView()
+ const Tag = props.as || 'div'
+
+ return (
+
+ )
+}
diff --git a/packages/react/src/NodeViewWrapper.tsx b/packages/react/src/NodeViewWrapper.tsx
new file mode 100644
index 00000000..6e159123
--- /dev/null
+++ b/packages/react/src/NodeViewWrapper.tsx
@@ -0,0 +1,21 @@
+import React from 'react'
+import { useReactNodeView } from './useReactNodeView'
+
+export interface NodeViewWrapperProps {
+ as: React.ElementType
+}
+
+export const NodeViewWrapper: React.FC = props => {
+ const { onDragStart } = useReactNodeView()
+ const Tag = props.as || 'div'
+
+ return (
+
+ {props.children}
+
+ )
+}
diff --git a/packages/react/src/ReactNodeViewRenderer.ts b/packages/react/src/ReactNodeViewRenderer.ts
deleted file mode 100644
index b735fbc1..00000000
--- a/packages/react/src/ReactNodeViewRenderer.ts
+++ /dev/null
@@ -1,208 +0,0 @@
-// // @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,
-// }
-
-// 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) {
-// 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): 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/ReactNodeViewRenderer.tsx b/packages/react/src/ReactNodeViewRenderer.tsx
new file mode 100644
index 00000000..a122ef60
--- /dev/null
+++ b/packages/react/src/ReactNodeViewRenderer.tsx
@@ -0,0 +1,273 @@
+import React, { useState, useEffect } from 'react'
+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 { Editor } from './Editor'
+import { ReactRenderer } from './ReactRenderer'
+import { ReactNodeViewContext } from './useReactNodeView'
+
+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) {
+ 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)
+ }
+
+ onDragStart(event: DragEvent) {
+ const { view } = this.editor
+ const target = (event.target as HTMLElement)
+
+ if (this.contentDOM?.contains(target)) {
+ return
+ }
+
+ // sometimes `event.target` is not the `dom` element
+ event.dataTransfer?.setDragImage(this.dom, 0, 0)
+
+ const selection = NodeSelection.create(view.state.doc, this.getPos())
+ const transaction = view.state.tr.setSelection(selection)
+
+ view.dispatch(transaction)
+ }
+
+ 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),
+ }
+
+ if (!Component.displayName) {
+ const capitalizeFirstChar = (string: string): string => {
+ return string.charAt(0).toUpperCase() + string.substring(1)
+ }
+
+ Component.displayName = capitalizeFirstChar(this.extension.config.name)
+ }
+
+ const ReactNodeView: React.FunctionComponent = (props) => {
+ const [isEditable, setIsEditable] = useState(this.editor.isEditable)
+ const onDragStart = this.onDragStart.bind(this)
+ const onViewUpdate = () => {
+ setIsEditable(this.editor.isEditable)
+ }
+
+ useEffect(() => {
+ this.editor.on('viewUpdate', onViewUpdate)
+
+ return () => {
+ this.editor.off('viewUpdate', onViewUpdate)
+ }
+ }, [])
+
+ return (
+
+
+
+ )
+ }
+
+ this.renderer = new ReactRenderer(ReactNodeView, {
+ editor: this.editor,
+ props,
+ })
+ }
+
+ get dom() {
+ if (!this.renderer.element.firstElementChild?.hasAttribute('data-node-view-wrapper')) {
+ throw Error('Please use the ReactViewWrapper component for your node view.')
+ }
+
+ return this.renderer.element
+ }
+
+ get contentDOM() {
+ if (this.node.isLeaf) {
+ 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
+ }
+
+ updateAttributes(attributes: {}) {
+ if (!this.editor.view.editable) {
+ return
+ }
+
+ const { state } = this.editor.view
+ const pos = this.getPos()
+ const transaction = state.tr.setNodeMarkup(pos, undefined, {
+ ...this.node.attrs,
+ ...attributes,
+ })
+
+ this.editor.view.dispatch(transaction)
+ }
+
+ 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
deleted file mode 100644
index 3bae50d0..00000000
--- a/packages/react/src/ReactRenderer.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-// // @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]'))
-// }
-
-// }
diff --git a/packages/react/src/ReactRenderer.tsx b/packages/react/src/ReactRenderer.tsx
new file mode 100644
index 00000000..3f2e5763
--- /dev/null
+++ b/packages/react/src/ReactRenderer.tsx
@@ -0,0 +1,84 @@
+import React from 'react'
+import { AnyObject } from '@tiptap/core'
+import { Editor } from './Editor'
+
+function isClassComponent(Component: any) {
+ return !!(
+ typeof Component === 'function'
+ && Component.prototype
+ && Component.prototype.isReactComponent
+ )
+}
+
+export interface ReactRendererOptions {
+ as?: string,
+ editor: Editor,
+ props?: AnyObject,
+}
+
+export class ReactRenderer {
+ id: string
+
+ editor: Editor
+
+ component: any
+
+ element: Element
+
+ props: AnyObject
+
+ reactElement: React.ReactNode
+
+ ref: React.Component | null = null
+
+ constructor(component: React.Component | React.FunctionComponent, { props = {}, editor }: ReactRendererOptions) {
+ this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString()
+ this.component = component
+ this.editor = editor
+ this.props = props
+ this.element = document.createElement('div')
+ this.element.classList.add('react-renderer')
+ this.render()
+ }
+
+ render(): void {
+ const Component = this.component
+ const props = this.props
+
+ if (isClassComponent(Component)) {
+ props.ref = (ref: React.Component) => this.ref = ref
+ }
+
+ this.reactElement =
+
+ if (this.editor?.contentComponent) {
+ this.editor.contentComponent.setState({
+ renderers: this.editor.contentComponent.state.renderers.set(
+ this.id,
+ this,
+ ),
+ })
+ }
+ }
+
+ updateProps(props: AnyObject = {}): void {
+ this.props = {
+ ...this.props,
+ ...props,
+ }
+
+ this.render()
+ }
+
+ destroy(): void {
+ 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..6ce42719 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -1,7 +1,8 @@
-// @ts-nocheck
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'
+export * from './NodeViewWrapper'
+export * from './NodeViewContent'
diff --git a/packages/react/src/useReactNodeView.ts b/packages/react/src/useReactNodeView.ts
new file mode 100644
index 00000000..142edbfa
--- /dev/null
+++ b/packages/react/src/useReactNodeView.ts
@@ -0,0 +1,13 @@
+import { createContext, useContext } from 'react'
+
+export interface ReactNodeViewContextProps {
+ isEditable: boolean,
+ onDragStart: (event: DragEvent) => void,
+}
+
+export const ReactNodeViewContext = createContext>({
+ isEditable: undefined,
+ onDragStart: undefined,
+})
+
+export const useReactNodeView = () => useContext(ReactNodeViewContext)
diff --git a/packages/vue-2/src/VueNodeViewRenderer.ts b/packages/vue-2/src/VueNodeViewRenderer.ts
index f36533f4..5401c39d 100644
--- a/packages/vue-2/src/VueNodeViewRenderer.ts
+++ b/packages/vue-2/src/VueNodeViewRenderer.ts
@@ -106,9 +106,7 @@ class VueNodeView implements NodeView {
}
get contentDOM() {
- const hasContent = !this.node.type.isAtom
-
- if (!hasContent) {
+ if (this.node.isLeaf) {
return null
}
diff --git a/packages/vue-2/src/VueRenderer.ts b/packages/vue-2/src/VueRenderer.ts
index 74ddceec..19aa3cab 100644
--- a/packages/vue-2/src/VueRenderer.ts
+++ b/packages/vue-2/src/VueRenderer.ts
@@ -1,21 +1,22 @@
import Vue from 'vue'
+import { AnyObject } from '@tiptap/core'
import { VueConstructor } from 'vue/types/umd'
export class VueRenderer {
- vm!: Vue
+ ref!: Vue
constructor(component: Vue | VueConstructor, props: any) {
const Component = Vue.extend(component)
- this.vm = new Component(props).$mount()
+ this.ref = new Component(props).$mount()
}
get element() {
- return this.vm.$el
+ return this.ref.$el
}
- updateProps(props: { [key: string]: any } = {}) {
- if (!this.vm.$props) {
+ updateProps(props: AnyObject = {}) {
+ if (!this.ref.$props) {
return
}
@@ -26,13 +27,13 @@ export class VueRenderer {
Object
.entries(props)
.forEach(([key, value]) => {
- this.vm.$props[key] = value
+ this.ref.$props[key] = value
})
Vue.config.silent = originalSilent
}
destroy() {
- this.vm.$destroy()
+ this.ref.$destroy()
}
}
diff --git a/packages/vue-3/src/VueNodeViewRenderer.ts b/packages/vue-3/src/VueNodeViewRenderer.ts
index 634ca18c..175a68c3 100644
--- a/packages/vue-3/src/VueNodeViewRenderer.ts
+++ b/packages/vue-3/src/VueNodeViewRenderer.ts
@@ -107,9 +107,7 @@ class VueNodeView implements NodeView {
}
get contentDOM() {
- const hasContent = !this.node.type.isAtom
-
- if (!hasContent) {
+ if (this.node.isLeaf) {
return null
}
diff --git a/packages/vue-3/src/VueRenderer.ts b/packages/vue-3/src/VueRenderer.ts
index 1d6a33f7..45f6e683 100644
--- a/packages/vue-3/src/VueRenderer.ts
+++ b/packages/vue-3/src/VueRenderer.ts
@@ -1,10 +1,10 @@
import { reactive, markRaw, Component } from 'vue'
+import { AnyObject } from '@tiptap/core'
import { Editor } from './Editor'
export interface VueRendererOptions {
- as?: string;
- editor: Editor;
- props?: { [key: string]: any };
+ editor: Editor,
+ props?: AnyObject,
}
export class VueRenderer {
@@ -18,7 +18,7 @@ export class VueRenderer {
element: Element
- props: { [key: string]: any }
+ props: AnyObject
constructor(component: Component, { props = {}, editor }: VueRendererOptions) {
this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString()
@@ -44,7 +44,7 @@ export class VueRenderer {
return this.editor.contentComponent?.ctx.$refs[this.id]
}
- updateProps(props: { [key: string]: any } = {}) {
+ updateProps(props: AnyObject = {}) {
Object
.entries(props)
.forEach(([key, value]) => {
diff --git a/tsconfig.json b/tsconfig.json
index 9700a48c..fcd16506 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -28,7 +28,8 @@
"./shims/vue.d.ts"
],
"include": [
- "**/*.ts"
+ "**/*.ts",
+ "**/*.tsx"
],
"exclude": [
"**/node_modules",