try to improve react support

This commit is contained in:
Philipp Kühn
2021-03-08 13:19:05 +01:00
parent 48c747be02
commit c20b43aa6c
14 changed files with 797 additions and 197 deletions

View File

@@ -5,5 +5,6 @@ module.exports = {
],
plugins: [
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-class-properties',
],
}

View File

@@ -57,6 +57,7 @@ module.exports = function (api) {
api.chainWebpack(config => {
config.resolve.extensions
.add('.ts')
.add('.tsx')
.add('.jsx')
config.module

View File

@@ -35,7 +35,9 @@
"yjs": "^13.5.1"
},
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-optional-chaining": "^7.13.0",
"@babel/plugin-syntax-class-properties": "^7.12.13",
"@babel/preset-env": "^7.13.5",
"@babel/preset-react": "^7.12.13",
"html-loader": "^1.3.2",

View File

@@ -0,0 +1,143 @@
import * as React from 'react'
import ReactDOM from 'react-dom'
import { EditorState } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view'
import { Node, Schema } from 'prosemirror-model'
// import applyDevTools from 'prosemirror-dev-tools'
// import styled from 'styled-components'
// Here we have the (too simple) React component which
// we'll be rendering content into.
//
class Underlined extends React.Component {
constructor(props) {
super(props)
this.hole = React.createRef()
}
// We'll put the content into what we render using
// this function, which appends a given node to
// a ref HTMLElement, if present.
//
append(node) {
if (this.hole) {
this.hole.current.appendChild(node)
}
}
render() {
// Just really wanted to prove I could get React AND
// styled-component abilities at the same time.
//
// const UnderlinedText = styled.p`
// text-decoration: underline;
// `
// The styled components version is basically just a wrapper to do SCSS styling.
// Questionable if it's even needed for such simple styling and because you can't clearly see the
// DOM structure from the code (hence making `& > ${Component}` selectors quite unintuitive)
// return <span style="text-decoration: underline" ref={this.hole} />
return <p ref={this.hole} style={{textDecoration: 'underline'}}></p>
}
}
// This class is our actual interactor for ProseMirror itself.
// It glues DOM rendering, React, and ProseMirror nodes together.
//
class Underline {
constructor(node) {
// We'll use this to access our Underlined component's
// instance methods.
//
this.ref = React.createRef()
// Here, we'll provide a container to render React into.
// Coincidentally, this is where ProseMirror will put its
// generated contentDOM. React will throw out that content
// once rendered, and at the same time we'll append it into
// the component tree, like a fancy shell game. This isn't
// obvious to the user, but would it be more obvious on an
// expensive render?
//
this.dom = document.createElement('span')
// Finally, we provide an element to render content into.
// We will be moving this node around as we need to.
//
this.contentDOM = document.createElement('span')
// Better way of doing this would be portals https://reactjs.org/docs/portals.html
ReactDOM.render(
<Underlined ref={this.ref} />,
this.dom,
this.putContentDomInRef
)
}
update(node) {
return true
}
// This is the least complex part. Now we've put
// all of our interlocking pieces behind refs and
// instance properties, this becomes the callback
// which performs the actual shell game.
//
putContentDomInRef = () => {
this.ref.current.append(this.contentDOM)
}
// Required to not to leave the React nodes orphaned.
destroy() {
ReactDOM.unmountComponentAtNode(this.dom)
}
}
export default class Editor extends React.Component {
constructor(props) {
super(props)
this.editorState = EditorState.create({
schema: new Schema({
nodes: {
doc: {
content: 'block+'
},
underline: {
group: 'block',
content: 'inline*',
parseDOM: [{ tag: 'p' }],
toDOM() { return ['p', 0] }
},
text: {
group: 'inline'
},
}
})
})
}
createEditorView = (element) => {
if (element != null) {
this.editorView = new EditorView(element, {
nodeViews: {
underline: (node) => new Underline(node)
},
state: this.editorState,
})
// applyDevTools(this.editorView)
}
}
componentWillUnmount() {
if (this.editorView) {
this.editorView.destroy()
}
}
shouldComponentUpdate() {
return false
}
render() {
return <div id="editor" ref={ref => { this.createEditorView(ref) }} />
}
}

View File

@@ -0,0 +1,167 @@
import React, { useState } from 'react'
import { defaultExtensions } from '@tiptap/starter-kit'
import { useEditor, Editor } from '@tiptap/react'
import './styles.scss'
// useEditor only works for child components of <Editor />
const MenuBar = () => {
const editor = useEditor()
return (
<>
<button
onClick={() => editor.chain().focus().toggleBold().run()}
className={`${editor.isActive('bold') ? 'is-active' : ''}`}
>
bold
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
className={`${editor.isActive('italic') ? 'active' : ''}`}
>
italic
</button>
<button
onClick={() => editor.chain().focus().toggleStrike().run()}
className={`${editor.isActive('strike') ? 'active' : ''}`}
>
strike
</button>
<button
onClick={() => editor.chain().focus().toggleCode().run()}
className={`${editor.isActive('code') ? 'active' : ''}`}
>
code
</button>
<button onClick={() => editor.chain().focus().unsetAllMarks().run()}>
clear marks
</button>
<button onClick={() => editor.chain().focus().clearNodes().run()}>
clear nodes
</button>
<button
onClick={() => editor.chain().focus().setParagraph().run()}
className={`${editor.isActive('paragraph') ? 'active' : ''}`}
>
paragraph
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
className={`${editor.isActive('heading', { level: 1 }) ? 'active' : ''}`}
>
h1
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
className={`${editor.isActive('heading', { level: 2 }) ? 'active' : ''}`}
>
h2
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
className={`${editor.isActive('heading', { level: 3 }) ? 'active' : ''}`}
>
h3
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
className={`${editor.isActive('heading', { level: 4 }) ? 'active' : ''}`}
>
h4
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
className={`${editor.isActive('heading', { level: 5 }) ? 'active' : ''}`}
>
h5
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
className={`${editor.isActive('heading', { level: 6 }) ? 'active' : ''}`}
>
h6
</button>
<button
onClick={() => editor.chain().focus().toggleBulletList().run()}
className={`${editor.isActive('bulletList') ? 'active' : ''}`}
>
bullet list
</button>
<button
onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={`${editor.isActive('orderedList') ? 'active' : ''}`}
>
ordered list
</button>
<button
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
className={`${editor.isActive('codeBlock') ? 'active' : ''}`}
>
code block
</button>
<button
onClick={() => editor.chain().focus().toggleBlockquote().run()}
className={`${editor.isActive('blockquote') ? 'active' : ''}`}
>
blockquote
</button>
<button onClick={() => editor.chain().focus().setHorizontalRule().run()}>
horizontal rule
</button>
<button onClick={() => editor.chain().focus().setHardBreak().run()}>
hard break
</button>
<button onClick={() => editor.chain().focus().undo().run()}>
undo
</button>
<button onClick={() => editor.chain().focus().redo().run()}>
redo
</button>
</>
)
}
export default () => {
const [value, setValue] = useState(`
<h2>
Hi there,
</h2>
<p>
this is a basic <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists:
</p>
<ul>
<li>
Thats a bullet list with one …
</li>
<li>
… or two list items.
</li>
</ul>
<p>
Isnt that great? And all of that is editable. But wait, theres more. Lets try a code block:
</p>
<pre><code class="language-css">body {
display: none;
}</code></pre>
<p>
I know, I know, this is impressive. Its only the tip of the iceberg though. Give it a try and click a little bit around. Dont forget to check the other examples too.
</p>
<blockquote>
Wow, thats amazing. Good work, boy! 👏
<br />
— Mom
</blockquote>
`)
return (
<>
<Editor
value={value}
onChange={setValue}
extensions={defaultExtensions()}
>
<MenuBar />
</Editor>
</>
)
}

View File

@@ -1,167 +1,84 @@
import React, { useState } from 'react'
import React, { useState, useEffect } from 'react'
import { defaultExtensions } from '@tiptap/starter-kit'
import { useEditor, Editor } from '@tiptap/react'
import Paragraph from '@tiptap/extension-paragraph'
import { Editor, EditorContent, ReactNodeViewRenderer } from '@tiptap/react'
import './styles.scss'
import { render, unmountComponentAtNode } from 'react-dom'
// useEditor only works for child components of <Editor />
const MenuBar = () => {
const editor = useEditor()
const useEditor = (options = {}) => {
const [editor, setEditor] = useState(null)
return (
<>
<button
onClick={() => editor.chain().focus().toggleBold().run()}
className={`${editor.isActive('bold') ? 'is-active' : ''}`}
>
bold
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
className={`${editor.isActive('italic') ? 'active' : ''}`}
>
italic
</button>
<button
onClick={() => editor.chain().focus().toggleStrike().run()}
className={`${editor.isActive('strike') ? 'active' : ''}`}
>
strike
</button>
<button
onClick={() => editor.chain().focus().toggleCode().run()}
className={`${editor.isActive('code') ? 'active' : ''}`}
>
code
</button>
<button onClick={() => editor.chain().focus().unsetAllMarks().run()}>
clear marks
</button>
<button onClick={() => editor.chain().focus().clearNodes().run()}>
clear nodes
</button>
<button
onClick={() => editor.chain().focus().setParagraph().run()}
className={`${editor.isActive('paragraph') ? 'active' : ''}`}
>
paragraph
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
className={`${editor.isActive('heading', { level: 1 }) ? 'active' : ''}`}
>
h1
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
className={`${editor.isActive('heading', { level: 2 }) ? 'active' : ''}`}
>
h2
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
className={`${editor.isActive('heading', { level: 3 }) ? 'active' : ''}`}
>
h3
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
className={`${editor.isActive('heading', { level: 4 }) ? 'active' : ''}`}
>
h4
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
className={`${editor.isActive('heading', { level: 5 }) ? 'active' : ''}`}
>
h5
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
className={`${editor.isActive('heading', { level: 6 }) ? 'active' : ''}`}
>
h6
</button>
<button
onClick={() => editor.chain().focus().toggleBulletList().run()}
className={`${editor.isActive('bulletList') ? 'active' : ''}`}
>
bullet list
</button>
<button
onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={`${editor.isActive('orderedList') ? 'active' : ''}`}
>
ordered list
</button>
<button
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
className={`${editor.isActive('codeBlock') ? 'active' : ''}`}
>
code block
</button>
<button
onClick={() => editor.chain().focus().toggleBlockquote().run()}
className={`${editor.isActive('blockquote') ? 'active' : ''}`}
>
blockquote
</button>
<button onClick={() => editor.chain().focus().setHorizontalRule().run()}>
horizontal rule
</button>
<button onClick={() => editor.chain().focus().setHardBreak().run()}>
hard break
</button>
<button onClick={() => editor.chain().focus().undo().run()}>
undo
</button>
<button onClick={() => editor.chain().focus().redo().run()}>
redo
</button>
</>
)
useEffect(() => {
const instance = new Editor(options)
setEditor(instance)
return () => {
instance.destroy()
}
}, [])
return editor
}
function reactNodeView(Component) {
const renderComponent = (props, dom) => render(<Component {...props} />, dom)
return (node, view, getPos, decorations) => {
let dom = document.createElement("div")
renderComponent({ node, view, decorations, getPos }, dom)
console.log(dom)
return {
dom,
contentDOM: dom.querySelector('[data-node-view-content]'),
update(node, decorations) {
renderComponent({ node, view, decorations, getPos }, dom)
return true
},
destroy() {
unmountComponentAtNode(dom)
},
}
}
}
export default () => {
const [value, setValue] = useState(`
<h2>
Hi there,
</h2>
<p>
this is a basic <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists:
</p>
<ul>
<li>
Thats a bullet list with one …
</li>
<li>
… or two list items.
</li>
</ul>
<p>
Isnt that great? And all of that is editable. But wait, theres more. Lets try a code block:
</p>
<pre><code class="language-css">body {
display: none;
}</code></pre>
<p>
I know, I know, this is impressive. Its only the tip of the iceberg though. Give it a try and click a little bit around. Dont forget to check the other examples too.
</p>
<blockquote>
Wow, thats amazing. Good work, boy! 👏
<br />
— Mom
</blockquote>
`)
const editor = useEditor({
content: '<p>hello react</p>',
extensions: [
...defaultExtensions(),
Paragraph.extend({
addNodeView() {
return reactNodeView(() => {
// useEffect(() => {
// console.log('effect')
// }, []);
return (
<p className="jooo" data-node-view-wrapper>
<span data-node-view-content></span>
</p>
)
})
return ReactNodeViewRenderer(() => {
// useEffect(() => {
// console.log('effect')
// }, []);
return (
<p className="jooo" data-node-view-wrapper>
<span data-node-view-content></span>
</p>
)
})
},
}),
]
})
return (
<>
<Editor
value={value}
onChange={setValue}
extensions={defaultExtensions()}
>
<MenuBar />
</Editor>
</>
<EditorContent editor={editor} />
)
}