mirror of
https://github.com/ueberdosis/tiptap.git
synced 2025-06-11 03:33:12 +08:00
fix(vue-3): reapply performance updates (#5373)
--------- Co-authored-by: relchapt <reynald.lechapt@getmayday.co> Co-authored-by: Rirax <rlechapt@student.42.fr> Co-authored-by: Segev Finer <segev@swimm.io>
This commit is contained in:
parent
a64cbf8f8a
commit
ab8389a32c
5
.changeset/shy-clouds-smoke.md
Normal file
5
.changeset/shy-clouds-smoke.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tiptap/vue-3": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
VueNodeViewRenderer should return `null` for `contentDOM` for a non-leaf node with no `NodeViewContent`
|
@ -404,6 +404,11 @@ export class Editor extends EventEmitter<EditorEvents> {
|
|||||||
const state = this.state.apply(transaction)
|
const state = this.state.apply(transaction)
|
||||||
const selectionHasChanged = !this.state.selection.eq(state.selection)
|
const selectionHasChanged = !this.state.selection.eq(state.selection)
|
||||||
|
|
||||||
|
this.emit('beforeTransaction', {
|
||||||
|
editor: this,
|
||||||
|
transaction,
|
||||||
|
nextState: state,
|
||||||
|
})
|
||||||
this.view.updateState(state)
|
this.view.updateState(state)
|
||||||
this.emit('transaction', {
|
this.emit('transaction', {
|
||||||
editor: this,
|
editor: this,
|
||||||
|
@ -50,6 +50,7 @@ export interface EditorEvents {
|
|||||||
}
|
}
|
||||||
update: { editor: Editor; transaction: Transaction }
|
update: { editor: Editor; transaction: Transaction }
|
||||||
selectionUpdate: { editor: Editor; transaction: Transaction }
|
selectionUpdate: { editor: Editor; transaction: Transaction }
|
||||||
|
beforeTransaction: { editor: Editor; transaction: Transaction, nextState: EditorState }
|
||||||
transaction: { editor: Editor; transaction: Transaction }
|
transaction: { editor: Editor; transaction: Transaction }
|
||||||
focus: { editor: Editor; event: FocusEvent; transaction: Transaction }
|
focus: { editor: Editor; event: FocusEvent; transaction: Transaction }
|
||||||
blur: { editor: Editor; event: FocusEvent; transaction: Transaction }
|
blur: { editor: Editor; event: FocusEvent; transaction: Transaction }
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import { Editor as CoreEditor, EditorOptions } from '@tiptap/core'
|
import { Editor as CoreEditor, EditorOptions } from '@tiptap/core'
|
||||||
import { EditorState, Plugin, PluginKey } from '@tiptap/pm/state'
|
import { EditorState, Plugin, PluginKey } from '@tiptap/pm/state'
|
||||||
import {
|
import {
|
||||||
|
AppContext,
|
||||||
ComponentInternalInstance,
|
ComponentInternalInstance,
|
||||||
ComponentPublicInstance,
|
ComponentPublicInstance,
|
||||||
customRef,
|
customRef,
|
||||||
markRaw,
|
markRaw,
|
||||||
reactive,
|
|
||||||
Ref,
|
Ref,
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
|
|
||||||
import { VueRenderer } from './VueRenderer.js'
|
|
||||||
|
|
||||||
function useDebouncedRef<T>(value: T) {
|
function useDebouncedRef<T>(value: T) {
|
||||||
return customRef<T>((track, trigger) => {
|
return customRef<T>((track, trigger) => {
|
||||||
return {
|
return {
|
||||||
@ -42,18 +40,18 @@ export class Editor extends CoreEditor {
|
|||||||
|
|
||||||
private reactiveExtensionStorage: Ref<Record<string, any>>
|
private reactiveExtensionStorage: Ref<Record<string, any>>
|
||||||
|
|
||||||
public vueRenderers = reactive<Map<string, VueRenderer>>(new Map())
|
|
||||||
|
|
||||||
public contentComponent: ContentComponent | null = null
|
public contentComponent: ContentComponent | null = null
|
||||||
|
|
||||||
|
public appContext: AppContext | null = null
|
||||||
|
|
||||||
constructor(options: Partial<EditorOptions> = {}) {
|
constructor(options: Partial<EditorOptions> = {}) {
|
||||||
super(options)
|
super(options)
|
||||||
|
|
||||||
this.reactiveState = useDebouncedRef(this.view.state)
|
this.reactiveState = useDebouncedRef(this.view.state)
|
||||||
this.reactiveExtensionStorage = useDebouncedRef(this.extensionStorage)
|
this.reactiveExtensionStorage = useDebouncedRef(this.extensionStorage)
|
||||||
|
|
||||||
this.on('transaction', () => {
|
this.on('beforeTransaction', ({ nextState }) => {
|
||||||
this.reactiveState.value = this.view.state
|
this.reactiveState.value = nextState
|
||||||
this.reactiveExtensionStorage.value = this.extensionStorage
|
this.reactiveExtensionStorage.value = this.extensionStorage
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
DefineComponent,
|
|
||||||
defineComponent,
|
defineComponent,
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
h,
|
h,
|
||||||
@ -8,7 +7,6 @@ import {
|
|||||||
PropType,
|
PropType,
|
||||||
Ref,
|
Ref,
|
||||||
ref,
|
ref,
|
||||||
Teleport,
|
|
||||||
unref,
|
unref,
|
||||||
watchEffect,
|
watchEffect,
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
@ -45,6 +43,17 @@ export const EditorContent = defineComponent({
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
editor.contentComponent = instance.ctx._
|
editor.contentComponent = instance.ctx._
|
||||||
|
|
||||||
|
if (instance) {
|
||||||
|
editor.appContext = {
|
||||||
|
...instance.appContext,
|
||||||
|
provides: {
|
||||||
|
// @ts-ignore
|
||||||
|
...instance.provides,
|
||||||
|
...instance.appContext.provides,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
editor.setOptions({
|
editor.setOptions({
|
||||||
element,
|
element,
|
||||||
})
|
})
|
||||||
@ -69,6 +78,7 @@ export const EditorContent = defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
editor.contentComponent = null
|
editor.contentComponent = null
|
||||||
|
editor.appContext = null
|
||||||
|
|
||||||
if (!editor.options.element.firstChild) {
|
if (!editor.options.element.firstChild) {
|
||||||
return
|
return
|
||||||
@ -87,35 +97,11 @@ export const EditorContent = defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const vueRenderers: any[] = []
|
|
||||||
|
|
||||||
if (this.editor) {
|
|
||||||
this.editor.vueRenderers.forEach(vueRenderer => {
|
|
||||||
const node = h(
|
|
||||||
Teleport,
|
|
||||||
{
|
|
||||||
to: vueRenderer.teleportElement,
|
|
||||||
key: vueRenderer.id,
|
|
||||||
},
|
|
||||||
h(
|
|
||||||
vueRenderer.component as DefineComponent,
|
|
||||||
{
|
|
||||||
ref: vueRenderer.id,
|
|
||||||
...vueRenderer.props,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
vueRenderers.push(node)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return h(
|
return h(
|
||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
ref: (el: any) => { this.rootEl = el },
|
ref: (el: any) => { this.rootEl = el },
|
||||||
},
|
},
|
||||||
...vueRenderers,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -124,7 +124,7 @@ class VueNodeView extends NodeView<Component, Editor, VueNodeViewRendererOptions
|
|||||||
}
|
}
|
||||||
|
|
||||||
get dom() {
|
get dom() {
|
||||||
if (!this.renderer.element.hasAttribute('data-node-view-wrapper')) {
|
if (!this.renderer.element || !this.renderer.element.hasAttribute('data-node-view-wrapper')) {
|
||||||
throw Error('Please use the NodeViewWrapper component for your node view.')
|
throw Error('Please use the NodeViewWrapper component for your node view.')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,9 +136,7 @@ class VueNodeView extends NodeView<Component, Editor, VueNodeViewRendererOptions
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentElement = this.dom.querySelector('[data-node-view-content]')
|
return this.dom.querySelector('[data-node-view-content]') as HTMLElement | null
|
||||||
|
|
||||||
return (contentElement || this.dom) as HTMLElement | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update(node: ProseMirrorNode, decorations: DecorationWithType[]) {
|
update(node: ProseMirrorNode, decorations: DecorationWithType[]) {
|
||||||
@ -183,15 +181,19 @@ class VueNodeView extends NodeView<Component, Editor, VueNodeViewRendererOptions
|
|||||||
this.renderer.updateProps({
|
this.renderer.updateProps({
|
||||||
selected: true,
|
selected: true,
|
||||||
})
|
})
|
||||||
|
if (this.renderer.element) {
|
||||||
this.renderer.element.classList.add('ProseMirror-selectednode')
|
this.renderer.element.classList.add('ProseMirror-selectednode')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
deselectNode() {
|
deselectNode() {
|
||||||
this.renderer.updateProps({
|
this.renderer.updateProps({
|
||||||
selected: false,
|
selected: false,
|
||||||
})
|
})
|
||||||
|
if (this.renderer.element) {
|
||||||
this.renderer.element.classList.remove('ProseMirror-selectednode')
|
this.renderer.element.classList.remove('ProseMirror-selectednode')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getDecorationClasses() {
|
getDecorationClasses() {
|
||||||
return (
|
return (
|
||||||
|
@ -1,62 +1,87 @@
|
|||||||
import { Editor } from '@tiptap/core'
|
import { Editor } from '@tiptap/core'
|
||||||
import { Component, markRaw, reactive } from 'vue'
|
import {
|
||||||
|
Component, DefineComponent, h, markRaw, reactive, render,
|
||||||
|
} from 'vue'
|
||||||
|
|
||||||
import { Editor as ExtendedEditor } from './Editor.js'
|
import { Editor as ExtendedEditor } from './Editor.js'
|
||||||
|
|
||||||
export interface VueRendererOptions {
|
export interface VueRendererOptions {
|
||||||
editor: Editor,
|
editor: Editor;
|
||||||
props?: Record<string, any>,
|
props?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtendedVNode = ReturnType<typeof h> | null;
|
||||||
|
|
||||||
|
interface RenderedComponent {
|
||||||
|
vNode: ExtendedVNode;
|
||||||
|
destroy: () => void;
|
||||||
|
el: Element | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is used to render Vue components inside the editor.
|
* This class is used to render Vue components inside the editor.
|
||||||
*/
|
*/
|
||||||
export class VueRenderer {
|
export class VueRenderer {
|
||||||
id: string
|
renderedComponent!: RenderedComponent
|
||||||
|
|
||||||
editor: ExtendedEditor
|
editor: ExtendedEditor
|
||||||
|
|
||||||
component: Component
|
component: Component
|
||||||
|
|
||||||
teleportElement: Element
|
el: Element | null
|
||||||
|
|
||||||
element: Element
|
|
||||||
|
|
||||||
props: Record<string, any>
|
props: Record<string, any>
|
||||||
|
|
||||||
constructor(component: Component, { props = {}, editor }: VueRendererOptions) {
|
constructor(component: Component, { props = {}, editor }: VueRendererOptions) {
|
||||||
this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString()
|
|
||||||
this.editor = editor as ExtendedEditor
|
this.editor = editor as ExtendedEditor
|
||||||
this.component = markRaw(component)
|
this.component = markRaw(component)
|
||||||
this.teleportElement = document.createElement('div')
|
this.el = document.createElement('div')
|
||||||
this.element = this.teleportElement
|
|
||||||
this.props = reactive(props)
|
this.props = reactive(props)
|
||||||
this.editor.vueRenderers.set(this.id, this)
|
this.renderedComponent = this.renderComponent()
|
||||||
|
|
||||||
if (this.editor.contentComponent) {
|
|
||||||
this.editor.contentComponent.update()
|
|
||||||
|
|
||||||
if (this.teleportElement.children.length !== 1) {
|
|
||||||
throw Error('VueRenderer doesn’t support multiple child elements.')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.element = this.teleportElement.firstElementChild as Element
|
get element(): Element | null {
|
||||||
}
|
return this.renderedComponent.el
|
||||||
}
|
}
|
||||||
|
|
||||||
get ref(): any {
|
get ref(): any {
|
||||||
return this.editor.contentComponent?.refs[this.id]
|
// Composition API
|
||||||
|
if (this.renderedComponent.vNode?.component?.exposed) {
|
||||||
|
return this.renderedComponent.vNode.component.exposed
|
||||||
|
}
|
||||||
|
// Option API
|
||||||
|
return this.renderedComponent.vNode?.component?.proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
renderComponent() {
|
||||||
|
let vNode: ExtendedVNode = h(this.component as DefineComponent, this.props)
|
||||||
|
|
||||||
|
if (this.editor.appContext) {
|
||||||
|
vNode.appContext = this.editor.appContext
|
||||||
|
}
|
||||||
|
if (typeof document !== 'undefined' && this.el) {
|
||||||
|
render(vNode, this.el)
|
||||||
|
}
|
||||||
|
|
||||||
|
const destroy = () => {
|
||||||
|
if (this.el) {
|
||||||
|
render(null, this.el)
|
||||||
|
}
|
||||||
|
this.el = null
|
||||||
|
vNode = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return { vNode, destroy, el: this.el ? this.el.firstElementChild : null }
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProps(props: Record<string, any> = {}): void {
|
updateProps(props: Record<string, any> = {}): void {
|
||||||
Object
|
Object.entries(props).forEach(([key, value]) => {
|
||||||
.entries(props)
|
|
||||||
.forEach(([key, value]) => {
|
|
||||||
this.props[key] = value
|
this.props[key] = value
|
||||||
})
|
})
|
||||||
|
this.renderComponent()
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
this.editor.vueRenderers.delete(this.id)
|
this.renderedComponent.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user