diff --git a/demos/src/Examples/CustomParagraph/React/Paragraph.jsx b/demos/src/Examples/CustomParagraph/React/Paragraph.jsx
new file mode 100644
index 00000000000..cec21f2fd6d
--- /dev/null
+++ b/demos/src/Examples/CustomParagraph/React/Paragraph.jsx
@@ -0,0 +1,25 @@
+import { Paragraph as BaseParagraph } from '@tiptap/extension-paragraph'
+import {
+ NodeViewContent,
+ NodeViewWrapper,
+ ReactNodeViewRenderer,
+} from '@tiptap/react'
+
+const ParagraphComponent = ({ node }) => {
+ return (
+
+
+ {node.textContent.length}
+
+
+
+ )
+}
+
+export const Paragraph = BaseParagraph.extend({
+ addNodeView() {
+ return ReactNodeViewRenderer(ParagraphComponent)
+ },
+})
diff --git a/demos/src/Examples/CustomParagraph/React/index.html b/demos/src/Examples/CustomParagraph/React/index.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/demos/src/Examples/CustomParagraph/React/index.jsx b/demos/src/Examples/CustomParagraph/React/index.jsx
new file mode 100644
index 00000000000..67f7df514e0
--- /dev/null
+++ b/demos/src/Examples/CustomParagraph/React/index.jsx
@@ -0,0 +1,27 @@
+import './styles.scss'
+
+import { EditorContent, useEditor } from '@tiptap/react'
+import StarterKit from '@tiptap/starter-kit'
+import React from 'react'
+
+import { Paragraph } from './Paragraph.jsx'
+
+export default () => {
+ const editor = useEditor({
+ extensions: [
+ StarterKit.configure({
+ paragraph: false,
+ }),
+ Paragraph,
+ ],
+ content: `
+
+ Each line shows the number of characters in the paragraph.
+
+ `,
+ })
+
+ return (
+
+ )
+}
diff --git a/demos/src/Examples/CustomParagraph/React/index.spec.js b/demos/src/Examples/CustomParagraph/React/index.spec.js
new file mode 100644
index 00000000000..1df3abf8d34
--- /dev/null
+++ b/demos/src/Examples/CustomParagraph/React/index.spec.js
@@ -0,0 +1,23 @@
+context('/src/Examples/CustomParagraph/React/', () => {
+ beforeEach(() => {
+ cy.visit('/src/Examples/CustomParagraph/React/')
+ })
+
+ it('should have a working tiptap instance', () => {
+ cy.get('.ProseMirror').then(([{ editor }]) => {
+ // eslint-disable-next-line
+ expect(editor).to.not.be.null
+ })
+ })
+
+ it('should have a paragraph and text length', () => {
+ cy.get('.ProseMirror p').should('exist').should('have.text', 'Each line shows the number of characters in the paragraph.')
+ cy.get('.ProseMirror .label').should('exist').should('have.text', '58')
+ })
+
+ it('should have new paragraph', () => {
+ cy.get('.ProseMirror').type('{selectall}{moveToEnd}{enter}')
+ cy.get('.ProseMirror p').eq(1).should('exist').should('have.text', '')
+ cy.get('.ProseMirror .label').eq(1).should('exist').should('have.text', '0')
+ })
+})
diff --git a/demos/src/Examples/CustomParagraph/React/styles.scss b/demos/src/Examples/CustomParagraph/React/styles.scss
new file mode 100644
index 00000000000..85f390477a5
--- /dev/null
+++ b/demos/src/Examples/CustomParagraph/React/styles.scss
@@ -0,0 +1,24 @@
+/* Basic editor styles */
+.ProseMirror {
+ > * + * {
+ margin-top: 0.75em;
+ }
+}
+
+/* Placeholder (at the top) */
+/*.ProseMirror p.is-editor-empty:first-child::before {
+ content: attr(data-placeholder);
+ float: left;
+ color: #ced4da;
+ pointer-events: none;
+ height: 0;
+}*/
+
+/* Placeholder (on every new line) */
+.ProseMirror .is-empty::before {
+ content: attr(data-placeholder);
+ float: left;
+ color: #ced4da;
+ pointer-events: none;
+ height: 0;
+}
diff --git a/demos/src/Examples/CustomParagraph/Vue/Component.vue b/demos/src/Examples/CustomParagraph/Vue/Component.vue
new file mode 100644
index 00000000000..3dbaeac920b
--- /dev/null
+++ b/demos/src/Examples/CustomParagraph/Vue/Component.vue
@@ -0,0 +1,32 @@
+
+
+ {{ node.textContent.length }}
+
+
+
+
+
+
+
diff --git a/demos/src/Examples/CustomParagraph/Vue/Extension.js b/demos/src/Examples/CustomParagraph/Vue/Extension.js
new file mode 100644
index 00000000000..cc53c39195f
--- /dev/null
+++ b/demos/src/Examples/CustomParagraph/Vue/Extension.js
@@ -0,0 +1,10 @@
+import { Paragraph as BaseParagraph } from '@tiptap/extension-paragraph'
+import { VueNodeViewRenderer } from '@tiptap/vue-3'
+
+import Component from './Component.vue'
+
+export default BaseParagraph.extend({
+ addNodeView() {
+ return VueNodeViewRenderer(Component)
+ },
+})
diff --git a/demos/src/Examples/CustomParagraph/Vue/index.html b/demos/src/Examples/CustomParagraph/Vue/index.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/demos/src/Examples/CustomParagraph/Vue/index.spec.js b/demos/src/Examples/CustomParagraph/Vue/index.spec.js
new file mode 100644
index 00000000000..1df3abf8d34
--- /dev/null
+++ b/demos/src/Examples/CustomParagraph/Vue/index.spec.js
@@ -0,0 +1,23 @@
+context('/src/Examples/CustomParagraph/React/', () => {
+ beforeEach(() => {
+ cy.visit('/src/Examples/CustomParagraph/React/')
+ })
+
+ it('should have a working tiptap instance', () => {
+ cy.get('.ProseMirror').then(([{ editor }]) => {
+ // eslint-disable-next-line
+ expect(editor).to.not.be.null
+ })
+ })
+
+ it('should have a paragraph and text length', () => {
+ cy.get('.ProseMirror p').should('exist').should('have.text', 'Each line shows the number of characters in the paragraph.')
+ cy.get('.ProseMirror .label').should('exist').should('have.text', '58')
+ })
+
+ it('should have new paragraph', () => {
+ cy.get('.ProseMirror').type('{selectall}{moveToEnd}{enter}')
+ cy.get('.ProseMirror p').eq(1).should('exist').should('have.text', '')
+ cy.get('.ProseMirror .label').eq(1).should('exist').should('have.text', '0')
+ })
+})
diff --git a/demos/src/Examples/CustomParagraph/Vue/index.vue b/demos/src/Examples/CustomParagraph/Vue/index.vue
new file mode 100644
index 00000000000..6dfb471955e
--- /dev/null
+++ b/demos/src/Examples/CustomParagraph/Vue/index.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+n
diff --git a/packages/react/src/EditorContent.tsx b/packages/react/src/EditorContent.tsx
index 9f159c51909..0a7ea773238 100644
--- a/packages/react/src/EditorContent.tsx
+++ b/packages/react/src/EditorContent.tsx
@@ -75,9 +75,7 @@ export class PureEditorContent extends React.Component {
- flushSync(fn)
- })
+ flushSync(fn)
} else {
fn()
}