mirror of
https://github.com/ueberdosis/tiptap.git
synced 2024-11-24 03:39:01 +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 selectionHasChanged = !this.state.selection.eq(state.selection)
|
||||
|
||||
this.emit('beforeTransaction', {
|
||||
editor: this,
|
||||
transaction,
|
||||
nextState: state,
|
||||
})
|
||||
this.view.updateState(state)
|
||||
this.emit('transaction', {
|
||||
editor: this,
|
||||
|
@ -50,6 +50,7 @@ export interface EditorEvents {
|
||||
}
|
||||
update: { editor: Editor; transaction: Transaction }
|
||||
selectionUpdate: { editor: Editor; transaction: Transaction }
|
||||
beforeTransaction: { editor: Editor; transaction: Transaction, nextState: EditorState }
|
||||
transaction: { editor: Editor; transaction: Transaction }
|
||||
focus: { 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 { EditorState, Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import {
|
||||
AppContext,
|
||||
ComponentInternalInstance,
|
||||
ComponentPublicInstance,
|
||||
customRef,
|
||||
markRaw,
|
||||
reactive,
|
||||
Ref,
|
||||
} from 'vue'
|
||||
|
||||
import { VueRenderer } from './VueRenderer.js'
|
||||
|
||||
function useDebouncedRef<T>(value: T) {
|
||||
return customRef<T>((track, trigger) => {
|
||||
return {
|
||||
@ -42,18 +40,18 @@ export class Editor extends CoreEditor {
|
||||
|
||||
private reactiveExtensionStorage: Ref<Record<string, any>>
|
||||
|
||||
public vueRenderers = reactive<Map<string, VueRenderer>>(new Map())
|
||||
|
||||
public contentComponent: ContentComponent | null = null
|
||||
|
||||
public appContext: AppContext | null = null
|
||||
|
||||
constructor(options: Partial<EditorOptions> = {}) {
|
||||
super(options)
|
||||
|
||||
this.reactiveState = useDebouncedRef(this.view.state)
|
||||
this.reactiveExtensionStorage = useDebouncedRef(this.extensionStorage)
|
||||
|
||||
this.on('transaction', () => {
|
||||
this.reactiveState.value = this.view.state
|
||||
this.on('beforeTransaction', ({ nextState }) => {
|
||||
this.reactiveState.value = nextState
|
||||
this.reactiveExtensionStorage.value = this.extensionStorage
|
||||
})
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import {
|
||||
DefineComponent,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
@ -8,7 +7,6 @@ import {
|
||||
PropType,
|
||||
Ref,
|
||||
ref,
|
||||
Teleport,
|
||||
unref,
|
||||
watchEffect,
|
||||
} from 'vue'
|
||||
@ -45,6 +43,17 @@ export const EditorContent = defineComponent({
|
||||
// @ts-ignore
|
||||
editor.contentComponent = instance.ctx._
|
||||
|
||||
if (instance) {
|
||||
editor.appContext = {
|
||||
...instance.appContext,
|
||||
provides: {
|
||||
// @ts-ignore
|
||||
...instance.provides,
|
||||
...instance.appContext.provides,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
editor.setOptions({
|
||||
element,
|
||||
})
|
||||
@ -69,6 +78,7 @@ export const EditorContent = defineComponent({
|
||||
}
|
||||
|
||||
editor.contentComponent = null
|
||||
editor.appContext = null
|
||||
|
||||
if (!editor.options.element.firstChild) {
|
||||
return
|
||||
@ -87,35 +97,11 @@ export const EditorContent = defineComponent({
|
||||
},
|
||||
|
||||
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(
|
||||
'div',
|
||||
{
|
||||
ref: (el: any) => { this.rootEl = el },
|
||||
},
|
||||
...vueRenderers,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
@ -124,7 +124,7 @@ class VueNodeView extends NodeView<Component, Editor, VueNodeViewRendererOptions
|
||||
}
|
||||
|
||||
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.')
|
||||
}
|
||||
|
||||
@ -136,9 +136,7 @@ class VueNodeView extends NodeView<Component, Editor, VueNodeViewRendererOptions
|
||||
return null
|
||||
}
|
||||
|
||||
const contentElement = this.dom.querySelector('[data-node-view-content]')
|
||||
|
||||
return (contentElement || this.dom) as HTMLElement | null
|
||||
return this.dom.querySelector('[data-node-view-content]') as HTMLElement | null
|
||||
}
|
||||
|
||||
update(node: ProseMirrorNode, decorations: DecorationWithType[]) {
|
||||
@ -183,14 +181,18 @@ class VueNodeView extends NodeView<Component, Editor, VueNodeViewRendererOptions
|
||||
this.renderer.updateProps({
|
||||
selected: true,
|
||||
})
|
||||
this.renderer.element.classList.add('ProseMirror-selectednode')
|
||||
if (this.renderer.element) {
|
||||
this.renderer.element.classList.add('ProseMirror-selectednode')
|
||||
}
|
||||
}
|
||||
|
||||
deselectNode() {
|
||||
this.renderer.updateProps({
|
||||
selected: false,
|
||||
})
|
||||
this.renderer.element.classList.remove('ProseMirror-selectednode')
|
||||
if (this.renderer.element) {
|
||||
this.renderer.element.classList.remove('ProseMirror-selectednode')
|
||||
}
|
||||
}
|
||||
|
||||
getDecorationClasses() {
|
||||
|
@ -1,62 +1,87 @@
|
||||
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'
|
||||
|
||||
export interface VueRendererOptions {
|
||||
editor: Editor,
|
||||
props?: Record<string, any>,
|
||||
editor: Editor;
|
||||
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.
|
||||
*/
|
||||
export class VueRenderer {
|
||||
id: string
|
||||
renderedComponent!: RenderedComponent
|
||||
|
||||
editor: ExtendedEditor
|
||||
|
||||
component: Component
|
||||
|
||||
teleportElement: Element
|
||||
|
||||
element: Element
|
||||
el: Element | null
|
||||
|
||||
props: Record<string, any>
|
||||
|
||||
constructor(component: Component, { props = {}, editor }: VueRendererOptions) {
|
||||
this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString()
|
||||
this.editor = editor as ExtendedEditor
|
||||
this.component = markRaw(component)
|
||||
this.teleportElement = document.createElement('div')
|
||||
this.element = this.teleportElement
|
||||
this.el = document.createElement('div')
|
||||
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 {
|
||||
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 {
|
||||
Object
|
||||
.entries(props)
|
||||
.forEach(([key, value]) => {
|
||||
this.props[key] = value
|
||||
})
|
||||
Object.entries(props).forEach(([key, value]) => {
|
||||
this.props[key] = value
|
||||
})
|
||||
this.renderComponent()
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.editor.vueRenderers.delete(this.id)
|
||||
this.renderedComponent.destroy()
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user