mirror of
https://github.com/ueberdosis/tiptap.git
synced 2024-11-27 14:59:27 +08:00
React context implementation for Tiptap (#4192)
* feat(react): add react context implementation * chore(docs): updated react docs & demos for new context * chore(docs): added slot docs * chore(docs): fix typo * chore(react): use correct editor package * fix typo in react installation docs * update react typings to latest version * fix types --------- Co-authored-by: bdbch <dominik@bdbch.com>
This commit is contained in:
parent
e661bbbbc9
commit
d689e2d9c1
@ -3,11 +3,13 @@ import './styles.scss'
|
||||
import { Color } from '@tiptap/extension-color'
|
||||
import ListItem from '@tiptap/extension-list-item'
|
||||
import TextStyle from '@tiptap/extension-text-style'
|
||||
import { EditorContent, useEditor } from '@tiptap/react'
|
||||
import { EditorProvider, useCurrentEditor } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import React from 'react'
|
||||
|
||||
const MenuBar = ({ editor }) => {
|
||||
const MenuBar = () => {
|
||||
const { editor } = useCurrentEditor()
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
@ -178,58 +180,54 @@ const MenuBar = ({ editor }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
||||
TextStyle.configure({ types: [ListItem.name] }),
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
keepMarks: true,
|
||||
keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
|
||||
},
|
||||
orderedList: {
|
||||
keepMarks: true,
|
||||
keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
|
||||
},
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
<h2>
|
||||
Hi there,
|
||||
</h2>
|
||||
<p>
|
||||
this is a <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
That’s a bullet list with one …
|
||||
</li>
|
||||
<li>
|
||||
… or two list items.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block:
|
||||
</p>
|
||||
<pre><code class="language-css">body {
|
||||
display: none;
|
||||
}</code></pre>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<blockquote>
|
||||
Wow, that’s amazing. Good work, boy! 👏
|
||||
<br />
|
||||
— Mom
|
||||
</blockquote>
|
||||
`,
|
||||
})
|
||||
const extensions = [
|
||||
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
||||
TextStyle.configure({ types: [ListItem.name] }),
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
keepMarks: true,
|
||||
keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
|
||||
},
|
||||
orderedList: {
|
||||
keepMarks: true,
|
||||
keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
const content = `
|
||||
<h2>
|
||||
Hi there,
|
||||
</h2>
|
||||
<p>
|
||||
this is a <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
That’s a bullet list with one …
|
||||
</li>
|
||||
<li>
|
||||
… or two list items.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block:
|
||||
</p>
|
||||
<pre><code class="language-css">body {
|
||||
display: none;
|
||||
}</code></pre>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<blockquote>
|
||||
Wow, that’s amazing. Good work, boy! 👏
|
||||
<br />
|
||||
— Mom
|
||||
</blockquote>
|
||||
`
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<div>
|
||||
<MenuBar editor={editor} />
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
<EditorProvider slotBefore={<MenuBar />} extensions={extensions} content={content}></EditorProvider>
|
||||
)
|
||||
}
|
||||
|
@ -47,19 +47,54 @@ To actually start using Tiptap we need to create a new component. Let’s call i
|
||||
|
||||
```jsx
|
||||
// src/Tiptap.jsx
|
||||
import { useEditor, EditorContent } from '@tiptap/react'
|
||||
import { EditorProvider, FloatingMenu, BubbleMenu } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
|
||||
// define your extension array
|
||||
const extensions = [
|
||||
StarterKit,
|
||||
]
|
||||
|
||||
const content = '<p>Hello World!</p>'
|
||||
|
||||
const Tiptap = () => {
|
||||
return (
|
||||
<EditorProvider extensions={extensions} content={content}>
|
||||
<FloatingMenu>This is the floating menu</FloatingMenu>
|
||||
<BubbleMenu>This is the bubble menu</BubbleMenu>
|
||||
</EditorProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tiptap
|
||||
```
|
||||
|
||||
**Important Note**: You can always use the `useEditor` hook if you want to avoid using the Editor context.
|
||||
|
||||
```jsx
|
||||
// src/Tiptap.jsx
|
||||
import { useEditor, EditorContent FloatingMenu, BubbleMenu } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
|
||||
// define your extension array
|
||||
const extensions = [
|
||||
StarterKit,
|
||||
]
|
||||
|
||||
const content = '<p>Hello World!</p>'
|
||||
|
||||
const Tiptap = () => {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
],
|
||||
content: '<p>Hello World!</p>',
|
||||
extensions,
|
||||
content,
|
||||
})
|
||||
|
||||
return (
|
||||
<EditorContent editor={editor} />
|
||||
<>
|
||||
<EditorContent editor={editor} />
|
||||
<FloatingMenu editor={editor}>This is the floating menu</FloatingMenu>
|
||||
<BubbleMenu editor={editor}>This is the bubble menu</BubbleMenu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -83,9 +118,41 @@ const App = () => {
|
||||
export default App
|
||||
```
|
||||
|
||||
#### 5. Consume the Editor context in child components
|
||||
|
||||
If you use the `EditorProvider` to setup your Tiptap editor, you can now easily access your editor instance from any child component using the `useCurrentEditor` hook.
|
||||
|
||||
```jsx
|
||||
import { useCurrentEditor } from '@tiptap/react'
|
||||
|
||||
const EditorJSONPreview = () => {
|
||||
const { editor } = useCurrentEditor()
|
||||
|
||||
return (
|
||||
<pre>
|
||||
{JSON.stringify(editor.getJSON(), null, 2)}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: This won't work if you use the `useEditor` hook to setup your editor.
|
||||
|
||||
You should now see a pretty barebones example of Tiptap in your browser.
|
||||
|
||||
#### 5. The complete setup (optional)
|
||||
#### 6. Add before or after slots
|
||||
Since the EditorContent component is rendered by the `EditorProvider` component, we now can't directly define where to render before or after content of our editor. For that we can use the `slotBefore` & `slotAfter` props on the `EditorProvider` component.
|
||||
|
||||
```jsx
|
||||
<EditorProvider
|
||||
extensions={extensions}
|
||||
content={content}
|
||||
slotBefore={<MyEditorToolbar />}
|
||||
slotAfter={<MyEditorFooter />}
|
||||
/>
|
||||
```
|
||||
|
||||
#### 7. The complete setup (optional)
|
||||
Ready to add more? Below is a demo that shows how you could set up a basic toolbar. Feel free to take it and start customizing it to your needs:
|
||||
|
||||
https://embed.tiptap.dev/preview/Examples/Default
|
||||
|
90
package-lock.json
generated
90
package-lock.json
generated
@ -23,6 +23,8 @@
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-commonjs": "^21.0.1",
|
||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.2",
|
||||
"@typescript-eslint/parser": "^5.10.2",
|
||||
"babel-loader": "^8.2.3",
|
||||
@ -509,7 +511,6 @@
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.18.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@ -5047,9 +5048,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.0.14",
|
||||
"version": "18.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.14.tgz",
|
||||
"integrity": "sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
@ -5057,9 +5059,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.0.5",
|
||||
"version": "18.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.6.tgz",
|
||||
"integrity": "sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
@ -5357,7 +5360,6 @@
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
@ -5368,7 +5370,6 @@
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.2.37",
|
||||
@ -5377,7 +5378,6 @@
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
@ -5394,7 +5394,6 @@
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.2.37",
|
||||
@ -5408,7 +5407,6 @@
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.2.37"
|
||||
@ -5416,7 +5414,6 @@
|
||||
},
|
||||
"node_modules/@vue/reactivity-transform": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
@ -5428,7 +5425,6 @@
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.2.37",
|
||||
@ -5437,7 +5433,6 @@
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/runtime-core": "3.2.37",
|
||||
@ -5447,12 +5442,10 @@
|
||||
},
|
||||
"node_modules/@vue/runtime-dom/node_modules/csstype": {
|
||||
"version": "2.6.20",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.2.37",
|
||||
@ -5464,7 +5457,6 @@
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
@ -8954,7 +8946,6 @@
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
@ -12708,7 +12699,6 @@
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.25.9",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sourcemap-codec": "^1.4.8"
|
||||
@ -13498,7 +13488,6 @@
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
@ -15363,7 +15352,6 @@
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
@ -15472,7 +15460,6 @@
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.14",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@ -17091,7 +17078,6 @@
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@ -17099,7 +17085,6 @@
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@ -17127,7 +17112,6 @@
|
||||
},
|
||||
"node_modules/sourcemap-codec": {
|
||||
"version": "1.4.8",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/spdx-correct": {
|
||||
@ -18875,7 +18859,6 @@
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.2.37",
|
||||
@ -20308,8 +20291,8 @@
|
||||
"devDependencies": {
|
||||
"@tiptap/core": "^2.1.0-rc.11",
|
||||
"@tiptap/pm": "^2.1.0-rc.11",
|
||||
"@types/react": "^18.0.1",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
@ -20709,8 +20692,7 @@
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.18.5",
|
||||
"dev": true
|
||||
"version": "7.18.5"
|
||||
},
|
||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
|
||||
"version": "7.17.12",
|
||||
@ -23980,8 +23962,8 @@
|
||||
"@tiptap/extension-bubble-menu": "^2.1.0-rc.11",
|
||||
"@tiptap/extension-floating-menu": "^2.1.0-rc.11",
|
||||
"@tiptap/pm": "^2.1.0-rc.11",
|
||||
"@types/react": "^18.0.1",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
}
|
||||
@ -24025,7 +24007,7 @@
|
||||
"@tiptap/extension-floating-menu": "^2.1.0-rc.11",
|
||||
"@tiptap/pm": "^2.1.0-rc.11",
|
||||
"vue": "^2.6.0",
|
||||
"vue-ts-types": "*"
|
||||
"vue-ts-types": "^1.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": {
|
||||
@ -24150,7 +24132,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "18.0.14",
|
||||
"version": "18.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.14.tgz",
|
||||
"integrity": "sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/prop-types": "*",
|
||||
@ -24159,7 +24143,9 @@
|
||||
}
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"version": "18.0.5",
|
||||
"version": "18.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.6.tgz",
|
||||
"integrity": "sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
@ -24332,7 +24318,6 @@
|
||||
},
|
||||
"@vue/compiler-core": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
"@vue/shared": "3.2.37",
|
||||
@ -24342,7 +24327,6 @@
|
||||
},
|
||||
"@vue/compiler-dom": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@vue/compiler-core": "3.2.37",
|
||||
"@vue/shared": "3.2.37"
|
||||
@ -24350,7 +24334,6 @@
|
||||
},
|
||||
"@vue/compiler-sfc": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
"@vue/compiler-core": "3.2.37",
|
||||
@ -24366,7 +24349,6 @@
|
||||
},
|
||||
"@vue/compiler-ssr": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@vue/compiler-dom": "3.2.37",
|
||||
"@vue/shared": "3.2.37"
|
||||
@ -24378,14 +24360,12 @@
|
||||
},
|
||||
"@vue/reactivity": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@vue/shared": "3.2.37"
|
||||
}
|
||||
},
|
||||
"@vue/reactivity-transform": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/parser": "^7.16.4",
|
||||
"@vue/compiler-core": "3.2.37",
|
||||
@ -24396,7 +24376,6 @@
|
||||
},
|
||||
"@vue/runtime-core": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@vue/reactivity": "3.2.37",
|
||||
"@vue/shared": "3.2.37"
|
||||
@ -24404,7 +24383,6 @@
|
||||
},
|
||||
"@vue/runtime-dom": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@vue/runtime-core": "3.2.37",
|
||||
"@vue/shared": "3.2.37",
|
||||
@ -24412,22 +24390,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"csstype": {
|
||||
"version": "2.6.20",
|
||||
"dev": true
|
||||
"version": "2.6.20"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@vue/server-renderer": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@vue/compiler-ssr": "3.2.37",
|
||||
"@vue/shared": "3.2.37"
|
||||
}
|
||||
},
|
||||
"@vue/shared": {
|
||||
"version": "3.2.37",
|
||||
"dev": true
|
||||
"version": "3.2.37"
|
||||
},
|
||||
"@webassemblyjs/ast": {
|
||||
"version": "1.11.1",
|
||||
@ -26790,8 +26765,7 @@
|
||||
"dev": true
|
||||
},
|
||||
"estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"dev": true
|
||||
"version": "2.0.2"
|
||||
},
|
||||
"esutils": {
|
||||
"version": "2.0.3",
|
||||
@ -29328,7 +29302,6 @@
|
||||
},
|
||||
"magic-string": {
|
||||
"version": "0.25.9",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"sourcemap-codec": "^1.4.8"
|
||||
}
|
||||
@ -29923,8 +29896,7 @@
|
||||
}
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.3.4",
|
||||
"dev": true
|
||||
"version": "3.3.4"
|
||||
},
|
||||
"natural-compare": {
|
||||
"version": "1.4.0",
|
||||
@ -31296,8 +31268,7 @@
|
||||
"dev": true
|
||||
},
|
||||
"picocolors": {
|
||||
"version": "1.0.0",
|
||||
"dev": true
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.1"
|
||||
@ -31358,7 +31329,6 @@
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.4.14",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"nanoid": "^3.3.4",
|
||||
"picocolors": "^1.0.0",
|
||||
@ -32470,12 +32440,10 @@
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"dev": true
|
||||
"version": "0.6.1"
|
||||
},
|
||||
"source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"dev": true
|
||||
"version": "1.0.2"
|
||||
},
|
||||
"source-map-resolve": {
|
||||
"version": "0.6.0",
|
||||
@ -32496,8 +32464,7 @@
|
||||
}
|
||||
},
|
||||
"sourcemap-codec": {
|
||||
"version": "1.4.8",
|
||||
"dev": true
|
||||
"version": "1.4.8"
|
||||
},
|
||||
"spdx-correct": {
|
||||
"version": "3.1.1",
|
||||
@ -33679,7 +33646,6 @@
|
||||
},
|
||||
"vue": {
|
||||
"version": "3.2.37",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@vue/compiler-dom": "3.2.37",
|
||||
"@vue/compiler-sfc": "3.2.37",
|
||||
|
@ -50,6 +50,8 @@
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-commonjs": "^21.0.1",
|
||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.2",
|
||||
"@typescript-eslint/parser": "^5.10.2",
|
||||
"babel-loader": "^8.2.3",
|
||||
|
@ -35,8 +35,8 @@
|
||||
"devDependencies": {
|
||||
"@tiptap/core": "^2.1.0-rc.11",
|
||||
"@tiptap/pm": "^2.1.0-rc.11",
|
||||
"@types/react": "^18.0.1",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { BubbleMenuPlugin, BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { useCurrentEditor } from './Context.js'
|
||||
|
||||
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
||||
|
||||
export type BubbleMenuProps = Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'> & {
|
||||
export type BubbleMenuProps = Omit<Optional<BubbleMenuPluginProps, 'pluginKey' | 'editor'>, 'element'> & {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
updateDelay?: number;
|
||||
@ -11,13 +13,14 @@ export type BubbleMenuProps = Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>,
|
||||
|
||||
export const BubbleMenu = (props: BubbleMenuProps) => {
|
||||
const [element, setElement] = useState<HTMLDivElement | null>(null)
|
||||
const { editor: currentEditor } = useCurrentEditor()
|
||||
|
||||
useEffect(() => {
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
|
||||
if (props.editor.isDestroyed) {
|
||||
if (props.editor?.isDestroyed || currentEditor?.isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -25,18 +28,25 @@ export const BubbleMenu = (props: BubbleMenuProps) => {
|
||||
pluginKey = 'bubbleMenu', editor, tippyOptions = {}, updateDelay, shouldShow = null,
|
||||
} = props
|
||||
|
||||
const menuEditor = editor || currentEditor
|
||||
|
||||
if (!menuEditor) {
|
||||
console.warn('BubbleMenu component is not rendered inside of an editor component or does not have editor prop.')
|
||||
return
|
||||
}
|
||||
|
||||
const plugin = BubbleMenuPlugin({
|
||||
updateDelay,
|
||||
editor,
|
||||
editor: menuEditor,
|
||||
element,
|
||||
pluginKey,
|
||||
shouldShow,
|
||||
tippyOptions,
|
||||
})
|
||||
|
||||
editor.registerPlugin(plugin)
|
||||
return () => editor.unregisterPlugin(pluginKey)
|
||||
}, [props.editor, element])
|
||||
menuEditor.registerPlugin(plugin)
|
||||
return () => menuEditor.unregisterPlugin(pluginKey)
|
||||
}, [props.editor, currentEditor, element])
|
||||
|
||||
return (
|
||||
<div ref={setElement} className={props.className} style={{ visibility: 'hidden' }}>
|
||||
|
47
packages/react/src/Context.tsx
Normal file
47
packages/react/src/Context.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { EditorOptions } from '@tiptap/core'
|
||||
import React, { createContext, ReactNode, useContext } from 'react'
|
||||
|
||||
import { Editor } from './Editor.js'
|
||||
import { EditorContent } from './EditorContent.js'
|
||||
import { useEditor } from './useEditor.js'
|
||||
|
||||
export type EditorContextValue = {
|
||||
editor: Editor | null;
|
||||
}
|
||||
|
||||
export const EditorContext = createContext<EditorContextValue>({
|
||||
editor: null,
|
||||
})
|
||||
|
||||
export const EditorConsumer = EditorContext.Consumer
|
||||
|
||||
export const useCurrentEditor = () => useContext(EditorContext)
|
||||
|
||||
export type EditorProviderProps = {
|
||||
children: ReactNode;
|
||||
slotBefore?: ReactNode;
|
||||
slotAfter?: ReactNode;
|
||||
} & Partial<EditorOptions>
|
||||
|
||||
export const EditorProvider = ({
|
||||
children, slotAfter, slotBefore, ...editorOptions
|
||||
}: EditorProviderProps) => {
|
||||
const editor = useEditor(editorOptions)
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<EditorContext.Provider value={{ editor }}>
|
||||
{slotBefore}
|
||||
<EditorConsumer>
|
||||
{({ editor: currentEditor }) => (
|
||||
<EditorContent editor={currentEditor} />
|
||||
)}
|
||||
</EditorConsumer>
|
||||
{children}
|
||||
{slotAfter}
|
||||
</EditorContext.Provider>
|
||||
)
|
||||
}
|
@ -3,22 +3,25 @@ import React, {
|
||||
useEffect, useState,
|
||||
} from 'react'
|
||||
|
||||
import { useCurrentEditor } from './Context.js'
|
||||
|
||||
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
|
||||
|
||||
export type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element'> & {
|
||||
export type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey' | 'editor'>, 'element'> & {
|
||||
className?: string,
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const FloatingMenu = (props: FloatingMenuProps) => {
|
||||
const [element, setElement] = useState<HTMLDivElement | null>(null)
|
||||
const { editor: currentEditor } = useCurrentEditor()
|
||||
|
||||
useEffect(() => {
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
|
||||
if (props.editor.isDestroyed) {
|
||||
if (props.editor?.isDestroyed || currentEditor?.isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -29,18 +32,26 @@ export const FloatingMenu = (props: FloatingMenuProps) => {
|
||||
shouldShow = null,
|
||||
} = props
|
||||
|
||||
const menuEditor = editor || currentEditor
|
||||
|
||||
if (!menuEditor) {
|
||||
console.warn('FloatingMenu component is not rendered inside of an editor component or does not have editor prop.')
|
||||
return
|
||||
}
|
||||
|
||||
const plugin = FloatingMenuPlugin({
|
||||
pluginKey,
|
||||
editor,
|
||||
editor: menuEditor,
|
||||
element,
|
||||
tippyOptions,
|
||||
shouldShow,
|
||||
})
|
||||
|
||||
editor.registerPlugin(plugin)
|
||||
return () => editor.unregisterPlugin(pluginKey)
|
||||
menuEditor.registerPlugin(plugin)
|
||||
return () => menuEditor.unregisterPlugin(pluginKey)
|
||||
}, [
|
||||
props.editor,
|
||||
currentEditor,
|
||||
element,
|
||||
])
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
export * from './BubbleMenu.js'
|
||||
export * from './Context.js'
|
||||
export { Editor } from './Editor.js'
|
||||
export * from './EditorContent.js'
|
||||
export * from './FloatingMenu.js'
|
||||
|
@ -30,8 +30,12 @@ module.exports = on => {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
loader: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
// tsconfig:
|
||||
configFile: path.resolve(__dirname, '..', 'tsconfig.json'),
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
|
@ -4,11 +4,18 @@
|
||||
"strict": false,
|
||||
"noEmit": false,
|
||||
"sourceMap": false,
|
||||
"types": ["cypress"],
|
||||
"types": ["cypress", "react", "react-dom"],
|
||||
"paths": {
|
||||
"@tiptap/*": ["packages/*/dist", "packages/*/src"],
|
||||
"@tiptap/pm/*": ["../../pm/*/dist"]
|
||||
}
|
||||
},
|
||||
"typeRoots": ["../../node_modules/@types"]
|
||||
},
|
||||
"include": ["./*/*.ts", "../../**/*.ts"]
|
||||
"include": ["./*/*.ts", "../../**/*.ts"],
|
||||
"exclude": [
|
||||
"../../packages/react",
|
||||
"../../packages/vue-2",
|
||||
"../../packages/vue-3",
|
||||
"../../packages/extension-code-block-lowlight"
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user