mirror of
https://github.com/ueberdosis/tiptap.git
synced 2025-06-07 17:43:49 +08:00
Merge branch 'develop' into next
This commit is contained in:
commit
06ffa88cf6
5
.changeset/pink-bobcats-grin.md
Normal file
5
.changeset/pink-bobcats-grin.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tiptap/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Resolve several selection related bug #2690 #5208
|
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -114,7 +114,7 @@ jobs:
|
|||||||
quiet: true
|
quiet: true
|
||||||
|
|
||||||
- name: Export screenshots (on failure only)
|
- name: Export screenshots (on failure only)
|
||||||
uses: actions/upload-artifact@v4.3.5
|
uses: actions/upload-artifact@v4.3.6
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: cypress-screenshots
|
name: cypress-screenshots
|
||||||
@ -122,7 +122,7 @@ jobs:
|
|||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
- name: Export screen recordings (on failure only)
|
- name: Export screen recordings (on failure only)
|
||||||
uses: actions/upload-artifact@v4.3.5
|
uses: actions/upload-artifact@v4.3.6
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: cypress-videos
|
name: cypress-videos
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2023, Tiptap GmbH
|
Copyright (c) 2024, Tiptap GmbH
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@ -93,7 +93,7 @@ For help, discussion about best practices, or any other conversation that would
|
|||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
[iFixit](https://www.ifixit.com/), [ApostropheCMS](https://apostrophecms.com/), [Novadiscovery](http://www.novadiscovery.com/), [Omics Data Automation](https://www.omicsautomation.com), [Flow Mobile](https://www.flowmobile.app/), [DocIQ](https://www.dociq.io/) and [hundreds of awesome inviduals](https://github.com/sponsors/ueberdosis).
|
[iFixit](https://www.ifixit.com/), [ApostropheCMS](https://apostrophecms.com/), [Novadiscovery](http://www.novadiscovery.com/), [Omics Data Automation](https://www.omicsautomation.com), [Flow Mobile](https://www.flowmobile.app/), [DocIQ](https://www.dociq.io/) and [hundreds of awesome individuals](https://github.com/sponsors/ueberdosis).
|
||||||
|
|
||||||
### Contributing
|
### Contributing
|
||||||
Feel like adding some magic of your own to Tiptap Editor Core? We welcome contributions! Please see our [CONTRIBUTING](CONTRIBUTING.md) guidelines for how to get started.
|
Feel like adding some magic of your own to Tiptap Editor Core? We welcome contributions! Please see our [CONTRIBUTING](CONTRIBUTING.md) guidelines for how to get started.
|
||||||
|
@ -20,6 +20,7 @@ prosemirror-view
|
|||||||
react
|
react
|
||||||
react-dom
|
react-dom
|
||||||
react-dom/client
|
react-dom/client
|
||||||
|
use-sync-external-store/shim
|
||||||
use-sync-external-store/shim/with-selector
|
use-sync-external-store/shim/with-selector
|
||||||
shiki
|
shiki
|
||||||
simplify-js
|
simplify-js
|
||||||
@ -27,3 +28,5 @@ simplify-js
|
|||||||
uuid
|
uuid
|
||||||
y-webrtc
|
y-webrtc
|
||||||
yjs
|
yjs
|
||||||
|
@hocuspocus/provider
|
||||||
|
@lifeomic/attempt
|
||||||
|
@ -10,8 +10,9 @@
|
|||||||
"ts": "tsc --project tsconfig.base.json --noEmit && tsc --project tsconfig.react.json --noEmit && tsc --project tsconfig.vue-2.json --noEmit && tsc --project tsconfig.vue-3.json --noEmit"
|
"ts": "tsc --project tsconfig.base.json --noEmit && tsc --project tsconfig.react.json --noEmit && tsc --project tsconfig.vue-2.json --noEmit && tsc --project tsconfig.vue-3.json --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hocuspocus/provider": "^2.13.5",
|
"@hocuspocus/provider": "2.13.5",
|
||||||
"@lexical/react": "^0.11.1",
|
"@lexical/react": "^0.11.1",
|
||||||
|
"@shikijs/core": "1.10.3",
|
||||||
"d3": "^7.3.0",
|
"d3": "^7.3.0",
|
||||||
"fast-glob": "^3.2.11",
|
"fast-glob": "^3.2.11",
|
||||||
"highlight.js": "^11.10.0",
|
"highlight.js": "^11.10.0",
|
||||||
@ -20,16 +21,17 @@
|
|||||||
"remixicon": "^2.5.0",
|
"remixicon": "^2.5.0",
|
||||||
"shiki": "^1.10.3",
|
"shiki": "^1.10.3",
|
||||||
"simplify-js": "^1.2.4",
|
"simplify-js": "^1.2.4",
|
||||||
"y-prosemirror": "^1.2.11",
|
"y-prosemirror": "1.2.11",
|
||||||
"y-webrtc": "^10.3.0",
|
"y-webrtc": "^10.3.0",
|
||||||
"yjs": "^13.6.18"
|
"yjs": "13.6.18"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^2.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"@vitejs/plugin-react": "^1.3.2",
|
"@vitejs/plugin-react": "^1.3.2",
|
||||||
"@vitejs/plugin-vue": "^5.0.0",
|
"@vitejs/plugin-vue": "^5.0.0",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
|
"esbuild": "0.21.5",
|
||||||
"iframe-resizer": "^4.3.2",
|
"iframe-resizer": "^4.3.2",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"postcss-import": "^15.1.0",
|
"postcss-import": "^15.1.0",
|
||||||
@ -37,7 +39,7 @@
|
|||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
"sass": "^1.49.7",
|
"sass": "^1.49.7",
|
||||||
"svelte": "^3.49.0",
|
"svelte": "^4.0.0",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
|
@ -3,5 +3,22 @@ context('/src/Demos/CollaborationSplitPane/React/', () => {
|
|||||||
cy.visit('/src/Demos/CollaborationSplitPane/React/')
|
cy.visit('/src/Demos/CollaborationSplitPane/React/')
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Write tests
|
it('should have a working tiptap instance', () => {
|
||||||
|
cy.get('.tiptap').then(([{ editor }]) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
expect(editor).to.not.be.null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have a ydoc', () => {
|
||||||
|
cy.get('.tiptap').then(([{ editor }]) => {
|
||||||
|
/**
|
||||||
|
* @type {import('yjs').Doc}
|
||||||
|
*/
|
||||||
|
const yDoc = editor.extensionManager.extensions.find(a => a.name === 'collaboration').options.document
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
expect(yDoc).to.not.be.null
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -3,5 +3,22 @@ context('/src/Extensions/Collaboration/React/', () => {
|
|||||||
cy.visit('/src/Extensions/Collaboration/React/')
|
cy.visit('/src/Extensions/Collaboration/React/')
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Write tests
|
it('should have a working tiptap instance', () => {
|
||||||
|
cy.get('.tiptap').then(([{ editor }]) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
expect(editor).to.not.be.null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have a ydoc', () => {
|
||||||
|
cy.get('.tiptap').then(([{ editor }]) => {
|
||||||
|
/**
|
||||||
|
* @type {import('yjs').Doc}
|
||||||
|
*/
|
||||||
|
const yDoc = editor.extensionManager.extensions.find(a => a.name === 'collaboration').options.document
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
expect(yDoc).to.not.be.null
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -3,5 +3,22 @@ context('/src/Extensions/Collaboration/Vue/', () => {
|
|||||||
cy.visit('/src/Extensions/Collaboration/Vue/')
|
cy.visit('/src/Extensions/Collaboration/Vue/')
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Write tests
|
it('should have a working tiptap instance', () => {
|
||||||
|
cy.get('.tiptap').then(([{ editor }]) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
expect(editor).to.not.be.null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have a ydoc', () => {
|
||||||
|
cy.get('.tiptap').then(([{ editor }]) => {
|
||||||
|
/**
|
||||||
|
* @type {import('yjs').Doc}
|
||||||
|
*/
|
||||||
|
const yDoc = editor.extensionManager.extensions.find(a => a.name === 'collaboration').options.document
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
expect(yDoc).to.not.be.null
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,24 @@
|
|||||||
context('/src/Extensions/CollaborationCursor/React', () => {
|
context('/src/Extensions/CollaborationCursor/React', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.visit('/src/Extensions/CollaborationCursor/React')
|
cy.visit('/src/Extensions/CollaborationCursor/React/')
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Write tests
|
it('should have a working tiptap instance', () => {
|
||||||
|
cy.get('.tiptap').then(([{ editor }]) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
expect(editor).to.not.be.null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have a ydoc', () => {
|
||||||
|
cy.get('.tiptap').then(([{ editor }]) => {
|
||||||
|
/**
|
||||||
|
* @type {import('yjs').Doc}
|
||||||
|
*/
|
||||||
|
const yDoc = editor.extensionManager.extensions.find(a => a.name === 'collaboration').options.document
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
expect(yDoc).to.not.be.null
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,24 @@
|
|||||||
context('/src/Extensions/CollaborationCursor/Vue', () => {
|
context('/src/Extensions/CollaborationCursor/Vue', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.visit('/src/Extensions/CollaborationCursor/Vue')
|
cy.visit('/src/Extensions/CollaborationCursor/Vue/')
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Write tests
|
it('should have a working tiptap instance', () => {
|
||||||
|
cy.get('.tiptap').then(([{ editor }]) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
expect(editor).to.not.be.null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have a ydoc', () => {
|
||||||
|
cy.get('.tiptap').then(([{ editor }]) => {
|
||||||
|
/**
|
||||||
|
* @type {import('yjs').Doc}
|
||||||
|
*/
|
||||||
|
const yDoc = editor.extensionManager.extensions.find(a => a.name === 'collaboration').options.document
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
expect(yDoc).to.not.be.null
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -21,6 +21,10 @@ const getPackageDependencies = () => {
|
|||||||
find: 'yjs',
|
find: 'yjs',
|
||||||
replacement: resolve('../node_modules/yjs/src/index.js'),
|
replacement: resolve('../node_modules/yjs/src/index.js'),
|
||||||
})
|
})
|
||||||
|
paths.push({
|
||||||
|
find: 'y-prosemirror',
|
||||||
|
replacement: resolve('../node_modules/y-prosemirror/src/y-prosemirror.js'),
|
||||||
|
})
|
||||||
|
|
||||||
fg.sync('../packages/*', { onlyDirectories: true })
|
fg.sync('../packages/*', { onlyDirectories: true })
|
||||||
.map(name => name.replace('../packages/', ''))
|
.map(name => name.replace('../packages/', ''))
|
||||||
|
6157
package-lock.json
generated
6157
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -57,6 +57,11 @@ export class Editor extends EventEmitter<EditorEvents> {
|
|||||||
|
|
||||||
public isFocused = false
|
public isFocused = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The editor is considered initialized after the `create` event has been emitted.
|
||||||
|
*/
|
||||||
|
public isInitialized = false
|
||||||
|
|
||||||
public extensionStorage: Record<string, any> = {}
|
public extensionStorage: Record<string, any> = {}
|
||||||
|
|
||||||
public options: EditorOptions = {
|
public options: EditorOptions = {
|
||||||
@ -111,6 +116,7 @@ export class Editor extends EventEmitter<EditorEvents> {
|
|||||||
|
|
||||||
this.commands.focus(this.options.autofocus)
|
this.commands.focus(this.options.autofocus)
|
||||||
this.emit('create', { editor: this })
|
this.emit('create', { editor: this })
|
||||||
|
this.isInitialized = true
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,10 +32,10 @@ declare module '@tiptap/core' {
|
|||||||
name: string
|
name: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The priority of your extension. The higher, the later it will be called
|
* The priority of your extension. The higher, the earlier it will be called
|
||||||
* and will take precedence over other extensions with a lower priority.
|
* and will take precedence over other extensions with a lower priority.
|
||||||
* @default 1000
|
* @default 100
|
||||||
* @example 1001
|
* @example 101
|
||||||
*/
|
*/
|
||||||
priority?: number
|
priority?: number
|
||||||
|
|
||||||
@ -339,6 +339,7 @@ declare module '@tiptap/core' {
|
|||||||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onTransaction']
|
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onTransaction']
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
editor: Editor
|
||||||
transaction: Transaction
|
transaction: Transaction
|
||||||
},
|
},
|
||||||
) => void)
|
) => void)
|
||||||
|
@ -35,10 +35,10 @@ declare module '@tiptap/core' {
|
|||||||
name: string
|
name: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The priority of your extension. The higher, the later it will be called
|
* The priority of your extension. The higher, the earlier it will be called
|
||||||
* and will take precedence over other extensions with a lower priority.
|
* and will take precedence over other extensions with a lower priority.
|
||||||
* @default 1000
|
* @default 100
|
||||||
* @example 1001
|
* @example 101
|
||||||
*/
|
*/
|
||||||
priority?: number
|
priority?: number
|
||||||
|
|
||||||
@ -352,6 +352,7 @@ declare module '@tiptap/core' {
|
|||||||
parent: ParentConfig<MarkConfig<Options, Storage>>['onTransaction']
|
parent: ParentConfig<MarkConfig<Options, Storage>>['onTransaction']
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
editor: Editor
|
||||||
transaction: Transaction
|
transaction: Transaction
|
||||||
},
|
},
|
||||||
) => void)
|
) => void)
|
||||||
|
@ -36,10 +36,10 @@ declare module '@tiptap/core' {
|
|||||||
name: string
|
name: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The priority of your extension. The higher, the later it will be called
|
* The priority of your extension. The higher, the earlier it will be called
|
||||||
* and will take precedence over other extensions with a lower priority.
|
* and will take precedence over other extensions with a lower priority.
|
||||||
* @default 1000
|
* @default 100
|
||||||
* @example 1001
|
* @example 101
|
||||||
*/
|
*/
|
||||||
priority?: number
|
priority?: number
|
||||||
|
|
||||||
@ -354,6 +354,7 @@ declare module '@tiptap/core' {
|
|||||||
parent: ParentConfig<NodeConfig<Options, Storage>>['onTransaction']
|
parent: ParentConfig<NodeConfig<Options, Storage>>['onTransaction']
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
editor: Editor
|
||||||
transaction: Transaction
|
transaction: Transaction
|
||||||
},
|
},
|
||||||
) => void)
|
) => void)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import type { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||||
|
|
||||||
import { RawCommands } from '../types.js'
|
import { RawCommands } from '../types.js'
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
||||||
@ -9,7 +11,7 @@ declare module '@tiptap/core' {
|
|||||||
* @param value The value to store.
|
* @param value The value to store.
|
||||||
* @example editor.commands.setMeta('foo', 'bar')
|
* @example editor.commands.setMeta('foo', 'bar')
|
||||||
*/
|
*/
|
||||||
setMeta: (key: string, value: any) => ReturnType,
|
setMeta: (key: string | Plugin | PluginKey, value: any) => ReturnType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,14 +14,15 @@ declare module '@tiptap/core' {
|
|||||||
/**
|
/**
|
||||||
* Splits one list item into two list items.
|
* Splits one list item into two list items.
|
||||||
* @param typeOrName The type or name of the node.
|
* @param typeOrName The type or name of the node.
|
||||||
|
* @param overrideAttrs The attributes to ensure on the new node.
|
||||||
* @example editor.commands.splitListItem('listItem')
|
* @example editor.commands.splitListItem('listItem')
|
||||||
*/
|
*/
|
||||||
splitListItem: (typeOrName: string | NodeType) => ReturnType
|
splitListItem: (typeOrName: string | NodeType, overrideAttrs?: Record<string, any>) => ReturnType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const splitListItem: RawCommands['splitListItem'] = typeOrName => ({
|
export const splitListItem: RawCommands['splitListItem'] = (typeOrName, overrideAttrs = {}) => ({
|
||||||
tr, state, dispatch, editor,
|
tr, state, dispatch, editor,
|
||||||
}) => {
|
}) => {
|
||||||
const type = getNodeType(typeOrName, state.schema)
|
const type = getNodeType(typeOrName, state.schema)
|
||||||
@ -70,11 +71,14 @@ export const splitListItem: RawCommands['splitListItem'] = typeOrName => ({
|
|||||||
const depthAfter = $from.indexAfter(-1) < $from.node(-2).childCount ? 1 : $from.indexAfter(-2) < $from.node(-3).childCount ? 2 : 3
|
const depthAfter = $from.indexAfter(-1) < $from.node(-2).childCount ? 1 : $from.indexAfter(-2) < $from.node(-3).childCount ? 2 : 3
|
||||||
|
|
||||||
// Add a second list item with an empty default start node
|
// Add a second list item with an empty default start node
|
||||||
const newNextTypeAttributes = getSplittedAttributes(
|
const newNextTypeAttributes = {
|
||||||
extensionAttributes,
|
...getSplittedAttributes(
|
||||||
$from.node().type.name,
|
extensionAttributes,
|
||||||
$from.node().attrs,
|
$from.node().type.name,
|
||||||
)
|
$from.node().attrs,
|
||||||
|
),
|
||||||
|
...overrideAttrs,
|
||||||
|
}
|
||||||
const nextType = type.contentMatch.defaultType?.createAndFill(newNextTypeAttributes) || undefined
|
const nextType = type.contentMatch.defaultType?.createAndFill(newNextTypeAttributes) || undefined
|
||||||
|
|
||||||
wrap = wrap.append(Fragment.from(type.createAndFill(null, nextType) || undefined))
|
wrap = wrap.append(Fragment.from(type.createAndFill(null, nextType) || undefined))
|
||||||
@ -107,16 +111,22 @@ export const splitListItem: RawCommands['splitListItem'] = typeOrName => ({
|
|||||||
|
|
||||||
const nextType = $to.pos === $from.end() ? grandParent.contentMatchAt(0).defaultType : null
|
const nextType = $to.pos === $from.end() ? grandParent.contentMatchAt(0).defaultType : null
|
||||||
|
|
||||||
const newTypeAttributes = getSplittedAttributes(
|
const newTypeAttributes = {
|
||||||
extensionAttributes,
|
...getSplittedAttributes(
|
||||||
grandParent.type.name,
|
extensionAttributes,
|
||||||
grandParent.attrs,
|
grandParent.type.name,
|
||||||
)
|
grandParent.attrs,
|
||||||
const newNextTypeAttributes = getSplittedAttributes(
|
),
|
||||||
extensionAttributes,
|
...overrideAttrs,
|
||||||
$from.node().type.name,
|
}
|
||||||
$from.node().attrs,
|
const newNextTypeAttributes = {
|
||||||
)
|
...getSplittedAttributes(
|
||||||
|
extensionAttributes,
|
||||||
|
$from.node().type.name,
|
||||||
|
$from.node().attrs,
|
||||||
|
),
|
||||||
|
...overrideAttrs,
|
||||||
|
}
|
||||||
|
|
||||||
tr.delete($from.pos, $to.pos)
|
tr.delete($from.pos, $to.pos)
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { NodeType } from '@tiptap/pm/model'
|
import { NodeType } from '@tiptap/pm/model'
|
||||||
|
|
||||||
import { PasteRule, PasteRuleFinder } from '../PasteRule.js'
|
import { PasteRule, PasteRuleFinder } from '../PasteRule.js'
|
||||||
import { ExtendedRegExpMatchArray } from '../types.js'
|
import { ExtendedRegExpMatchArray, JSONContent } from '../types.js'
|
||||||
import { callOrReturn } from '../utilities/index.js'
|
import { callOrReturn } from '../utilities/index.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,6 +17,11 @@ export function nodePasteRule(config: {
|
|||||||
| ((match: ExtendedRegExpMatchArray, event: ClipboardEvent) => Record<string, any>)
|
| ((match: ExtendedRegExpMatchArray, event: ClipboardEvent) => Record<string, any>)
|
||||||
| false
|
| false
|
||||||
| null
|
| null
|
||||||
|
getContent?:
|
||||||
|
| JSONContent[]
|
||||||
|
| ((attrs: Record<string, any>) => JSONContent[])
|
||||||
|
| false
|
||||||
|
| null
|
||||||
}) {
|
}) {
|
||||||
return new PasteRule({
|
return new PasteRule({
|
||||||
find: config.find,
|
find: config.find,
|
||||||
@ -24,16 +29,20 @@ export function nodePasteRule(config: {
|
|||||||
match, chain, range, pasteEvent,
|
match, chain, range, pasteEvent,
|
||||||
}) {
|
}) {
|
||||||
const attributes = callOrReturn(config.getAttributes, undefined, match, pasteEvent)
|
const attributes = callOrReturn(config.getAttributes, undefined, match, pasteEvent)
|
||||||
|
const content = callOrReturn(config.getContent, undefined, attributes)
|
||||||
|
|
||||||
if (attributes === false || attributes === null) {
|
if (attributes === false || attributes === null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const node = { type: config.type.name, attrs: attributes } as JSONContent
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
node.content = content
|
||||||
|
}
|
||||||
|
|
||||||
if (match.input) {
|
if (match.input) {
|
||||||
chain().deleteRange(range).insertContentAt(range.from, {
|
chain().deleteRange(range).insertContentAt(range.from, node)
|
||||||
type: config.type.name,
|
|
||||||
attrs: attributes,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -27,8 +27,8 @@ img.ProseMirror-separator {
|
|||||||
display: inline !important;
|
display: inline !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
width: 1px !important;
|
width: 0 !important;
|
||||||
height: 1px !important;
|
height: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-gapcursor {
|
.ProseMirror-gapcursor {
|
||||||
|
@ -96,6 +96,8 @@ const defaultOnUpdate = () => null
|
|||||||
export const CollaborationCursor = Extension.create<CollaborationCursorOptions, CollaborationCursorStorage>({
|
export const CollaborationCursor = Extension.create<CollaborationCursorOptions, CollaborationCursorStorage>({
|
||||||
name: 'collaborationCursor',
|
name: 'collaborationCursor',
|
||||||
|
|
||||||
|
priority: 999,
|
||||||
|
|
||||||
addOptions() {
|
addOptions() {
|
||||||
return {
|
return {
|
||||||
provider: null,
|
provider: null,
|
||||||
|
@ -164,6 +164,9 @@ export const Link = Mark.create<LinkOptions>({
|
|||||||
return {
|
return {
|
||||||
href: {
|
href: {
|
||||||
default: null,
|
default: null,
|
||||||
|
parseHTML(element) {
|
||||||
|
return element.getAttribute('href')
|
||||||
|
},
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
default: this.options.HTMLAttributes.target,
|
default: this.options.HTMLAttributes.target,
|
||||||
@ -187,7 +190,7 @@ export const Link = Mark.create<LinkOptions>({
|
|||||||
if (!href || !isAllowedUri(href)) {
|
if (!href || !isAllowedUri(href)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return { href }
|
return null
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
|
@ -84,6 +84,10 @@ export const OrderedList = Node.create<OrderedListOptions>({
|
|||||||
: 1
|
: 1
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
type: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: element => element.getAttribute('type'),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -33,6 +33,8 @@ declare module '@tiptap/core' {
|
|||||||
export const TextStyle = Mark.create<TextStyleOptions>({
|
export const TextStyle = Mark.create<TextStyleOptions>({
|
||||||
name: 'textStyle',
|
name: 'textStyle',
|
||||||
|
|
||||||
|
priority: 101,
|
||||||
|
|
||||||
addOptions() {
|
addOptions() {
|
||||||
return {
|
return {
|
||||||
HTMLAttributes: {},
|
HTMLAttributes: {},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
import { Editor } from '@tiptap/core'
|
||||||
import React, { createContext, ReactNode, useContext } from 'react'
|
import React, { createContext, ReactNode, useContext } from 'react'
|
||||||
|
|
||||||
import { Editor } from './Editor.js'
|
|
||||||
import { EditorContent } from './EditorContent.js'
|
import { EditorContent } from './EditorContent.js'
|
||||||
import { useEditor, UseEditorOptions } from './useEditor.js'
|
import { useEditor, UseEditorOptions } from './useEditor.js'
|
||||||
|
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { Editor as CoreEditor } from '@tiptap/core'
|
import { Editor } from '@tiptap/core'
|
||||||
import React from 'react'
|
import { ReactPortal } from 'react'
|
||||||
|
|
||||||
import { EditorContentProps, EditorContentState } from './EditorContent.js'
|
|
||||||
import { ReactRenderer } from './ReactRenderer.js'
|
import { ReactRenderer } from './ReactRenderer.js'
|
||||||
|
|
||||||
type ContentComponent = React.Component<EditorContentProps, EditorContentState> & {
|
export type EditorWithContentComponent = Editor & { contentComponent?: ContentComponent | null }
|
||||||
|
export type ContentComponent = {
|
||||||
setRenderer(id: string, renderer: ReactRenderer): void;
|
setRenderer(id: string, renderer: ReactRenderer): void;
|
||||||
removeRenderer(id: string): void;
|
removeRenderer(id: string): void;
|
||||||
}
|
subscribe: (callback: () => void) => () => void;
|
||||||
|
getSnapshot: () => Record<string, ReactPortal>;
|
||||||
export class Editor extends CoreEditor {
|
getServerSnapshot: () => Record<string, ReactPortal>;
|
||||||
public contentComponent: ContentComponent | null = null
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
import { Editor } from '@tiptap/core'
|
||||||
import React, {
|
import React, {
|
||||||
ForwardedRef, forwardRef, HTMLProps, LegacyRef, MutableRefObject,
|
ForwardedRef, forwardRef, HTMLProps, LegacyRef, MutableRefObject,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import ReactDOM, { flushSync } from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
|
import { useSyncExternalStore } from 'use-sync-external-store/shim'
|
||||||
|
|
||||||
import { Editor } from './Editor.js'
|
import { ContentComponent, EditorWithContentComponent } from './Editor.js'
|
||||||
import { ReactRenderer } from './ReactRenderer.js'
|
import { ReactRenderer } from './ReactRenderer.js'
|
||||||
|
|
||||||
const mergeRefs = <T extends HTMLDivElement>(
|
const mergeRefs = <T extends HTMLDivElement>(
|
||||||
@ -20,12 +22,23 @@ const mergeRefs = <T extends HTMLDivElement>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Portals: React.FC<{ renderers: Record<string, ReactRenderer> }> = ({ renderers }) => {
|
/**
|
||||||
|
* This component renders all of the editor's node views.
|
||||||
|
*/
|
||||||
|
const Portals: React.FC<{ contentComponent: ContentComponent }> = ({
|
||||||
|
contentComponent,
|
||||||
|
}) => {
|
||||||
|
// For performance reasons, we render the node view portals on state changes only
|
||||||
|
const renderers = useSyncExternalStore(
|
||||||
|
contentComponent.subscribe,
|
||||||
|
contentComponent.getSnapshot,
|
||||||
|
contentComponent.getServerSnapshot,
|
||||||
|
)
|
||||||
|
|
||||||
|
// This allows us to directly render the portals without any additional wrapper
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{Object.entries(renderers).map(([key, renderer]) => {
|
{Object.values(renderers)}
|
||||||
return ReactDOM.createPortal(renderer.reactElement, renderer.element, key)
|
|
||||||
})}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -35,22 +48,67 @@ export interface EditorContentProps extends HTMLProps<HTMLDivElement> {
|
|||||||
innerRef?: ForwardedRef<HTMLDivElement | null>;
|
innerRef?: ForwardedRef<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorContentState {
|
function getInstance(): ContentComponent {
|
||||||
renderers: Record<string, ReactRenderer>;
|
const subscribers = new Set<() => void>()
|
||||||
|
let renderers: Record<string, React.ReactPortal> = {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Subscribe to the editor instance's changes.
|
||||||
|
*/
|
||||||
|
subscribe(callback: () => void) {
|
||||||
|
subscribers.add(callback)
|
||||||
|
return () => {
|
||||||
|
subscribers.delete(callback)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getSnapshot() {
|
||||||
|
return renderers
|
||||||
|
},
|
||||||
|
getServerSnapshot() {
|
||||||
|
return renderers
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Adds a new NodeView Renderer to the editor.
|
||||||
|
*/
|
||||||
|
setRenderer(id: string, renderer: ReactRenderer) {
|
||||||
|
renderers = {
|
||||||
|
...renderers,
|
||||||
|
[id]: ReactDOM.createPortal(renderer.reactElement, renderer.element, id),
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribers.forEach(subscriber => subscriber())
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Removes a NodeView Renderer from the editor.
|
||||||
|
*/
|
||||||
|
removeRenderer(id: string) {
|
||||||
|
const nextRenderers = { ...renderers }
|
||||||
|
|
||||||
|
delete nextRenderers[id]
|
||||||
|
renderers = nextRenderers
|
||||||
|
subscribers.forEach(subscriber => subscriber())
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PureEditorContent extends React.Component<EditorContentProps, EditorContentState> {
|
export class PureEditorContent extends React.Component<
|
||||||
|
EditorContentProps,
|
||||||
|
{ hasContentComponentInitialized: boolean }
|
||||||
|
> {
|
||||||
editorContentRef: React.RefObject<any>
|
editorContentRef: React.RefObject<any>
|
||||||
|
|
||||||
initialized: boolean
|
initialized: boolean
|
||||||
|
|
||||||
|
unsubscribeToContentComponent?: () => void
|
||||||
|
|
||||||
constructor(props: EditorContentProps) {
|
constructor(props: EditorContentProps) {
|
||||||
super(props)
|
super(props)
|
||||||
this.editorContentRef = React.createRef()
|
this.editorContentRef = React.createRef()
|
||||||
this.initialized = false
|
this.initialized = false
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
renderers: {},
|
hasContentComponentInitialized: Boolean((props.editor as EditorWithContentComponent | null)?.contentComponent),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +121,7 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
|
|||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
const { editor } = this.props
|
const editor = this.props.editor as EditorWithContentComponent | null
|
||||||
|
|
||||||
if (editor && !editor.isDestroyed && editor.options.element) {
|
if (editor && !editor.isDestroyed && editor.options.element) {
|
||||||
if (editor.contentComponent) {
|
if (editor.contentComponent) {
|
||||||
@ -78,7 +136,27 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
|
|||||||
element,
|
element,
|
||||||
})
|
})
|
||||||
|
|
||||||
editor.contentComponent = this
|
editor.contentComponent = getInstance()
|
||||||
|
|
||||||
|
// Has the content component been initialized?
|
||||||
|
if (!this.state.hasContentComponentInitialized) {
|
||||||
|
// Subscribe to the content component
|
||||||
|
this.unsubscribeToContentComponent = editor.contentComponent.subscribe(() => {
|
||||||
|
this.setState(prevState => {
|
||||||
|
if (!prevState.hasContentComponentInitialized) {
|
||||||
|
return {
|
||||||
|
hasContentComponentInitialized: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prevState
|
||||||
|
})
|
||||||
|
|
||||||
|
// Unsubscribe to previous content component
|
||||||
|
if (this.unsubscribeToContentComponent) {
|
||||||
|
this.unsubscribeToContentComponent()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
editor.createNodeViews()
|
editor.createNodeViews()
|
||||||
|
|
||||||
@ -86,43 +164,8 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
maybeFlushSync(fn: () => void) {
|
|
||||||
// Avoid calling flushSync until the editor is initialized.
|
|
||||||
// Initialization happens during the componentDidMount or componentDidUpdate
|
|
||||||
// lifecycle methods, and React doesn't allow calling flushSync from inside
|
|
||||||
// a lifecycle method.
|
|
||||||
if (this.initialized) {
|
|
||||||
flushSync(fn)
|
|
||||||
} else {
|
|
||||||
fn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setRenderer(id: string, renderer: ReactRenderer) {
|
|
||||||
this.maybeFlushSync(() => {
|
|
||||||
this.setState(({ renderers }) => ({
|
|
||||||
renderers: {
|
|
||||||
...renderers,
|
|
||||||
[id]: renderer,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
removeRenderer(id: string) {
|
|
||||||
this.maybeFlushSync(() => {
|
|
||||||
this.setState(({ renderers }) => {
|
|
||||||
const nextRenderers = { ...renderers }
|
|
||||||
|
|
||||||
delete nextRenderers[id]
|
|
||||||
|
|
||||||
return { renderers: nextRenderers }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
const { editor } = this.props
|
const editor = this.props.editor as EditorWithContentComponent | null
|
||||||
|
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
return
|
return
|
||||||
@ -136,6 +179,10 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.unsubscribeToContentComponent) {
|
||||||
|
this.unsubscribeToContentComponent()
|
||||||
|
}
|
||||||
|
|
||||||
editor.contentComponent = null
|
editor.contentComponent = null
|
||||||
|
|
||||||
if (!editor.options.element.firstChild) {
|
if (!editor.options.element.firstChild) {
|
||||||
@ -158,7 +205,7 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
|
|||||||
<>
|
<>
|
||||||
<div ref={mergeRefs(innerRef, this.editorContentRef)} {...rest} />
|
<div ref={mergeRefs(innerRef, this.editorContentRef)} {...rest} />
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<Portals renderers={this.state.renderers} />
|
{editor?.contentComponent && <Portals contentComponent={editor.contentComponent} />}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -168,7 +215,8 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
|
|||||||
const EditorContentWithKey = forwardRef<HTMLDivElement, EditorContentProps>(
|
const EditorContentWithKey = forwardRef<HTMLDivElement, EditorContentProps>(
|
||||||
(props: Omit<EditorContentProps, 'innerRef'>, ref) => {
|
(props: Omit<EditorContentProps, 'innerRef'>, ref) => {
|
||||||
const key = React.useMemo(() => {
|
const key = React.useMemo(() => {
|
||||||
return Math.floor(Math.random() * 0xFFFFFFFF).toString()
|
return Math.floor(Math.random() * 0xffffffff).toString()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [props.editor])
|
}, [props.editor])
|
||||||
|
|
||||||
// Can't use JSX here because it conflicts with the type definition of Vue's JSX, so use createElement
|
// Can't use JSX here because it conflicts with the type definition of Vue's JSX, so use createElement
|
||||||
|
@ -12,6 +12,7 @@ export const NodeViewContent: React.FC<NodeViewContentProps> = props => {
|
|||||||
const { nodeViewContentRef } = useReactNodeView()
|
const { nodeViewContentRef } = useReactNodeView()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// @ts-ignore
|
||||||
<Tag
|
<Tag
|
||||||
{...props}
|
{...props}
|
||||||
ref={nodeViewContentRef}
|
ref={nodeViewContentRef}
|
||||||
|
@ -12,6 +12,7 @@ export const NodeViewWrapper: React.FC<NodeViewWrapperProps> = React.forwardRef(
|
|||||||
const Tag = props.as || 'div'
|
const Tag = props.as || 'div'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// @ts-ignore
|
||||||
<Tag
|
<Tag
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
DecorationWithType,
|
DecorationWithType,
|
||||||
|
Editor,
|
||||||
NodeView,
|
NodeView,
|
||||||
NodeViewProps,
|
NodeViewProps,
|
||||||
NodeViewRenderer,
|
NodeViewRenderer,
|
||||||
@ -10,7 +11,7 @@ import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
|||||||
import { Decoration, NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
|
import { Decoration, NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { Editor } from './Editor.js'
|
import { EditorWithContentComponent } from './Editor.js'
|
||||||
import { ReactRenderer } from './ReactRenderer.js'
|
import { ReactRenderer } from './ReactRenderer.js'
|
||||||
import { ReactNodeViewContext, ReactNodeViewContextProps } from './useReactNodeView.js'
|
import { ReactNodeViewContext, ReactNodeViewContextProps } from './useReactNodeView.js'
|
||||||
|
|
||||||
@ -58,25 +59,23 @@ class ReactNodeView extends NodeView<
|
|||||||
this.component.displayName = capitalizeFirstChar(this.extension.name)
|
this.component.displayName = capitalizeFirstChar(this.extension.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReactNodeViewProvider: React.FunctionComponent = componentProps => {
|
const onDragStart = this.onDragStart.bind(this)
|
||||||
const Component = this.component
|
const nodeViewContentRef: ReactNodeViewContextProps['nodeViewContentRef'] = element => {
|
||||||
const onDragStart = this.onDragStart.bind(this)
|
if (element && this.contentDOMElement && element.firstChild !== this.contentDOMElement) {
|
||||||
const nodeViewContentRef: ReactNodeViewContextProps['nodeViewContentRef'] = element => {
|
element.appendChild(this.contentDOMElement)
|
||||||
if (element && this.contentDOMElement && element.firstChild !== this.contentDOMElement) {
|
|
||||||
element.appendChild(this.contentDOMElement)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* @ts-ignore */}
|
|
||||||
<ReactNodeViewContext.Provider value={{ onDragStart, nodeViewContentRef }}>
|
|
||||||
{/* @ts-ignore */}
|
|
||||||
<Component {...componentProps} />
|
|
||||||
</ReactNodeViewContext.Provider>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
const context = { onDragStart, nodeViewContentRef }
|
||||||
|
const Component = this.component
|
||||||
|
// For performance reasons, we memoize the provider component
|
||||||
|
// And all of the things it requires are declared outside of the component, so it doesn't need to re-render
|
||||||
|
const ReactNodeViewProvider: React.FunctionComponent = React.memo(componentProps => {
|
||||||
|
return (
|
||||||
|
<ReactNodeViewContext.Provider value={context}>
|
||||||
|
{React.createElement(Component, componentProps)}
|
||||||
|
</ReactNodeViewContext.Provider>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
ReactNodeViewProvider.displayName = 'ReactNodeView'
|
ReactNodeViewProvider.displayName = 'ReactNodeView'
|
||||||
|
|
||||||
@ -218,7 +217,7 @@ export function ReactNodeViewRenderer(
|
|||||||
// try to get the parent component
|
// try to get the parent component
|
||||||
// this is important for vue devtools to show the component hierarchy correctly
|
// this is important for vue devtools to show the component hierarchy correctly
|
||||||
// maybe it’s `undefined` because <editor-content> isn’t rendered yet
|
// maybe it’s `undefined` because <editor-content> isn’t rendered yet
|
||||||
if (!(props.editor as Editor).contentComponent) {
|
if (!(props.editor as EditorWithContentComponent).contentComponent) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Editor } from '@tiptap/core'
|
import { Editor } from '@tiptap/core'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { flushSync } from 'react-dom'
|
||||||
|
|
||||||
import { Editor as ExtendedEditor } from './Editor.js'
|
import { EditorWithContentComponent } from './Editor.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a component is a class component.
|
* Check if a component is a class component.
|
||||||
@ -85,7 +86,7 @@ type ComponentType<R, P> =
|
|||||||
export class ReactRenderer<R = unknown, P = unknown> {
|
export class ReactRenderer<R = unknown, P = unknown> {
|
||||||
id: string
|
id: string
|
||||||
|
|
||||||
editor: ExtendedEditor
|
editor: Editor
|
||||||
|
|
||||||
component: any
|
component: any
|
||||||
|
|
||||||
@ -106,7 +107,7 @@ export class ReactRenderer<R = unknown, P = unknown> {
|
|||||||
}: ReactRendererOptions) {
|
}: ReactRendererOptions) {
|
||||||
this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString()
|
this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString()
|
||||||
this.component = component
|
this.component = component
|
||||||
this.editor = editor as ExtendedEditor
|
this.editor = editor as EditorWithContentComponent
|
||||||
this.props = props
|
this.props = props
|
||||||
this.element = document.createElement(as)
|
this.element = document.createElement(as)
|
||||||
this.element.classList.add('react-renderer')
|
this.element.classList.add('react-renderer')
|
||||||
@ -121,12 +122,21 @@ export class ReactRenderer<R = unknown, P = unknown> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.render()
|
if (this.editor.isInitialized) {
|
||||||
|
// On first render, we need to flush the render synchronously
|
||||||
|
// Renders afterwards can be async, but this fixes a cursor positioning issue
|
||||||
|
flushSync(() => {
|
||||||
|
this.render()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): void {
|
render(): void {
|
||||||
const Component = this.component
|
const Component = this.component
|
||||||
const props = this.props
|
const props = this.props
|
||||||
|
const editor = this.editor as EditorWithContentComponent
|
||||||
|
|
||||||
if (isClassComponent(Component) || isForwardRefComponent(Component)) {
|
if (isClassComponent(Component) || isForwardRefComponent(Component)) {
|
||||||
props.ref = (ref: R) => {
|
props.ref = (ref: R) => {
|
||||||
@ -134,9 +144,9 @@ export class ReactRenderer<R = unknown, P = unknown> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reactElement = <Component {...props } />
|
this.reactElement = React.createElement(Component, props)
|
||||||
|
|
||||||
this.editor?.contentComponent?.setRenderer(this.id, this)
|
editor?.contentComponent?.setRenderer(this.id, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProps(props: Record<string, any> = {}): void {
|
updateProps(props: Record<string, any> = {}): void {
|
||||||
@ -149,6 +159,8 @@ export class ReactRenderer<R = unknown, P = unknown> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
this.editor?.contentComponent?.removeRenderer(this.id)
|
const editor = this.editor as EditorWithContentComponent
|
||||||
|
|
||||||
|
editor?.contentComponent?.removeRenderer(this.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
export * from './BubbleMenu.js'
|
export * from './BubbleMenu.js'
|
||||||
export * from './Context.js'
|
export * from './Context.js'
|
||||||
export { Editor } from './Editor.js'
|
|
||||||
export * from './EditorContent.js'
|
export * from './EditorContent.js'
|
||||||
export * from './FloatingMenu.js'
|
export * from './FloatingMenu.js'
|
||||||
export * from './NodeViewContent.js'
|
export * from './NodeViewContent.js'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { EditorOptions } from '@tiptap/core'
|
import { type EditorOptions, Editor } from '@tiptap/core'
|
||||||
import {
|
import {
|
||||||
DependencyList,
|
DependencyList,
|
||||||
MutableRefObject,
|
MutableRefObject,
|
||||||
@ -9,7 +9,6 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { useSyncExternalStore } from 'use-sync-external-store/shim'
|
import { useSyncExternalStore } from 'use-sync-external-store/shim'
|
||||||
|
|
||||||
import { Editor } from './Editor.js'
|
|
||||||
import { useEditorState } from './useEditorState.js'
|
import { useEditorState } from './useEditorState.js'
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV !== 'production'
|
const isDev = process.env.NODE_ENV !== 'production'
|
||||||
@ -136,18 +135,20 @@ class EditorInstanceManager {
|
|||||||
* Create a new editor instance. And attach event listeners.
|
* Create a new editor instance. And attach event listeners.
|
||||||
*/
|
*/
|
||||||
private createEditor(): Editor {
|
private createEditor(): Editor {
|
||||||
const editor = new Editor(this.options.current)
|
const optionsToApply: Partial<EditorOptions> = {
|
||||||
|
...this.options.current,
|
||||||
// Always call the most recent version of the callback function by default
|
// Always call the most recent version of the callback function by default
|
||||||
editor.on('beforeCreate', (...args) => this.options.current.onBeforeCreate?.(...args))
|
onBeforeCreate: (...args) => this.options.current.onBeforeCreate?.(...args),
|
||||||
editor.on('blur', (...args) => this.options.current.onBlur?.(...args))
|
onBlur: (...args) => this.options.current.onBlur?.(...args),
|
||||||
editor.on('create', (...args) => this.options.current.onCreate?.(...args))
|
onCreate: (...args) => this.options.current.onCreate?.(...args),
|
||||||
editor.on('destroy', (...args) => this.options.current.onDestroy?.(...args))
|
onDestroy: (...args) => this.options.current.onDestroy?.(...args),
|
||||||
editor.on('focus', (...args) => this.options.current.onFocus?.(...args))
|
onFocus: (...args) => this.options.current.onFocus?.(...args),
|
||||||
editor.on('selectionUpdate', (...args) => this.options.current.onSelectionUpdate?.(...args))
|
onSelectionUpdate: (...args) => this.options.current.onSelectionUpdate?.(...args),
|
||||||
editor.on('transaction', (...args) => this.options.current.onTransaction?.(...args))
|
onTransaction: (...args) => this.options.current.onTransaction?.(...args),
|
||||||
editor.on('update', (...args) => this.options.current.onUpdate?.(...args))
|
onUpdate: (...args) => this.options.current.onUpdate?.(...args),
|
||||||
editor.on('contentError', (...args) => this.options.current.onContentError?.(...args))
|
onContentError: (...args) => this.options.current.onContentError?.(...args),
|
||||||
|
}
|
||||||
|
const editor = new Editor(optionsToApply)
|
||||||
|
|
||||||
// no need to keep track of the event listeners, they will be removed when the editor is destroyed
|
// no need to keep track of the event listeners, they will be removed when the editor is destroyed
|
||||||
|
|
||||||
@ -215,7 +216,6 @@ class EditorInstanceManager {
|
|||||||
* Recreate the editor instance if the dependencies have changed.
|
* Recreate the editor instance if the dependencies have changed.
|
||||||
*/
|
*/
|
||||||
private refreshEditorInstance(deps: DependencyList) {
|
private refreshEditorInstance(deps: DependencyList) {
|
||||||
|
|
||||||
if (this.editor && !this.editor.isDestroyed) {
|
if (this.editor && !this.editor.isDestroyed) {
|
||||||
// Editor instance already exists
|
// Editor instance already exists
|
||||||
if (this.previousDeps === null) {
|
if (this.previousDeps === null) {
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
|
import type { Editor } from '@tiptap/core'
|
||||||
import { useDebugValue, useEffect, useState } from 'react'
|
import { useDebugValue, useEffect, useState } from 'react'
|
||||||
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'
|
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'
|
||||||
|
|
||||||
import type { Editor } from './Editor.js'
|
|
||||||
|
|
||||||
export type EditorStateSnapshot<TEditor extends Editor | null = Editor | null> = {
|
export type EditorStateSnapshot<TEditor extends Editor | null = Editor | null> = {
|
||||||
editor: TEditor;
|
editor: TEditor;
|
||||||
transactionNumber: number;
|
transactionNumber: number;
|
||||||
|
@ -49,7 +49,8 @@
|
|||||||
"@tiptap/extension-paragraph": "^3.0.0-next.0",
|
"@tiptap/extension-paragraph": "^3.0.0-next.0",
|
||||||
"@tiptap/extension-strike": "^3.0.0-next.0",
|
"@tiptap/extension-strike": "^3.0.0-next.0",
|
||||||
"@tiptap/extension-underline": "^3.0.0-next.0",
|
"@tiptap/extension-underline": "^3.0.0-next.0",
|
||||||
"@tiptap/extension-text": "^3.0.0-next.0"
|
"@tiptap/extension-text": "^3.0.0-next.0",
|
||||||
|
"@tiptap/pm": "^3.0.0-next.0"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -221,7 +221,12 @@ export function VueNodeViewRenderer(
|
|||||||
if (!(props.editor as Editor).contentComponent) {
|
if (!(props.editor as Editor).contentComponent) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
// check for class-component and normalize if neccessary
|
||||||
|
const normalizedComponent = typeof component === 'function' && '__vccOpts' in component
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
? component.__vccOpts as Component
|
||||||
|
: component
|
||||||
|
|
||||||
return new VueNodeView(component, props, options) as unknown as ProseMirrorNodeView
|
return new VueNodeView(normalizedComponent, props, options) as unknown as ProseMirrorNodeView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user