mirror of
https://github.com/ueberdosis/tiptap.git
synced 2025-06-14 22:32:24 +08:00
parent
20c7c95e54
commit
cbe06d12db
5
.changeset/nasty-poems-kick.md
Normal file
5
.changeset/nasty-poems-kick.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tiptap/vue-3": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix registerPlugin() for late-registering plugins
|
69
demos/src/Extensions/CollaborationWithMenus/React/index.jsx
Normal file
69
demos/src/Extensions/CollaborationWithMenus/React/index.jsx
Normal 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 … It’ll 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} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
157
demos/src/Extensions/CollaborationWithMenus/Vue/index.vue
Normal file
157
demos/src/Extensions/CollaborationWithMenus/Vue/index.vue
Normal 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 … It’ll 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>
|
@ -224,11 +224,12 @@ export class Editor extends EventEmitter<EditorEvents> {
|
|||||||
*
|
*
|
||||||
* @param plugin A ProseMirror plugin
|
* @param plugin A ProseMirror plugin
|
||||||
* @param handlePlugins Control how to merge the plugin into the existing plugins.
|
* @param handlePlugins Control how to merge the plugin into the existing plugins.
|
||||||
|
* @returns The new editor state
|
||||||
*/
|
*/
|
||||||
public registerPlugin(
|
public registerPlugin(
|
||||||
plugin: Plugin,
|
plugin: Plugin,
|
||||||
handlePlugins?: (newPlugin: Plugin, plugins: Plugin[]) => Plugin[],
|
handlePlugins?: (newPlugin: Plugin, plugins: Plugin[]) => Plugin[],
|
||||||
): void {
|
): EditorState {
|
||||||
const plugins = isFunction(handlePlugins)
|
const plugins = isFunction(handlePlugins)
|
||||||
? handlePlugins(plugin, [...this.state.plugins])
|
? handlePlugins(plugin, [...this.state.plugins])
|
||||||
: [...this.state.plugins, plugin]
|
: [...this.state.plugins, plugin]
|
||||||
@ -236,16 +237,19 @@ export class Editor extends EventEmitter<EditorEvents> {
|
|||||||
const state = this.state.reconfigure({ plugins })
|
const state = this.state.reconfigure({ plugins })
|
||||||
|
|
||||||
this.view.updateState(state)
|
this.view.updateState(state)
|
||||||
|
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregister a ProseMirror plugin.
|
* Unregister a ProseMirror plugin.
|
||||||
*
|
*
|
||||||
* @param nameOrPluginKey The plugins name
|
* @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) {
|
if (this.isDestroyed) {
|
||||||
return
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -257,6 +261,8 @@ export class Editor extends EventEmitter<EditorEvents> {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.view.updateState(state)
|
this.view.updateState(state)
|
||||||
|
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,7 +46,7 @@ export const BubbleMenu = (props: BubbleMenuProps) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
menuEditor.registerPlugin(plugin)
|
menuEditor.registerPlugin(plugin)
|
||||||
return () => menuEditor.unregisterPlugin(pluginKey)
|
return () => { menuEditor.unregisterPlugin(pluginKey) }
|
||||||
}, [props.editor, currentEditor, element])
|
}, [props.editor, currentEditor, element])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -49,7 +49,7 @@ export const FloatingMenu = (props: FloatingMenuProps) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
menuEditor.registerPlugin(plugin)
|
menuEditor.registerPlugin(plugin)
|
||||||
return () => menuEditor.unregisterPlugin(pluginKey)
|
return () => { menuEditor.unregisterPlugin(pluginKey) }
|
||||||
}, [
|
}, [
|
||||||
props.editor,
|
props.editor,
|
||||||
currentEditor,
|
currentEditor,
|
||||||
|
@ -73,20 +73,26 @@ export class Editor extends CoreEditor {
|
|||||||
public registerPlugin(
|
public registerPlugin(
|
||||||
plugin: Plugin,
|
plugin: Plugin,
|
||||||
handlePlugins?: (newPlugin: Plugin, plugins: Plugin[]) => Plugin[],
|
handlePlugins?: (newPlugin: Plugin, plugins: Plugin[]) => Plugin[],
|
||||||
): void {
|
): EditorState {
|
||||||
super.registerPlugin(plugin, handlePlugins)
|
const nextState = super.registerPlugin(plugin, handlePlugins)
|
||||||
|
|
||||||
if (this.reactiveState) {
|
if (this.reactiveState) {
|
||||||
this.reactiveState.value = this.view.state
|
this.reactiveState.value = nextState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nextState
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregister a ProseMirror plugin.
|
* Unregister a ProseMirror plugin.
|
||||||
*/
|
*/
|
||||||
public unregisterPlugin(nameOrPluginKey: string | PluginKey): void {
|
public unregisterPlugin(nameOrPluginKey: string | PluginKey): EditorState | undefined {
|
||||||
super.unregisterPlugin(nameOrPluginKey)
|
const nextState = super.unregisterPlugin(nameOrPluginKey)
|
||||||
if (this.reactiveState) {
|
|
||||||
this.reactiveState.value = this.view.state
|
if (this.reactiveState && nextState) {
|
||||||
}
|
this.reactiveState.value = nextState
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user