Merge branch 'feature/react-renderer' into main
This commit is contained in:
@@ -110,7 +110,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onKeyDown(props) {
|
onKeyDown(props) {
|
||||||
return component.vm.onKeyDown(props)
|
return component.ref.onKeyDown(props)
|
||||||
},
|
},
|
||||||
onExit() {
|
onExit() {
|
||||||
popup[0].destroy()
|
popup[0].destroy()
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onKeyDown(props) {
|
onKeyDown(props) {
|
||||||
return component.vm.onKeyDown(props)
|
return component.ref.onKeyDown(props)
|
||||||
},
|
},
|
||||||
onExit() {
|
onExit() {
|
||||||
popup[0].destroy()
|
popup[0].destroy()
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onKeyDown(props) {
|
onKeyDown(props) {
|
||||||
return component.vm.onKeyDown(props)
|
return component.ref.onKeyDown(props)
|
||||||
},
|
},
|
||||||
onExit() {
|
onExit() {
|
||||||
popup[0].destroy()
|
popup[0].destroy()
|
||||||
|
|||||||
290
docs/src/demos/ReactPlayground/index.jsx
Normal file
290
docs/src/demos/ReactPlayground/index.jsx
Normal file
@@ -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 (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
|
className={editor.isActive('bold') ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
bold
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
|
className={editor.isActive('italic') ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
italic
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||||
|
className={editor.isActive('strike') ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
strike
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||||
|
className={editor.isActive('code') ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
code
|
||||||
|
</button>
|
||||||
|
<button onClick={() => editor.chain().focus().unsetAllMarks().run()}>
|
||||||
|
clear marks
|
||||||
|
</button>
|
||||||
|
<button onClick={() => editor.chain().focus().clearNodes().run()}>
|
||||||
|
clear nodes
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().setParagraph().run()}
|
||||||
|
className={editor.isActive('paragraph') ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
paragraph
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||||
|
className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
h1
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||||
|
className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
h2
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
||||||
|
className={editor.isActive('heading', { level: 3 }) ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
h3
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
|
||||||
|
className={editor.isActive('heading', { level: 4 }) ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
h4
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
|
||||||
|
className={editor.isActive('heading', { level: 5 }) ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
h5
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
|
||||||
|
className={editor.isActive('heading', { level: 6 }) ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
h6
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||||
|
className={editor.isActive('bulletList') ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
bullet list
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||||
|
className={editor.isActive('orderedList') ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
ordered list
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||||
|
className={editor.isActive('codeBlock') ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
code block
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||||
|
className={editor.isActive('blockquote') ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
blockquote
|
||||||
|
</button>
|
||||||
|
<button onClick={() => editor.chain().focus().setHorizontalRule().run()}>
|
||||||
|
horizontal rule
|
||||||
|
</button>
|
||||||
|
<button onClick={() => editor.chain().focus().setHardBreak().run()}>
|
||||||
|
hard break
|
||||||
|
</button>
|
||||||
|
<button onClick={() => editor.chain().focus().undo().run()}>
|
||||||
|
undo
|
||||||
|
</button>
|
||||||
|
<button onClick={() => editor.chain().focus().redo().run()}>
|
||||||
|
redo
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const MentionList = (props) => {
|
||||||
|
console.log({props})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
mentions
|
||||||
|
{props.items.map((item) => (
|
||||||
|
<div>
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class MentionList2 extends React.Component {
|
||||||
|
|
||||||
|
onKeyDown(props) {
|
||||||
|
console.log('onKeyDown', props)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
mentions
|
||||||
|
{this.props.items.map((item, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<NodeViewWrapper>
|
||||||
|
<div className="heading">
|
||||||
|
<span
|
||||||
|
data-drag-handle
|
||||||
|
contentEditable={false}
|
||||||
|
draggable={true}
|
||||||
|
suppressContentEditableWarning={true}
|
||||||
|
>⠿</span>
|
||||||
|
level: {props.node.attrs.level}
|
||||||
|
<button onClick={() => props.updateAttributes({ level: 1 })}>
|
||||||
|
set level 1
|
||||||
|
</button>
|
||||||
|
<NodeViewContent />
|
||||||
|
</div>
|
||||||
|
</NodeViewWrapper>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
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: `
|
||||||
|
<h1>heading</h1>
|
||||||
|
<h2>heading</h2>
|
||||||
|
<p>paragraph</p>
|
||||||
|
`,
|
||||||
|
// content: `
|
||||||
|
// <h2>
|
||||||
|
// Hi there,
|
||||||
|
// </h2>
|
||||||
|
// <p>
|
||||||
|
// this is a basic <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists:
|
||||||
|
// </p>
|
||||||
|
// <ul>
|
||||||
|
// <li>
|
||||||
|
// That’s a bullet list with one …
|
||||||
|
// </li>
|
||||||
|
// <li>
|
||||||
|
// … or two list items.
|
||||||
|
// </li>
|
||||||
|
// </ul>
|
||||||
|
// <p>
|
||||||
|
// Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block:
|
||||||
|
// </p>
|
||||||
|
// <pre><code class="language-css">body {
|
||||||
|
// display: none;
|
||||||
|
// }</code></pre>
|
||||||
|
// <p>
|
||||||
|
// 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.
|
||||||
|
// </p>
|
||||||
|
// <blockquote>
|
||||||
|
// Wow, that’s amazing. Good work, boy! 👏
|
||||||
|
// <br />
|
||||||
|
// — Mom
|
||||||
|
// </blockquote>
|
||||||
|
// `,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => editor.setEditable(true)}>editable</button>
|
||||||
|
<button onClick={() => editor.setEditable(false)}>readonly</button>
|
||||||
|
</div>
|
||||||
|
<MenuBar editor={editor} />
|
||||||
|
<EditorContent editor={editor} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
22
docs/src/demos/ReactPlayground/index.spec.js
Normal file
22
docs/src/demos/ReactPlayground/index.spec.js
Normal file
@@ -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('<h1>Example Text</h1>')
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
55
docs/src/demos/ReactPlayground/styles.scss
Normal file
55
docs/src/demos/ReactPlayground/styles.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Editor as CoreEditor } from '@tiptap/core'
|
import { Editor as CoreEditor } from '@tiptap/core'
|
||||||
|
import { EditorContentProps, EditorContentState } from './EditorContent'
|
||||||
|
|
||||||
export class Editor extends CoreEditor {
|
export class Editor extends CoreEditor {
|
||||||
public contentComponent: React.Component | null = null
|
public contentComponent: React.Component<EditorContentProps, EditorContentState> | null = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,32 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
import { Editor } from './Editor'
|
import { Editor } from './Editor'
|
||||||
|
import { ReactRenderer } from './ReactRenderer'
|
||||||
|
|
||||||
type EditorContentProps = {
|
const Portals: React.FC<{ renderers: Map<string, ReactRenderer> }> = ({ renderers }) => {
|
||||||
editor: Editor | null
|
return (
|
||||||
|
<>
|
||||||
|
{Array.from(renderers).map(([key, renderer]) => {
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
renderer.reactElement,
|
||||||
|
renderer.element,
|
||||||
|
key,
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PureEditorContent extends React.Component<EditorContentProps, EditorContentProps> {
|
export interface EditorContentProps {
|
||||||
|
editor: Editor | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditorContentState {
|
||||||
|
editor: Editor | null,
|
||||||
|
renderers: Map<string, ReactRenderer>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PureEditorContent extends React.Component<EditorContentProps, EditorContentState> {
|
||||||
editorContentRef: React.RefObject<any>
|
editorContentRef: React.RefObject<any>
|
||||||
|
|
||||||
constructor(props: EditorContentProps) {
|
constructor(props: EditorContentProps) {
|
||||||
@@ -13,7 +34,8 @@ 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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,6 +43,14 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
|
|||||||
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,
|
||||||
|
// })
|
||||||
|
|
||||||
const element = this.editorContentRef.current
|
const element = this.editorContentRef.current
|
||||||
|
|
||||||
element.appendChild(editor.options.element.firstChild)
|
element.appendChild(editor.options.element.firstChild)
|
||||||
@@ -40,9 +70,12 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div ref={this.editorContentRef} />
|
<div ref={this.editorContentRef} />
|
||||||
|
<Portals renderers={this.state.renderers} />
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditorContent = React.memo(PureEditorContent);
|
export const EditorContent = React.memo(PureEditorContent)
|
||||||
|
|||||||
19
packages/react/src/NodeViewContent.tsx
Normal file
19
packages/react/src/NodeViewContent.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { useReactNodeView } from './useReactNodeView'
|
||||||
|
|
||||||
|
export interface NodeViewContentProps {
|
||||||
|
as: React.ElementType
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NodeViewContent: React.FC<NodeViewContentProps> = props => {
|
||||||
|
const { isEditable } = useReactNodeView()
|
||||||
|
const Tag = props.as || 'div'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
data-node-view-content=""
|
||||||
|
contentEditable={isEditable}
|
||||||
|
style={{ whiteSpace: 'pre-wrap' }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
packages/react/src/NodeViewWrapper.tsx
Normal file
21
packages/react/src/NodeViewWrapper.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { useReactNodeView } from './useReactNodeView'
|
||||||
|
|
||||||
|
export interface NodeViewWrapperProps {
|
||||||
|
as: React.ElementType
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NodeViewWrapper: React.FC<NodeViewWrapperProps> = props => {
|
||||||
|
const { onDragStart } = useReactNodeView()
|
||||||
|
const Tag = props.as || 'div'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
data-node-view-wrapper=""
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
style={{ whiteSpace: 'normal' }}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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<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 it’s `undefined` because <editor-content> isn’t rendered yet
|
|
||||||
// if (!(props.editor as Editor).contentComponent) {
|
|
||||||
// return {}
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return new ReactNodeView(component, props, options) as NodeView
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
273
packages/react/src/ReactNodeViewRenderer.tsx
Normal file
273
packages/react/src/ReactNodeViewRenderer.tsx
Normal file
@@ -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<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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<ReactNodeViewContext.Provider value={{ onDragStart, isEditable }}>
|
||||||
|
<Component {...props} />
|
||||||
|
</ReactNodeViewContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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<ReactNodeViewRendererOptions>): 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 <editor-content> isn’t rendered yet
|
||||||
|
if (!(props.editor as Editor).contentComponent) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ReactNodeView(component, props, options) as NodeView
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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]'))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// }
|
|
||||||
84
packages/react/src/ReactRenderer.tsx
Normal file
84
packages/react/src/ReactRenderer.tsx
Normal file
@@ -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 = <Component {...props } />
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
// @ts-nocheck
|
|
||||||
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'
|
||||||
|
export * from './NodeViewWrapper'
|
||||||
|
export * from './NodeViewContent'
|
||||||
|
|||||||
13
packages/react/src/useReactNodeView.ts
Normal file
13
packages/react/src/useReactNodeView.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { createContext, useContext } from 'react'
|
||||||
|
|
||||||
|
export interface ReactNodeViewContextProps {
|
||||||
|
isEditable: boolean,
|
||||||
|
onDragStart: (event: DragEvent) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReactNodeViewContext = createContext<Partial<ReactNodeViewContextProps>>({
|
||||||
|
isEditable: undefined,
|
||||||
|
onDragStart: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useReactNodeView = () => useContext(ReactNodeViewContext)
|
||||||
@@ -106,9 +106,7 @@ class VueNodeView implements NodeView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get contentDOM() {
|
get contentDOM() {
|
||||||
const hasContent = !this.node.type.isAtom
|
if (this.node.isLeaf) {
|
||||||
|
|
||||||
if (!hasContent) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
import { AnyObject } from '@tiptap/core'
|
||||||
import { VueConstructor } from 'vue/types/umd'
|
import { VueConstructor } from 'vue/types/umd'
|
||||||
|
|
||||||
export class VueRenderer {
|
export class VueRenderer {
|
||||||
vm!: Vue
|
ref!: Vue
|
||||||
|
|
||||||
constructor(component: Vue | VueConstructor, props: any) {
|
constructor(component: Vue | VueConstructor, props: any) {
|
||||||
const Component = Vue.extend(component)
|
const Component = Vue.extend(component)
|
||||||
|
|
||||||
this.vm = new Component(props).$mount()
|
this.ref = new Component(props).$mount()
|
||||||
}
|
}
|
||||||
|
|
||||||
get element() {
|
get element() {
|
||||||
return this.vm.$el
|
return this.ref.$el
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProps(props: { [key: string]: any } = {}) {
|
updateProps(props: AnyObject = {}) {
|
||||||
if (!this.vm.$props) {
|
if (!this.ref.$props) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,13 +27,13 @@ export class VueRenderer {
|
|||||||
Object
|
Object
|
||||||
.entries(props)
|
.entries(props)
|
||||||
.forEach(([key, value]) => {
|
.forEach(([key, value]) => {
|
||||||
this.vm.$props[key] = value
|
this.ref.$props[key] = value
|
||||||
})
|
})
|
||||||
|
|
||||||
Vue.config.silent = originalSilent
|
Vue.config.silent = originalSilent
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.vm.$destroy()
|
this.ref.$destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,9 +107,7 @@ class VueNodeView implements NodeView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get contentDOM() {
|
get contentDOM() {
|
||||||
const hasContent = !this.node.type.isAtom
|
if (this.node.isLeaf) {
|
||||||
|
|
||||||
if (!hasContent) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { reactive, markRaw, Component } from 'vue'
|
import { reactive, markRaw, Component } from 'vue'
|
||||||
|
import { AnyObject } from '@tiptap/core'
|
||||||
import { Editor } from './Editor'
|
import { Editor } from './Editor'
|
||||||
|
|
||||||
export interface VueRendererOptions {
|
export interface VueRendererOptions {
|
||||||
as?: string;
|
editor: Editor,
|
||||||
editor: Editor;
|
props?: AnyObject,
|
||||||
props?: { [key: string]: any };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VueRenderer {
|
export class VueRenderer {
|
||||||
@@ -18,7 +18,7 @@ export class VueRenderer {
|
|||||||
|
|
||||||
element: Element
|
element: Element
|
||||||
|
|
||||||
props: { [key: string]: any }
|
props: AnyObject
|
||||||
|
|
||||||
constructor(component: Component, { props = {}, editor }: VueRendererOptions) {
|
constructor(component: Component, { props = {}, editor }: VueRendererOptions) {
|
||||||
this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString()
|
this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString()
|
||||||
@@ -44,7 +44,7 @@ export class VueRenderer {
|
|||||||
return this.editor.contentComponent?.ctx.$refs[this.id]
|
return this.editor.contentComponent?.ctx.$refs[this.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProps(props: { [key: string]: any } = {}) {
|
updateProps(props: AnyObject = {}) {
|
||||||
Object
|
Object
|
||||||
.entries(props)
|
.entries(props)
|
||||||
.forEach(([key, value]) => {
|
.forEach(([key, value]) => {
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"./shims/vue.d.ts"
|
"./shims/vue.d.ts"
|
||||||
],
|
],
|
||||||
"include": [
|
"include": [
|
||||||
"**/*.ts"
|
"**/*.ts",
|
||||||
|
"**/*.tsx"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"**/node_modules",
|
"**/node_modules",
|
||||||
|
|||||||
Reference in New Issue
Block a user