From 84ebd511d29db566e66e8f9b617179cf19ed2531 Mon Sep 17 00:00:00 2001 From: Nick Perez Date: Thu, 1 Aug 2024 09:02:34 +0200 Subject: [PATCH 01/13] fix(core): resolve `isNodeEmpty` criteria #5415 (#5419) --- .changeset/cuddly-pants-destroy.md | 5 ++++ packages/core/src/helpers/isNodeEmpty.ts | 8 ++--- .../integration/core/isNodeEmpty.spec.ts | 29 +++++++++++++++++-- 3 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 .changeset/cuddly-pants-destroy.md diff --git a/.changeset/cuddly-pants-destroy.md b/.changeset/cuddly-pants-destroy.md new file mode 100644 index 000000000..f3602beb1 --- /dev/null +++ b/.changeset/cuddly-pants-destroy.md @@ -0,0 +1,5 @@ +--- +"@tiptap/core": patch +--- + +Fix change criteria for isNodeEmpty to resolve #5415 diff --git a/packages/core/src/helpers/isNodeEmpty.ts b/packages/core/src/helpers/isNodeEmpty.ts index 5d7a777d4..90d94f6ee 100644 --- a/packages/core/src/helpers/isNodeEmpty.ts +++ b/packages/core/src/helpers/isNodeEmpty.ts @@ -12,12 +12,12 @@ export function isNodeEmpty( return !node.text } - if (node.content.childCount === 0) { - return true + if (node.isAtom || node.isLeaf) { + return false } - if (node.isLeaf) { - return false + if (node.content.childCount === 0) { + return true } if (checkChildren) { diff --git a/tests/cypress/integration/core/isNodeEmpty.spec.ts b/tests/cypress/integration/core/isNodeEmpty.spec.ts index 7db88996d..7e1a8e6c8 100644 --- a/tests/cypress/integration/core/isNodeEmpty.spec.ts +++ b/tests/cypress/integration/core/isNodeEmpty.spec.ts @@ -3,9 +3,10 @@ import { getSchema, isNodeEmpty } from '@tiptap/core' import Document from '@tiptap/extension-document' import Image from '@tiptap/extension-image' +import Mention from '@tiptap/extension-mention' import StarterKit from '@tiptap/starter-kit' -const schema = getSchema([StarterKit]) +const schema = getSchema([StarterKit, Mention]) const modifiedSchema = getSchema([StarterKit.configure({ document: false }), Document.extend({ content: 'heading block*' })]) const imageSchema = getSchema([StarterKit.configure({ document: false }), Document.extend({ content: 'image block*' }), Image]) @@ -26,6 +27,30 @@ describe('isNodeEmpty', () => { expect(isNodeEmpty(node)).to.eq(false) }) + it('should return false when a paragraph has hardbreaks', () => { + const node = schema.nodeFromJSON({ + type: 'paragraph', + content: [{ type: 'hardBreak' }], + }) + + expect(isNodeEmpty(node)).to.eq(false) + }) + + it('should return false when a paragraph has a mention', () => { + const node = schema.nodeFromJSON({ + type: 'paragraph', + content: [{ + type: 'mention', + attrs: { + id: 'Winona Ryder', + label: null, + }, + }], + }) + + expect(isNodeEmpty(node)).to.eq(false) + }) + it('should return true when a paragraph has no content', () => { const node = schema.nodeFromJSON({ type: 'paragraph', @@ -177,7 +202,7 @@ describe('isNodeEmpty', () => { ], }) - expect(isNodeEmpty(node)).to.eq(true) + expect(isNodeEmpty(node)).to.eq(false) }) }) }) From bde3328ba244507e83a98bd5fde162ac464c2451 Mon Sep 17 00:00:00 2001 From: svenadlung Date: Thu, 1 Aug 2024 12:51:45 +0200 Subject: [PATCH 02/13] docs: update demo styles --- demos/setup/style.scss | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/demos/setup/style.scss b/demos/setup/style.scss index 2628b1418..0056cedf5 100644 --- a/demos/setup/style.scss +++ b/demos/setup/style.scss @@ -205,8 +205,7 @@ form { font-size: 0.75rem; gap: 0.25rem; line-height: 1.15; - min-height: 1.75rem; - padding: 0.25rem 0.5rem; + padding: 0.3rem 0.5rem; &.purple-spinner, &.error { @@ -215,6 +214,17 @@ form { width: 100%; } + .badge { + background-color: var(--gray-1); + border: 1px solid var(--gray-3); + border-radius: 2rem; + color: var(--gray-5); + font-size: 0.625rem; + font-weight: 700; + line-height: 1; + padding: 0.25rem 0.5rem; + } + &.purple-spinner { background-color: var(--purple-light); From 7c8889a2a61c7bc05bac7f0d54ecac1d40e87d94 Mon Sep 17 00:00:00 2001 From: Nick Perez Date: Mon, 5 Aug 2024 17:46:19 +0200 Subject: [PATCH 03/13] fix(react): optimize `useEditor` and `useEditorState` to reduce number of instances created while being performant #5432 (#5445) --- .changeset/smooth-rice-obey.md | 12 + .eslintrc.js | 9 + demos/src/Commands/Cut/React/index.jsx | 8 +- .../src/Examples/Performance/React/index.jsx | 8 +- .../src/GuideContent/ReadOnly/React/index.jsx | 1 + package-lock.json | 479 +++++++++--------- package.json | 1 + packages/react/src/useEditor.ts | 344 +++++++++---- packages/react/src/useEditorState.ts | 131 ++--- packages/vue-3/src/Editor.ts | 1 + 10 files changed, 601 insertions(+), 393 deletions(-) create mode 100644 .changeset/smooth-rice-obey.md diff --git a/.changeset/smooth-rice-obey.md b/.changeset/smooth-rice-obey.md new file mode 100644 index 000000000..8a73ee788 --- /dev/null +++ b/.changeset/smooth-rice-obey.md @@ -0,0 +1,12 @@ +--- +"@tiptap/react": patch +--- + +Optimize `useEditor` and `useEditorState` to reduce number of instances created while still being performant #5432 + +The core of this change is two-fold: + - have the effect run on every render (i.e. without a dep array) + - schedule destruction of instances, but bail on the actual destruction if the instance was still mounted and a new instance had not been created yet + +It should plug a memory leak, where editor instances could be created but not cleaned up in strict mode. +As well as fixing a bug where a re-render, with deps, was not applying new options that were set on `useEditor`. diff --git a/.eslintrc.js b/.eslintrc.js index ebea8e454..18cd55d3a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,6 +8,15 @@ module.exports = { node: true, }, overrides: [ + { + files: [ + './**/*.ts', + './**/*.tsx', + './**/*.js', + './**/*.jsx', + ], + extends: ['plugin:react-hooks/recommended'], + }, { files: [ './**/*.ts', diff --git a/demos/src/Commands/Cut/React/index.jsx b/demos/src/Commands/Cut/React/index.jsx index 66a871ec2..02bea76cd 100644 --- a/demos/src/Commands/Cut/React/index.jsx +++ b/demos/src/Commands/Cut/React/index.jsx @@ -8,10 +8,6 @@ import StarterKit from '@tiptap/starter-kit' import React, { useCallback } from 'react' const MenuBar = ({ editor }) => { - if (!editor) { - return null - } - const onCutToStart = useCallback(() => { editor.chain().cut({ from: editor.state.selection.$from.pos, to: editor.state.selection.$to.pos }, 1).run() }, [editor]) @@ -20,6 +16,10 @@ const MenuBar = ({ editor }) => { editor.chain().cut({ from: editor.state.selection.$from.pos, to: editor.state.selection.$to.pos }, editor.state.doc.nodeSize - 2).run() }, [editor]) + if (!editor) { + return null + } + return (
diff --git a/demos/src/Examples/Performance/React/index.jsx b/demos/src/Examples/Performance/React/index.jsx index 4dce96ff9..827ec6991 100644 --- a/demos/src/Examples/Performance/React/index.jsx +++ b/demos/src/Examples/Performance/React/index.jsx @@ -62,7 +62,7 @@ function EditorInstance({ shouldOptimizeRendering }) { }) return ( - <> +
Number of renders: {countRenderRef.current}
@@ -89,12 +89,13 @@ function EditorInstance({ shouldOptimizeRendering }) { )} - +
) } const EditorControls = () => { const [shouldOptimizeRendering, setShouldOptimizeRendering] = React.useState(true) + const [rendered, setRendered] = React.useState(true) return ( <> @@ -123,8 +124,9 @@ const EditorControls = () => { Render every transaction (default behavior)
+
- + {rendered && } ) } diff --git a/demos/src/GuideContent/ReadOnly/React/index.jsx b/demos/src/GuideContent/ReadOnly/React/index.jsx index 657c33382..095d3540c 100644 --- a/demos/src/GuideContent/ReadOnly/React/index.jsx +++ b/demos/src/GuideContent/ReadOnly/React/index.jsx @@ -7,6 +7,7 @@ import React, { useEffect, useState } from 'react' export default () => { const [editable, setEditable] = useState(false) const editor = useEditor({ + shouldRerenderOnTransaction: false, editable, content: `

diff --git a/package-lock.json b/package-lock.json index 04c67060c..795aab055 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "eslint-plugin-cypress": "^2.15.2", "eslint-plugin-html": "^6.2.0", "eslint-plugin-import": "^2.29.1", + "eslint-plugin-react-hooks": "4.6.2", "eslint-plugin-simple-import-sort": "^7.0.0", "eslint-plugin-vue": "^9.27.0", "husky": "^8.0.3", @@ -8942,6 +8943,18 @@ "node": ">=0.10.0" } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, "node_modules/eslint-plugin-simple-import-sort": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-7.0.0.tgz", @@ -17902,159 +17915,159 @@ }, "packages/core": { "name": "@tiptap/core", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/pm": "^2.5.7" + "@tiptap/pm": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/pm": "^2.5.7" + "@tiptap/pm": "^2.5.8" } }, "packages/extension-blockquote": { "name": "@tiptap/extension-blockquote", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-bold": { "name": "@tiptap/extension-bold", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-bubble-menu": { "name": "@tiptap/extension-bubble-menu", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "dependencies": { "tippy.js": "^6.3.7" }, "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" } }, "packages/extension-bullet-list": { "name": "@tiptap/extension-bullet-list", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-character-count": { "name": "@tiptap/extension-character-count", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" } }, "packages/extension-code": { "name": "@tiptap/extension-code", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-code-block": { "name": "@tiptap/extension-code-block", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" } }, "packages/extension-code-block-lowlight": { "name": "@tiptap/extension-code-block-lowlight", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/extension-code-block": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/extension-code-block": "^2.5.8", + "@tiptap/pm": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/extension-code-block": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/extension-code-block": "^2.5.8", + "@tiptap/pm": "^2.5.8" } }, "packages/extension-collaboration": { "name": "@tiptap/extension-collaboration", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7", + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8", "y-prosemirror": "^1.2.9" }, "funding": { @@ -18062,17 +18075,17 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7", + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8", "y-prosemirror": "^1.2.6" } }, "packages/extension-collaboration-cursor": { "name": "@tiptap/extension-collaboration-cursor", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7", + "@tiptap/core": "^2.5.8", "y-prosemirror": "^1.2.9" }, "funding": { @@ -18080,607 +18093,607 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", + "@tiptap/core": "^2.5.8", "y-prosemirror": "^1.2.6" } }, "packages/extension-color": { "name": "@tiptap/extension-color", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/extension-text-style": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/extension-text-style": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/extension-text-style": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/extension-text-style": "^2.5.8" } }, "packages/extension-document": { "name": "@tiptap/extension-document", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-dropcursor": { "name": "@tiptap/extension-dropcursor", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" } }, "packages/extension-floating-menu": { "name": "@tiptap/extension-floating-menu", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "dependencies": { "tippy.js": "^6.3.7" }, "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" } }, "packages/extension-focus": { "name": "@tiptap/extension-focus", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" } }, "packages/extension-font-family": { "name": "@tiptap/extension-font-family", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/extension-text-style": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/extension-text-style": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/extension-text-style": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/extension-text-style": "^2.5.8" } }, "packages/extension-gapcursor": { "name": "@tiptap/extension-gapcursor", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" } }, "packages/extension-hard-break": { "name": "@tiptap/extension-hard-break", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-heading": { "name": "@tiptap/extension-heading", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-highlight": { "name": "@tiptap/extension-highlight", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-history": { "name": "@tiptap/extension-history", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" } }, "packages/extension-horizontal-rule": { "name": "@tiptap/extension-horizontal-rule", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" } }, "packages/extension-image": { "name": "@tiptap/extension-image", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-italic": { "name": "@tiptap/extension-italic", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-link": { "name": "@tiptap/extension-link", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "dependencies": { "linkifyjs": "^4.1.0" }, "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" } }, "packages/extension-list-item": { "name": "@tiptap/extension-list-item", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-list-keymap": { "name": "@tiptap/extension-list-keymap", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-mention": { "name": "@tiptap/extension-mention", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7", - "@tiptap/suggestion": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8", + "@tiptap/suggestion": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7", - "@tiptap/suggestion": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8", + "@tiptap/suggestion": "^2.5.8" } }, "packages/extension-ordered-list": { "name": "@tiptap/extension-ordered-list", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-paragraph": { "name": "@tiptap/extension-paragraph", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-placeholder": { "name": "@tiptap/extension-placeholder", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" } }, "packages/extension-strike": { "name": "@tiptap/extension-strike", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-subscript": { "name": "@tiptap/extension-subscript", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-superscript": { "name": "@tiptap/extension-superscript", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-table": { "name": "@tiptap/extension-table", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" } }, "packages/extension-table-cell": { "name": "@tiptap/extension-table-cell", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-table-header": { "name": "@tiptap/extension-table-header", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-table-row": { "name": "@tiptap/extension-table-row", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-task-item": { "name": "@tiptap/extension-task-item", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" } }, "packages/extension-task-list": { "name": "@tiptap/extension-task-list", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-text": { "name": "@tiptap/extension-text", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-text-align": { "name": "@tiptap/extension-text-align", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-text-style": { "name": "@tiptap/extension-text-style", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-typography": { "name": "@tiptap/extension-typography", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-underline": { "name": "@tiptap/extension-underline", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/extension-youtube": { "name": "@tiptap/extension-youtube", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7" + "@tiptap/core": "^2.5.8" } }, "packages/html": { "name": "@tiptap/html", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "dependencies": { "zeed-dom": "^0.10.9" }, "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" } }, "packages/pm": { "name": "@tiptap/pm", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "dependencies": { "prosemirror-changeset": "^2.2.1", @@ -18709,17 +18722,17 @@ }, "packages/react": { "name": "@tiptap/react", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "dependencies": { - "@tiptap/extension-bubble-menu": "^2.5.7", - "@tiptap/extension-floating-menu": "^2.5.7", + "@tiptap/extension-bubble-menu": "^2.5.8", + "@tiptap/extension-floating-menu": "^2.5.8", "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.2.2" }, "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7", + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "react": "^18.0.0", @@ -18730,36 +18743,36 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7", + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8", "react": "^17.0.0 || ^18.0.0", "react-dom": "^17.0.0 || ^18.0.0" } }, "packages/starter-kit": { "name": "@tiptap/starter-kit", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "dependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/extension-blockquote": "^2.5.7", - "@tiptap/extension-bold": "^2.5.7", - "@tiptap/extension-bullet-list": "^2.5.7", - "@tiptap/extension-code": "^2.5.7", - "@tiptap/extension-code-block": "^2.5.7", - "@tiptap/extension-document": "^2.5.7", - "@tiptap/extension-dropcursor": "^2.5.7", - "@tiptap/extension-gapcursor": "^2.5.7", - "@tiptap/extension-hard-break": "^2.5.7", - "@tiptap/extension-heading": "^2.5.7", - "@tiptap/extension-history": "^2.5.7", - "@tiptap/extension-horizontal-rule": "^2.5.7", - "@tiptap/extension-italic": "^2.5.7", - "@tiptap/extension-list-item": "^2.5.7", - "@tiptap/extension-ordered-list": "^2.5.7", - "@tiptap/extension-paragraph": "^2.5.7", - "@tiptap/extension-strike": "^2.5.7", - "@tiptap/extension-text": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/extension-blockquote": "^2.5.8", + "@tiptap/extension-bold": "^2.5.8", + "@tiptap/extension-bullet-list": "^2.5.8", + "@tiptap/extension-code": "^2.5.8", + "@tiptap/extension-code-block": "^2.5.8", + "@tiptap/extension-document": "^2.5.8", + "@tiptap/extension-dropcursor": "^2.5.8", + "@tiptap/extension-gapcursor": "^2.5.8", + "@tiptap/extension-hard-break": "^2.5.8", + "@tiptap/extension-heading": "^2.5.8", + "@tiptap/extension-history": "^2.5.8", + "@tiptap/extension-horizontal-rule": "^2.5.8", + "@tiptap/extension-italic": "^2.5.8", + "@tiptap/extension-list-item": "^2.5.8", + "@tiptap/extension-ordered-list": "^2.5.8", + "@tiptap/extension-paragraph": "^2.5.8", + "@tiptap/extension-strike": "^2.5.8", + "@tiptap/extension-text": "^2.5.8" }, "funding": { "type": "github", @@ -18768,33 +18781,33 @@ }, "packages/suggestion": { "name": "@tiptap/suggestion", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7" + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8" } }, "packages/vue-2": { "name": "@tiptap/vue-2", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "dependencies": { - "@tiptap/extension-bubble-menu": "^2.5.7", - "@tiptap/extension-floating-menu": "^2.5.7", + "@tiptap/extension-bubble-menu": "^2.5.8", + "@tiptap/extension-floating-menu": "^2.5.8", "vue-ts-types": "^1.6.0" }, "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7", + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8", "vue": "^2.6.0" }, "funding": { @@ -18802,8 +18815,8 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7", + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8", "vue": "^2.6.0" } }, @@ -18834,15 +18847,15 @@ }, "packages/vue-3": { "name": "@tiptap/vue-3", - "version": "2.5.7", + "version": "2.5.8", "license": "MIT", "dependencies": { - "@tiptap/extension-bubble-menu": "^2.5.7", - "@tiptap/extension-floating-menu": "^2.5.7" + "@tiptap/extension-bubble-menu": "^2.5.8", + "@tiptap/extension-floating-menu": "^2.5.8" }, "devDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7", + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8", "vue": "^3.0.0" }, "funding": { @@ -18850,8 +18863,8 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.7", - "@tiptap/pm": "^2.5.7", + "@tiptap/core": "^2.5.8", + "@tiptap/pm": "^2.5.8", "vue": "^3.0.0" } }, diff --git a/package.json b/package.json index cf21df115..84132d251 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "eslint-plugin-cypress": "^2.15.2", "eslint-plugin-html": "^6.2.0", "eslint-plugin-import": "^2.29.1", + "eslint-plugin-react-hooks": "4.6.2", "eslint-plugin-simple-import-sort": "^7.0.0", "eslint-plugin-vue": "^9.27.0", "husky": "^8.0.3", diff --git a/packages/react/src/useEditor.ts b/packages/react/src/useEditor.ts index 2eca1f2f0..2fde5b2ce 100644 --- a/packages/react/src/useEditor.ts +++ b/packages/react/src/useEditor.ts @@ -1,8 +1,13 @@ import { EditorOptions } from '@tiptap/core' import { - DependencyList, MutableRefObject, - useDebugValue, useEffect, useRef, useState, + DependencyList, + MutableRefObject, + useDebugValue, + useEffect, + useRef, + useState, } from 'react' +import { useSyncExternalStore } from 'use-sync-external-store/shim' import { Editor } from './Editor.js' import { useEditorState } from './useEditorState.js' @@ -31,22 +36,241 @@ export type UseEditorOptions = Partial & { }; /** - * Create a new editor instance. And attach event listeners. + * This class handles the creation, destruction, and re-creation of the editor instance. */ -function createEditor(options: MutableRefObject): Editor { - const editor = new Editor(options.current) +class EditorInstanceManager { + /** + * The current editor instance. + */ + private editor: Editor | null = null - editor.on('beforeCreate', (...args) => options.current.onBeforeCreate?.(...args)) - editor.on('blur', (...args) => options.current.onBlur?.(...args)) - editor.on('create', (...args) => options.current.onCreate?.(...args)) - editor.on('destroy', (...args) => options.current.onDestroy?.(...args)) - editor.on('focus', (...args) => options.current.onFocus?.(...args)) - editor.on('selectionUpdate', (...args) => options.current.onSelectionUpdate?.(...args)) - editor.on('transaction', (...args) => options.current.onTransaction?.(...args)) - editor.on('update', (...args) => options.current.onUpdate?.(...args)) - editor.on('contentError', (...args) => options.current.onContentError?.(...args)) + /** + * The most recent options to apply to the editor. + */ + private options: MutableRefObject - return editor + /** + * The subscriptions to notify when the editor instance + * has been created or destroyed. + */ + private subscriptions = new Set<() => void>() + + /** + * A timeout to destroy the editor if it was not mounted within a time frame. + */ + private scheduledDestructionTimeout: ReturnType | undefined + + /** + * Whether the editor has been mounted. + */ + private isComponentMounted = false + + /** + * The most recent dependencies array. + */ + private previousDeps: DependencyList | null = null + + /** + * The unique instance ID. This is used to identify the editor instance. And will be re-generated for each new instance. + */ + public instanceId = '' + + constructor(options: MutableRefObject) { + this.options = options + this.subscriptions = new Set<() => void>() + this.setEditor(this.getInitialEditor()) + + this.getEditor = this.getEditor.bind(this) + this.getServerSnapshot = this.getServerSnapshot.bind(this) + this.subscribe = this.subscribe.bind(this) + this.refreshEditorInstance = this.refreshEditorInstance.bind(this) + this.scheduleDestroy = this.scheduleDestroy.bind(this) + this.onRender = this.onRender.bind(this) + this.createEditor = this.createEditor.bind(this) + } + + private setEditor(editor: Editor | null) { + this.editor = editor + this.instanceId = Math.random().toString(36).slice(2, 9) + + // Notify all subscribers that the editor instance has been created + this.subscriptions.forEach(cb => cb()) + } + + private getInitialEditor() { + if (this.options.current.immediatelyRender === undefined) { + if (isSSR || isNext) { + // TODO in the next major release, we should throw an error here + if (isDev) { + /** + * Throw an error in development, to make sure the developer is aware that tiptap cannot be SSR'd + * and that they need to set `immediatelyRender` to `false` to avoid hydration mismatches. + */ + console.warn( + 'Tiptap Error: SSR has been detected, please set `immediatelyRender` explicitly to `false` to avoid hydration mismatches.', + ) + } + + // Best faith effort in production, run the code in the legacy mode to avoid hydration mismatches and errors in production + return null + } + + // Default to immediately rendering when client-side rendering + return this.createEditor() + } + + if (this.options.current.immediatelyRender && isSSR && isDev) { + // Warn in development, to make sure the developer is aware that tiptap cannot be SSR'd, set `immediatelyRender` to `false` to avoid hydration mismatches. + throw new Error( + 'Tiptap Error: SSR has been detected, and `immediatelyRender` has been set to `true` this is an unsupported configuration that may result in errors, explicitly set `immediatelyRender` to `false` to avoid hydration mismatches.', + ) + } + + if (this.options.current.immediatelyRender) { + return this.createEditor() + } + + return null + } + + /** + * Create a new editor instance. And attach event listeners. + */ + private createEditor(): Editor { + const editor = new Editor(this.options.current) + + // Always call the most recent version of the callback function by default + editor.on('beforeCreate', (...args) => this.options.current.onBeforeCreate?.(...args)) + editor.on('blur', (...args) => this.options.current.onBlur?.(...args)) + editor.on('create', (...args) => this.options.current.onCreate?.(...args)) + editor.on('destroy', (...args) => this.options.current.onDestroy?.(...args)) + editor.on('focus', (...args) => this.options.current.onFocus?.(...args)) + editor.on('selectionUpdate', (...args) => this.options.current.onSelectionUpdate?.(...args)) + editor.on('transaction', (...args) => this.options.current.onTransaction?.(...args)) + editor.on('update', (...args) => this.options.current.onUpdate?.(...args)) + editor.on('contentError', (...args) => this.options.current.onContentError?.(...args)) + + // no need to keep track of the event listeners, they will be removed when the editor is destroyed + + return editor + } + + /** + * Get the current editor instance. + */ + getEditor(): Editor | null { + return this.editor + } + + /** + * Always disable the editor on the server-side. + */ + getServerSnapshot(): null { + return null + } + + /** + * Subscribe to the editor instance's changes. + */ + subscribe(onStoreChange: () => void) { + this.subscriptions.add(onStoreChange) + + return () => { + this.subscriptions.delete(onStoreChange) + } + } + + /** + * On each render, we will create, update, or destroy the editor instance. + * @param deps The dependencies to watch for changes + * @returns A cleanup function + */ + onRender(deps: DependencyList) { + // The returned callback will run on each render + return () => { + this.isComponentMounted = true + // Cleanup any scheduled destructions, since we are currently rendering + clearTimeout(this.scheduledDestructionTimeout) + + if (this.editor && !this.editor.isDestroyed && deps.length === 0) { + // if the editor does exist & deps are empty, we don't need to re-initialize the editor + // we can fast-path to update the editor options on the existing instance + this.editor.setOptions(this.options.current) + } else { + // When the editor: + // - does not yet exist + // - is destroyed + // - the deps array changes + // We need to destroy the editor instance and re-initialize it + this.refreshEditorInstance(deps) + } + + return () => { + this.isComponentMounted = false + this.scheduleDestroy() + } + } + } + + /** + * Recreate the editor instance if the dependencies have changed. + */ + private refreshEditorInstance(deps: DependencyList) { + + if (this.editor && !this.editor.isDestroyed) { + // Editor instance already exists + if (this.previousDeps === null) { + // If lastDeps has not yet been initialized, reuse the current editor instance + this.previousDeps = deps + return + } + const depsAreEqual = this.previousDeps.length === deps.length + && this.previousDeps.every((dep, index) => dep === deps[index]) + + if (depsAreEqual) { + // deps exist and are equal, no need to recreate + return + } + } + + if (this.editor && !this.editor.isDestroyed) { + // Destroy the editor instance if it exists + this.editor.destroy() + } + + this.setEditor(this.createEditor()) + + // Update the lastDeps to the current deps + this.previousDeps = deps + } + + /** + * Schedule the destruction of the editor instance. + * This will only destroy the editor if it was not mounted on the next tick. + * This is to avoid destroying the editor instance when it's actually still mounted. + */ + private scheduleDestroy() { + const currentInstanceId = this.instanceId + const currentEditor = this.editor + + // Wait a tick to see if the component is still mounted + this.scheduledDestructionTimeout = setTimeout(() => { + if (this.isComponentMounted && this.instanceId === currentInstanceId) { + // If still mounted on the next tick, with the same instanceId, do not destroy the editor + if (currentEditor) { + // just re-apply options as they might have changed + currentEditor.setOptions(this.options.current) + } + return + } + if (currentEditor && !currentEditor.isDestroyed) { + currentEditor.destroy() + if (this.instanceId === currentInstanceId) { + this.setEditor(null) + } + } + }, 0) + } } /** @@ -68,99 +292,29 @@ export function useEditor( * @returns The editor instance * @example const editor = useEditor({ extensions: [...] }) */ -export function useEditor( - options?: UseEditorOptions, - deps?: DependencyList -): Editor | null; +export function useEditor(options?: UseEditorOptions, deps?: DependencyList): Editor | null; export function useEditor( options: UseEditorOptions = {}, deps: DependencyList = [], ): Editor | null { const mostRecentOptions = useRef(options) - const [editor, setEditor] = useState(() => { - if (options.immediatelyRender === undefined) { - if (isSSR || isNext) { - // TODO in the next major release, we should throw an error here - if (isDev) { - /** - * Throw an error in development, to make sure the developer is aware that tiptap cannot be SSR'd - * and that they need to set `immediatelyRender` to `false` to avoid hydration mismatches. - */ - console.warn( - 'Tiptap Error: SSR has been detected, please set `immediatelyRender` explicitly to `false` to avoid hydration mismatches.', - ) - } - // Best faith effort in production, run the code in the legacy mode to avoid hydration mismatches and errors in production - return null - } + mostRecentOptions.current = options - // Default to immediately rendering when client-side rendering - return createEditor(mostRecentOptions) - } + const [instanceManager] = useState(() => new EditorInstanceManager(mostRecentOptions)) - if (options.immediatelyRender && isSSR && isDev) { - // Warn in development, to make sure the developer is aware that tiptap cannot be SSR'd, set `immediatelyRender` to `false` to avoid hydration mismatches. - throw new Error( - 'Tiptap Error: SSR has been detected, and `immediatelyRender` has been set to `true` this is an unsupported configuration that may result in errors, explicitly set `immediatelyRender` to `false` to avoid hydration mismatches.', - ) - } - - if (options.immediatelyRender) { - return createEditor(mostRecentOptions) - } - - return null - }) - const mostRecentEditor = useRef(editor) - - mostRecentEditor.current = editor + const editor = useSyncExternalStore( + instanceManager.subscribe, + instanceManager.getEditor, + instanceManager.getServerSnapshot, + ) useDebugValue(editor) // This effect will handle creating/updating the editor instance - useEffect(() => { - const destroyUnusedEditor = (editorInstance: Editor | null) => { - if (editorInstance) { - // We need to destroy the editor asynchronously to avoid memory leaks - // because the editor instance is still being used in the component. - - setTimeout(() => { - // re-use the editor instance if it hasn't been replaced yet - // otherwise, asynchronously destroy the old editor instance - if (editorInstance !== mostRecentEditor.current && !editorInstance.isDestroyed) { - editorInstance.destroy() - } - }) - } - } - - let editorInstance = mostRecentEditor.current - - if (!editorInstance) { - editorInstance = createEditor(mostRecentOptions) - setEditor(editorInstance) - return () => destroyUnusedEditor(editorInstance) - } - - if (!Array.isArray(deps) || deps.length === 0) { - // if the editor does exist & deps are empty, we don't need to re-initialize the editor - // we can fast-path to update the editor options on the existing instance - editorInstance.setOptions(options) - - return () => destroyUnusedEditor(editorInstance) - } - - // We need to destroy the editor instance and re-initialize it - // when the deps array changes - editorInstance.destroy() - - // the deps array is used to re-initialize the editor instance - editorInstance = createEditor(mostRecentOptions) - setEditor(editorInstance) - return () => destroyUnusedEditor(editorInstance) - }, deps) + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(instanceManager.onRender(deps)) // The default behavior is to re-render on each transaction // This is legacy behavior that will be removed in future versions diff --git a/packages/react/src/useEditorState.ts b/packages/react/src/useEditorState.ts index 0c8f02517..079605a80 100644 --- a/packages/react/src/useEditorState.ts +++ b/packages/react/src/useEditorState.ts @@ -30,68 +30,83 @@ export type UseEditorStateOptions< * To synchronize the editor instance with the component state, * we need to create a separate instance that is not affected by the component re-renders. */ -function makeEditorStateInstance(initialEditor: TEditor) { - let transactionNumber = 0 - let lastTransactionNumber = 0 - let lastSnapshot: EditorStateSnapshot = { editor: initialEditor, transactionNumber: 0 } - let editor = initialEditor - const subscribers = new Set<() => void>() +class EditorStateManager { + private transactionNumber = 0 - const editorInstance = { - /** - * Get the current editor instance. - */ - getSnapshot(): EditorStateSnapshot { - if (transactionNumber === lastTransactionNumber) { - return lastSnapshot - } - lastTransactionNumber = transactionNumber - lastSnapshot = { editor, transactionNumber } - return lastSnapshot - }, - /** - * Always disable the editor on the server-side. - */ - getServerSnapshot(): EditorStateSnapshot { - return { editor: null, transactionNumber: 0 } - }, - /** - * Subscribe to the editor instance's changes. - */ - subscribe(callback: () => void) { - subscribers.add(callback) - return () => { - subscribers.delete(callback) - } - }, - /** - * Watch the editor instance for changes. - */ - watch(nextEditor: Editor | null) { - editor = nextEditor as TEditor + private lastTransactionNumber = 0 - if (editor) { - /** - * This will force a re-render when the editor state changes. - * This is to support things like `editor.can().toggleBold()` in components that `useEditor`. - * This could be more efficient, but it's a good trade-off for now. - */ - const fn = () => { - transactionNumber += 1 - subscribers.forEach(callback => callback()) - } + private lastSnapshot: EditorStateSnapshot - const currentEditor = editor + private editor: TEditor - currentEditor.on('transaction', fn) - return () => { - currentEditor.off('transaction', fn) - } - } - }, + private subscribers = new Set<() => void>() + + constructor(initialEditor: TEditor) { + this.editor = initialEditor + this.lastSnapshot = { editor: initialEditor, transactionNumber: 0 } + + this.getSnapshot = this.getSnapshot.bind(this) + this.getServerSnapshot = this.getServerSnapshot.bind(this) + this.watch = this.watch.bind(this) + this.subscribe = this.subscribe.bind(this) } - return editorInstance + /** + * Get the current editor instance. + */ + getSnapshot(): EditorStateSnapshot { + if (this.transactionNumber === this.lastTransactionNumber) { + return this.lastSnapshot + } + this.lastTransactionNumber = this.transactionNumber + this.lastSnapshot = { editor: this.editor, transactionNumber: this.transactionNumber } + return this.lastSnapshot + } + + /** + * Always disable the editor on the server-side. + */ + getServerSnapshot(): EditorStateSnapshot { + return { editor: null, transactionNumber: 0 } + } + + /** + * Subscribe to the editor instance's changes. + */ + subscribe(callback: () => void): () => void { + this.subscribers.add(callback) + return () => { + this.subscribers.delete(callback) + } + } + + /** + * Watch the editor instance for changes. + */ + watch(nextEditor: Editor | null): undefined | (() => void) { + this.editor = nextEditor as TEditor + + if (this.editor) { + /** + * This will force a re-render when the editor state changes. + * This is to support things like `editor.can().toggleBold()` in components that `useEditor`. + * This could be more efficient, but it's a good trade-off for now. + */ + const fn = () => { + this.transactionNumber += 1 + this.subscribers.forEach(callback => callback()) + } + + const currentEditor = this.editor + + currentEditor.on('transaction', fn) + return () => { + currentEditor.off('transaction', fn) + } + } + + return undefined + } } export function useEditorState( @@ -104,7 +119,7 @@ export function useEditorState( export function useEditorState( options: UseEditorStateOptions | UseEditorStateOptions, ): TSelectorResult | null { - const [editorInstance] = useState(() => makeEditorStateInstance(options.editor)) + const [editorInstance] = useState(() => new EditorStateManager(options.editor)) // Using the `useSyncExternalStore` hook to sync the editor instance with the component state const selectedState = useSyncExternalStoreWithSelector( @@ -117,7 +132,7 @@ export function useEditorState( useEffect(() => { return editorInstance.watch(options.editor) - }, [options.editor]) + }, [options.editor, editorInstance]) useDebugValue(selectedState) diff --git a/packages/vue-3/src/Editor.ts b/packages/vue-3/src/Editor.ts index 8d10526ca..9ed5597d3 100644 --- a/packages/vue-3/src/Editor.ts +++ b/packages/vue-3/src/Editor.ts @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/rules-of-hooks */ import { Editor as CoreEditor, EditorOptions } from '@tiptap/core' import { EditorState, Plugin, PluginKey } from '@tiptap/pm/state' import { From c1ff1b0d4d3c47404dc1028621640d964dd61bb6 Mon Sep 17 00:00:00 2001 From: Nick Perez Date: Mon, 5 Aug 2024 18:09:10 +0200 Subject: [PATCH 04/13] fix(placeholder): add back-compat to deprecated placeholder functionality (#5409) --- .changeset/wet-terms-fetch.md | 5 +++++ packages/extension-placeholder/src/placeholder.ts | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 .changeset/wet-terms-fetch.md diff --git a/.changeset/wet-terms-fetch.md b/.changeset/wet-terms-fetch.md new file mode 100644 index 000000000..899f3200f --- /dev/null +++ b/.changeset/wet-terms-fetch.md @@ -0,0 +1,5 @@ +--- +"@tiptap/extension-placeholder": patch +--- + +add back `considerAsAny` type but mark it deprecated diff --git a/packages/extension-placeholder/src/placeholder.ts b/packages/extension-placeholder/src/placeholder.ts index ad0fb8638..08427a257 100644 --- a/packages/extension-placeholder/src/placeholder.ts +++ b/packages/extension-placeholder/src/placeholder.ts @@ -31,6 +31,12 @@ export interface PlaceholderOptions { }) => string) | string + /** + * See https://github.com/ueberdosis/tiptap/pull/5278 for more information. + * @deprecated This option is no longer respected and this type will be removed in the next major version. + */ + considerAnyAsEmpty?: boolean + /** * **Checks if the placeholder should be only shown when the editor is editable.** * From 3f4e2808bb73fe858969d217ec7a216a1340b4a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:34:39 +0200 Subject: [PATCH 05/13] build(deps): bump cypress-io/github-action from 6.7.1 to 6.7.2 (#5443) Bumps [cypress-io/github-action](https://github.com/cypress-io/github-action) from 6.7.1 to 6.7.2. - [Release notes](https://github.com/cypress-io/github-action/releases) - [Changelog](https://github.com/cypress-io/github-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/cypress-io/github-action/compare/v6.7.1...v6.7.2) --- updated-dependencies: - dependency-name: cypress-io/github-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f34ac2906..310fcdd32 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -103,7 +103,7 @@ jobs: - name: Test ${{ matrix.test-spec.name }} id: cypress - uses: cypress-io/github-action@v6.7.1 + uses: cypress-io/github-action@v6.7.2 with: cache-key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} start: npm run serve From e143fb4ca3df4f613d2be86415c9e846661ca7f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:35:02 +0200 Subject: [PATCH 06/13] build(deps): bump actions/upload-artifact from 4.3.3 to 4.3.5 (#5444) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.3 to 4.3.5. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4.3.3...v4.3.5) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 310fcdd32..6b2ef9167 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -114,7 +114,7 @@ jobs: quiet: true - name: Export screenshots (on failure only) - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@v4.3.5 if: failure() with: name: cypress-screenshots @@ -122,7 +122,7 @@ jobs: retention-days: 7 - name: Export screen recordings (on failure only) - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@v4.3.5 if: failure() with: name: cypress-videos From efb27faf54efed8e0e828a230026551d42019e14 Mon Sep 17 00:00:00 2001 From: Nick Perez Date: Mon, 5 Aug 2024 20:46:23 +0200 Subject: [PATCH 07/13] fix(core): use correct position for getMarksBetween (#5412) --- .changeset/real-kiwis-double.md | 5 +++++ packages/core/src/helpers/getMarksBetween.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/real-kiwis-double.md diff --git a/.changeset/real-kiwis-double.md b/.changeset/real-kiwis-double.md new file mode 100644 index 000000000..d10f60149 --- /dev/null +++ b/.changeset/real-kiwis-double.md @@ -0,0 +1,5 @@ +--- +"@tiptap/core": patch +--- + +This fixes a discrepency between `getMarksBetween` and `isActive(markName)` where the position used for getMarksBetween was off by one diff --git a/packages/core/src/helpers/getMarksBetween.ts b/packages/core/src/helpers/getMarksBetween.ts index 177b9bbce..99c85940a 100644 --- a/packages/core/src/helpers/getMarksBetween.ts +++ b/packages/core/src/helpers/getMarksBetween.ts @@ -12,7 +12,7 @@ export function getMarksBetween(from: number, to: number, doc: ProseMirrorNode): .resolve(from) .marks() .forEach(mark => { - const $pos = doc.resolve(from - 1) + const $pos = doc.resolve(from) const range = getMarkRange($pos, mark.type) if (!range) { From ae0254db97e5eed4a5dcd1077f198b4063c9af26 Mon Sep 17 00:00:00 2001 From: Nick Perez Date: Tue, 6 Aug 2024 10:05:50 +0200 Subject: [PATCH 08/13] feat(core): add ignoreWhitespace option to isNodeEmpty (#5446) --- .changeset/hungry-poems-bake.md | 5 ++ packages/core/src/helpers/isNodeEmpty.ts | 37 +++++++--- .../integration/core/isNodeEmpty.spec.ts | 69 ++++++++++++++----- 3 files changed, 86 insertions(+), 25 deletions(-) create mode 100644 .changeset/hungry-poems-bake.md diff --git a/.changeset/hungry-poems-bake.md b/.changeset/hungry-poems-bake.md new file mode 100644 index 000000000..b3f056421 --- /dev/null +++ b/.changeset/hungry-poems-bake.md @@ -0,0 +1,5 @@ +--- +"@tiptap/core": minor +--- + +Add `ignoreWhitespace` option to `isNodeEmpty` to ignore any whitespace and hardbreaks in a node to check for emptiness diff --git a/packages/core/src/helpers/isNodeEmpty.ts b/packages/core/src/helpers/isNodeEmpty.ts index 90d94f6ee..8d9a76400 100644 --- a/packages/core/src/helpers/isNodeEmpty.ts +++ b/packages/core/src/helpers/isNodeEmpty.ts @@ -1,13 +1,34 @@ import { Node as ProseMirrorNode } from '@tiptap/pm/model' /** - * Returns true if the given node is empty. - * When `checkChildren` is true (default), it will also check if all children are empty. + * Returns true if the given prosemirror node is empty. */ export function isNodeEmpty( node: ProseMirrorNode, - { checkChildren }: { checkChildren: boolean } = { checkChildren: true }, + { + checkChildren = true, + ignoreWhitespace = false, + }: { + /** + * When true (default), it will also check if all children are empty. + */ + checkChildren?: boolean; + /** + * When true, it will ignore whitespace when checking for emptiness. + */ + ignoreWhitespace?: boolean; + } = {}, ): boolean { + if (ignoreWhitespace) { + if (node.type.name === 'hardBreak') { + // Hard breaks are considered empty + return true + } + if (node.isText) { + return /^\s*$/m.test(node.text ?? '') + } + } + if (node.isText) { return !node.text } @@ -21,20 +42,20 @@ export function isNodeEmpty( } if (checkChildren) { - let hasSameContent = true + let isContentEmpty = true node.content.forEach(childNode => { - if (hasSameContent === false) { + if (isContentEmpty === false) { // Exit early for perf return } - if (!isNodeEmpty(childNode)) { - hasSameContent = false + if (!isNodeEmpty(childNode, { ignoreWhitespace, checkChildren })) { + isContentEmpty = false } }) - return hasSameContent + return isContentEmpty } return false diff --git a/tests/cypress/integration/core/isNodeEmpty.spec.ts b/tests/cypress/integration/core/isNodeEmpty.spec.ts index 7e1a8e6c8..e4c1dc946 100644 --- a/tests/cypress/integration/core/isNodeEmpty.spec.ts +++ b/tests/cypress/integration/core/isNodeEmpty.spec.ts @@ -7,10 +7,49 @@ import Mention from '@tiptap/extension-mention' import StarterKit from '@tiptap/starter-kit' const schema = getSchema([StarterKit, Mention]) -const modifiedSchema = getSchema([StarterKit.configure({ document: false }), Document.extend({ content: 'heading block*' })]) -const imageSchema = getSchema([StarterKit.configure({ document: false }), Document.extend({ content: 'image block*' }), Image]) +const modifiedSchema = getSchema([ + StarterKit.configure({ document: false }), + Document.extend({ content: 'heading block*' }), +]) +const imageSchema = getSchema([ + StarterKit.configure({ document: false }), + Document.extend({ content: 'image block*' }), + Image, +]) describe('isNodeEmpty', () => { + describe('ignoreWhitespace=true', () => { + it('should return true when text has only whitespace', () => { + const node = schema.nodeFromJSON({ type: 'text', text: ' \n\t\r\n' }) + + expect(isNodeEmpty(node, { ignoreWhitespace: true })).to.eq(true) + }) + + it('should return true when a paragraph has only whitespace', () => { + const node = schema.nodeFromJSON({ + type: 'paragraph', + content: [{ type: 'text', text: ' \n\t\r\n' }], + }) + + expect(isNodeEmpty(node, { ignoreWhitespace: true })).to.eq(true) + }) + + it('should return true for a hardbreak', () => { + const node = schema.nodeFromJSON({ type: 'hardBreak' }) + + expect(isNodeEmpty(node, { ignoreWhitespace: true })).to.eq(true) + }) + + it('should return true when a paragraph has only a hardbreak', () => { + const node = schema.nodeFromJSON({ + type: 'paragraph', + content: [{ type: 'hardBreak' }], + }) + + expect(isNodeEmpty(node, { ignoreWhitespace: true })).to.eq(true) + }) + }) + describe('with default schema', () => { it('should return false when text has content', () => { const node = schema.nodeFromJSON({ type: 'text', text: 'Hello world!' }) @@ -39,13 +78,15 @@ describe('isNodeEmpty', () => { it('should return false when a paragraph has a mention', () => { const node = schema.nodeFromJSON({ type: 'paragraph', - content: [{ - type: 'mention', - attrs: { - id: 'Winona Ryder', - label: null, + content: [ + { + type: 'mention', + attrs: { + id: 'Winona Ryder', + label: null, + }, }, - }], + ], }) expect(isNodeEmpty(node)).to.eq(false) @@ -120,9 +161,7 @@ describe('isNodeEmpty', () => { content: [ { type: 'heading', - content: [ - { type: 'text', text: 'Hello world!' }, - ], + content: [{ type: 'text', text: 'Hello world!' }], }, ], }) @@ -137,9 +176,7 @@ describe('isNodeEmpty', () => { { type: 'heading' }, { type: 'paragraph', - content: [ - { type: 'text', text: 'Hello world!' }, - ], + content: [{ type: 'text', text: 'Hello world!' }], }, ], }) @@ -162,9 +199,7 @@ describe('isNodeEmpty', () => { it('should return true when a document has an empty heading with attrs', () => { const node = modifiedSchema.nodeFromJSON({ type: 'doc', - content: [ - { type: 'heading', content: [], attrs: { level: 2 } }, - ], + content: [{ type: 'heading', content: [], attrs: { level: 2 } }], }) expect(isNodeEmpty(node)).to.eq(true) From 174aefe0f1ddb8dd8a89700d9a7d484fdafd8513 Mon Sep 17 00:00:00 2001 From: Nick Perez Date: Tue, 6 Aug 2024 11:23:20 +0200 Subject: [PATCH 09/13] fix(collaboration): update y-prosemirror, respect `onFirstRender` (#5411) --- .changeset/yellow-rice-collect.md | 6 + demos/package.json | 2 +- package-lock.json | 104 +++++++++++++----- .../package.json | 4 +- .../src/collaboration-cursor.ts | 3 + packages/extension-collaboration/package.json | 4 +- .../src/collaboration.ts | 4 +- 7 files changed, 92 insertions(+), 35 deletions(-) create mode 100644 .changeset/yellow-rice-collect.md diff --git a/.changeset/yellow-rice-collect.md b/.changeset/yellow-rice-collect.md new file mode 100644 index 000000000..2d7c184db --- /dev/null +++ b/.changeset/yellow-rice-collect.md @@ -0,0 +1,6 @@ +--- +"@tiptap/extension-collaboration-cursor": patch +"@tiptap/extension-collaboration": patch +--- + +This updates y-prosemirror to a version that no longer has syncing problems and extension collaboration now respects the onFirstRender option diff --git a/demos/package.json b/demos/package.json index e9efcdaa3..362eb234a 100644 --- a/demos/package.json +++ b/demos/package.json @@ -20,7 +20,7 @@ "remixicon": "^2.5.0", "shiki": "^1.10.3", "simplify-js": "^1.2.4", - "y-prosemirror": "^1.2.9", + "y-prosemirror": "^1.2.11", "y-webrtc": "^10.3.0", "yjs": "^13.6.18" }, diff --git a/package-lock.json b/package-lock.json index 795aab055..c14c41295 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,7 +66,7 @@ "remixicon": "^2.5.0", "shiki": "^1.10.3", "simplify-js": "^1.2.4", - "y-prosemirror": "^1.2.9", + "y-prosemirror": "^1.2.11", "y-webrtc": "^10.3.0", "yjs": "^13.6.18" }, @@ -791,6 +791,29 @@ } } }, + "demos/node_modules/y-prosemirror": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/y-prosemirror/-/y-prosemirror-1.2.11.tgz", + "integrity": "sha512-MUGMYyokOb9DpBRHr4Cadob2KheDCKW2LHceAM2yrWp9dfX+3HZZUNEubEPd4zszq4DF2fGCFhE3N66zOTLoxA==", + "dependencies": { + "lib0": "^0.2.42" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "prosemirror-model": "^1.7.1", + "prosemirror-state": "^1.2.3", + "prosemirror-view": "^1.9.10", + "y-protocols": "^1.0.1", + "yjs": "^13.5.38" + } + }, "demos/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -17741,29 +17764,6 @@ "node": ">=12" } }, - "node_modules/y-prosemirror": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/y-prosemirror/-/y-prosemirror-1.2.9.tgz", - "integrity": "sha512-fThGIVmSqrqnG/ckywEGlHM9ElfILC4TcMZd5zxWPe/i+UuP97TEr4swsopRKG3Y+KHBVt4Y/5NVBC3AAsUoUg==", - "dependencies": { - "lib0": "^0.2.42" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=8.0.0" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - }, - "peerDependencies": { - "prosemirror-model": "^1.7.1", - "prosemirror-state": "^1.2.3", - "prosemirror-view": "^1.9.10", - "y-protocols": "^1.0.1", - "yjs": "^13.5.38" - } - }, "node_modules/y-protocols": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", @@ -18068,7 +18068,7 @@ "devDependencies": { "@tiptap/core": "^2.5.8", "@tiptap/pm": "^2.5.8", - "y-prosemirror": "^1.2.9" + "y-prosemirror": "^1.2.11" }, "funding": { "type": "github", @@ -18077,7 +18077,7 @@ "peerDependencies": { "@tiptap/core": "^2.5.8", "@tiptap/pm": "^2.5.8", - "y-prosemirror": "^1.2.6" + "y-prosemirror": "^1.2.11" } }, "packages/extension-collaboration-cursor": { @@ -18086,7 +18086,7 @@ "license": "MIT", "devDependencies": { "@tiptap/core": "^2.5.8", - "y-prosemirror": "^1.2.9" + "y-prosemirror": "^1.2.11" }, "funding": { "type": "github", @@ -18094,7 +18094,55 @@ }, "peerDependencies": { "@tiptap/core": "^2.5.8", - "y-prosemirror": "^1.2.6" + "y-prosemirror": "^1.2.11" + } + }, + "packages/extension-collaboration-cursor/node_modules/y-prosemirror": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/y-prosemirror/-/y-prosemirror-1.2.11.tgz", + "integrity": "sha512-MUGMYyokOb9DpBRHr4Cadob2KheDCKW2LHceAM2yrWp9dfX+3HZZUNEubEPd4zszq4DF2fGCFhE3N66zOTLoxA==", + "dev": true, + "dependencies": { + "lib0": "^0.2.42" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "prosemirror-model": "^1.7.1", + "prosemirror-state": "^1.2.3", + "prosemirror-view": "^1.9.10", + "y-protocols": "^1.0.1", + "yjs": "^13.5.38" + } + }, + "packages/extension-collaboration/node_modules/y-prosemirror": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/y-prosemirror/-/y-prosemirror-1.2.11.tgz", + "integrity": "sha512-MUGMYyokOb9DpBRHr4Cadob2KheDCKW2LHceAM2yrWp9dfX+3HZZUNEubEPd4zszq4DF2fGCFhE3N66zOTLoxA==", + "dev": true, + "dependencies": { + "lib0": "^0.2.42" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "prosemirror-model": "^1.7.1", + "prosemirror-state": "^1.2.3", + "prosemirror-view": "^1.9.10", + "y-protocols": "^1.0.1", + "yjs": "^13.5.38" } }, "packages/extension-color": { diff --git a/packages/extension-collaboration-cursor/package.json b/packages/extension-collaboration-cursor/package.json index c596cb07c..fcd0a9e73 100644 --- a/packages/extension-collaboration-cursor/package.json +++ b/packages/extension-collaboration-cursor/package.json @@ -30,11 +30,11 @@ ], "devDependencies": { "@tiptap/core": "^2.5.8", - "y-prosemirror": "^1.2.9" + "y-prosemirror": "^1.2.11" }, "peerDependencies": { "@tiptap/core": "^2.5.8", - "y-prosemirror": "^1.2.6" + "y-prosemirror": "^1.2.11" }, "repository": { "type": "git", diff --git a/packages/extension-collaboration-cursor/src/collaboration-cursor.ts b/packages/extension-collaboration-cursor/src/collaboration-cursor.ts index ed17ad06f..3c14708ed 100644 --- a/packages/extension-collaboration-cursor/src/collaboration-cursor.ts +++ b/packages/extension-collaboration-cursor/src/collaboration-cursor.ts @@ -127,6 +127,9 @@ export const CollaborationCursor = Extension.create({ } const ySyncPluginOptions: YSyncOpts = { - ...(this.options.ySyncOptions ? { ...this.options.ySyncOptions } : {}), - ...(this.options.onFirstRender ? { ...this.options.onFirstRender } : {}), + ...this.options.ySyncOptions, + onFirstRender: this.options.onFirstRender, } const ySyncPluginInstance = ySyncPlugin(fragment, ySyncPluginOptions) From 4b215f794e13b81780395d500952b27d35995456 Mon Sep 17 00:00:00 2001 From: Nick Perez Date: Tue, 6 Aug 2024 12:25:01 +0200 Subject: [PATCH 10/13] fix(code-block): respect `defaultLanguage` on code-block-lowlight add option to `code-block` (#5406) --- .changeset/good-schools-pretend.md | 6 ++++++ .../src/code-block-lowlight.ts | 8 -------- packages/extension-code-block/src/code-block.ts | 9 ++++++++- 3 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 .changeset/good-schools-pretend.md diff --git a/.changeset/good-schools-pretend.md b/.changeset/good-schools-pretend.md new file mode 100644 index 000000000..6d7244219 --- /dev/null +++ b/.changeset/good-schools-pretend.md @@ -0,0 +1,6 @@ +--- +"@tiptap/extension-code-block": minor +"@tiptap/extension-code-block-lowlight": patch +--- + +`defaultLanguage` on Code Block Lowlight was not being respected properly, to address this we added `defaultLanguage` as an option to the code-block extension. diff --git a/packages/extension-code-block-lowlight/src/code-block-lowlight.ts b/packages/extension-code-block-lowlight/src/code-block-lowlight.ts index c1eb4dcbf..3b952e329 100644 --- a/packages/extension-code-block-lowlight/src/code-block-lowlight.ts +++ b/packages/extension-code-block-lowlight/src/code-block-lowlight.ts @@ -7,13 +7,6 @@ export interface CodeBlockLowlightOptions extends CodeBlockOptions { * The lowlight instance. */ lowlight: any, - - /** - * The default language. - * @default null - * @example 'javascript' - */ - defaultLanguage: string | null | undefined, } /** @@ -25,7 +18,6 @@ export const CodeBlockLowlight = CodeBlock.extend({ return { ...this.parent?.(), lowlight: {}, - defaultLanguage: null, } }, diff --git a/packages/extension-code-block/src/code-block.ts b/packages/extension-code-block/src/code-block.ts index 8d6b22d4c..122e476b4 100644 --- a/packages/extension-code-block/src/code-block.ts +++ b/packages/extension-code-block/src/code-block.ts @@ -22,6 +22,12 @@ export interface CodeBlockOptions { * @default true */ exitOnArrowDown: boolean + /** + * The default language. + * @default null + * @example 'js' + */ + defaultLanguage: string | null | undefined /** * Custom HTML attributes that should be added to the rendered HTML tag. * @default {} @@ -71,6 +77,7 @@ export const CodeBlock = Node.create({ languageClassPrefix: 'language-', exitOnTripleEnter: true, exitOnArrowDown: true, + defaultLanguage: null, HTMLAttributes: {}, } }, @@ -88,7 +95,7 @@ export const CodeBlock = Node.create({ addAttributes() { return { language: { - default: null, + default: this.options.defaultLanguage, parseHTML: element => { const { languageClassPrefix } = this.options const classNames = [...(element.firstElementChild?.classList || [])] From d6e56c41e35f5053561a5a50054dfd7a848d277a Mon Sep 17 00:00:00 2001 From: Nick Perez Date: Tue, 6 Aug 2024 13:53:58 +0200 Subject: [PATCH 11/13] fix(extension-code-block-lowlight): use lowlight v3 and update demos (#5374) --- .changeset/bright-mayflies-care.md | 6 + demos/includeDependencies.txt | 2 +- demos/package.json | 4 +- .../CodeBlockLanguage/React/index.jsx | 25 +++-- .../Examples/CodeBlockLanguage/Vue/index.vue | 21 ++-- demos/src/Experiments/All/Vue/index.vue | 16 +-- .../Nodes/CodeBlockLowlight/React/index.jsx | 33 ++++-- .../src/Nodes/CodeBlockLowlight/Vue/index.vue | 18 +-- package-lock.json | 103 ++++++++---------- .../package.json | 7 +- .../extensions/codeBlockLowlight.spec.ts | 4 +- tests/cypress/tsconfig.json | 2 +- 12 files changed, 128 insertions(+), 113 deletions(-) create mode 100644 .changeset/bright-mayflies-care.md diff --git a/.changeset/bright-mayflies-care.md b/.changeset/bright-mayflies-care.md new file mode 100644 index 000000000..a3ada7855 --- /dev/null +++ b/.changeset/bright-mayflies-care.md @@ -0,0 +1,6 @@ +--- +"tiptap-demos": patch +"@tiptap/extension-code-block-lowlight": patch +--- + +declare lowlight to be a peer dep of extension-code-block-lowlight, update usage to v3 diff --git a/demos/includeDependencies.txt b/demos/includeDependencies.txt index 54929e3c2..20de61e95 100644 --- a/demos/includeDependencies.txt +++ b/demos/includeDependencies.txt @@ -6,7 +6,6 @@ highlight.js/lib/languages/xml highlight.js/lib/core linkifyjs lowlight -lowlight/lib/core prosemirror-commands prosemirror-dropcursor prosemirror-gapcursor @@ -21,6 +20,7 @@ prosemirror-view react react-dom react-dom/client +use-sync-external-store/shim/with-selector shiki simplify-js tippy.js diff --git a/demos/package.json b/demos/package.json index 362eb234a..d4d4759a2 100644 --- a/demos/package.json +++ b/demos/package.json @@ -14,9 +14,9 @@ "@lexical/react": "^0.11.1", "d3": "^7.3.0", "fast-glob": "^3.2.11", - "highlight.js": "^11.6.0", + "highlight.js": "^11.10.0", "lexical": "^0.11.1", - "lowlight": "^2.7.0", + "lowlight": "^3.1.0", "remixicon": "^2.5.0", "shiki": "^1.10.3", "simplify-js": "^1.2.4", diff --git a/demos/src/Examples/CodeBlockLanguage/React/index.jsx b/demos/src/Examples/CodeBlockLanguage/React/index.jsx index cdb1d35de..a94b42327 100644 --- a/demos/src/Examples/CodeBlockLanguage/React/index.jsx +++ b/demos/src/Examples/CodeBlockLanguage/React/index.jsx @@ -1,7 +1,3 @@ -// load specific languages only -// import { lowlight } from 'lowlight/lib/core' -// import javascript from 'highlight.js/lib/languages/javascript' -// lowlight.registerLanguage('javascript', javascript) import './styles.scss' import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight' @@ -13,16 +9,21 @@ import css from 'highlight.js/lib/languages/css' import js from 'highlight.js/lib/languages/javascript' import ts from 'highlight.js/lib/languages/typescript' import html from 'highlight.js/lib/languages/xml' -// load all highlight.js languages -import { lowlight } from 'lowlight' +// load all languages with "all" or common languages with "common" +import { all, createLowlight } from 'lowlight' import React from 'react' -import CodeBlockComponent from './CodeBlockComponent.jsx' +// eslint-disable-next-line +import CodeBlockComponent from './CodeBlockComponent' -lowlight.registerLanguage('html', html) -lowlight.registerLanguage('css', css) -lowlight.registerLanguage('js', js) -lowlight.registerLanguage('ts', ts) +// create a lowlight instance +const lowlight = createLowlight(all) + +// you can also register individual languages +lowlight.register('html', html) +lowlight.register('css', css) +lowlight.register('js', js) +lowlight.register('ts', ts) const MenuBar = ({ editor }) => { if (!editor) { @@ -56,7 +57,7 @@ export default () => { ], content: `

- That’s a boring paragraph followed by a fenced code block: + That's a boring paragraph followed by a fenced code block:

for (var i=1; i <= 20; i++)
 {
diff --git a/demos/src/Examples/CodeBlockLanguage/Vue/index.vue b/demos/src/Examples/CodeBlockLanguage/Vue/index.vue
index 6a30301cd..4846fb30c 100644
--- a/demos/src/Examples/CodeBlockLanguage/Vue/index.vue
+++ b/demos/src/Examples/CodeBlockLanguage/Vue/index.vue
@@ -21,20 +21,19 @@ import css from 'highlight.js/lib/languages/css'
 import js from 'highlight.js/lib/languages/javascript'
 import ts from 'highlight.js/lib/languages/typescript'
 import html from 'highlight.js/lib/languages/xml'
-// load all highlight.js languages
-import { lowlight } from 'lowlight'
+// load all languages with "all" or common languages with "common"
+import { all, createLowlight } from 'lowlight'
 
 import CodeBlockComponent from './CodeBlockComponent.vue'
 
-lowlight.registerLanguage('html', html)
-lowlight.registerLanguage('css', css)
-lowlight.registerLanguage('js', js)
-lowlight.registerLanguage('ts', ts)
+// create a lowlight instance
+const lowlight = createLowlight(all)
 
-// load specific languages only
-// import { lowlight } from 'lowlight/lib/core'
-// import javascript from 'highlight.js/lib/languages/javascript'
-// lowlight.registerLanguage('javascript', javascript)
+// you can also register languages
+lowlight.register('html', html)
+lowlight.register('css', css)
+lowlight.register('js', js)
+lowlight.register('ts', ts)
 
 export default {
   components: {
@@ -63,7 +62,7 @@ export default {
       ],
       content: `
         

- That’s a boring paragraph followed by a fenced code block: + That's a boring paragraph followed by a fenced code block:

for (var i=1; i <= 20; i++)
 {
diff --git a/demos/src/Experiments/All/Vue/index.vue b/demos/src/Experiments/All/Vue/index.vue
index cdd835a23..5b1e370f7 100644
--- a/demos/src/Experiments/All/Vue/index.vue
+++ b/demos/src/Experiments/All/Vue/index.vue
@@ -108,7 +108,9 @@ import TextAlign from '@tiptap/extension-text-align'
 import TextStyle from '@tiptap/extension-text-style'
 import Underline from '@tiptap/extension-underline'
 import { Editor, EditorContent } from '@tiptap/vue-3'
-import { lowlight } from 'lowlight'
+import { all, createLowlight } from 'lowlight'
+
+const lowlight = createLowlight(all)
 
 export default {
   components: {
@@ -182,14 +184,14 @@ export default {
         

  • - That’s a bullet list with one … + That's a bullet list with one …
  • … or two list items.

- Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block: + Isn't that great? And all of that is editable. But wait, there's more. Let's try a code block:

for (var i=1; i <= 20; i++)
 {
@@ -203,10 +205,10 @@ export default {
     console.log(i);
 }

- 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. + 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.

- Wow, that’s amazing. Good work, boy! 👏 + Wow, that's amazing. Good work, boy! 👏
— Mom
@@ -214,9 +216,9 @@ export default {

first paragraph

second paragraph

Color

-

Oh, for some reason that’s purple.

+

Oh, for some reason that's purple.

Highlight

-

This isn’t highlighted.

+

This isn't highlighted.

But that one is.

And this is highlighted too, but in a different color.

And this one has a data attribute.

diff --git a/demos/src/Nodes/CodeBlockLowlight/React/index.jsx b/demos/src/Nodes/CodeBlockLowlight/React/index.jsx index 6a5264f0c..2f131dd35 100644 --- a/demos/src/Nodes/CodeBlockLowlight/React/index.jsx +++ b/demos/src/Nodes/CodeBlockLowlight/React/index.jsx @@ -1,7 +1,3 @@ -// load specific languages only -// import { lowlight } from 'lowlight/lib/core' -// import javascript from 'highlight.js/lib/languages/javascript' -// lowlight.registerLanguage('javascript', javascript) import './styles.scss' import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight' @@ -13,14 +9,29 @@ import css from 'highlight.js/lib/languages/css' import js from 'highlight.js/lib/languages/javascript' import ts from 'highlight.js/lib/languages/typescript' import html from 'highlight.js/lib/languages/xml' -// load all highlight.js languages -import { lowlight } from 'lowlight' +// load all languages with "all" or common languages with "common" +import { all, createLowlight } from 'lowlight' import React from 'react' -lowlight.registerLanguage('html', html) -lowlight.registerLanguage('css', css) -lowlight.registerLanguage('js', js) -lowlight.registerLanguage('ts', ts) +// create a lowlight instance with all languages loaded +const lowlight = createLowlight(all) + +// This is only an example, all supported languages are already loaded above +// but you can also register only specific languages to reduce bundle-size +lowlight.register('html', html) +lowlight.register('css', css) +lowlight.register('js', js) +lowlight.register('ts', ts) + +/** + * Lowlight version 2.x had a different API + * import { lowlight } from 'lowlight' + * + * lowlight.registerLanguage('html', html) + * lowlight.registerLanguage('css', css) + * lowlight.registerLanguage('js', js) + * lowlight.registerLanguage('ts', ts) + */ export default () => { const editor = useEditor({ @@ -34,7 +45,7 @@ export default () => { ], content: `

- That’s a boring paragraph followed by a fenced code block: + That's a boring paragraph followed by a fenced code block:

for (var i=1; i <= 20; i++)
 {
diff --git a/demos/src/Nodes/CodeBlockLowlight/Vue/index.vue b/demos/src/Nodes/CodeBlockLowlight/Vue/index.vue
index dfd6a3def..5ee047ec5 100644
--- a/demos/src/Nodes/CodeBlockLowlight/Vue/index.vue
+++ b/demos/src/Nodes/CodeBlockLowlight/Vue/index.vue
@@ -25,13 +25,17 @@ import css from 'highlight.js/lib/languages/css'
 import js from 'highlight.js/lib/languages/javascript'
 import ts from 'highlight.js/lib/languages/typescript'
 import html from 'highlight.js/lib/languages/xml'
-// load all highlight.js languages
-import { lowlight } from 'lowlight'
+// load all languages with "all" or common languages with "common"
+import { all, createLowlight } from 'lowlight'
 
-lowlight.registerLanguage('html', html)
-lowlight.registerLanguage('css', css)
-lowlight.registerLanguage('js', js)
-lowlight.registerLanguage('ts', ts)
+// create a lowlight instance
+const lowlight = createLowlight(all)
+
+// you can also register languages
+lowlight.register('html', html)
+lowlight.register('css', css)
+lowlight.register('js', js)
+lowlight.register('ts', ts)
 
 export default {
   components: {
@@ -56,7 +60,7 @@ export default {
       ],
       content: `
         

- That’s a boring paragraph followed by a fenced code block: + That's a boring paragraph followed by a fenced code block:

for (var i=1; i <= 20; i++)
 {
diff --git a/package-lock.json b/package-lock.json
index c14c41295..119ba5ec5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -60,9 +60,9 @@
         "@lexical/react": "^0.11.1",
         "d3": "^7.3.0",
         "fast-glob": "^3.2.11",
-        "highlight.js": "^11.6.0",
+        "highlight.js": "^11.10.0",
         "lexical": "^0.11.1",
-        "lowlight": "^2.7.0",
+        "lowlight": "^3.1.0",
         "remixicon": "^2.5.0",
         "shiki": "^1.10.3",
         "simplify-js": "^1.2.4",
@@ -116,14 +116,6 @@
         "yjs": "^13.6.8"
       }
     },
-    "demos/node_modules/@types/hast": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
-      "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
-      "dependencies": {
-        "@types/unist": "*"
-      }
-    },
     "demos/node_modules/@vitejs/plugin-vue": {
       "version": "5.0.5",
       "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.5.tgz",
@@ -345,6 +337,14 @@
         "node": ">=8"
       }
     },
+    "demos/node_modules/highlight.js": {
+      "version": "11.10.0",
+      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz",
+      "integrity": "sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "demos/node_modules/hosted-git-info": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@@ -5066,14 +5066,6 @@
         "@types/hast": "^3.0.4"
       }
     },
-    "node_modules/@shikijs/core/node_modules/@types/hast": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
-      "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
-      "dependencies": {
-        "@types/unist": "*"
-      }
-    },
     "node_modules/@sveltejs/vite-plugin-svelte": {
       "version": "2.5.3",
       "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.5.3.tgz",
@@ -5424,11 +5416,11 @@
       "dev": true
     },
     "node_modules/@types/hast": {
-      "version": "2.3.10",
-      "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz",
-      "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==",
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+      "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
       "dependencies": {
-        "@types/unist": "^2"
+        "@types/unist": "*"
       }
     },
     "node_modules/@types/json-schema": {
@@ -8328,6 +8320,14 @@
         "node": ">=0.4.0"
       }
     },
+    "node_modules/dequal": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+      "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/detect-file": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
@@ -8346,6 +8346,18 @@
         "node": ">=8"
       }
     },
+    "node_modules/devlop": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+      "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+      "dependencies": {
+        "dequal": "^2.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm"
+      }
+    },
     "node_modules/didyoumean": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -9603,18 +9615,6 @@
         "reusify": "^1.0.4"
       }
     },
-    "node_modules/fault": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz",
-      "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==",
-      "dependencies": {
-        "format": "^0.2.0"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/wooorm"
-      }
-    },
     "node_modules/fd-slicer": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
@@ -9877,14 +9877,6 @@
         "node": ">= 0.12"
       }
     },
-    "node_modules/format": {
-      "version": "0.2.2",
-      "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
-      "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
-      "engines": {
-        "node": ">=0.4.x"
-      }
-    },
     "node_modules/fraction.js": {
       "version": "4.3.7",
       "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -12398,27 +12390,19 @@
       }
     },
     "node_modules/lowlight": {
-      "version": "2.9.0",
-      "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-2.9.0.tgz",
-      "integrity": "sha512-OpcaUTCLmHuVuBcyNckKfH5B0oA4JUavb/M/8n9iAvanJYNQkrVm4pvyX0SUaqkBG4dnWHKt7p50B3ngAG2Rfw==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.1.0.tgz",
+      "integrity": "sha512-CEbNVoSikAxwDMDPjXlqlFYiZLkDJHwyGu/MfOsJnF3d7f3tds5J3z8s/l9TMXhzfsJCCJEAsD78842mwmg0PQ==",
       "dependencies": {
-        "@types/hast": "^2.0.0",
-        "fault": "^2.0.0",
-        "highlight.js": "~11.8.0"
+        "@types/hast": "^3.0.0",
+        "devlop": "^1.0.0",
+        "highlight.js": "~11.9.0"
       },
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/wooorm"
       }
     },
-    "node_modules/lowlight/node_modules/highlight.js": {
-      "version": "11.8.0",
-      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz",
-      "integrity": "sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==",
-      "engines": {
-        "node": ">=12.0.0"
-      }
-    },
     "node_modules/lru-cache": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -18049,7 +18033,8 @@
       "devDependencies": {
         "@tiptap/core": "^2.5.8",
         "@tiptap/extension-code-block": "^2.5.8",
-        "@tiptap/pm": "^2.5.8"
+        "@tiptap/pm": "^2.5.8",
+        "lowlight": "^2 || ^3"
       },
       "funding": {
         "type": "github",
@@ -18058,7 +18043,9 @@
       "peerDependencies": {
         "@tiptap/core": "^2.5.8",
         "@tiptap/extension-code-block": "^2.5.8",
-        "@tiptap/pm": "^2.5.8"
+        "@tiptap/pm": "^2.5.8",
+        "highlight.js": "^11",
+        "lowlight": "^2 || ^3"
       }
     },
     "packages/extension-collaboration": {
diff --git a/packages/extension-code-block-lowlight/package.json b/packages/extension-code-block-lowlight/package.json
index f9dc1502a..5e19d72a1 100644
--- a/packages/extension-code-block-lowlight/package.json
+++ b/packages/extension-code-block-lowlight/package.json
@@ -31,12 +31,15 @@
   "devDependencies": {
     "@tiptap/core": "^2.5.8",
     "@tiptap/extension-code-block": "^2.5.8",
-    "@tiptap/pm": "^2.5.8"
+    "@tiptap/pm": "^2.5.8",
+    "lowlight": "^2 || ^3"
   },
   "peerDependencies": {
     "@tiptap/core": "^2.5.8",
     "@tiptap/extension-code-block": "^2.5.8",
-    "@tiptap/pm": "^2.5.8"
+    "@tiptap/pm": "^2.5.8",
+    "lowlight": "^2 || ^3",
+    "highlight.js": "^11"
   },
   "repository": {
     "type": "git",
diff --git a/tests/cypress/integration/extensions/codeBlockLowlight.spec.ts b/tests/cypress/integration/extensions/codeBlockLowlight.spec.ts
index e3b6fd6d1..00f1df625 100644
--- a/tests/cypress/integration/extensions/codeBlockLowlight.spec.ts
+++ b/tests/cypress/integration/extensions/codeBlockLowlight.spec.ts
@@ -5,7 +5,9 @@ import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight'
 import { Document } from '@tiptap/extension-document'
 import { Paragraph } from '@tiptap/extension-paragraph'
 import { Text } from '@tiptap/extension-text'
-import { lowlight } from 'lowlight'
+import { all, createLowlight } from 'lowlight'
+
+const lowlight = createLowlight(all)
 
 describe('code block highlight', () => {
   let Frontmatter
diff --git a/tests/cypress/tsconfig.json b/tests/cypress/tsconfig.json
index ebd9a998f..7967d1ebd 100644
--- a/tests/cypress/tsconfig.json
+++ b/tests/cypress/tsconfig.json
@@ -6,7 +6,7 @@
     "sourceMap": false,
     "types": ["cypress", "react", "react-dom"],
     "paths": {
-      "@tiptap/*": ["packages/*/dist", "packages/*/src"],
+      "@tiptap/*": ["packages/*/src", "packages/*/dist"],
       "@tiptap/pm/*": ["../../pm/*/dist"]
     },
     "typeRoots": ["../../node_modules/@types", "../../node_modules/"],

From c01eccc0ea3a0a71511d0d773942f330432fa37f Mon Sep 17 00:00:00 2001
From: Nick the Sick 
Date: Tue, 6 Aug 2024 16:53:31 +0200
Subject: [PATCH 12/13] chore: make into a patch version

---
 .changeset/good-schools-pretend.md | 2 +-
 .changeset/hungry-poems-bake.md    | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.changeset/good-schools-pretend.md b/.changeset/good-schools-pretend.md
index 6d7244219..42ec46d18 100644
--- a/.changeset/good-schools-pretend.md
+++ b/.changeset/good-schools-pretend.md
@@ -1,5 +1,5 @@
 ---
-"@tiptap/extension-code-block": minor
+"@tiptap/extension-code-block": patch
 "@tiptap/extension-code-block-lowlight": patch
 ---
 
diff --git a/.changeset/hungry-poems-bake.md b/.changeset/hungry-poems-bake.md
index b3f056421..189fc8fa2 100644
--- a/.changeset/hungry-poems-bake.md
+++ b/.changeset/hungry-poems-bake.md
@@ -1,5 +1,5 @@
 ---
-"@tiptap/core": minor
+"@tiptap/core": patch
 ---
 
 Add `ignoreWhitespace` option to `isNodeEmpty` to ignore any whitespace and hardbreaks in a node to check for emptiness

From 535dcccb40ca2f17d5db48d5f7a26220a636aafa Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 6 Aug 2024 17:03:26 +0200
Subject: [PATCH 13/13] chore(release): publish version v2.5.9 (#5451)

Co-authored-by: github-actions[bot] 
---
 .changeset/bright-mayflies-care.md            |  6 ---
 .changeset/cuddly-pants-destroy.md            |  5 ---
 .changeset/early-singers-begin.md             |  5 ---
 .changeset/good-schools-pretend.md            |  6 ---
 .changeset/hungry-poems-bake.md               |  5 ---
 .changeset/real-kiwis-double.md               |  5 ---
 .changeset/smooth-rice-obey.md                | 12 ------
 .changeset/wet-terms-fetch.md                 |  5 ---
 .changeset/wise-beers-reflect.md              |  5 ---
 .changeset/yellow-rice-collect.md             |  6 ---
 demos/CHANGELOG.md                            |  6 +++
 demos/package.json                            |  2 +-
 packages/core/CHANGELOG.md                    | 10 +++++
 packages/core/package.json                    |  6 +--
 packages/extension-blockquote/CHANGELOG.md    | 10 +++++
 packages/extension-blockquote/package.json    |  6 +--
 packages/extension-bold/CHANGELOG.md          | 10 +++++
 packages/extension-bold/package.json          |  6 +--
 packages/extension-bubble-menu/CHANGELOG.md   | 11 +++++
 packages/extension-bubble-menu/package.json   | 10 ++---
 packages/extension-bullet-list/CHANGELOG.md   | 10 +++++
 packages/extension-bullet-list/package.json   |  6 +--
 .../extension-character-count/CHANGELOG.md    | 11 +++++
 .../extension-character-count/package.json    | 10 ++---
 .../CHANGELOG.md                              | 15 +++++++
 .../package.json                              | 14 +++----
 packages/extension-code-block/CHANGELOG.md    | 12 ++++++
 packages/extension-code-block/package.json    | 10 ++---
 packages/extension-code/CHANGELOG.md          | 10 +++++
 packages/extension-code/package.json          |  6 +--
 .../CHANGELOG.md                              | 11 +++++
 .../package.json                              |  6 +--
 packages/extension-collaboration/CHANGELOG.md | 12 ++++++
 packages/extension-collaboration/package.json | 10 ++---
 packages/extension-color/CHANGELOG.md         | 11 +++++
 packages/extension-color/package.json         | 10 ++---
 packages/extension-document/CHANGELOG.md      | 10 +++++
 packages/extension-document/package.json      |  6 +--
 packages/extension-dropcursor/CHANGELOG.md    | 11 +++++
 packages/extension-dropcursor/package.json    | 10 ++---
 packages/extension-floating-menu/CHANGELOG.md | 11 +++++
 packages/extension-floating-menu/package.json | 10 ++---
 packages/extension-focus/CHANGELOG.md         | 11 +++++
 packages/extension-focus/package.json         | 10 ++---
 packages/extension-font-family/CHANGELOG.md   | 11 +++++
 packages/extension-font-family/package.json   | 10 ++---
 packages/extension-gapcursor/CHANGELOG.md     | 11 +++++
 packages/extension-gapcursor/package.json     | 10 ++---
 packages/extension-hard-break/CHANGELOG.md    | 10 +++++
 packages/extension-hard-break/package.json    |  6 +--
 packages/extension-heading/CHANGELOG.md       | 10 +++++
 packages/extension-heading/package.json       |  6 +--
 packages/extension-highlight/CHANGELOG.md     | 10 +++++
 packages/extension-highlight/package.json     |  6 +--
 packages/extension-history/CHANGELOG.md       | 11 +++++
 packages/extension-history/package.json       | 10 ++---
 .../extension-horizontal-rule/CHANGELOG.md    | 11 +++++
 .../extension-horizontal-rule/package.json    | 10 ++---
 packages/extension-image/CHANGELOG.md         | 10 +++++
 packages/extension-image/package.json         |  6 +--
 packages/extension-italic/CHANGELOG.md        | 10 +++++
 packages/extension-italic/package.json        |  6 +--
 packages/extension-link/CHANGELOG.md          | 11 +++++
 packages/extension-link/package.json          | 10 ++---
 packages/extension-list-item/CHANGELOG.md     | 10 +++++
 packages/extension-list-item/package.json     |  6 +--
 packages/extension-list-keymap/CHANGELOG.md   | 10 +++++
 packages/extension-list-keymap/package.json   |  6 +--
 packages/extension-mention/CHANGELOG.md       | 12 ++++++
 packages/extension-mention/package.json       | 14 +++----
 packages/extension-ordered-list/CHANGELOG.md  | 10 +++++
 packages/extension-ordered-list/package.json  |  6 +--
 packages/extension-paragraph/CHANGELOG.md     | 10 +++++
 packages/extension-paragraph/package.json     |  6 +--
 packages/extension-placeholder/CHANGELOG.md   | 12 ++++++
 packages/extension-placeholder/package.json   | 10 ++---
 packages/extension-strike/CHANGELOG.md        | 10 +++++
 packages/extension-strike/package.json        |  6 +--
 packages/extension-subscript/CHANGELOG.md     | 10 +++++
 packages/extension-subscript/package.json     |  6 +--
 packages/extension-superscript/CHANGELOG.md   | 10 +++++
 packages/extension-superscript/package.json   |  6 +--
 packages/extension-table-cell/CHANGELOG.md    | 10 +++++
 packages/extension-table-cell/package.json    |  6 +--
 packages/extension-table-header/CHANGELOG.md  | 10 +++++
 packages/extension-table-header/package.json  |  6 +--
 packages/extension-table-row/CHANGELOG.md     | 10 +++++
 packages/extension-table-row/package.json     |  6 +--
 packages/extension-table/CHANGELOG.md         | 11 +++++
 packages/extension-table/package.json         | 10 ++---
 packages/extension-task-item/CHANGELOG.md     | 12 ++++++
 packages/extension-task-item/package.json     | 10 ++---
 packages/extension-task-list/CHANGELOG.md     | 10 +++++
 packages/extension-task-list/package.json     |  6 +--
 packages/extension-text-align/CHANGELOG.md    | 10 +++++
 packages/extension-text-align/package.json    |  6 +--
 packages/extension-text-style/CHANGELOG.md    | 10 +++++
 packages/extension-text-style/package.json    |  6 +--
 packages/extension-text/CHANGELOG.md          | 10 +++++
 packages/extension-text/package.json          |  6 +--
 packages/extension-typography/CHANGELOG.md    | 10 +++++
 packages/extension-typography/package.json    |  6 +--
 packages/extension-underline/CHANGELOG.md     | 10 +++++
 packages/extension-underline/package.json     |  6 +--
 packages/extension-youtube/CHANGELOG.md       | 10 +++++
 packages/extension-youtube/package.json       |  6 +--
 packages/html/CHANGELOG.md                    | 11 +++++
 packages/html/package.json                    | 10 ++---
 packages/pm/CHANGELOG.md                      |  2 +
 packages/pm/package.json                      |  2 +-
 packages/react/CHANGELOG.md                   | 23 +++++++++++
 packages/react/package.json                   | 14 +++----
 packages/starter-kit/CHANGELOG.md             | 29 ++++++++++++++
 packages/starter-kit/package.json             | 40 +++++++++----------
 packages/suggestion/CHANGELOG.md              | 11 +++++
 packages/suggestion/package.json              | 10 ++---
 packages/vue-2/CHANGELOG.md                   | 13 ++++++
 packages/vue-2/package.json                   | 14 +++----
 packages/vue-3/CHANGELOG.md                   | 13 ++++++
 packages/vue-3/package.json                   | 14 +++----
 120 files changed, 840 insertions(+), 294 deletions(-)
 delete mode 100644 .changeset/bright-mayflies-care.md
 delete mode 100644 .changeset/cuddly-pants-destroy.md
 delete mode 100644 .changeset/early-singers-begin.md
 delete mode 100644 .changeset/good-schools-pretend.md
 delete mode 100644 .changeset/hungry-poems-bake.md
 delete mode 100644 .changeset/real-kiwis-double.md
 delete mode 100644 .changeset/smooth-rice-obey.md
 delete mode 100644 .changeset/wet-terms-fetch.md
 delete mode 100644 .changeset/wise-beers-reflect.md
 delete mode 100644 .changeset/yellow-rice-collect.md

diff --git a/.changeset/bright-mayflies-care.md b/.changeset/bright-mayflies-care.md
deleted file mode 100644
index a3ada7855..000000000
--- a/.changeset/bright-mayflies-care.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"tiptap-demos": patch
-"@tiptap/extension-code-block-lowlight": patch
----
-
-declare lowlight to be a peer dep of extension-code-block-lowlight, update usage to v3
diff --git a/.changeset/cuddly-pants-destroy.md b/.changeset/cuddly-pants-destroy.md
deleted file mode 100644
index f3602beb1..000000000
--- a/.changeset/cuddly-pants-destroy.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@tiptap/core": patch
----
-
-Fix change criteria for isNodeEmpty to resolve #5415
diff --git a/.changeset/early-singers-begin.md b/.changeset/early-singers-begin.md
deleted file mode 100644
index 2b37530fa..000000000
--- a/.changeset/early-singers-begin.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@tiptap/core": patch
----
-
-fix(core): findDuplicates - use Array.from when converting Set
diff --git a/.changeset/good-schools-pretend.md b/.changeset/good-schools-pretend.md
deleted file mode 100644
index 42ec46d18..000000000
--- a/.changeset/good-schools-pretend.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"@tiptap/extension-code-block": patch
-"@tiptap/extension-code-block-lowlight": patch
----
-
-`defaultLanguage` on Code Block Lowlight was not being respected properly, to address this we added `defaultLanguage` as an option to the code-block extension.
diff --git a/.changeset/hungry-poems-bake.md b/.changeset/hungry-poems-bake.md
deleted file mode 100644
index 189fc8fa2..000000000
--- a/.changeset/hungry-poems-bake.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@tiptap/core": patch
----
-
-Add `ignoreWhitespace` option to `isNodeEmpty` to ignore any whitespace and hardbreaks in a node to check for emptiness
diff --git a/.changeset/real-kiwis-double.md b/.changeset/real-kiwis-double.md
deleted file mode 100644
index d10f60149..000000000
--- a/.changeset/real-kiwis-double.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@tiptap/core": patch
----
-
-This fixes a discrepency between `getMarksBetween` and `isActive(markName)` where the position used for getMarksBetween was off by one
diff --git a/.changeset/smooth-rice-obey.md b/.changeset/smooth-rice-obey.md
deleted file mode 100644
index 8a73ee788..000000000
--- a/.changeset/smooth-rice-obey.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-"@tiptap/react": patch
----
-
-Optimize `useEditor` and `useEditorState` to reduce number of instances created while still being performant #5432
-
-The core of this change is two-fold:
- - have the effect run on every render (i.e. without a dep array)
- - schedule destruction of instances, but bail on the actual destruction if the instance was still mounted and a new instance had not been created yet
-
-It should plug a memory leak, where editor instances could be created but not cleaned up in strict mode.
-As well as fixing a bug where a re-render, with deps, was not applying new options that were set on `useEditor`.
diff --git a/.changeset/wet-terms-fetch.md b/.changeset/wet-terms-fetch.md
deleted file mode 100644
index 899f3200f..000000000
--- a/.changeset/wet-terms-fetch.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@tiptap/extension-placeholder": patch
----
-
-add back `considerAsAny` type but mark it deprecated
diff --git a/.changeset/wise-beers-reflect.md b/.changeset/wise-beers-reflect.md
deleted file mode 100644
index 7815d7891..000000000
--- a/.changeset/wise-beers-reflect.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@tiptap/extension-task-item": patch
----
-
-allow task items to be parsed when only having `