From bc60a91904ceee5320836d8178bed44d996c17ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Fri, 12 Mar 2021 21:21:13 +0100 Subject: [PATCH 01/12] wip react renderer --- .../demos/Examples/Default/React/index.jsx | 76 ++-- packages/react/src/EditorContent.tsx | 93 ++++- packages/react/src/ReactNodeViewRenderer.ts | 364 ++++++++++-------- packages/react/src/ReactRenderer.ts | 119 +++--- packages/react/src/index.ts | 4 +- 5 files changed, 412 insertions(+), 244 deletions(-) diff --git a/docs/src/demos/Examples/Default/React/index.jsx b/docs/src/demos/Examples/Default/React/index.jsx index 3f8d89df..b54f9e66 100644 --- a/docs/src/demos/Examples/Default/React/index.jsx +++ b/docs/src/demos/Examples/Default/React/index.jsx @@ -1,6 +1,7 @@ import React from 'react' -import { useEditor, EditorContent } from '@tiptap/react' +import { useEditor, EditorContent, ReactNodeViewRenderer } from '@tiptap/react' import { defaultExtensions } from '@tiptap/starter-kit' +import Paragraph from '@tiptap/extension-paragraph' import './styles.scss' const MenuBar = ({ editor }) => { @@ -125,38 +126,49 @@ const MenuBar = ({ editor }) => { export default () => { const editor = useEditor({ extensions: [ - ...defaultExtensions(), + Paragraph.extend({ + addNodeView() { + return ReactNodeViewRenderer(({ selected }) => { + console.log({selected}) + return ( +
noooode view {selected}
+ ) + }) + } + }), + ...defaultExtensions().filter(item => item.config.name !== 'paragraph'), ], - content: ` -

- Hi there, -

-

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

- -

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

-
body {
-  display: none;
-}
-

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

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

test

`, +// content: ` +//

+// Hi there, +//

+//

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

+// +//

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

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

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

+//
+// Wow, that’s amazing. Good work, boy! 👏 +//
+// — Mom +//
+// `, }) return ( diff --git a/packages/react/src/EditorContent.tsx b/packages/react/src/EditorContent.tsx index eb14d226..cf86bd0b 100644 --- a/packages/react/src/EditorContent.tsx +++ b/packages/react/src/EditorContent.tsx @@ -1,11 +1,67 @@ import React from 'react' +import ReactDOM from 'react-dom' import { Editor } from './Editor' type EditorContentProps = { editor: Editor | null } -export class PureEditorContent extends React.Component { +// const Portals = ({ editor }: { editor: Editor | null }) => { +// if (!editor?.contentComponent) { +// return null +// } + +// console.log('render portals') + +// return ( +//
portaaals
+// ) +// } + +const Portals = ({ renderers }: { renderers: Map }) => { + return ( +
+ {Array.from(renderers).map(([key, renderer]) => { + + // console.log({renderer}) + // return ( + //
{value}
+ // ) + + // return React.createElement(renderer.component) + + + // return ( + // + // {ReactDOM.createPortal( + // React.createElement(renderer.component), + // renderer.teleportElement, + // )} + // + // ) + + return ( + + {renderer.bla} + + ) + + // return ReactDOM.createPortal( + // React.createElement(renderer.component), + // renderer.teleportElement, + // ) + })} +
+ ) +} + +// const Content = React.memo(({ reference }: { reference: React.RefObject }) => { +// return ( +//
+// ) +// }) + +export class PureEditorContent extends React.Component { editorContentRef: React.RefObject constructor(props: EditorContentProps) { @@ -13,14 +69,33 @@ export class PureEditorContent extends React.Component { + // if (this.props?.editor?.contentComponent) { + // this.props.editor.contentComponent.setState({ + // renderers: this.state.renderers.set(Math.random(), Math.random()) + // }) + // } + // }, 1000) } componentDidUpdate() { const { editor } = this.props if (editor && editor.options.element) { + if (editor.contentComponent) { + return + } + + // this.setState({ + // editor, + // }) + + console.log('UPDATE') + const element = this.editorContentRef.current element.appendChild(editor.options.element.firstChild) @@ -39,10 +114,20 @@ export class PureEditorContent extends React.Component + <> +
+ {/* */} + {/* */} + + + ) } } -export const EditorContent = React.memo(PureEditorContent); +export const EditorContent = React.memo(PureEditorContent) + +// export const EditorContent = PureEditorContent diff --git a/packages/react/src/ReactNodeViewRenderer.ts b/packages/react/src/ReactNodeViewRenderer.ts index b735fbc1..f1d6383b 100644 --- a/packages/react/src/ReactNodeViewRenderer.ts +++ b/packages/react/src/ReactNodeViewRenderer.ts @@ -1,208 +1,246 @@ -// // @ts-nocheck -// import { Node, NodeViewRenderer, NodeViewRendererProps } from '@tiptap/core' -// import { Decoration, NodeView } from 'prosemirror-view' -// import { NodeSelection } from 'prosemirror-state' -// import { Node as ProseMirrorNode } from 'prosemirror-model' -// import React from 'react' -// import ReactDOM from 'react-dom' -// import { Editor } from './Editor' -// import { ReactRenderer } from './ReactRenderer' +// @ts-nocheck +import { Node, NodeViewRenderer, NodeViewRendererProps } from '@tiptap/core' +import { Decoration, NodeView } from 'prosemirror-view' +import { NodeSelection } from 'prosemirror-state' +import { Node as ProseMirrorNode } from 'prosemirror-model' +import React from 'react' +import ReactDOM from 'react-dom' +import { Editor } from './Editor' +import { ReactRenderer } from './ReactRenderer' -// interface ReactNodeViewRendererOptions { -// stopEvent: ((event: Event) => boolean) | null, -// update: ((node: ProseMirrorNode, decorations: Decoration[]) => boolean) | null, -// } +interface ReactNodeViewRendererOptions { + stopEvent: ((event: Event) => boolean) | null, + update: ((node: ProseMirrorNode, decorations: Decoration[]) => boolean) | null, +} -// class ReactNodeView implements NodeView { +class ReactNodeView implements NodeView { -// renderer!: ReactRenderer + renderer!: ReactRenderer -// editor: Editor + editor: Editor -// extension!: Node + extension!: Node -// node!: ProseMirrorNode + node!: ProseMirrorNode -// decorations!: Decoration[] + decorations!: Decoration[] -// getPos!: any + getPos!: any -// isDragging = false + isDragging = false -// options: ReactNodeViewRendererOptions = { -// stopEvent: null, -// update: null, -// } + options: ReactNodeViewRendererOptions = { + stopEvent: null, + update: null, + } -// constructor(component: any, props: NodeViewRendererProps, options?: Partial) { -// this.options = { ...this.options, ...options } -// this.editor = props.editor as Editor -// this.extension = props.extension -// this.node = props.node -// this.getPos = props.getPos -// this.mount(component) -// } + domWrapper: Element -// mount(component: any) { -// const props = {} + contentDOMWrapper: Element -// if (!component.displayName) { -// component.displayName = this.extension.config.name -// } + constructor(component: any, props: NodeViewRendererProps, options?: Partial) { + this.options = { ...this.options, ...options } + this.editor = props.editor as Editor + this.extension = props.extension + this.node = props.node + this.getPos = props.getPos -// this.renderer = new ReactRenderer(component, { -// editor: this.editor, -// props, -// }) -// } + this.domWrapper = document.createElement('div') + this.domWrapper.classList.add('dom-wrapper') + this.contentDOMWrapper = document.createElement('div') + this.contentDOMWrapper.classList.add('content-dom-wrapper') -// get dom() { -// // if (!this.renderer.element) { -// // return null -// // } + this.mount(component) + } -// // if (!this.renderer.element.hasAttribute('data-node-view-wrapper')) { -// // throw Error('Please use the NodeViewWrapper component for your node view.') -// // } + mount(component: any) { + const props = { + editor: this.editor, + node: this.node, + decorations: this.decorations, + selected: false, + extension: this.extension, + getPos: () => this.getPos(), + updateAttributes: (attributes = {}) => this.updateAttributes(attributes), + } -// return this.renderer.element -// } + if (!component.displayName) { + component.displayName = this.extension.config.name + } -// get contentDOM() { -// // console.log(this.dom) -// // return null -// // if (!this.renderer.element) { -// // return null -// // } + this.renderer = new ReactRenderer(component, { + editor: this.editor, + props, + }) + } -// const hasContent = !this.node.type.isAtom + get dom() { + return this.renderer.element + // return this.domWrapper -// if (!hasContent) { -// return null -// } + // if (!this.renderer.element) { + // return null + // } -// const contentElement = this.dom.querySelector('[data-node-view-content]') + // if (!this.renderer.element.hasAttribute('data-node-view-wrapper')) { + // throw Error('Please use the NodeViewWrapper component for your node view.') + // } -// return contentElement || this.dom -// } + // return this.renderer.element + } -// stopEvent(event: Event) { -// if (typeof this.options.stopEvent === 'function') { -// return this.options.stopEvent(event) -// } + get contentDOM() { + // return this.renderer.element + return undefined + // return this.renderer.element + // return this.contentDOMWrapper -// const target = (event.target as HTMLElement) -// const isInElement = this.dom.contains(target) && !this.contentDOM?.contains(target) + // console.log(this.dom) + // return null + // if (!this.renderer.element) { + // return null + // } -// // ignore all events from child nodes -// if (!isInElement) { -// return false -// } + // const hasContent = !this.node.type.isAtom -// const { isEditable } = this.editor -// const { isDragging } = this -// const isDraggable = !!this.node.type.spec.draggable -// const isSelectable = NodeSelection.isSelectable(this.node) -// const isCopyEvent = event.type === 'copy' -// const isPasteEvent = event.type === 'paste' -// const isCutEvent = event.type === 'cut' -// const isClickEvent = event.type === 'mousedown' -// const isDragEvent = event.type.startsWith('drag') || event.type === 'drop' + // if (!hasContent) { + // return null + // } -// // ProseMirror tries to drag selectable nodes -// // even if `draggable` is set to `false` -// // this fix prevents that -// if (!isDraggable && isSelectable && isDragEvent) { -// event.preventDefault() -// } + // const contentElement = this.dom.querySelector('[data-node-view-content]') -// if (isDraggable && isDragEvent && !isDragging) { -// event.preventDefault() -// return false -// } + // return contentElement || this.dom + } -// // we have to store that dragging started -// if (isDraggable && isEditable && !isDragging && isClickEvent) { -// const dragHandle = target.closest('[data-drag-handle]') -// const isValidDragHandle = dragHandle -// && (this.dom === dragHandle || (this.dom.contains(dragHandle))) + stopEvent(event: Event) { + if (typeof this.options.stopEvent === 'function') { + return this.options.stopEvent(event) + } -// if (isValidDragHandle) { -// this.isDragging = true -// document.addEventListener('dragend', () => { -// this.isDragging = false -// }, { once: true }) -// } -// } + const target = (event.target as HTMLElement) + const isInElement = this.dom.contains(target) && !this.contentDOM?.contains(target) -// // these events are handled by prosemirror -// if ( -// isDragging -// || isCopyEvent -// || isPasteEvent -// || isCutEvent -// || (isClickEvent && isSelectable) -// ) { -// return false -// } + // ignore all events from child nodes + if (!isInElement) { + return false + } -// return true -// } + const { isEditable } = this.editor + const { isDragging } = this + const isDraggable = !!this.node.type.spec.draggable + const isSelectable = NodeSelection.isSelectable(this.node) + const isCopyEvent = event.type === 'copy' + const isPasteEvent = event.type === 'paste' + const isCutEvent = event.type === 'cut' + const isClickEvent = event.type === 'mousedown' + const isDragEvent = event.type.startsWith('drag') || event.type === 'drop' -// ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) { -// if (mutation.type === 'selection') { -// if (this.node.isLeaf) { -// return true -// } + // ProseMirror tries to drag selectable nodes + // even if `draggable` is set to `false` + // this fix prevents that + if (!isDraggable && isSelectable && isDragEvent) { + event.preventDefault() + } -// return false -// } + if (isDraggable && isDragEvent && !isDragging) { + event.preventDefault() + return false + } -// if (!this.contentDOM) { -// return true -// } + // we have to store that dragging started + if (isDraggable && isEditable && !isDragging && isClickEvent) { + const dragHandle = target.closest('[data-drag-handle]') + const isValidDragHandle = dragHandle + && (this.dom === dragHandle || (this.dom.contains(dragHandle))) -// const contentDOMHasChanged = !this.contentDOM.contains(mutation.target) -// || this.contentDOM === mutation.target + if (isValidDragHandle) { + this.isDragging = true + document.addEventListener('dragend', () => { + this.isDragging = false + }, { once: true }) + } + } -// return contentDOMHasChanged -// } + // these events are handled by prosemirror + if ( + isDragging + || isCopyEvent + || isPasteEvent + || isCutEvent + || (isClickEvent && isSelectable) + ) { + return false + } -// destroy() { -// this.renderer.destroy() -// } + return true + } -// update(node: ProseMirrorNode, decorations: Decoration[]) { -// if (typeof this.options.update === 'function') { -// return this.options.update(node, decorations) -// } + ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) { + if (mutation.type === 'selection') { + if (this.node.isLeaf) { + return true + } -// if (node.type !== this.node.type) { -// return false -// } + return false + } -// if (node === this.node && this.decorations === decorations) { -// return true -// } + if (!this.contentDOM) { + return true + } -// this.node = node -// this.decorations = decorations -// // this.renderer.updateProps({ node, decorations }) -// this.renderer.render() + const contentDOMHasChanged = !this.contentDOM.contains(mutation.target) + || this.contentDOM === mutation.target -// return true -// } -// } + return contentDOMHasChanged + } -// export function ReactNodeViewRenderer(component: any, options?: Partial): NodeViewRenderer { -// return (props: NodeViewRendererProps) => { -// // try to get the parent component -// // this is important for vue devtools to show the component hierarchy correctly -// // maybe it’s `undefined` because isn’t rendered yet -// if (!(props.editor as Editor).contentComponent) { -// return {} -// } + destroy() { + this.renderer.destroy() + } -// return new ReactNodeView(component, props, options) as NodeView -// } -// } + update(node: ProseMirrorNode, decorations: Decoration[]) { + if (typeof this.options.update === 'function') { + return this.options.update(node, decorations) + } + + if (node.type !== this.node.type) { + return false + } + + if (node === this.node && this.decorations === decorations) { + return true + } + + this.node = node + this.decorations = decorations + this.renderer.updateProps({ node, decorations }) + this.renderer.render() + + return true + } + + selectNode() { + this.renderer.updateProps({ + selected: true, + }) + } + + deselectNode() { + this.renderer.updateProps({ + selected: false, + }) + } +} + +export function ReactNodeViewRenderer(component: any, options?: Partial): NodeViewRenderer { + return (props: NodeViewRendererProps) => { + // try to get the parent component + // this is important for vue devtools to show the component hierarchy correctly + // maybe it’s `undefined` because isn’t rendered yet + if (!(props.editor as Editor).contentComponent) { + return {} + } + + return new ReactNodeView(component, props, options) as NodeView + } +} diff --git a/packages/react/src/ReactRenderer.ts b/packages/react/src/ReactRenderer.ts index 3bae50d0..98341d71 100644 --- a/packages/react/src/ReactRenderer.ts +++ b/packages/react/src/ReactRenderer.ts @@ -1,58 +1,91 @@ -// // @ts-nocheck +// @ts-nocheck -// import React from 'react' -// import { render, unmountComponentAtNode } from 'react-dom' +import React from 'react' +import ReactDOM, { render, unmountComponentAtNode } from 'react-dom' -// import { Editor } from './Editor' +import { Editor } from './Editor' -// export interface VueRendererOptions { -// as?: string; -// editor: Editor; -// props?: { [key: string]: any }; -// } +export interface VueRendererOptions { + as?: string; + editor: Editor; + props?: { [key: string]: any }; +} -// export class ReactRenderer { -// id: string +export class ReactRenderer { + id: string -// editor: Editor + editor: Editor -// component: any + component: any -// teleportElement: Element + teleportElement: Element -// element: Element + element: Element -// props: { [key: string]: any } + props: { [key: string]: any } -// constructor(component: any, { props = {}, editor }: VueRendererOptions) { -// this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString() -// this.component = component -// this.editor = editor -// this.props = props + constructor(component: any, { props = {}, editor }: VueRendererOptions) { + this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString() + this.component = component + this.editor = editor + this.props = props -// this.teleportElement = document.createElement('div') -// // this.teleportElement.setAttribute('data-bla', '') -// // render(React.createElement(component), this.teleportElement) -// // render(React.createElement(component), this.teleportElement) -// this.render() -// // this.element = this.teleportElement.firstElementChild as Element -// this.element = this.teleportElement -// } + this.teleportElement = document.createElement('div') + this.teleportElement.classList.add('teleport-element') + this.element = this.teleportElement -// render() { -// render(React.createElement(this.component), this.teleportElement) -// } + // this.teleportElement.setAttribute('data-bla', '') + // render(React.createElement(component), this.teleportElement) + // render(React.createElement(component), this.teleportElement) + // this.render() + // // this.element = this.teleportElement.firstElementChild as Element -// updateProps(props: { [key: string]: any } = {}) { -// // TODO -// } + console.log({ props }) -// destroy() { -// // TODO -// // console.log('DESTROY', { bla: this.teleportElement }) -// // console.log(document.querySelector('[data-bla]')) -// unmountComponentAtNode(this.teleportElement) -// // unmountComponentAtNode(document.querySelector('[data-bla]')) -// } + // this.bla = ReactDOM.createPortal( + // React.createElement(this.component, props), + // this.teleportElement, + // ) -// } + this.bla = React.createElement(this.component, props) + + // console.log({ bla }) + + if (this.editor?.contentComponent) { + this.editor.contentComponent.setState({ + renderers: this.editor.contentComponent.state.renderers.set( + this.id, + this, + ), + }) + } + } + + render() { + render(React.createElement(this.component), this.teleportElement) + } + + updateProps(props: { [key: string]: any } = {}) { + // TODO + console.log('update props', { props }) + } + + destroy() { + // TODO + // console.log('DESTROY', { bla: this.teleportElement }) + // console.log(document.querySelector('[data-bla]')) + // unmountComponentAtNode(this.teleportElement) + // unmountComponentAtNode(document.querySelector('[data-bla]')) + + if (this.editor?.contentComponent) { + const { renderers } = this.editor.contentComponent.state + + renderers.delete(this.id) + + this.editor.contentComponent.setState({ + renderers, + }) + } + } + +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 19177902..27e45903 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -2,6 +2,6 @@ export * from '@tiptap/core' export { Editor } from './Editor' export * from './useEditor' -// export * from './ReactRenderer' -// export * from './ReactNodeViewRenderer' +export * from './ReactRenderer' +export * from './ReactNodeViewRenderer' export * from './EditorContent' From 5e9a0d4225efc5668b5ca545be106d5040bebd13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Sun, 14 Mar 2021 16:30:06 +0100 Subject: [PATCH 02/12] add basic react nodeviewrenderer --- .../demos/Examples/Default/React/index.jsx | 31 +++++++--- packages/react/src/EditorContent.tsx | 60 ++----------------- packages/react/src/NodeViewContent.tsx | 18 ++++++ packages/react/src/NodeViewWrapper.tsx | 23 +++++++ packages/react/src/ReactNodeViewRenderer.ts | 21 ++++++- packages/react/src/ReactRenderer.ts | 33 +++++++--- packages/react/src/index.ts | 2 + tsconfig.json | 3 +- 8 files changed, 118 insertions(+), 73 deletions(-) create mode 100644 packages/react/src/NodeViewContent.tsx create mode 100644 packages/react/src/NodeViewWrapper.tsx diff --git a/docs/src/demos/Examples/Default/React/index.jsx b/docs/src/demos/Examples/Default/React/index.jsx index b54f9e66..d42e7145 100644 --- a/docs/src/demos/Examples/Default/React/index.jsx +++ b/docs/src/demos/Examples/Default/React/index.jsx @@ -1,6 +1,7 @@ import React from 'react' -import { useEditor, EditorContent, ReactNodeViewRenderer } from '@tiptap/react' +import { useEditor, EditorContent, 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 './styles.scss' @@ -126,19 +127,35 @@ const MenuBar = ({ editor }) => { export default () => { const editor = useEditor({ extensions: [ - Paragraph.extend({ + ...defaultExtensions().filter(item => item.config.name !== 'heading'), + Heading.extend({ + draggable: true, addNodeView() { - return ReactNodeViewRenderer(({ selected }) => { - console.log({selected}) + return ReactNodeViewRenderer((props) => { + // console.log({props}) return ( -
noooode view {selected}
+ +
+ + +
+
) }) } }), - ...defaultExtensions().filter(item => item.config.name !== 'paragraph'), + ], - content: `

test

`, + content: ` +

h1

+

h2

+

paragraph

+ `, // content: ` //

// Hi there, diff --git a/packages/react/src/EditorContent.tsx b/packages/react/src/EditorContent.tsx index cf86bd0b..8127d2d5 100644 --- a/packages/react/src/EditorContent.tsx +++ b/packages/react/src/EditorContent.tsx @@ -6,50 +6,15 @@ type EditorContentProps = { editor: Editor | null } -// const Portals = ({ editor }: { editor: Editor | null }) => { -// if (!editor?.contentComponent) { -// return null -// } - -// console.log('render portals') - -// return ( -//
portaaals
-// ) -// } - const Portals = ({ renderers }: { renderers: Map }) => { return (
{Array.from(renderers).map(([key, renderer]) => { - - // console.log({renderer}) - // return ( - //
{value}
- // ) - - // return React.createElement(renderer.component) - - - // return ( - // - // {ReactDOM.createPortal( - // React.createElement(renderer.component), - // renderer.teleportElement, - // )} - // - // ) - - return ( - - {renderer.bla} - + return ReactDOM.createPortal( + renderer.comp, + renderer.teleportElement, + renderer.id, ) - - // return ReactDOM.createPortal( - // React.createElement(renderer.component), - // renderer.teleportElement, - // ) })}
) @@ -72,14 +37,6 @@ export class PureEditorContent extends React.Component editor: this.props.editor, renderers: new Map(), } - - // setInterval(() => { - // if (this.props?.editor?.contentComponent) { - // this.props.editor.contentComponent.setState({ - // renderers: this.state.renderers.set(Math.random(), Math.random()) - // }) - // } - // }, 1000) } componentDidUpdate() { @@ -94,8 +51,6 @@ export class PureEditorContent extends React.Component // editor, // }) - console.log('UPDATE') - const element = this.editorContentRef.current element.appendChild(editor.options.element.firstChild) @@ -114,14 +69,11 @@ export class PureEditorContent extends React.Component } render() { - console.log('render', this.state) + // console.log('render', this.state) // console.log('render', this.props.editor, this.state.editor) return ( <>
- {/* */} - {/* */} - ) @@ -129,5 +81,3 @@ export class PureEditorContent extends React.Component } export const EditorContent = React.memo(PureEditorContent) - -// export const EditorContent = PureEditorContent diff --git a/packages/react/src/NodeViewContent.tsx b/packages/react/src/NodeViewContent.tsx new file mode 100644 index 00000000..1a7eeb98 --- /dev/null +++ b/packages/react/src/NodeViewContent.tsx @@ -0,0 +1,18 @@ +import React from 'react' + +export const NodeViewContent: React.FC = props => { + + // TODO + const isEditable = true + + return ( +
+ ) + +} diff --git a/packages/react/src/NodeViewWrapper.tsx b/packages/react/src/NodeViewWrapper.tsx new file mode 100644 index 00000000..d200119a --- /dev/null +++ b/packages/react/src/NodeViewWrapper.tsx @@ -0,0 +1,23 @@ +import React from 'react' + +export const NodeViewWrapper: React.FC = props => { + + // TODO + const onDragStart = () => { + console.log('drag start') + } + + return ( +
+ {props.children} +
+ ) + +} diff --git a/packages/react/src/ReactNodeViewRenderer.ts b/packages/react/src/ReactNodeViewRenderer.ts index f1d6383b..c4c0baaf 100644 --- a/packages/react/src/ReactNodeViewRenderer.ts +++ b/packages/react/src/ReactNodeViewRenderer.ts @@ -72,11 +72,25 @@ class ReactNodeView implements NodeView { editor: this.editor, props, }) + + // console.log(this.renderer.element.firstChild) + + this.domWrapper.appendChild(this.renderer.element) + + const contentElement = this.renderer.element.querySelector('[data-node-view-content]') + + // console.log({ contentElement }) + + contentElement.appendChild(this.contentDOMWrapper) + + // this.renderer.element.firstChild?.appendChild(this.contentDOMWrapper) + // this.domWrapper.appendChild(this.contentDOMWrapper) } get dom() { - return this.renderer.element - // return this.domWrapper + // return this.renderer.element + // return this.renderer.element.firstChild + return this.domWrapper // if (!this.renderer.element) { // return null @@ -90,6 +104,7 @@ class ReactNodeView implements NodeView { } get contentDOM() { + return this.contentDOMWrapper // return this.renderer.element return undefined // return this.renderer.element @@ -196,6 +211,8 @@ class ReactNodeView implements NodeView { destroy() { this.renderer.destroy() + this.domWrapper = undefined + this.contentDOMWrapper = undefined } update(node: ProseMirrorNode, decorations: Decoration[]) { diff --git a/packages/react/src/ReactRenderer.ts b/packages/react/src/ReactRenderer.ts index 98341d71..b5bd0fef 100644 --- a/packages/react/src/ReactRenderer.ts +++ b/packages/react/src/ReactRenderer.ts @@ -40,16 +40,37 @@ export class ReactRenderer { // this.render() // // this.element = this.teleportElement.firstElementChild as Element - console.log({ props }) + // console.log({ props }) // this.bla = ReactDOM.createPortal( // React.createElement(this.component, props), // this.teleportElement, // ) + this.render() + // this.comp = React.createElement(this.component, { foo: 1 }) - this.bla = React.createElement(this.component, props) + // // this.bla = React.createElement(this.component, props) - // console.log({ bla }) + // // console.log({ bla }) + + // if (this.editor?.contentComponent) { + // this.editor.contentComponent.setState({ + // renderers: this.editor.contentComponent.state.renderers.set( + // this.id, + // this, + // ), + // }) + // } + } + + // get comp() { + // console.log('get comp') + // return React.createElement(this.component, { foo: 1 }) + // } + + render() { + this.comp = React.createElement(this.component, { foo: 1 }) + // render(React.createElement(this.component), this.teleportElement) if (this.editor?.contentComponent) { this.editor.contentComponent.setState({ @@ -61,13 +82,9 @@ export class ReactRenderer { } } - render() { - render(React.createElement(this.component), this.teleportElement) - } - updateProps(props: { [key: string]: any } = {}) { // TODO - console.log('update props', { props }) + // console.log('update props', { props }) } destroy() { diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 27e45903..58528a95 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -5,3 +5,5 @@ export * from './useEditor' export * from './ReactRenderer' export * from './ReactNodeViewRenderer' export * from './EditorContent' +export * from './NodeViewWrapper' +export * from './NodeViewContent' 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", From baa5bcc964d3e7f200b1a1f7b110cf720e8455a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Sun, 14 Mar 2021 17:01:52 +0100 Subject: [PATCH 03/12] refactoring --- .../demos/Examples/Default/React/index.jsx | 9 ++- packages/react/src/EditorContent.tsx | 2 +- packages/react/src/ReactNodeViewRenderer.ts | 15 +++++ packages/react/src/ReactRenderer.ts | 58 +++++-------------- 4 files changed, 37 insertions(+), 47 deletions(-) diff --git a/docs/src/demos/Examples/Default/React/index.jsx b/docs/src/demos/Examples/Default/React/index.jsx index d42e7145..e37d57b9 100644 --- a/docs/src/demos/Examples/Default/React/index.jsx +++ b/docs/src/demos/Examples/Default/React/index.jsx @@ -126,13 +126,16 @@ const MenuBar = ({ editor }) => { 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) => { - // console.log({props}) + console.log({props}) return (
@@ -142,6 +145,10 @@ export default () => { draggable={true} suppressContentEditableWarning={true} >⠿ + level: {props.node.attrs.level} +
diff --git a/packages/react/src/EditorContent.tsx b/packages/react/src/EditorContent.tsx index 8127d2d5..0f73847c 100644 --- a/packages/react/src/EditorContent.tsx +++ b/packages/react/src/EditorContent.tsx @@ -11,7 +11,7 @@ const Portals = ({ renderers }: { renderers: Map }) => {
{Array.from(renderers).map(([key, renderer]) => { return ReactDOM.createPortal( - renderer.comp, + renderer.reactElement, renderer.teleportElement, renderer.id, ) diff --git a/packages/react/src/ReactNodeViewRenderer.ts b/packages/react/src/ReactNodeViewRenderer.ts index c4c0baaf..55fd5ec6 100644 --- a/packages/react/src/ReactNodeViewRenderer.ts +++ b/packages/react/src/ReactNodeViewRenderer.ts @@ -236,6 +236,21 @@ class ReactNodeView implements NodeView { 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, diff --git a/packages/react/src/ReactRenderer.ts b/packages/react/src/ReactRenderer.ts index b5bd0fef..586a9350 100644 --- a/packages/react/src/ReactRenderer.ts +++ b/packages/react/src/ReactRenderer.ts @@ -1,8 +1,4 @@ -// @ts-nocheck - import React from 'react' -import ReactDOM, { render, unmountComponentAtNode } from 'react-dom' - import { Editor } from './Editor' export interface VueRendererOptions { @@ -24,6 +20,8 @@ export class ReactRenderer { props: { [key: string]: any } + reactElement: React.ReactNode + constructor(component: any, { props = {}, editor }: VueRendererOptions) { this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString() this.component = component @@ -34,46 +32,16 @@ export class ReactRenderer { this.teleportElement.classList.add('teleport-element') this.element = this.teleportElement - // this.teleportElement.setAttribute('data-bla', '') - // render(React.createElement(component), this.teleportElement) - // render(React.createElement(component), this.teleportElement) - // this.render() - // // this.element = this.teleportElement.firstElementChild as Element - - // console.log({ props }) - - // this.bla = ReactDOM.createPortal( - // React.createElement(this.component, props), - // this.teleportElement, - // ) this.render() - // this.comp = React.createElement(this.component, { foo: 1 }) - - // // this.bla = React.createElement(this.component, props) - - // // console.log({ bla }) - - // if (this.editor?.contentComponent) { - // this.editor.contentComponent.setState({ - // renderers: this.editor.contentComponent.state.renderers.set( - // this.id, - // this, - // ), - // }) - // } } - // get comp() { - // console.log('get comp') - // return React.createElement(this.component, { foo: 1 }) - // } - render() { - this.comp = React.createElement(this.component, { foo: 1 }) - // render(React.createElement(this.component), this.teleportElement) + this.reactElement = React.createElement(this.component, this.props) if (this.editor?.contentComponent) { this.editor.contentComponent.setState({ + // TODO + // @ts-ignore renderers: this.editor.contentComponent.state.renderers.set( this.id, this, @@ -83,18 +51,18 @@ export class ReactRenderer { } updateProps(props: { [key: string]: any } = {}) { - // TODO - // console.log('update props', { props }) + this.props = { + ...this.props, + ...props, + } + + this.render() } destroy() { - // TODO - // console.log('DESTROY', { bla: this.teleportElement }) - // console.log(document.querySelector('[data-bla]')) - // unmountComponentAtNode(this.teleportElement) - // unmountComponentAtNode(document.querySelector('[data-bla]')) - if (this.editor?.contentComponent) { + // TODO + // @ts-ignore const { renderers } = this.editor.contentComponent.state renderers.delete(this.id) From 109bff0892107238fc19ced1c2ba24d3d679817d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Sun, 14 Mar 2021 18:00:50 +0100 Subject: [PATCH 04/12] refactoring --- .../demos/Examples/Default/React/index.jsx | 6 +- packages/react/src/EditorContent.tsx | 19 ++---- packages/react/src/ReactNodeViewRenderer.ts | 68 +++---------------- packages/react/src/ReactRenderer.ts | 19 ++---- packages/vue-2/src/VueNodeViewRenderer.ts | 4 +- packages/vue-3/src/VueNodeViewRenderer.ts | 4 +- packages/vue-3/src/VueRenderer.ts | 6 +- 7 files changed, 30 insertions(+), 96 deletions(-) diff --git a/docs/src/demos/Examples/Default/React/index.jsx b/docs/src/demos/Examples/Default/React/index.jsx index e37d57b9..e7e6a894 100644 --- a/docs/src/demos/Examples/Default/React/index.jsx +++ b/docs/src/demos/Examples/Default/React/index.jsx @@ -135,7 +135,7 @@ export default () => { draggable: true, addNodeView() { return ReactNodeViewRenderer((props) => { - console.log({props}) + // console.log({props}) return (
@@ -159,8 +159,8 @@ export default () => { ], content: ` -

h1

-

h2

+

heading

+

heading

paragraph

`, // content: ` diff --git a/packages/react/src/EditorContent.tsx b/packages/react/src/EditorContent.tsx index 0f73847c..537c493e 100644 --- a/packages/react/src/EditorContent.tsx +++ b/packages/react/src/EditorContent.tsx @@ -1,31 +1,26 @@ import React from 'react' import ReactDOM from 'react-dom' import { Editor } from './Editor' +import { ReactRenderer } from './ReactRenderer' type EditorContentProps = { editor: Editor | null } -const Portals = ({ renderers }: { renderers: Map }) => { +const Portals: React.FC<{ renderers: Map }> = ({ renderers }) => { return ( -
+ <> {Array.from(renderers).map(([key, renderer]) => { return ReactDOM.createPortal( renderer.reactElement, - renderer.teleportElement, - renderer.id, + renderer.element, + key, ) })} -
+ ) } -// const Content = React.memo(({ reference }: { reference: React.RefObject }) => { -// return ( -//
-// ) -// }) - export class PureEditorContent extends React.Component { editorContentRef: React.RefObject @@ -69,8 +64,6 @@ export class PureEditorContent extends React.Component } render() { - // console.log('render', this.state) - // console.log('render', this.props.editor, this.state.editor) return ( <>
diff --git a/packages/react/src/ReactNodeViewRenderer.ts b/packages/react/src/ReactNodeViewRenderer.ts index 55fd5ec6..d104faa4 100644 --- a/packages/react/src/ReactNodeViewRenderer.ts +++ b/packages/react/src/ReactNodeViewRenderer.ts @@ -1,10 +1,7 @@ -// @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' @@ -34,22 +31,12 @@ class ReactNodeView implements NodeView { update: null, } - domWrapper: Element - - contentDOMWrapper: Element - 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.domWrapper = document.createElement('div') - this.domWrapper.classList.add('dom-wrapper') - this.contentDOMWrapper = document.createElement('div') - this.contentDOMWrapper.classList.add('content-dom-wrapper') - this.mount(component) } @@ -72,59 +59,24 @@ class ReactNodeView implements NodeView { editor: this.editor, props, }) - - // console.log(this.renderer.element.firstChild) - - this.domWrapper.appendChild(this.renderer.element) - - const contentElement = this.renderer.element.querySelector('[data-node-view-content]') - - // console.log({ contentElement }) - - contentElement.appendChild(this.contentDOMWrapper) - - // this.renderer.element.firstChild?.appendChild(this.contentDOMWrapper) - // this.domWrapper.appendChild(this.contentDOMWrapper) } get dom() { - // return this.renderer.element - // return this.renderer.element.firstChild - return this.domWrapper + if (!this.renderer.element.firstElementChild?.hasAttribute('data-node-view-wrapper')) { + throw Error('Please use the ReactViewWrapper component for your node view.') + } - // 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 + return this.renderer.element } get contentDOM() { - return this.contentDOMWrapper - // return this.renderer.element - return undefined - // return this.renderer.element - // return this.contentDOMWrapper + if (this.node.isLeaf) { + return null + } - // console.log(this.dom) - // return null - // if (!this.renderer.element) { - // return null - // } + const contentElement = this.dom.querySelector('[data-node-view-content]') - // const hasContent = !this.node.type.isAtom - - // if (!hasContent) { - // return null - // } - - // const contentElement = this.dom.querySelector('[data-node-view-content]') - - // return contentElement || this.dom + return contentElement || this.dom } stopEvent(event: Event) { @@ -211,8 +163,6 @@ class ReactNodeView implements NodeView { destroy() { this.renderer.destroy() - this.domWrapper = undefined - this.contentDOMWrapper = undefined } update(node: ProseMirrorNode, decorations: Decoration[]) { diff --git a/packages/react/src/ReactRenderer.ts b/packages/react/src/ReactRenderer.ts index 586a9350..6769f2cb 100644 --- a/packages/react/src/ReactRenderer.ts +++ b/packages/react/src/ReactRenderer.ts @@ -1,10 +1,10 @@ import React from 'react' import { Editor } from './Editor' -export interface VueRendererOptions { - as?: string; - editor: Editor; - props?: { [key: string]: any }; +export interface ReactRendererOptions { + as?: string, + editor: Editor, + props?: { [key: string]: any }, } export class ReactRenderer { @@ -14,24 +14,19 @@ export class ReactRenderer { component: any - teleportElement: Element - element: Element props: { [key: string]: any } reactElement: React.ReactNode - constructor(component: any, { props = {}, editor }: VueRendererOptions) { + constructor(component: any, { props = {}, editor }: ReactRendererOptions) { this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString() this.component = component this.editor = editor this.props = props - - this.teleportElement = document.createElement('div') - this.teleportElement.classList.add('teleport-element') - this.element = this.teleportElement - + this.element = document.createElement('div') + this.element.classList.add('react-renderer') this.render() } 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-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..e8dd4cca 100644 --- a/packages/vue-3/src/VueRenderer.ts +++ b/packages/vue-3/src/VueRenderer.ts @@ -2,9 +2,9 @@ import { reactive, markRaw, Component } from 'vue' import { Editor } from './Editor' export interface VueRendererOptions { - as?: string; - editor: Editor; - props?: { [key: string]: any }; + as?: string, + editor: Editor, + props?: { [key: string]: any }, } export class VueRenderer { From 380d27fe86ac93d915f21d5eff243be52981801a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Sun, 14 Mar 2021 20:40:40 +0100 Subject: [PATCH 05/12] add useReactNodeView hook --- .../demos/Examples/Default/React/index.jsx | 6 +- packages/react/src/EditorContent.tsx | 1 + packages/react/src/NodeViewContent.tsx | 5 +- packages/react/src/NodeViewWrapper.tsx | 8 +-- ...wRenderer.ts => ReactNodeViewRenderer.tsx} | 60 +++++++++++++++++-- packages/react/src/useReactNodeView.ts | 8 +++ 6 files changed, 75 insertions(+), 13 deletions(-) rename packages/react/src/{ReactNodeViewRenderer.ts => ReactNodeViewRenderer.tsx} (78%) create mode 100644 packages/react/src/useReactNodeView.ts diff --git a/docs/src/demos/Examples/Default/React/index.jsx b/docs/src/demos/Examples/Default/React/index.jsx index e7e6a894..e8d984b0 100644 --- a/docs/src/demos/Examples/Default/React/index.jsx +++ b/docs/src/demos/Examples/Default/React/index.jsx @@ -135,7 +135,6 @@ export default () => { draggable: true, addNodeView() { return ReactNodeViewRenderer((props) => { - // console.log({props}) return (
@@ -156,7 +155,6 @@ export default () => { }) } }), - ], content: `

heading

@@ -197,6 +195,10 @@ export default () => { return (
+
+ + +
diff --git a/packages/react/src/EditorContent.tsx b/packages/react/src/EditorContent.tsx index 537c493e..93029683 100644 --- a/packages/react/src/EditorContent.tsx +++ b/packages/react/src/EditorContent.tsx @@ -2,6 +2,7 @@ import React from 'react' import ReactDOM from 'react-dom' import { Editor } from './Editor' import { ReactRenderer } from './ReactRenderer' +import { ReactNodeViewContext } from './useReactNodeView' type EditorContentProps = { editor: Editor | null diff --git a/packages/react/src/NodeViewContent.tsx b/packages/react/src/NodeViewContent.tsx index 1a7eeb98..ad205bac 100644 --- a/packages/react/src/NodeViewContent.tsx +++ b/packages/react/src/NodeViewContent.tsx @@ -1,9 +1,10 @@ import React from 'react' +import { useReactNodeView } from './useReactNodeView' export const NodeViewContent: React.FC = props => { - // TODO - const isEditable = true + // @ts-ignore + const { isEditable } = useReactNodeView() return (
{ - // TODO - const onDragStart = () => { - console.log('drag start') - } + // @ts-ignore + const { onDragStart } = useReactNodeView() return (
boolean) | null, @@ -40,7 +42,24 @@ class ReactNodeView implements NodeView { this.mount(component) } - mount(component: any) { + 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, @@ -51,11 +70,44 @@ class ReactNodeView implements NodeView { updateAttributes: (attributes = {}) => this.updateAttributes(attributes), } - if (!component.displayName) { - component.displayName = this.extension.config.name + if (!Component.displayName) { + const capitalizeFirstChar = (string: string): string => { + return string.charAt(0).toUpperCase() + string.substring(1) + } + + Component.displayName = capitalizeFirstChar(this.extension.config.name) } - this.renderer = new ReactRenderer(component, { + const ReactNodeView: React.FC = (props) => { + const [isEditable, setIsEditable] = useState(this.editor.isEditable) + + const handleEditableChange = () => { + setIsEditable(this.editor.isEditable) + } + + const onDragStart = this.onDragStart.bind(this) + + useEffect(() => { + this.editor.on('viewUpdate', handleEditableChange) + + return () => { + this.editor.off('viewUpdate', handleEditableChange) + } + }, []) + + return ( + + + + ) + } + + this.renderer = new ReactRenderer(ReactNodeView, { editor: this.editor, props, }) diff --git a/packages/react/src/useReactNodeView.ts b/packages/react/src/useReactNodeView.ts new file mode 100644 index 00000000..0225a201 --- /dev/null +++ b/packages/react/src/useReactNodeView.ts @@ -0,0 +1,8 @@ +import React, { useContext } from 'react' + +export const ReactNodeViewContext = React.createContext({ + isEditable: undefined, + onDragStart: undefined, +}) + +export const useReactNodeView = () => useContext(ReactNodeViewContext) From 8ccb1d08cf0920eca62db59c1d7e3fb872f4ee3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Sun, 14 Mar 2021 21:45:14 +0100 Subject: [PATCH 06/12] add dynamic tags --- packages/react/src/NodeViewContent.tsx | 11 ++++++++--- packages/react/src/NodeViewWrapper.tsx | 13 +++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/react/src/NodeViewContent.tsx b/packages/react/src/NodeViewContent.tsx index ad205bac..62d9cb28 100644 --- a/packages/react/src/NodeViewContent.tsx +++ b/packages/react/src/NodeViewContent.tsx @@ -1,13 +1,19 @@ import React from 'react' import { useReactNodeView } from './useReactNodeView' -export const NodeViewContent: React.FC = props => { +export interface NodeViewContentProps { + as: React.ElementType +} + +export const NodeViewContent: React.FC = props => { // TODO // @ts-ignore const { isEditable } = useReactNodeView() + const Tag = props.as || 'div' + return ( -
{ }} /> ) - } diff --git a/packages/react/src/NodeViewWrapper.tsx b/packages/react/src/NodeViewWrapper.tsx index 5905377e..3941106e 100644 --- a/packages/react/src/NodeViewWrapper.tsx +++ b/packages/react/src/NodeViewWrapper.tsx @@ -1,13 +1,19 @@ import React from 'react' import { useReactNodeView } from './useReactNodeView' -export const NodeViewWrapper: React.FC = props => { +export interface NodeViewWrapperProps { + as: React.ElementType +} + +export const NodeViewWrapper: React.FC = props => { // TODO // @ts-ignore const { onDragStart } = useReactNodeView() + const Tag = props.as || 'div' + return ( -
{ onDragStart={onDragStart} > {props.children} -
+ ) - } From 56aaf75db5778ee275ebd537c7bba7bc7cc5e220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Sun, 14 Mar 2021 22:41:25 +0100 Subject: [PATCH 07/12] refactoring --- packages/react/src/NodeViewContent.tsx | 7 +------ packages/react/src/NodeViewWrapper.tsx | 7 +------ packages/react/src/useReactNodeView.ts | 7 ++++++- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/react/src/NodeViewContent.tsx b/packages/react/src/NodeViewContent.tsx index 62d9cb28..fbc64d8a 100644 --- a/packages/react/src/NodeViewContent.tsx +++ b/packages/react/src/NodeViewContent.tsx @@ -6,19 +6,14 @@ export interface NodeViewContentProps { } export const NodeViewContent: React.FC = props => { - // TODO - // @ts-ignore const { isEditable } = useReactNodeView() - const Tag = props.as || 'div' return ( ) } diff --git a/packages/react/src/NodeViewWrapper.tsx b/packages/react/src/NodeViewWrapper.tsx index 3941106e..6e159123 100644 --- a/packages/react/src/NodeViewWrapper.tsx +++ b/packages/react/src/NodeViewWrapper.tsx @@ -6,19 +6,14 @@ export interface NodeViewWrapperProps { } export const NodeViewWrapper: React.FC = props => { - // TODO - // @ts-ignore const { onDragStart } = useReactNodeView() - const Tag = props.as || 'div' return ( {props.children} diff --git a/packages/react/src/useReactNodeView.ts b/packages/react/src/useReactNodeView.ts index 0225a201..094a388e 100644 --- a/packages/react/src/useReactNodeView.ts +++ b/packages/react/src/useReactNodeView.ts @@ -1,6 +1,11 @@ import React, { useContext } from 'react' -export const ReactNodeViewContext = React.createContext({ +export interface ReactNodeViewContextProps { + isEditable: boolean, + onDragStart: (event: DragEvent) => void, +} + +export const ReactNodeViewContext = React.createContext>({ isEditable: undefined, onDragStart: undefined, }) From 29abba56e7dc97854c209b96dddcfdddb4f115ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Sun, 14 Mar 2021 23:09:32 +0100 Subject: [PATCH 08/12] refactoring --- packages/react/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 58528a95..6ce42719 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,4 +1,3 @@ -// @ts-nocheck export * from '@tiptap/core' export { Editor } from './Editor' export * from './useEditor' From 8505a24995f033f6cef769c64ed7465fb25a3ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Sun, 14 Mar 2021 23:10:01 +0100 Subject: [PATCH 09/12] add basic react mentions --- .../demos/Examples/Default/React/index.jsx | 67 ++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/docs/src/demos/Examples/Default/React/index.jsx b/docs/src/demos/Examples/Default/React/index.jsx index e8d984b0..08d6cb1a 100644 --- a/docs/src/demos/Examples/Default/React/index.jsx +++ b/docs/src/demos/Examples/Default/React/index.jsx @@ -1,9 +1,12 @@ import React from 'react' -import { useEditor, EditorContent, ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from '@tiptap/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) { @@ -124,6 +127,21 @@ const MenuBar = ({ editor }) => { ) } +const MentionList = (props) => { + console.log({props}) + + return ( +
+ mentions + {props.items.map((item) => ( +
+ {item} +
+ ))} +
+ ) +} + export default () => { const editor = useEditor({ // onTransaction({ editor }) { @@ -155,6 +173,53 @@ export default () => { }) } }), + 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(MentionList, { + 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) { + console.log('keydown', props) + // return reactRenderer.ref.onKeyDown(props) + }, + onExit() { + popup[0].destroy() + reactRenderer.destroy() + }, + } + } + }, + }) ], content: `

heading

From 385ee1d1f4978b8021b4aaa8acfa37a0f7d45395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Mon, 15 Mar 2021 00:22:17 +0100 Subject: [PATCH 10/12] add ref to reactrenderer --- .../demos/Examples/Default/React/index.jsx | 25 ++++++++++++++-- .../{ReactRenderer.ts => ReactRenderer.tsx} | 29 ++++++++++++++++++- 2 files changed, 50 insertions(+), 4 deletions(-) rename packages/react/src/{ReactRenderer.ts => ReactRenderer.tsx} (65%) diff --git a/docs/src/demos/Examples/Default/React/index.jsx b/docs/src/demos/Examples/Default/React/index.jsx index 08d6cb1a..085713b2 100644 --- a/docs/src/demos/Examples/Default/React/index.jsx +++ b/docs/src/demos/Examples/Default/React/index.jsx @@ -142,6 +142,26 @@ const MentionList = (props) => { ) } +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 }) { @@ -186,7 +206,7 @@ export default () => { return { onStart: props => { - reactRenderer = new ReactRenderer(MentionList, { + reactRenderer = new ReactRenderer(MentionList2, { props, editor: props.editor, }) @@ -209,8 +229,7 @@ export default () => { }) }, onKeyDown(props) { - console.log('keydown', props) - // return reactRenderer.ref.onKeyDown(props) + return reactRenderer.ref.onKeyDown(props) }, onExit() { popup[0].destroy() diff --git a/packages/react/src/ReactRenderer.ts b/packages/react/src/ReactRenderer.tsx similarity index 65% rename from packages/react/src/ReactRenderer.ts rename to packages/react/src/ReactRenderer.tsx index 6769f2cb..a1953ee9 100644 --- a/packages/react/src/ReactRenderer.ts +++ b/packages/react/src/ReactRenderer.tsx @@ -7,6 +7,24 @@ export interface ReactRendererOptions { props?: { [key: string]: any }, } +function isFunctionalComponent(Component: any) { + return ( + typeof Component === 'function' // can be various things + && !( + Component.prototype // native arrows don't have prototypes + && Component.prototype.isReactComponent // special property + ) + ); +} + +function isClassComponent(Component: any) { + return !!( + typeof Component === 'function' + && Component.prototype + && Component.prototype.isReactComponent + ); +} + export class ReactRenderer { id: string @@ -20,6 +38,8 @@ export class ReactRenderer { reactElement: React.ReactNode + ref: React.Component | null = null + constructor(component: any, { props = {}, editor }: ReactRendererOptions) { this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString() this.component = component @@ -31,7 +51,14 @@ export class ReactRenderer { } render() { - this.reactElement = React.createElement(this.component, this.props) + 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({ From e765c8e981280b12d27f76abd919dcb3d84e17d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Mon, 15 Mar 2021 09:00:44 +0100 Subject: [PATCH 11/12] refactoring --- docs/src/demos/Examples/Community/index.vue | 2 +- docs/src/demos/Experiments/Commands/index.vue | 2 +- docs/src/demos/Nodes/Mention/index.vue | 2 +- packages/react/src/Editor.ts | 3 +- packages/react/src/EditorContent.tsx | 16 +++++--- packages/react/src/ReactNodeViewRenderer.tsx | 19 +++------ packages/react/src/ReactRenderer.tsx | 40 ++++++------------- packages/react/src/useReactNodeView.ts | 4 +- packages/vue-2/src/VueRenderer.ts | 15 +++---- packages/vue-3/src/VueRenderer.ts | 8 ++-- 10 files changed, 48 insertions(+), 63 deletions(-) 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/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 93029683..0e96b494 100644 --- a/packages/react/src/EditorContent.tsx +++ b/packages/react/src/EditorContent.tsx @@ -2,11 +2,6 @@ import React from 'react' import ReactDOM from 'react-dom' import { Editor } from './Editor' import { ReactRenderer } from './ReactRenderer' -import { ReactNodeViewContext } from './useReactNodeView' - -type EditorContentProps = { - editor: Editor | null -} const Portals: React.FC<{ renderers: Map }> = ({ renderers }) => { return ( @@ -22,7 +17,16 @@ const Portals: React.FC<{ renderers: Map }> = ({ renderer ) } -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) { diff --git a/packages/react/src/ReactNodeViewRenderer.tsx b/packages/react/src/ReactNodeViewRenderer.tsx index fb606091..a122ef60 100644 --- a/packages/react/src/ReactNodeViewRenderer.tsx +++ b/packages/react/src/ReactNodeViewRenderer.tsx @@ -78,30 +78,23 @@ class ReactNodeView implements NodeView { Component.displayName = capitalizeFirstChar(this.extension.config.name) } - const ReactNodeView: React.FC = (props) => { + const ReactNodeView: React.FunctionComponent = (props) => { const [isEditable, setIsEditable] = useState(this.editor.isEditable) - - const handleEditableChange = () => { + const onDragStart = this.onDragStart.bind(this) + const onViewUpdate = () => { setIsEditable(this.editor.isEditable) } - const onDragStart = this.onDragStart.bind(this) - useEffect(() => { - this.editor.on('viewUpdate', handleEditableChange) + this.editor.on('viewUpdate', onViewUpdate) return () => { - this.editor.off('viewUpdate', handleEditableChange) + this.editor.off('viewUpdate', onViewUpdate) } }, []) return ( - + ) diff --git a/packages/react/src/ReactRenderer.tsx b/packages/react/src/ReactRenderer.tsx index a1953ee9..3f2e5763 100644 --- a/packages/react/src/ReactRenderer.tsx +++ b/packages/react/src/ReactRenderer.tsx @@ -1,28 +1,19 @@ import React from 'react' +import { AnyObject } from '@tiptap/core' import { Editor } from './Editor' -export interface ReactRendererOptions { - as?: string, - editor: Editor, - props?: { [key: string]: any }, -} - -function isFunctionalComponent(Component: any) { - return ( - typeof Component === 'function' // can be various things - && !( - Component.prototype // native arrows don't have prototypes - && Component.prototype.isReactComponent // special property - ) - ); -} - 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 { @@ -34,13 +25,13 @@ export class ReactRenderer { element: Element - props: { [key: string]: any } + props: AnyObject reactElement: React.ReactNode ref: React.Component | null = null - constructor(component: any, { props = {}, editor }: ReactRendererOptions) { + constructor(component: React.Component | React.FunctionComponent, { props = {}, editor }: ReactRendererOptions) { this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString() this.component = component this.editor = editor @@ -50,7 +41,7 @@ export class ReactRenderer { this.render() } - render() { + render(): void { const Component = this.component const props = this.props @@ -62,8 +53,6 @@ export class ReactRenderer { if (this.editor?.contentComponent) { this.editor.contentComponent.setState({ - // TODO - // @ts-ignore renderers: this.editor.contentComponent.state.renderers.set( this.id, this, @@ -72,7 +61,7 @@ export class ReactRenderer { } } - updateProps(props: { [key: string]: any } = {}) { + updateProps(props: AnyObject = {}): void { this.props = { ...this.props, ...props, @@ -81,10 +70,8 @@ export class ReactRenderer { this.render() } - destroy() { + destroy(): void { if (this.editor?.contentComponent) { - // TODO - // @ts-ignore const { renderers } = this.editor.contentComponent.state renderers.delete(this.id) @@ -94,5 +81,4 @@ export class ReactRenderer { }) } } - } diff --git a/packages/react/src/useReactNodeView.ts b/packages/react/src/useReactNodeView.ts index 094a388e..142edbfa 100644 --- a/packages/react/src/useReactNodeView.ts +++ b/packages/react/src/useReactNodeView.ts @@ -1,11 +1,11 @@ -import React, { useContext } from 'react' +import { createContext, useContext } from 'react' export interface ReactNodeViewContextProps { isEditable: boolean, onDragStart: (event: DragEvent) => void, } -export const ReactNodeViewContext = React.createContext>({ +export const ReactNodeViewContext = createContext>({ isEditable: undefined, onDragStart: undefined, }) 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/VueRenderer.ts b/packages/vue-3/src/VueRenderer.ts index e8dd4cca..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 }, + 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]) => { From ead5f77d0855e9462043ed7320cd5592f9610eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Mon, 15 Mar 2021 09:05:13 +0100 Subject: [PATCH 12/12] create ReactPlayground example --- .../demos/Examples/Default/React/index.jsx | 182 ++--------- docs/src/demos/ReactPlayground/index.jsx | 290 ++++++++++++++++++ docs/src/demos/ReactPlayground/index.spec.js | 22 ++ docs/src/demos/ReactPlayground/styles.scss | 55 ++++ 4 files changed, 397 insertions(+), 152 deletions(-) create mode 100644 docs/src/demos/ReactPlayground/index.jsx create mode 100644 docs/src/demos/ReactPlayground/index.spec.js create mode 100644 docs/src/demos/ReactPlayground/styles.scss diff --git a/docs/src/demos/Examples/Default/React/index.jsx b/docs/src/demos/Examples/Default/React/index.jsx index 085713b2..3f8d89df 100644 --- a/docs/src/demos/Examples/Default/React/index.jsx +++ b/docs/src/demos/Examples/Default/React/index.jsx @@ -1,12 +1,7 @@ import React from 'react' -import tippy from 'tippy.js' -import { useEditor, EditorContent, ReactRenderer, ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from '@tiptap/react' +import { useEditor, EditorContent } 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) { @@ -127,162 +122,45 @@ const MenuBar = ({ editor }) => { ) } -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} - - -
-
- ) - }) - } - }), - 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() - }, - } - } - }, - }) + ...defaultExtensions(), ], content: ` -

heading

-

heading

-

paragraph

+

+ 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 +
`, -// 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 (
-
- - -
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 ( + <> + + + + + + + + + + + + + + + + + + + + + + + ) +} + +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} + + +
+
+ ) + }) + } + }), + 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 ( +
+
+ + +
+ + +
+ ) +} 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; + } +}