From df5609cdff27f97e11860579a7af852cf3e50ce5 Mon Sep 17 00:00:00 2001 From: Nick Perez Date: Wed, 10 Jul 2024 11:41:43 +0200 Subject: [PATCH] fix: default to rendering the editor immediately, while staying backward compatible (#5161) * fix: default to rendering the editor immediately, while staying backward compatible * feat: add `useEditorWithState` hook for extracting state and editor instance simultaneously * feat(react): add `useEditorState` hook for subscribing to selected editor state * docs: add an example to show the concept * chore: add changeset --- .changeset/fresh-chefs-agree.md | 67 ++ .../src/Examples/Performance/React/index.html | 0 .../src/Examples/Performance/React/index.jsx | 130 ++++ .../Examples/Performance/React/index.spec.js | 12 + .../Examples/Performance/React/styles.scss | 91 +++ package-lock.json | 593 +++++++++--------- packages/react/package.json | 4 +- packages/react/src/Context.tsx | 19 +- packages/react/src/index.ts | 1 + packages/react/src/useEditor.ts | 211 +++++-- packages/react/src/useEditorState.ts | 125 ++++ 11 files changed, 905 insertions(+), 348 deletions(-) create mode 100644 .changeset/fresh-chefs-agree.md create mode 100644 demos/src/Examples/Performance/React/index.html create mode 100644 demos/src/Examples/Performance/React/index.jsx create mode 100644 demos/src/Examples/Performance/React/index.spec.js create mode 100644 demos/src/Examples/Performance/React/styles.scss create mode 100644 packages/react/src/useEditorState.ts diff --git a/.changeset/fresh-chefs-agree.md b/.changeset/fresh-chefs-agree.md new file mode 100644 index 0000000000..8d214db93f --- /dev/null +++ b/.changeset/fresh-chefs-agree.md @@ -0,0 +1,67 @@ +--- +"@tiptap/react": patch +--- + +We've heard a number of complaints around the performance of our React integration, and we finally have a solution that we believe will satisfy everyone. We've made a number of optimizations to how the editor is rendered, as well give you more control over the rendering process. + +Here is a summary of the changes and how you can take advantage of them: + +- SSR rendering was holding back our ability to have an editor instance on first render of `useEditor`. We've now made the default behavior to render the editor immediately on the client. This behavior can be controlled with the new `immediatelyRender` option which when set to `false` will defer rendering until the second render (via a useEffect), this should only be used when server-side rendering. +- The default behavior of the useEditor hook is to re-render the editor on every editor transaction. Now with the `shouldRerenderOnTransaction` option, you can disable this behavior to optimize performance. Instead, to access the new editor state, you can use the `useEditorState` hook. +- `useEditorState` this new hook allows you to select from the editor instance any state you need to render your UI. This is useful when you want to optimize performance by only re-rendering the parts of your UI that need to be updated. + +Here is a usage example: + +```jsx + const editor = useEditor({ + /** + * This option gives us the control to enable the default behavior of rendering the editor immediately. + */ + immediatelyRender: true, + /** + * This option gives us the control to disable the default behavior of re-rendering the editor on every transaction. + */ + shouldRerenderOnTransaction: false, + extensions: [StarterKit], + content: ` +

+ A highly optimized editor that only re-renders when it’s necessary. +

+ `, + }) + + /** + * This hook allows us to select the editor state we want to use in our component. + */ + const currentEditorState = useEditorState({ + /** + * The editor instance we want to use. + */ + editor, + /** + * This selector allows us to select the data we want to use in our component. + * It is evaluated on every editor transaction and compared to it's previously returned value. + * You can return any data shape you want. + */ + selector: ctx => ({ + isBold: ctx.editor.isActive('bold'), + isItalic: ctx.editor.isActive('italic'), + isStrike: ctx.editor.isActive('strike'), + }), + /** + * This function allows us to customize the equality check for the selector. + * By default it is a `===` check. + */ + equalityFn: (prev, next) => { + // A deep-equal function would probably be more maintainable here, but, we use a shallow one to show that it can be customized. + if (!next) { + return false + } + return ( + prev.isBold === next.isBold + && prev.isItalic === next.isItalic + && prev.isStrike === next.isStrike + ) + }, + }) +``` diff --git a/demos/src/Examples/Performance/React/index.html b/demos/src/Examples/Performance/React/index.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/demos/src/Examples/Performance/React/index.jsx b/demos/src/Examples/Performance/React/index.jsx new file mode 100644 index 0000000000..b68b18cdce --- /dev/null +++ b/demos/src/Examples/Performance/React/index.jsx @@ -0,0 +1,130 @@ +import './styles.scss' + +import { + BubbleMenu, EditorContent, useEditor, useEditorState, +} from '@tiptap/react' +import StarterKit from '@tiptap/starter-kit' +import React from 'react' + +function EditorInstance({ shouldOptimizeRendering }) { + const countRenderRef = React.useRef(0) + + countRenderRef.current += 1 + + const editor = useEditor({ + /** + * This option gives us the control to enable the default behavior of rendering the editor immediately. + */ + immediatelyRender: true, + /** + * This option gives us the control to disable the default behavior of re-rendering the editor on every transaction. + */ + shouldRerenderOnTransaction: !shouldOptimizeRendering, + extensions: [StarterKit], + content: ` +

+ A highly optimized editor that only re-renders when it’s necessary. +

+ `, + }) + /** + * This hook allows us to select the editor state we want to use in our component. + */ + const currentEditorState = useEditorState({ + /** + * The editor instance we want to use. + */ + editor, + /** + * This selector allows us to select the data we want to use in our component. + * It is evaluated on every editor transaction and compared to it's previously returned value. + */ + selector: ctx => ({ + isBold: ctx.editor.isActive('bold'), + isItalic: ctx.editor.isActive('italic'), + isStrike: ctx.editor.isActive('strike'), + }), + /** + * This function allows us to customize the equality check for the selector. + * By default it is a `===` check. + */ + equalityFn: (prev, next) => { + // A deep-equal function would probably be more maintainable here, but, we use a shallow one to show that it can be customized. + if (!next) { + return false + } + return ( + prev.isBold === next.isBold + && prev.isItalic === next.isItalic + && prev.isStrike === next.isStrike + ) + }, + }) + + return ( + <> +
+
Number of renders: {countRenderRef.current}
+
+ {currentEditorState && ( + + + + + + )} + + + ) +} + +export default () => { + const [shouldOptimizeRendering, setShouldOptimizeRendering] = React.useState(true) + + return ( + <> +
+
+ + +
+
+ + + ) +} diff --git a/demos/src/Examples/Performance/React/index.spec.js b/demos/src/Examples/Performance/React/index.spec.js new file mode 100644 index 0000000000..89eb4b510f --- /dev/null +++ b/demos/src/Examples/Performance/React/index.spec.js @@ -0,0 +1,12 @@ +context('/src/Examples/Performance/React/', () => { + beforeEach(() => { + cy.visit('/src/Examples/Performance/React/') + }) + + it('should have a working tiptap instance', () => { + cy.get('.tiptap').then(([{ editor }]) => { + // eslint-disable-next-line + expect(editor).to.not.be.null + }) + }) +}) diff --git a/demos/src/Examples/Performance/React/styles.scss b/demos/src/Examples/Performance/React/styles.scss new file mode 100644 index 0000000000..7694d7322a --- /dev/null +++ b/demos/src/Examples/Performance/React/styles.scss @@ -0,0 +1,91 @@ +/* Basic editor styles */ +.tiptap { + :first-child { + margin-top: 0; + } + + /* List styles */ + ul, + ol { + padding: 0 1rem; + margin: 1.25rem 1rem 1.25rem 0.4rem; + + li p { + margin-top: 0.25em; + margin-bottom: 0.25em; + } + } + + /* Heading styles */ + h1, + h2, + h3, + h4, + h5, + h6 { + line-height: 1.1; + margin-top: 2.5rem; + text-wrap: pretty; + } + + h1, + h2 { + margin-top: 3.5rem; + margin-bottom: 1.5rem; + } + + h1 { + font-size: 1.4rem; + } + + h2 { + font-size: 1.2rem; + } + + h3 { + font-size: 1.1rem; + } + + h4, + h5, + h6 { + font-size: 1rem; + } + + /* Code and preformatted text styles */ + code { + background-color: var(--purple-light); + border-radius: 0.4rem; + color: var(--black); + font-size: 0.85rem; + padding: 0.25em 0.3em; + } + + pre { + background: var(--black); + border-radius: 0.5rem; + color: var(--white); + font-family: 'JetBrainsMono', monospace; + margin: 1.5rem 0; + padding: 0.75rem 1rem; + + code { + background: none; + color: inherit; + font-size: 0.8rem; + padding: 0; + } + } + + blockquote { + border-left: 3px solid var(--gray-3); + margin: 1.5rem 0; + padding-left: 1rem; + } + + hr { + border: none; + border-top: 1px solid var(--gray-2); + margin: 2rem 0; + } +} diff --git a/package-lock.json b/package-lock.json index 7f4161a267..5be747ea1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -141,12 +141,11 @@ }, "demos/node_modules/@esbuild/darwin-arm64": { "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -631,9 +630,8 @@ }, "demos/node_modules/jsonfile": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -907,9 +905,8 @@ }, "demos/node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.0.0" } @@ -1067,9 +1064,8 @@ }, "demos/node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, + "license": "ISC", "optional": true, "peer": true }, @@ -4106,22 +4102,22 @@ } }, "node_modules/@hocuspocus/common": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@hocuspocus/common/-/common-2.13.2.tgz", - "integrity": "sha512-NMsXx/Dl9xu1KlhNbGzLYLOjKtUOVmU+zA/br+EA4DhNDEtGWvHAmqY5r9SBTr2mGtGJagt+GEqZydSzBUFj4g==", + "version": "2.13.5", + "resolved": "https://registry.npmjs.org/@hocuspocus/common/-/common-2.13.5.tgz", + "integrity": "sha512-8D9FzhZFlt0WsgXw5yT2zwSxi6z9d4V2vUz6co2vo3Cj+Y2bvGZsdDiTvU/MerGcCLME5k/w6PwLPojLYH/4pg==", "dependencies": { "lib0": "^0.2.87" } }, "node_modules/@hocuspocus/provider": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@hocuspocus/provider/-/provider-2.13.2.tgz", - "integrity": "sha512-Pi+b8gcXHomSDRzohbmVW4dwo5OIqEBoLVrsuEG/pCCS13gBAeUCKCxfrl0q5XCNhK1nu3/j6UW9lxuExqiY+g==", + "version": "2.13.5", + "resolved": "https://registry.npmjs.org/@hocuspocus/provider/-/provider-2.13.5.tgz", + "integrity": "sha512-G3S0OiFSYkmbOwnbhV7FyJs4OBqB/+1YT9c44Ujux1RKowGm5H8+0p3FUHfXwd/3v9V0jE+E1FnFKoGonJSQwA==", "dependencies": { - "@hocuspocus/common": "^2.13.2", + "@hocuspocus/common": "^2.13.5", "@lifeomic/attempt": "^3.0.2", "lib0": "^0.2.87", - "ws": "^8.14.2" + "ws": "^8.17.1" }, "peerDependencies": { "y-protocols": "^1.0.6", @@ -5310,9 +5306,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", - "integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -5372,6 +5368,10 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "license": "MIT" + }, "node_modules/@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -5904,9 +5904,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", - "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -6679,9 +6679,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001639", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz", - "integrity": "sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==", + "version": "1.0.30001640", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz", + "integrity": "sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==", "dev": true, "funding": [ { @@ -8395,9 +8395,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.816", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.816.tgz", - "integrity": "sha512-EKH5X5oqC6hLmiS7/vYtZHZFTNdhsYG5NVPRN6Yn0kQHNBlT59+xSM8HBy66P5fxWpKgZbPqb+diC64ng295Jw==", + "version": "1.4.818", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.818.tgz", + "integrity": "sha512-eGvIk2V0dGImV9gWLq8fDfTTsCAeMDwZqEPMr+jMInxZdnp9Us8UpovYpRCf9NQ7VOFgrN2doNSgvISbsbNpxA==", "dev": true }, "node_modules/emoji-regex": { @@ -9984,9 +9984,9 @@ } }, "node_modules/glob": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", - "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.3.tgz", + "integrity": "sha512-Q38SGlYRpVtDBPSWEylRyctn7uDeTp4NQERTLiCT1FqA9JXPYWqAVmQU6qh4r/zMM5ehxTcbaO8EjhWnvEhmyg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", @@ -10000,7 +10000,7 @@ "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -10286,9 +10286,9 @@ } }, "node_modules/highlight.js": { - "version": "11.9.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", - "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", + "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" } @@ -10418,9 +10418,9 @@ ] }, "node_modules/iframe-resizer": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/iframe-resizer/-/iframe-resizer-4.4.2.tgz", - "integrity": "sha512-2SupFCq9V9osWac4q+PodF0E9QdWY5A9VdCpKrrE7HlDrcIsaTp7D6k14mkGXWoWMS9jCavYusik25wTc0YB2Q==", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/iframe-resizer/-/iframe-resizer-4.4.4.tgz", + "integrity": "sha512-9E4+pTpOPEeSz3P/o04uy5hpG+z/fy+BDzQnM3PT/G1zINRqgVJNdHgO8bJ8s8szLFztjKugnjtvZdvBJ5UXWQ==", "dev": true, "hasInstallScript": true, "engines": { @@ -11094,15 +11094,15 @@ "dev": true }, "node_modules/jackspeak": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", - "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.1.tgz", + "integrity": "sha512-U23pQPDnmYybVkYjObcuYMk43VRlMLLqLI+RdZy8s8WV8WsxO9SnqSroKaluuvcNOdCAlauKszDwd+umbot5Mg==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -13043,12 +13043,12 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", - "integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.1.tgz", + "integrity": "sha512-9/8QXrtbGeMB6LxwQd4x1tIMnsmUxMvIH/qWGsccz6bt9Uln3S+sgAaqfQNhbGA8ufzs2fHuP/yqapGgP9Hh2g==", "dev": true, "engines": { - "node": "14 || >=16.14" + "node": ">=18" } }, "node_modules/path-type": { @@ -13319,15 +13319,15 @@ "dev": true }, "node_modules/preferred-pm": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.1.3.tgz", - "integrity": "sha512-MkXsENfftWSRpzCzImcp4FRsCc3y1opwB73CfCNWyzMqArju2CrlMHlqB7VexKiPEOjGMbttv1r9fSCn5S610w==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.1.4.tgz", + "integrity": "sha512-lEHd+yEm22jXdCphDrkvIJQU66EuLojPPtvZkpKIkiD+l0DMThF/niqZKJSoU8Vl7iuvtmzyMhir9LdVy5WMnA==", "dev": true, "dependencies": { "find-up": "^5.0.0", "find-yarn-workspace-root2": "1.2.16", "path-exists": "^4.0.0", - "which-pm": "2.0.0" + "which-pm": "^2.2.0" }, "engines": { "node": ">=10" @@ -16143,9 +16143,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "dev": true, "funding": [ { @@ -16191,6 +16191,13 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -16524,9 +16531,9 @@ } }, "node_modules/which-pm": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-pm/-/which-pm-2.0.0.tgz", - "integrity": "sha512-Lhs9Pmyph0p5n5Z3mVnN0yWcbQYUAD7rbQUiMsQxOJ3T57k7RFe35SUwWMf7dsbDZks1uOmw4AecB/JMDj3v/w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/which-pm/-/which-pm-2.2.0.tgz", + "integrity": "sha512-MOiaDbA5ZZgUjkeMWM5EkJp4loW5ZRoa5bc3/aeMox/PJelMhE6t7S/mLuiY43DBupyxH+S0U1bTui9kWUlmsw==", "dev": true, "dependencies": { "load-yaml-file": "^0.2.0", @@ -16681,9 +16688,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "engines": { "node": ">=10.0.0" }, @@ -16860,159 +16867,159 @@ }, "packages/core": { "name": "@tiptap/core", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/pm": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/pm": "^2.5.0-pre.13" } }, "packages/extension-blockquote": { "name": "@tiptap/extension-blockquote", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-bold": { "name": "@tiptap/extension-bold", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-bubble-menu": { "name": "@tiptap/extension-bubble-menu", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "dependencies": { "tippy.js": "^6.3.7" }, "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" } }, "packages/extension-bullet-list": { "name": "@tiptap/extension-bullet-list", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-character-count": { "name": "@tiptap/extension-character-count", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" } }, "packages/extension-code": { "name": "@tiptap/extension-code", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-code-block": { "name": "@tiptap/extension-code-block", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" } }, "packages/extension-code-block-lowlight": { "name": "@tiptap/extension-code-block-lowlight", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/extension-code-block": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/extension-code-block": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/extension-code-block": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/extension-code-block": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" } }, "packages/extension-collaboration": { "name": "@tiptap/extension-collaboration", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8", + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13", "y-prosemirror": "^1.2.6" }, "funding": { @@ -17020,17 +17027,17 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8", + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13", "y-prosemirror": "^1.2.6" } }, "packages/extension-collaboration-cursor": { "name": "@tiptap/extension-collaboration-cursor", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", + "@tiptap/core": "^2.5.0-pre.13", "y-prosemirror": "^1.2.6" }, "funding": { @@ -17038,7 +17045,7 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", + "@tiptap/core": "^2.5.0-pre.13", "y-prosemirror": "^1.2.6" } }, @@ -17090,601 +17097,601 @@ }, "packages/extension-color": { "name": "@tiptap/extension-color", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/extension-text-style": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/extension-text-style": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/extension-text-style": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/extension-text-style": "^2.5.0-pre.13" } }, "packages/extension-document": { "name": "@tiptap/extension-document", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-dropcursor": { "name": "@tiptap/extension-dropcursor", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" } }, "packages/extension-floating-menu": { "name": "@tiptap/extension-floating-menu", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "dependencies": { "tippy.js": "^6.3.7" }, "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" } }, "packages/extension-focus": { "name": "@tiptap/extension-focus", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" } }, "packages/extension-font-family": { "name": "@tiptap/extension-font-family", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/extension-text-style": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/extension-text-style": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/extension-text-style": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/extension-text-style": "^2.5.0-pre.13" } }, "packages/extension-gapcursor": { "name": "@tiptap/extension-gapcursor", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" } }, "packages/extension-hard-break": { "name": "@tiptap/extension-hard-break", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-heading": { "name": "@tiptap/extension-heading", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-highlight": { "name": "@tiptap/extension-highlight", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-history": { "name": "@tiptap/extension-history", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" } }, "packages/extension-horizontal-rule": { "name": "@tiptap/extension-horizontal-rule", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" } }, "packages/extension-image": { "name": "@tiptap/extension-image", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-italic": { "name": "@tiptap/extension-italic", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-link": { "name": "@tiptap/extension-link", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "dependencies": { "linkifyjs": "^4.1.0" }, "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" } }, "packages/extension-list-item": { "name": "@tiptap/extension-list-item", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-list-keymap": { "name": "@tiptap/extension-list-keymap", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-mention": { "name": "@tiptap/extension-mention", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8", - "@tiptap/suggestion": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13", + "@tiptap/suggestion": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8", - "@tiptap/suggestion": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13", + "@tiptap/suggestion": "^2.5.0-pre.13" } }, "packages/extension-ordered-list": { "name": "@tiptap/extension-ordered-list", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-paragraph": { "name": "@tiptap/extension-paragraph", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-placeholder": { "name": "@tiptap/extension-placeholder", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" } }, "packages/extension-strike": { "name": "@tiptap/extension-strike", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-subscript": { "name": "@tiptap/extension-subscript", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-superscript": { "name": "@tiptap/extension-superscript", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-table": { "name": "@tiptap/extension-table", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" } }, "packages/extension-table-cell": { "name": "@tiptap/extension-table-cell", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-table-header": { "name": "@tiptap/extension-table-header", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-table-row": { "name": "@tiptap/extension-table-row", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-task-item": { "name": "@tiptap/extension-task-item", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" } }, "packages/extension-task-list": { "name": "@tiptap/extension-task-list", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-text": { "name": "@tiptap/extension-text", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-text-align": { "name": "@tiptap/extension-text-align", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-text-style": { "name": "@tiptap/extension-text-style", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-typography": { "name": "@tiptap/extension-typography", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-underline": { "name": "@tiptap/extension-underline", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/extension-youtube": { "name": "@tiptap/extension-youtube", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13" } }, "packages/html": { "name": "@tiptap/html", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "dependencies": { "zeed-dom": "^0.10.9" }, "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" } }, "packages/pm": { "name": "@tiptap/pm", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "dependencies": { "prosemirror-changeset": "^2.2.1", @@ -17772,15 +17779,17 @@ }, "packages/react": { "name": "@tiptap/react", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "dependencies": { - "@tiptap/extension-bubble-menu": "^2.5.0-pre.8", - "@tiptap/extension-floating-menu": "^2.5.0-pre.8" + "@tiptap/extension-bubble-menu": "^2.5.0-pre.13", + "@tiptap/extension-floating-menu": "^2.5.0-pre.13", + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.2.2" }, "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8", + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "react": "^18.0.0", @@ -17791,36 +17800,36 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8", + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13", "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.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "dependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/extension-blockquote": "^2.5.0-pre.8", - "@tiptap/extension-bold": "^2.5.0-pre.8", - "@tiptap/extension-bullet-list": "^2.5.0-pre.8", - "@tiptap/extension-code": "^2.5.0-pre.8", - "@tiptap/extension-code-block": "^2.5.0-pre.8", - "@tiptap/extension-document": "^2.5.0-pre.8", - "@tiptap/extension-dropcursor": "^2.5.0-pre.8", - "@tiptap/extension-gapcursor": "^2.5.0-pre.8", - "@tiptap/extension-hard-break": "^2.5.0-pre.8", - "@tiptap/extension-heading": "^2.5.0-pre.8", - "@tiptap/extension-history": "^2.5.0-pre.8", - "@tiptap/extension-horizontal-rule": "^2.5.0-pre.8", - "@tiptap/extension-italic": "^2.5.0-pre.8", - "@tiptap/extension-list-item": "^2.5.0-pre.8", - "@tiptap/extension-ordered-list": "^2.5.0-pre.8", - "@tiptap/extension-paragraph": "^2.5.0-pre.8", - "@tiptap/extension-strike": "^2.5.0-pre.8", - "@tiptap/extension-text": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/extension-blockquote": "^2.5.0-pre.13", + "@tiptap/extension-bold": "^2.5.0-pre.13", + "@tiptap/extension-bullet-list": "^2.5.0-pre.13", + "@tiptap/extension-code": "^2.5.0-pre.13", + "@tiptap/extension-code-block": "^2.5.0-pre.13", + "@tiptap/extension-document": "^2.5.0-pre.13", + "@tiptap/extension-dropcursor": "^2.5.0-pre.13", + "@tiptap/extension-gapcursor": "^2.5.0-pre.13", + "@tiptap/extension-hard-break": "^2.5.0-pre.13", + "@tiptap/extension-heading": "^2.5.0-pre.13", + "@tiptap/extension-history": "^2.5.0-pre.13", + "@tiptap/extension-horizontal-rule": "^2.5.0-pre.13", + "@tiptap/extension-italic": "^2.5.0-pre.13", + "@tiptap/extension-list-item": "^2.5.0-pre.13", + "@tiptap/extension-ordered-list": "^2.5.0-pre.13", + "@tiptap/extension-paragraph": "^2.5.0-pre.13", + "@tiptap/extension-strike": "^2.5.0-pre.13", + "@tiptap/extension-text": "^2.5.0-pre.13" }, "funding": { "type": "github", @@ -17829,33 +17838,33 @@ }, "packages/suggestion": { "name": "@tiptap/suggestion", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8" + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13" } }, "packages/vue-2": { "name": "@tiptap/vue-2", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "dependencies": { - "@tiptap/extension-bubble-menu": "^2.5.0-pre.8", - "@tiptap/extension-floating-menu": "^2.5.0-pre.8", + "@tiptap/extension-bubble-menu": "^2.5.0-pre.13", + "@tiptap/extension-floating-menu": "^2.5.0-pre.13", "vue-ts-types": "^1.6.0" }, "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8", + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13", "vue": "^2.6.0" }, "funding": { @@ -17863,8 +17872,8 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8", + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13", "vue": "^2.6.0" } }, @@ -17875,15 +17884,15 @@ }, "packages/vue-3": { "name": "@tiptap/vue-3", - "version": "2.5.0-pre.8", + "version": "2.5.0-pre.13", "license": "MIT", "dependencies": { - "@tiptap/extension-bubble-menu": "^2.5.0-pre.8", - "@tiptap/extension-floating-menu": "^2.5.0-pre.8" + "@tiptap/extension-bubble-menu": "^2.5.0-pre.13", + "@tiptap/extension-floating-menu": "^2.5.0-pre.13" }, "devDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8", + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13", "vue": "^3.0.0" }, "funding": { @@ -17891,8 +17900,8 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.5.0-pre.8", - "@tiptap/pm": "^2.5.0-pre.8", + "@tiptap/core": "^2.5.0-pre.13", + "@tiptap/pm": "^2.5.0-pre.13", "vue": "^3.0.0" } } diff --git a/packages/react/package.json b/packages/react/package.json index b2e6312aa3..6084c981d7 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -30,7 +30,9 @@ ], "dependencies": { "@tiptap/extension-bubble-menu": "^2.5.0-pre.13", - "@tiptap/extension-floating-menu": "^2.5.0-pre.13" + "@tiptap/extension-floating-menu": "^2.5.0-pre.13", + "use-sync-external-store": "^1.2.2", + "@types/use-sync-external-store": "^0.0.6" }, "devDependencies": { "@tiptap/core": "^2.5.0-pre.13", diff --git a/packages/react/src/Context.tsx b/packages/react/src/Context.tsx index 8046213e77..d5b24f5624 100644 --- a/packages/react/src/Context.tsx +++ b/packages/react/src/Context.tsx @@ -1,9 +1,8 @@ -import { EditorOptions } from '@tiptap/core' import React, { createContext, ReactNode, useContext } from 'react' import { Editor } from './Editor.js' import { EditorContent } from './EditorContent.js' -import { useEditor } from './useEditor.js' +import { useEditor, UseEditorOptions } from './useEditor.js' export type EditorContextValue = { editor: Editor | null; @@ -15,17 +14,25 @@ export const EditorContext = createContext({ export const EditorConsumer = EditorContext.Consumer +/** + * A hook to get the current editor instance. + */ export const useCurrentEditor = () => useContext(EditorContext) export type EditorProviderProps = { children?: ReactNode; slotBefore?: ReactNode; slotAfter?: ReactNode; -} & Partial - -export const EditorProvider = ({ +} & UseEditorOptions + +/** + * This is the provider component for the editor. + * It allows the editor to be accessible across the entire component tree + * with `useCurrentEditor`. + */ +export function EditorProvider({ children, slotAfter, slotBefore, ...editorOptions -}: EditorProviderProps) => { +}: EditorProviderProps) { const editor = useEditor(editorOptions) if (!editor) { diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 69128042f9..12f14edffc 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -8,5 +8,6 @@ export * from './NodeViewWrapper.js' export * from './ReactNodeViewRenderer.js' export * from './ReactRenderer.js' export * from './useEditor.js' +export * from './useEditorState.js' export * from './useReactNodeView.js' export * from '@tiptap/core' diff --git a/packages/react/src/useEditor.ts b/packages/react/src/useEditor.ts index b962238aee..d6abca92d8 100644 --- a/packages/react/src/useEditor.ts +++ b/packages/react/src/useEditor.ts @@ -1,12 +1,45 @@ import { EditorOptions } from '@tiptap/core' import { - DependencyList, - useEffect, - useRef, - useState, + DependencyList, useDebugValue, useEffect, useRef, useState, } from 'react' import { Editor } from './Editor.js' +import { useEditorState } from './useEditorState.js' + +const isDev = process.env.NODE_ENV !== 'production' +const isSSR = typeof window === 'undefined' +const isNext = isSSR || Boolean(typeof window !== 'undefined' && (window as any).next) + +/** + * The options for the `useEditor` hook. + */ +export type UseEditorOptions = Partial & { + /** + * Whether to render the editor on the first render. + * If client-side rendering, set this to `true`. + * If server-side rendering, set this to `false`. + * @default true + */ + immediatelyRender?: boolean; + /** + * Whether to re-render the editor on each transaction. + * This is legacy behavior that will be removed in future versions. + * @default true + */ + shouldRerenderOnTransaction?: boolean; +}; + +/** + * This hook allows you to create an editor instance. + * @param options The editor options + * @param deps The dependencies to watch for changes + * @returns The editor instance + * @example const editor = useEditor({ extensions: [...] }) + */ +export function useEditor( + options: UseEditorOptions & { immediatelyRender: true }, + deps?: DependencyList +): Editor; /** * This hook allows you to create an editor instance. @@ -15,9 +48,67 @@ import { Editor } from './Editor.js' * @returns The editor instance * @example const editor = useEditor({ extensions: [...] }) */ -export const useEditor = (options: Partial = {}, deps: DependencyList = []) => { - const editorRef = useRef(null) - const [, forceUpdate] = useState({}) +export function useEditor( + options?: UseEditorOptions, + deps?: DependencyList +): Editor | null; + +export function useEditor( + options: UseEditorOptions = {}, + deps: DependencyList = [], +): Editor | null { + 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 + } + + // Default to immediately rendering when client-side rendering + return new Editor(options) + } + + 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 new Editor(options) + } + + return null + }) + + useDebugValue(editor) + + // This effect will handle creating/updating the editor instance + useEffect(() => { + let editorInstance: Editor | null = editor + + if (!editorInstance) { + editorInstance = new Editor(options) + // instantiate the editor if it doesn't exist + // for ssr, this is the first time the editor is created + setEditor(editorInstance) + } else { + // if the editor does exist, update the editor options accordingly + editorInstance.setOptions(options) + } + }, deps) const { onBeforeCreate, @@ -44,96 +135,118 @@ export const useEditor = (options: Partial = {}, deps: Dependency // This effect will handle updating the editor instance // when the event handlers change. useEffect(() => { - if (!editorRef.current) { + if (!editor) { return } if (onBeforeCreate) { - editorRef.current.off('beforeCreate', onBeforeCreateRef.current) - editorRef.current.on('beforeCreate', onBeforeCreate) + editor.off('beforeCreate', onBeforeCreateRef.current) + editor.on('beforeCreate', onBeforeCreate) onBeforeCreateRef.current = onBeforeCreate } if (onBlur) { - editorRef.current.off('blur', onBlurRef.current) - editorRef.current.on('blur', onBlur) + editor.off('blur', onBlurRef.current) + editor.on('blur', onBlur) onBlurRef.current = onBlur } if (onCreate) { - editorRef.current.off('create', onCreateRef.current) - editorRef.current.on('create', onCreate) + editor.off('create', onCreateRef.current) + editor.on('create', onCreate) onCreateRef.current = onCreate } if (onDestroy) { - editorRef.current.off('destroy', onDestroyRef.current) - editorRef.current.on('destroy', onDestroy) + editor.off('destroy', onDestroyRef.current) + editor.on('destroy', onDestroy) onDestroyRef.current = onDestroy } if (onFocus) { - editorRef.current.off('focus', onFocusRef.current) - editorRef.current.on('focus', onFocus) + editor.off('focus', onFocusRef.current) + editor.on('focus', onFocus) onFocusRef.current = onFocus } if (onSelectionUpdate) { - editorRef.current.off('selectionUpdate', onSelectionUpdateRef.current) - editorRef.current.on('selectionUpdate', onSelectionUpdate) + editor.off('selectionUpdate', onSelectionUpdateRef.current) + editor.on('selectionUpdate', onSelectionUpdate) onSelectionUpdateRef.current = onSelectionUpdate } if (onTransaction) { - editorRef.current.off('transaction', onTransactionRef.current) - editorRef.current.on('transaction', onTransaction) + editor.off('transaction', onTransactionRef.current) + editor.on('transaction', onTransaction) onTransactionRef.current = onTransaction } if (onUpdate) { - editorRef.current.off('update', onUpdateRef.current) - editorRef.current.on('update', onUpdate) + editor.off('update', onUpdateRef.current) + editor.on('update', onUpdate) onUpdateRef.current = onUpdate } if (onContentError) { - editorRef.current.off('contentError', onContentErrorRef.current) - editorRef.current.on('contentError', onContentError) + editor.off('contentError', onContentErrorRef.current) + editor.on('contentError', onContentError) onContentErrorRef.current = onContentError } - }, [onBeforeCreate, onBlur, onCreate, onDestroy, onFocus, onSelectionUpdate, onTransaction, onUpdate, editorRef.current]) - + }, [ + onBeforeCreate, + onBlur, + onCreate, + onDestroy, + onFocus, + onSelectionUpdate, + onTransaction, + onUpdate, + onContentError, + editor, + ]) + + /** + * Destroy the editor instance when the component completely unmounts + * As opposed to the cleanup function in the effect above, this will + * only be called when the component is removed from the DOM, since it has no deps. + * */ useEffect(() => { - let isMounted = true - - const editor = new Editor(options) - - editorRef.current = editor - - editorRef.current.on('transaction', () => { - requestAnimationFrame(() => { - requestAnimationFrame(() => { - if (isMounted) { - forceUpdate({}) - } - }) - }) - }) - return () => { - isMounted = false - editor.destroy() - } - }, deps) + if (editor) { + // We need to destroy the editor asynchronously to avoid memory leaks + // because the editor instance is still being used in the component. - return editorRef.current + setTimeout(() => (editor.isDestroyed ? null : editor.destroy())) + } + } + }, []) + + // The default behavior is to re-render on each transaction + // This is legacy behavior that will be removed in future versions + useEditorState({ + editor, + selector: ({ transactionNumber }) => { + if (options.shouldRerenderOnTransaction === false) { + // This will prevent the editor from re-rendering on each transaction + return null + } + + // This will avoid re-rendering on the first transaction when `immediatelyRender` is set to `true` + if (options.immediatelyRender && transactionNumber === 0) { + return 0 + } + return transactionNumber + 1 + }, + }) + + return editor } diff --git a/packages/react/src/useEditorState.ts b/packages/react/src/useEditorState.ts new file mode 100644 index 0000000000..edbfc81ed4 --- /dev/null +++ b/packages/react/src/useEditorState.ts @@ -0,0 +1,125 @@ +import { useDebugValue, useEffect, useState } from 'react' +import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector' + +import type { Editor } from './Editor.js' + +export type EditorStateSnapshot = { + editor: TEditor; + transactionNumber: number; +}; +export type UseEditorStateOptions< + TSelectorResult, + TEditor extends Editor | null = Editor | null, +> = { + /** + * The editor instance. + */ + editor: TEditor; + /** + * A selector function to determine the value to compare for re-rendering. + */ + selector: (context: EditorStateSnapshot) => TSelectorResult; + /** + * A custom equality function to determine if the editor should re-render. + * @default `(a, b) => a === b` + */ + equalityFn?: (a: TSelectorResult, b: TSelectorResult | null) => boolean; +}; + +/** + * 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>() + + 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 + + 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()) + } + + const currentEditor = editor + + currentEditor.on('transaction', fn) + return () => { + currentEditor.off('transaction', fn) + } + } + }, + } + + return editorInstance +} + +export function useEditorState( + options: UseEditorStateOptions +): TSelectorResult | null; +export function useEditorState( + options: UseEditorStateOptions +): TSelectorResult; + +export function useEditorState( + options: UseEditorStateOptions | UseEditorStateOptions, +): TSelectorResult | null { + const [editorInstance] = useState(() => makeEditorStateInstance(options.editor)) + + // Using the `useSyncExternalStore` hook to sync the editor instance with the component state + const selectedState = useSyncExternalStoreWithSelector( + editorInstance.subscribe, + editorInstance.getSnapshot, + editorInstance.getServerSnapshot, + options.selector as UseEditorStateOptions['selector'], + options.equalityFn, + ) + + useEffect(() => { + return editorInstance.watch(options.editor) + }, [options.editor]) + + useDebugValue(selectedState) + + return selectedState +}