Merge branch 'main' of github.com:ueberdosis/tiptap-next into main

This commit is contained in:
Hans Pagel
2021-03-15 11:06:19 +01:00
21 changed files with 842 additions and 298 deletions

View File

@@ -110,7 +110,7 @@ export default {
})
},
onKeyDown(props) {
return component.vm.onKeyDown(props)
return component.ref.onKeyDown(props)
},
onExit() {
popup[0].destroy()

View File

@@ -105,7 +105,7 @@ export default {
})
},
onKeyDown(props) {
return component.vm.onKeyDown(props)
return component.ref.onKeyDown(props)
},
onExit() {
popup[0].destroy()

View File

@@ -69,7 +69,7 @@ export default {
})
},
onKeyDown(props) {
return component.vm.onKeyDown(props)
return component.ref.onKeyDown(props)
},
onExit() {
popup[0].destroy()

View 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 youd probably expect from a text editor. But wait until you see the lists:
// </p>
// <ul>
// <li>
// Thats a bullet list with one …
// </li>
// <li>
// … or two list items.
// </li>
// </ul>
// <p>
// Isnt that great? And all of that is editable. But wait, theres more. Lets try a code block:
// </p>
// <pre><code class="language-css">body {
// display: none;
// }</code></pre>
// <p>
// I know, I know, this is impressive. Its only the tip of the iceberg though. Give it a try and click a little bit around. Dont forget to check the other examples too.
// </p>
// <blockquote>
// Wow, thats 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>
)
}

View 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')
})
})

View 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;
}
}

View File

@@ -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<EditorContentProps, EditorContentState> | null = null
}

View File

@@ -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<string, ReactRenderer> }> = ({ renderers }) => {
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>
constructor(props: EditorContentProps) {
@@ -13,7 +34,8 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
this.editorContentRef = React.createRef()
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
if (editor && editor.options.element) {
if (editor.contentComponent) {
return
}
// this.setState({
// editor,
// })
const element = this.editorContentRef.current
element.appendChild(editor.options.element.firstChild)
@@ -40,9 +70,12 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
render() {
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)

View 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' }}
/>
)
}

View 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>
)
}

View File

@@ -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 its `undefined` because <editor-content> isnt rendered yet
// if (!(props.editor as Editor).contentComponent) {
// return {}
// }
// return new ReactNodeView(component, props, options) as NodeView
// }
// }

View File

@@ -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 its `undefined` because <editor-content> isnt rendered yet
if (!(props.editor as Editor).contentComponent) {
return {}
}
return new ReactNodeView(component, props, options) as NodeView
}
}

View File

@@ -1,58 +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]'))
// }
// }

View 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,
})
}
}
}

View File

@@ -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'

View 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)

View File

@@ -106,9 +106,7 @@ class VueNodeView implements NodeView {
}
get contentDOM() {
const hasContent = !this.node.type.isAtom
if (!hasContent) {
if (this.node.isLeaf) {
return null
}

View File

@@ -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()
}
}

View File

@@ -107,9 +107,7 @@ class VueNodeView implements NodeView {
}
get contentDOM() {
const hasContent = !this.node.type.isAtom
if (!hasContent) {
if (this.node.isLeaf) {
return null
}

View File

@@ -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]) => {

View File

@@ -28,7 +28,8 @@
"./shims/vue.d.ts"
],
"include": [
"**/*.ts"
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"**/node_modules",