├── .gitignore ├── .vscode └── settings.json ├── README.md ├── package-lock.json ├── package.json ├── public ├── Screenshot.png ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.tsx ├── EditorPage.tsx ├── components │ ├── Blockquote.tsx │ ├── CodeBlock.tsx │ ├── Container.tsx │ ├── Heading.tsx │ ├── Image.tsx │ ├── Paragraph.tsx │ ├── PrismRenderer.tsx │ ├── Prosemirror.tsx │ ├── ReactNodeView.tsx │ └── ReactNodeViewPortals.tsx ├── index.tsx ├── react-app-env.d.ts └── theme.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.organizeImports": true 4 | }, 5 | "files.exclude": { 6 | "**/node_modules": true, 7 | "**/dist": true, 8 | "**/.cache": true, 9 | "**/demo-gatsby/public": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ProseMirror React NodeViews 2 | 3 | This is an example repo of how to use React FC components as NodeViews for ProseMirror 4 | 5 | ![Screenshot](./public/Screenshot.png) 6 | 7 | How to use: 8 | 9 | ### Wrap your root component with `ReactNodeViewPortalsProvider` 10 | 11 | Lets use React portals to preserve your app context (css-in-js, data, etc) when the NodeViews are rendered. `ReactNodeViewPortalsProvider` is a convenient way to help you with this. 12 | 13 | ```tsx 14 | import { createReactNodeView } from "./ReactNodeView"; 15 | import ReactNodeViewPortalsProvider from "./ReactNodeViewPortals"; 16 | 17 | const App: React.FC = props => { 18 | return ( 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default App; 26 | ``` 27 | 28 | ### Loading ProseMirror with React components 29 | 30 | This is how you initialize your ProseMirror editor 31 | 32 | ```tsx 33 | import React from "react"; 34 | import { useReactNodeViewPortals } from "./ReactNodeViewPortals"; 35 | 36 | const ProseMirror: React.FC = () => { 37 | const { createPortal } = useReactNodeViewPortals(); 38 | const editorViewRef = useRef(null); 39 | 40 | const handleCreatePortal = useCallback(createPortal, []); 41 | const state = useMemo(() => { 42 | const doc = schema.nodeFromJSON(YOUR_PROSEMIRROR_SCHEMA); 43 | return EditorState.create({ 44 | doc, 45 | plugins: [ 46 | history(), 47 | keymap({ "Mod-z": undo, "Mod-y": redo }), 48 | keymap(baseKeymap) 49 | ] 50 | }); 51 | }, []); 52 | 53 | const createEditorView = useCallback( 54 | editorViewDOM => { 55 | const view = new EditorView(editorViewDOM, { 56 | state, 57 | nodeViews: { 58 | heading(node, view, getPos, decorations) { 59 | return createReactNodeView({ 60 | node, 61 | view, 62 | getPos, 63 | decorations, 64 | component: Heading, 65 | onCreatePortal: handleCreatePortal 66 | }); 67 | }, 68 | paragraph(node, view, getPos, decorations) { 69 | return createReactNodeView({ 70 | node, 71 | view, 72 | getPos, 73 | decorations, 74 | component: Paragraph, 75 | onCreatePortal: handleCreatePortal 76 | }); 77 | } 78 | }, 79 | dispatchTransaction(transaction) { 80 | const newState = view.state.apply(transaction); 81 | handleChange(newState.doc.toJSON()); 82 | view.updateState(newState); 83 | } 84 | }); 85 | }, 86 | [state, handleChange, handleCreatePortal] 87 | ); 88 | 89 | useEffect(() => { 90 | const editorViewDOM = editorViewRef.current; 91 | if (editorViewDOM) { 92 | createEditorView(editorViewDOM); 93 | } 94 | }, [createEditorView]); 95 | 96 | return
; 97 | }; 98 | 99 | export default ProseMirror; 100 | ``` 101 | 102 | ### Getting node props within your React components 103 | 104 | Each of the React components have been wrapped with a context provider before sending it through the portal, so its easy to access the nodeview's props: 105 | 106 | ```tsx 107 | import { Heading } from "@chakra-ui/core"; 108 | import React from "react"; 109 | import { useReactNodeView } from "./ReactNodeView"; 110 | 111 | const HeadingBlock: React.FC = ({ children }) => { 112 | const context = useReactNodeView(); 113 | const level = context.node?.attrs.level; 114 | return {children}; 115 | }; 116 | 117 | export default HeadingBlock; 118 | ``` 119 | 120 | ```tsx 121 | import { Box, Image, Text } from "@chakra-ui/core"; 122 | import React from "react"; 123 | import { useReactNodeView } from "./ReactNodeView"; 124 | 125 | const ImageBlock: React.FC = () => { 126 | const context = useReactNodeView(); 127 | const attrs = context.node?.attrs; 128 | return ( 129 | 130 | {attrs?.alt} 131 | {attrs?.title && ( 132 | 133 | {attrs.title} 134 | 135 | )} 136 | 137 | ); 138 | }; 139 | 140 | export default ImageBlock; 141 | ``` 142 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prosemirror-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@chakra-ui/core": "^0.6.1", 7 | "@emotion/core": "^10.0.28", 8 | "@emotion/styled": "^10.0.27", 9 | "@testing-library/jest-dom": "^4.2.4", 10 | "@testing-library/react": "^9.3.2", 11 | "@testing-library/user-event": "^7.1.2", 12 | "@types/jest": "^24.0.0", 13 | "@types/node": "^12.0.0", 14 | "@types/prosemirror-commands": "^1.0.1", 15 | "@types/prosemirror-history": "^1.0.1", 16 | "@types/prosemirror-keymap": "^1.0.1", 17 | "@types/prosemirror-schema-basic": "^1.0.1", 18 | "@types/prosemirror-state": "^1.2.3", 19 | "@types/prosemirror-view": "^1.11.2", 20 | "@types/react": "^16.9.0", 21 | "@types/react-dom": "^16.9.0", 22 | "@types/shortid": "^0.0.29", 23 | "emotion-theming": "^10.0.27", 24 | "prism-react-renderer": "^1.0.2", 25 | "prosemirror-commands": "^1.1.3", 26 | "prosemirror-history": "^1.1.3", 27 | "prosemirror-keymap": "^1.1.3", 28 | "prosemirror-schema-basic": "^1.1.2", 29 | "prosemirror-state": "^1.3.3", 30 | "prosemirror-view": "^1.14.4", 31 | "react": "^16.13.1", 32 | "react-dom": "^16.13.1", 33 | "react-scripts": "3.4.0", 34 | "shortid": "^2.2.15", 35 | "typescript": "~3.8.0" 36 | }, 37 | "scripts": { 38 | "start": "react-scripts start", 39 | "build": "react-scripts build", 40 | "test": "react-scripts test", 41 | "eject": "react-scripts eject" 42 | }, 43 | "eslintConfig": { 44 | "extends": "react-app" 45 | }, 46 | "browserslist": { 47 | "production": [ 48 | ">0.2%", 49 | "not dead", 50 | "not op_mini all" 51 | ], 52 | "development": [ 53 | "last 1 chrome version", 54 | "last 1 firefox version", 55 | "last 1 safari version" 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /public/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnkueh/prosemirror-react-nodeviews/b39c9c4fbb8286359917b1293b454265fc44fb79/public/Screenshot.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnkueh/prosemirror-react-nodeviews/b39c9c4fbb8286359917b1293b454265fc44fb79/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | ProseMirror React App 28 | 29 | 30 | 31 |
32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnkueh/prosemirror-react-nodeviews/b39c9c4fbb8286359917b1293b454265fc44fb79/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnkueh/prosemirror-react-nodeviews/b39c9c4fbb8286359917b1293b454265fc44fb79/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { CSSReset, ThemeProvider } from "@chakra-ui/core"; 2 | import React from "react"; 3 | import EditorPage from "./EditorPage"; 4 | import theme from "./theme"; 5 | 6 | function App() { 7 | return ( 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /src/EditorPage.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex } from "@chakra-ui/core"; 2 | import React, { useState } from "react"; 3 | import Container from "./components/Container"; 4 | import PrismRenderer from "./components/PrismRenderer"; 5 | import ProseMirror from "./components/Prosemirror"; 6 | 7 | const initialValue = { 8 | type: "doc", 9 | content: [ 10 | { 11 | type: "heading", 12 | attrs: { level: 1 }, 13 | content: [{ type: "text", text: "Heading one" }] 14 | }, 15 | { 16 | type: "heading", 17 | attrs: { level: 2 }, 18 | content: [{ type: "text", text: "Heading two" }] 19 | }, 20 | { 21 | type: "heading", 22 | attrs: { level: 3 }, 23 | content: [{ type: "text", text: "Heading three" }] 24 | }, 25 | { 26 | type: "heading", 27 | attrs: { level: 4 }, 28 | content: [{ type: "text", text: "Heading four" }] 29 | }, 30 | { 31 | type: "heading", 32 | attrs: { level: 5 }, 33 | content: [{ type: "text", text: "Heading five" }] 34 | }, 35 | { 36 | type: "paragraph", 37 | content: [ 38 | { 39 | type: "text", 40 | text: "A normal block of a paragraph of text" 41 | } 42 | ] 43 | }, 44 | { 45 | type: "paragraph", 46 | content: [ 47 | { 48 | type: "text", 49 | text: "A block of paragraphed text with " 50 | }, 51 | { 52 | type: "text", 53 | text: "bold", 54 | marks: [{ type: "strong" }] 55 | }, 56 | { 57 | type: "text", 58 | text: " and " 59 | }, 60 | { 61 | type: "text", 62 | text: "italics", 63 | marks: [{ type: "em" }] 64 | }, 65 | { 66 | type: "text", 67 | text: ", " 68 | }, 69 | { 70 | type: "text", 71 | text: "inline code", 72 | marks: [{ type: "code" }] 73 | }, 74 | { 75 | type: "text", 76 | text: ", and " 77 | }, 78 | { 79 | type: "text", 80 | text: "a link", 81 | marks: [ 82 | { 83 | type: "link", 84 | attrs: { href: "https://www.google.com", title: "Google" } 85 | } 86 | ] 87 | } 88 | ] 89 | }, 90 | { type: "blockquote", content: [{ type: "text", text: "A blockquote" }] }, 91 | { type: "code_block", content: [{ type: "text", text: "A code block" }] }, 92 | { 93 | type: "image", 94 | attrs: { 95 | alt: "Toilet paper", 96 | title: "Unsplash image of toilet paper", 97 | src: 98 | "https://images.unsplash.com/photo-1584556812952-905ffd0c611a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=3300&q=80" 99 | }, 100 | content: [{ type: "text", text: "A code block" }] 101 | } 102 | ] 103 | }; 104 | 105 | const Page: React.FC = () => { 106 | const [value, setValue] = useState(initialValue); 107 | return ( 108 | 109 | 110 | 111 | 112 | 113 | 114 | 118 | 119 | 120 | 121 | ); 122 | }; 123 | 124 | export default Page; 125 | -------------------------------------------------------------------------------- /src/components/Blockquote.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from "@chakra-ui/core"; 2 | import React from "react"; 3 | // import { useReactNodeView } from "./ReactNodeView"; 4 | 5 | const Blockquote: React.FC = ({ children }) => { 6 | // const context = useReactNodeView(); 7 | // console.log(context); 8 | return ( 9 | 10 | {children} 11 | 12 | ); 13 | }; 14 | export default Blockquote; 15 | -------------------------------------------------------------------------------- /src/components/CodeBlock.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from "@chakra-ui/core"; 2 | import React from "react"; 3 | // import { useReactNodeView } from "./ReactNodeView"; 4 | 5 | const CodeBlock: React.FC = ({ children }) => { 6 | // const context = useReactNodeView(); 7 | // console.log(context); 8 | return ( 9 | 10 | {children} 11 | 12 | ); 13 | }; 14 | 15 | export default CodeBlock; 16 | -------------------------------------------------------------------------------- /src/components/Container.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, BoxProps } from "@chakra-ui/core"; 3 | 4 | const Container: React.FC = props => { 5 | return ; 6 | }; 7 | 8 | export default Container; 9 | -------------------------------------------------------------------------------- /src/components/Heading.tsx: -------------------------------------------------------------------------------- 1 | import { Heading } from "@chakra-ui/core"; 2 | import React from "react"; 3 | import { useReactNodeView } from "./ReactNodeView"; 4 | 5 | const HeadingBlock: React.FC = ({ children }) => { 6 | const context = useReactNodeView(); 7 | const level = context.node?.attrs.level; 8 | return {children}; 9 | }; 10 | 11 | export default HeadingBlock; 12 | -------------------------------------------------------------------------------- /src/components/Image.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Image, Text } from "@chakra-ui/core"; 2 | import React from "react"; 3 | import { useReactNodeView } from "./ReactNodeView"; 4 | 5 | const ImageBlock: React.FC = () => { 6 | const context = useReactNodeView(); 7 | const attrs = context.node?.attrs; 8 | return ( 9 | 10 | {attrs?.alt} 11 | {attrs?.title && ( 12 | 13 | {attrs.title} 14 | 15 | )} 16 | 17 | ); 18 | }; 19 | 20 | export default ImageBlock; 21 | -------------------------------------------------------------------------------- /src/components/Paragraph.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from "@chakra-ui/core"; 2 | import React from "react"; 3 | // import { useReactNodeView } from "./ReactNodeView"; 4 | 5 | const Paragraph: React.FC = ({ children }) => { 6 | // const context = useReactNodeView(); 7 | // console.log(context); 8 | return {children}; 9 | }; 10 | 11 | export default Paragraph; 12 | -------------------------------------------------------------------------------- /src/components/PrismRenderer.tsx: -------------------------------------------------------------------------------- 1 | import { Box, BoxProps, Button, useClipboard } from "@chakra-ui/core"; 2 | import Highlight, { defaultProps, Language } from "prism-react-renderer"; 3 | import theme from "prism-react-renderer/themes/github"; 4 | import React from "react"; 5 | 6 | interface Props { 7 | code: string; 8 | language: Language; 9 | } 10 | 11 | const PrismRenderer: React.FC = ({ 12 | code, 13 | language, 14 | ...props 15 | }) => { 16 | const { onCopy, hasCopied } = useClipboard(code); 17 | 18 | return ( 19 | 20 | 26 | {({ style, tokens, getLineProps, getTokenProps }) => ( 27 | 37 | {tokens.map((line, i) => ( 38 |
39 | {line.map((token, key) => ( 40 | 41 | ))} 42 |
43 | ))} 44 | 56 |
57 | )} 58 |
59 |
60 | ); 61 | }; 62 | 63 | export default PrismRenderer; 64 | -------------------------------------------------------------------------------- /src/components/Prosemirror.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from "@chakra-ui/core"; 2 | import { baseKeymap } from "prosemirror-commands"; 3 | import { history, redo, undo } from "prosemirror-history"; 4 | import { keymap } from "prosemirror-keymap"; 5 | import { schema } from "prosemirror-schema-basic"; 6 | import { EditorState } from "prosemirror-state"; 7 | import { EditorView } from "prosemirror-view"; 8 | import React, { useCallback, useEffect, useMemo, useRef } from "react"; 9 | import Blockquote from "./Blockquote"; 10 | import CodeBlock from "./CodeBlock"; 11 | import Heading from "./Heading"; 12 | import Image from "./Image"; 13 | import Paragraph from "./Paragraph"; 14 | import { createReactNodeView } from "./ReactNodeView"; 15 | import ReactNodeViewPortalsProvider, { 16 | useReactNodeViewPortals 17 | } from "./ReactNodeViewPortals"; 18 | 19 | interface Props { 20 | defaultValue: any; 21 | onChange: any; 22 | } 23 | 24 | const ProseMirrorWrapper: React.FC = props => { 25 | return ( 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | const ProseMirror: React.FC = ({ defaultValue, onChange }) => { 33 | const { createPortal } = useReactNodeViewPortals(); 34 | const editorViewRef = useRef(null); 35 | const handleChange = useCallback(onChange, []); 36 | const handleCreatePortal = useCallback(createPortal, []); 37 | const state = useMemo(() => { 38 | const doc = schema.nodeFromJSON(defaultValue); 39 | return EditorState.create({ 40 | doc, 41 | plugins: [ 42 | history(), 43 | keymap({ "Mod-z": undo, "Mod-y": redo }), 44 | keymap(baseKeymap) 45 | ] 46 | }); 47 | }, [defaultValue]); 48 | const createEditorView = useCallback( 49 | editorViewDOM => { 50 | const view = new EditorView(editorViewDOM, { 51 | state, 52 | nodeViews: { 53 | blockquote(node, view, getPos, decorations) { 54 | return createReactNodeView({ 55 | node, 56 | view, 57 | getPos, 58 | decorations, 59 | component: Blockquote, 60 | onCreatePortal: handleCreatePortal 61 | }); 62 | }, 63 | heading(node, view, getPos, decorations) { 64 | return createReactNodeView({ 65 | node, 66 | view, 67 | getPos, 68 | decorations, 69 | component: Heading, 70 | onCreatePortal: handleCreatePortal 71 | }); 72 | }, 73 | paragraph(node, view, getPos, decorations) { 74 | return createReactNodeView({ 75 | node, 76 | view, 77 | getPos, 78 | decorations, 79 | component: Paragraph, 80 | onCreatePortal: handleCreatePortal 81 | }); 82 | }, 83 | code_block(node, view, getPos, decorations) { 84 | return createReactNodeView({ 85 | node, 86 | view, 87 | getPos, 88 | decorations, 89 | component: CodeBlock, 90 | onCreatePortal: handleCreatePortal 91 | }); 92 | }, 93 | image(node, view, getPos, decorations) { 94 | return createReactNodeView({ 95 | node, 96 | view, 97 | getPos, 98 | decorations, 99 | component: Image, 100 | onCreatePortal: handleCreatePortal 101 | }); 102 | // return new ImageView(node, view, getPos, decorations); 103 | } 104 | }, 105 | dispatchTransaction(transaction) { 106 | const newState = view.state.apply(transaction); 107 | handleChange(newState.doc.toJSON()); 108 | view.updateState(newState); 109 | } 110 | }); 111 | }, 112 | [state, handleChange, handleCreatePortal] 113 | ); 114 | 115 | useEffect(() => { 116 | const editorViewDOM = editorViewRef.current; 117 | if (editorViewDOM) { 118 | createEditorView(editorViewDOM); 119 | } 120 | }, [createEditorView]); 121 | 122 | return ( 123 | 124 |
125 |
126 | ); 127 | }; 128 | 129 | export default ProseMirrorWrapper; 130 | -------------------------------------------------------------------------------- /src/components/ReactNodeView.tsx: -------------------------------------------------------------------------------- 1 | import { Node } from "prosemirror-model"; 2 | import { Decoration, EditorView, NodeView } from "prosemirror-view"; 3 | import React, { useContext, useEffect, useRef } from "react"; 4 | import ReactDOM from "react-dom"; 5 | import shortid from "shortid"; 6 | 7 | interface IReactNodeViewContext { 8 | node: Node; 9 | view: EditorView; 10 | getPos: TGetPos; 11 | decorations: Decoration[]; 12 | } 13 | 14 | const ReactNodeViewContext = React.createContext< 15 | Partial 16 | >({ 17 | node: undefined, 18 | view: undefined, 19 | getPos: undefined, 20 | decorations: undefined 21 | }); 22 | 23 | type TGetPos = boolean | (() => number); 24 | 25 | class ReactNodeView implements NodeView { 26 | componentRef: React.RefObject; 27 | dom?: HTMLElement; 28 | contentDOM?: HTMLElement; 29 | component: React.FC; 30 | node: Node; 31 | view: EditorView; 32 | getPos: TGetPos; 33 | decorations: Decoration[]; 34 | 35 | constructor( 36 | node: Node, 37 | view: EditorView, 38 | getPos: TGetPos, 39 | decorations: Decoration[], 40 | component: React.FC 41 | ) { 42 | this.node = node; 43 | this.view = view; 44 | this.getPos = getPos; 45 | this.decorations = decorations; 46 | this.component = component; 47 | this.componentRef = React.createRef(); 48 | } 49 | 50 | init() { 51 | this.dom = document.createElement("div"); 52 | this.dom.classList.add("ProseMirror__dom"); 53 | 54 | if (!this.node.isLeaf) { 55 | this.contentDOM = document.createElement("div"); 56 | this.contentDOM.classList.add("ProseMirror__contentDOM"); 57 | this.dom.appendChild(this.contentDOM); 58 | } 59 | 60 | return { 61 | nodeView: this, 62 | portal: this.renderPortal(this.dom) 63 | }; 64 | } 65 | 66 | renderPortal(container: HTMLElement) { 67 | const Component: React.FC = props => { 68 | const componentRef = useRef(null); 69 | 70 | useEffect(() => { 71 | const componentDOM = componentRef.current; 72 | if (componentDOM != null && this.contentDOM != null) { 73 | if (!this.node.isLeaf) { 74 | componentDOM.firstChild?.appendChild(this.contentDOM); 75 | } 76 | } 77 | }, [componentRef]); 78 | 79 | return ( 80 |
81 | 89 | 90 | 91 |
92 | ); 93 | }; 94 | 95 | return ReactDOM.createPortal(, container, shortid.generate()); 96 | } 97 | 98 | update(node: Node) { 99 | return true; 100 | } 101 | 102 | destroy() { 103 | this.dom = undefined; 104 | this.contentDOM = undefined; 105 | } 106 | } 107 | 108 | interface TCreateReactNodeView extends IReactNodeViewContext { 109 | component: React.FC; 110 | onCreatePortal: (portal: any) => void; 111 | } 112 | 113 | export const createReactNodeView = ({ 114 | node, 115 | view, 116 | getPos, 117 | decorations, 118 | component, 119 | onCreatePortal 120 | }: TCreateReactNodeView) => { 121 | const reactNodeView = new ReactNodeView( 122 | node, 123 | view, 124 | getPos, 125 | decorations, 126 | component 127 | ); 128 | const { nodeView, portal } = reactNodeView.init(); 129 | onCreatePortal(portal); 130 | 131 | return nodeView; 132 | }; 133 | export const useReactNodeView = () => useContext(ReactNodeViewContext); 134 | export default ReactNodeView; 135 | -------------------------------------------------------------------------------- /src/components/ReactNodeViewPortals.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactText, useContext, useReducer } from "react"; 2 | 3 | interface Props {} 4 | 5 | const initialState = {}; 6 | const ReactNodeViewPortalsContext = React.createContext<{ 7 | createPortal: (portal: any) => void; 8 | state: Partial; 9 | }>({ 10 | createPortal: () => {}, 11 | state: {} 12 | }); 13 | 14 | const ReactNodeViewPortalsProvider: React.FC = ({ children }) => { 15 | const [data, dispatch] = useReducer(reducer, initialState); 16 | 17 | return ( 18 | { 21 | return dispatch({ type: "createPortal", key: portal.key!, portal }); 22 | }, 23 | state: data 24 | }} 25 | > 26 | {children} 27 | {Object.values(data).map(obj => obj.portal)} 28 | 29 | ); 30 | }; 31 | 32 | type State = { 33 | [key: string]: any; 34 | }; 35 | 36 | type Action = { 37 | type: "createPortal"; 38 | key: ReactText; 39 | portal: any; 40 | }; 41 | 42 | function reducer(state: State, action: Action) { 43 | switch (action.type) { 44 | case "createPortal": 45 | return { 46 | ...state, 47 | [action.key]: { 48 | portal: action.portal 49 | } 50 | }; 51 | default: 52 | return state; 53 | } 54 | } 55 | 56 | export const useReactNodeViewPortals = () => 57 | useContext(ReactNodeViewPortalsContext); 58 | 59 | export default ReactNodeViewPortalsProvider; 60 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | ReactDOM.render(, document.getElementById("root")); 6 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/theme.ts: -------------------------------------------------------------------------------- 1 | import { theme } from "@chakra-ui/core"; 2 | 3 | export default { 4 | ...theme 5 | }; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------