fix(vue-3): late-registering plugins #5259 (#5616)

This commit is contained in:
Sven Adlung 2024-09-18 14:46:45 +02:00 committed by GitHub
parent 20c7c95e54
commit cbe06d12db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 395 additions and 12 deletions

View File

@ -0,0 +1,5 @@
---
"@tiptap/vue-3": patch
---
Fix registerPlugin() for late-registering plugins

View File

@ -0,0 +1,69 @@
import './styles.scss'
import Bold from '@tiptap/extension-bold'
import Collaboration from '@tiptap/extension-collaboration'
import Document from '@tiptap/extension-document'
import Heading from '@tiptap/extension-heading'
import Paragraph from '@tiptap/extension-paragraph'
import Placeholder from '@tiptap/extension-placeholder'
import Text from '@tiptap/extension-text'
import {
BubbleMenu, EditorContent, FloatingMenu, useEditor,
} from '@tiptap/react'
import React from 'react'
import { WebrtcProvider } from 'y-webrtc'
import * as Y from 'yjs'
const ydoc = new Y.Doc()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const provider = new WebrtcProvider('tiptap-collaboration-extension', ydoc)
export default () => {
const editor = useEditor({
extensions: [
Document,
Paragraph,
Text,
Heading,
Bold,
Collaboration.configure({
document: ydoc,
}),
Placeholder.configure({
placeholder:
'Write something … Itll be shared with everyone else looking at this example.',
}),
],
})
return (
<>
{editor && (
<>
<BubbleMenu editor={editor}>
<div className="bubble-menu">
<button
onClick={() => editor.chain().focus().toggleBold().run()}
className={editor.isActive('bold') ? 'is-active' : ''}
>
Bold
</button>
</div>
</BubbleMenu>
<FloatingMenu editor={editor}>
<div className="floating-menu">
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
>
H1
</button>
</div>
</FloatingMenu>
</>
)}
<EditorContent editor={editor} />
</>
)
}

View File

@ -0,0 +1,34 @@
context('/src/Extensions/CollaborationWithMenus/React/', () => {
before(() => {
cy.visit('/src/Extensions/CollaborationWithMenus/React/')
})
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 menu plugins initiated', () => {
cy.get('.tiptap').then(([{ editor }]) => {
const bubbleMenuPlugin = editor.view.state.plugins.find(plugin => plugin.spec.key?.key === 'bubbleMenu$')
const floatingMenuPlugin = editor.view.state.plugins.find(plugin => plugin.spec.key?.key === 'floatingMenu$')
const hasBothMenuPluginsLoaded = !!bubbleMenuPlugin && !!floatingMenuPlugin
expect(hasBothMenuPluginsLoaded).to.equal(true)
})
})
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
})
})
})

View File

@ -0,0 +1,72 @@
/* Basic editor styles */
.tiptap {
:first-child {
margin-top: 0;
}
/* Placeholder (at the top) */
p.is-editor-empty:first-child::before {
color: var(--gray-4);
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
&.ProseMirror-focused p.is-editor-empty:first-child::before {
content: '';
}
}
/* Bubble menu */
.bubble-menu {
background-color: var(--white);
border: 1px solid var(--gray-1);
border-radius: 0.7rem;
box-shadow: var(--shadow);
display: flex;
padding: 0.2rem;
button {
background-color: unset;
&:hover {
background-color: var(--gray-3);
}
&.is-active {
background-color: var(--purple);
&:hover {
background-color: var(--purple-contrast);
}
}
}
}
/* Floating menu */
.floating-menu {
display: flex;
background-color: var(--gray-3);
padding: 0.1rem;
border-radius: 0.5rem;
button {
background-color: unset;
padding: 0.275rem 0.425rem;
border-radius: 0.3rem;
&:hover {
background-color: var(--gray-3);
}
&.is-active {
background-color: var(--white);
color: var(--purple);
&:hover {
color: var(--purple-contrast);
}
}
}
}

View File

@ -0,0 +1,34 @@
context('/src/Extensions/CollaborationWithMenus/Vue/', () => {
before(() => {
cy.visit('/src/Extensions/CollaborationWithMenus/Vue/')
})
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 menu plugins initiated', () => {
cy.get('.tiptap').then(([{ editor }]) => {
const bubbleMenuPlugin = editor.view.state.plugins.find(plugin => plugin.spec.key?.key === 'bubbleMenu$')
const floatingMenuPlugin = editor.view.state.plugins.find(plugin => plugin.spec.key?.key === 'floatingMenu$')
const hasBothMenuPluginsLoaded = !!bubbleMenuPlugin && !!floatingMenuPlugin
expect(hasBothMenuPluginsLoaded).to.equal(true)
})
})
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
})
})
})

View File

@ -0,0 +1,157 @@
<template>
<div v-if="editor">
<bubble-menu :editor="editor">
<div class="bubble-menu">
<button
@click="editor.chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
>
Bold
</button>
</div>
</bubble-menu>
<floating-menu :editor="editor">
<div class="floating-menu">
<button
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
>
H1
</button>
</div>
</floating-menu>
</div>
<editor-content :editor="editor" />
</template>
<script>
import Bold from '@tiptap/extension-bold'
import Collaboration from '@tiptap/extension-collaboration'
import Document from '@tiptap/extension-document'
import Heading from '@tiptap/extension-heading'
import Paragraph from '@tiptap/extension-paragraph'
import Placeholder from '@tiptap/extension-placeholder'
import Text from '@tiptap/extension-text'
import {
BubbleMenu, Editor, EditorContent, FloatingMenu,
} from '@tiptap/vue-3'
import { WebrtcProvider } from 'y-webrtc'
import * as Y from 'yjs'
export default {
components: {
EditorContent,
BubbleMenu,
FloatingMenu,
},
data() {
return {
editor: null,
provider: null,
}
},
mounted() {
const ydoc = new Y.Doc()
this.provider = new WebrtcProvider('tiptap-collaboration-extension', ydoc)
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
Heading,
Bold,
Collaboration.configure({
document: ydoc,
}),
Placeholder.configure({
placeholder: 'Write something … Itll be shared with everyone else looking at this example.',
}),
],
})
},
beforeUnmount() {
this.editor.destroy()
this.provider.destroy()
},
}
</script>
<style lang="scss">
/* Basic editor styles */
.tiptap {
:first-child {
margin-top: 0;
}
/* Placeholder (at the top) */
p.is-editor-empty:first-child::before {
color: var(--gray-4);
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
&.ProseMirror-focused p.is-editor-empty:first-child::before {
content: '';
}
}
/* Bubble menu */
.bubble-menu {
background-color: var(--white);
border: 1px solid var(--gray-1);
border-radius: 0.7rem;
box-shadow: var(--shadow);
display: flex;
padding: 0.2rem;
button {
background-color: unset;
&:hover {
background-color: var(--gray-3);
}
&.is-active {
background-color: var(--purple);
&:hover {
background-color: var(--purple-contrast);
}
}
}
}
/* Floating menu */
.floating-menu {
display: flex;
background-color: var(--gray-3);
padding: 0.1rem;
border-radius: 0.5rem;
button {
background-color: unset;
padding: 0.275rem 0.425rem;
border-radius: 0.3rem;
&:hover {
background-color: var(--gray-3);
}
&.is-active {
background-color: var(--white);
color: var(--purple);
&:hover {
color: var(--purple-contrast);
}
}
}
}
</style>

View File

@ -224,11 +224,12 @@ export class Editor extends EventEmitter<EditorEvents> {
*
* @param plugin A ProseMirror plugin
* @param handlePlugins Control how to merge the plugin into the existing plugins.
* @returns The new editor state
*/
public registerPlugin(
plugin: Plugin,
handlePlugins?: (newPlugin: Plugin, plugins: Plugin[]) => Plugin[],
): void {
): EditorState {
const plugins = isFunction(handlePlugins)
? handlePlugins(plugin, [...this.state.plugins])
: [...this.state.plugins, plugin]
@ -236,16 +237,19 @@ export class Editor extends EventEmitter<EditorEvents> {
const state = this.state.reconfigure({ plugins })
this.view.updateState(state)
return state
}
/**
* Unregister a ProseMirror plugin.
*
* @param nameOrPluginKey The plugins name
* @returns The new editor state or undefined if the editor is destroyed
*/
public unregisterPlugin(nameOrPluginKey: string | PluginKey): void {
public unregisterPlugin(nameOrPluginKey: string | PluginKey): EditorState | undefined {
if (this.isDestroyed) {
return
return undefined
}
// @ts-ignore
@ -257,6 +261,8 @@ export class Editor extends EventEmitter<EditorEvents> {
})
this.view.updateState(state)
return state
}
/**

View File

@ -46,7 +46,7 @@ export const BubbleMenu = (props: BubbleMenuProps) => {
})
menuEditor.registerPlugin(plugin)
return () => menuEditor.unregisterPlugin(pluginKey)
return () => { menuEditor.unregisterPlugin(pluginKey) }
}, [props.editor, currentEditor, element])
return (

View File

@ -49,7 +49,7 @@ export const FloatingMenu = (props: FloatingMenuProps) => {
})
menuEditor.registerPlugin(plugin)
return () => menuEditor.unregisterPlugin(pluginKey)
return () => { menuEditor.unregisterPlugin(pluginKey) }
}, [
props.editor,
currentEditor,

View File

@ -73,20 +73,26 @@ export class Editor extends CoreEditor {
public registerPlugin(
plugin: Plugin,
handlePlugins?: (newPlugin: Plugin, plugins: Plugin[]) => Plugin[],
): void {
super.registerPlugin(plugin, handlePlugins)
): EditorState {
const nextState = super.registerPlugin(plugin, handlePlugins)
if (this.reactiveState) {
this.reactiveState.value = this.view.state
this.reactiveState.value = nextState
}
return nextState
}
/**
* Unregister a ProseMirror plugin.
*/
public unregisterPlugin(nameOrPluginKey: string | PluginKey): void {
super.unregisterPlugin(nameOrPluginKey)
if (this.reactiveState) {
this.reactiveState.value = this.view.state
public unregisterPlugin(nameOrPluginKey: string | PluginKey): EditorState | undefined {
const nextState = super.unregisterPlugin(nameOrPluginKey)
if (this.reactiveState && nextState) {
this.reactiveState.value = nextState
}
return nextState
}
}