add useEditor

This commit is contained in:
Philipp Kühn
2021-03-08 22:00:07 +01:00
parent 0c494ab136
commit ed7f8c257e
5 changed files with 239 additions and 213 deletions

View File

@@ -1,143 +1,51 @@
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'
// export default () => {
// const editor = useEditor({
// content: '<p>hello react</p>',
// onTransaction() {
// // console.log('update', this)
// },
// extensions: [
// ...defaultExtensions(),
// // Paragraph.extend({
// // addNodeView() {
// // return reactNodeView(() => {
// // // useEffect(() => {
// // // console.log('effect')
// // // }, []);
// 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)
}
}
// // return (
// // <p className="jooo" data-node-view-wrapper>
// // <span data-node-view-content></span>
// // </p>
// // )
// // })
// // return ReactNodeViewRenderer(() => {
// // // useEffect(() => {
// // // console.log('effect')
// // // }, []);
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;
// `
// // return (
// // <p className="jooo" data-node-view-wrapper>
// // <span data-node-view-content></span>
// // </p>
// // )
// // })
// // },
// // }),
// ]
// })
// 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) }} />
}
}
// return (
// <>
// { editor &&
// <button
// onClick={() => editor.chain().focus().toggleBold().run()}
// className={editor.isActive('bold') ? 'is-active' : ''}
// >
// bold
// </button>
// }
// <EditorContent editor={editor} />
// </>
// )
// }

View File

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

View File

@@ -31,7 +31,9 @@ import { Editor } from './Editor'
// )
// }
export class EditorContent extends React.Component {
export class PureEditorContent extends React.Component {
constructor(props) {
super(props)
this.editorContentRef = React.createRef()
@@ -69,3 +71,6 @@ export class EditorContent extends React.Component {
)
}
}
export const EditorContent = React.memo(PureEditorContent);

View File

@@ -1,9 +1,7 @@
// @ts-nocheck
export * from '@tiptap/core'
export { Editor } from './Editor'
// export {
// Editor, EditorContext, useEditor,
// } from './components/Editor'
export * from './useEditor'
export * from './ReactRenderer'
export * from './ReactNodeViewRenderer'
export * from './EditorContent'

View File

@@ -0,0 +1,31 @@
// @ts-nocheck
import { useState, useEffect } from 'react'
import { Editor } from './Editor'
function useForceUpdate() {
const [_, setValue] = useState(0)
return () => setValue(value => value + 1)
}
export const useEditor = (options = {}) => {
const [editor, setEditor] = useState(null)
const forceUpdate = useForceUpdate()
useEffect(() => {
const instance = new Editor(options)
setEditor(instance)
instance.on('transaction', () => {
forceUpdate()
})
return () => {
instance.destroy()
}
}, [])
return editor
}