feat: add key option and shouldShow option to menus (fix #1480, fix #1043, fix #1268, fix #1503)

* add key option to bubble menu

* ignore react for now

* add shouldShow option to bubble menu extension

* improve types

* remove BubbleMenuPluginKey

* add key and shouldShow option to floating menu extension

* fix: don’t show floating menu within code block

* docs: add new menu options
This commit is contained in:
Philipp Kühn
2021-08-11 14:37:58 +02:00
committed by GitHub
parent bcc1309cd9
commit 9ba61c1582
12 changed files with 312 additions and 60 deletions

View File

@@ -15,10 +15,12 @@ yarn add @tiptap/extension-bubble-menu
``` ```
## Settings ## Settings
| Option | Type | Default | Description | | Option | Type | Default | Description |
| ------------ | ------------- | ------- | ----------------------------------------------------------------------- | | ------------ | -------------------- | -------------- | ----------------------------------------------------------------------- |
| element | `HTMLElement` | `null` | The DOM element that contains your menu. | | element | `HTMLElement` | `null` | The DOM element that contains your menu. |
| tippyOptions | `Object` | `{}` | [Options for tippy.js](https://atomiks.github.io/tippyjs/v6/all-props/) | | tippyOptions | `Object` | `{}` | [Options for tippy.js](https://atomiks.github.io/tippyjs/v6/all-props/) |
| key | `string | PluginKey` | `'bubbleMenu'` | The key for the underlying ProseMirror plugin. |
| shouldShow | `(props) => boolean` | | Controls whether the menu should be shown or not. |
## Source code ## Source code
[packages/extension-bubble-menu/](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-bubble-menu/) [packages/extension-bubble-menu/](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-bubble-menu/)
@@ -44,3 +46,57 @@ new Editor({
Vue: 'Extensions/BubbleMenu/Vue', Vue: 'Extensions/BubbleMenu/Vue',
React: 'Extensions/BubbleMenu/React', React: 'Extensions/BubbleMenu/React',
}" /> }" />
### Custom logic
Customize the logic for showing the menu with the `shouldShow` option. For components, `shouldShow` can be passed as a prop.
```js
BubbleMenu.configure({
shouldShow: ({ editor, view, state, oldState, from, to }) => {
// only show the bubble menu for images and links
return editor.isActive('image') || editor.isActive('link')
},
})
```
### Multiple menus
Use multiple menus by setting an unique `key`.
```js
import { Editor } from '@tiptap/core'
import BubbleMenu from '@tiptap/extension-bubble-menu'
new Editor({
extensions: [
BubbleMenu.configure({
key: 'bubbleMenuOne',
element: document.querySelector('.menu-one'),
}),
BubbleMenu.configure({
key: 'bubbleMenuTwo',
element: document.querySelector('.menu-two'),
}),
],
})
```
Alternatively you can pass a ProseMirror `PluginKey`.
```js
import { Editor } from '@tiptap/core'
import BubbleMenu from '@tiptap/extension-bubble-menu'
import { PluginKey } from 'prosemirror-state'
new Editor({
extensions: [
BubbleMenu.configure({
key: new PluginKey('bubbleMenuOne'),
element: document.querySelector('.menu-one'),
}),
BubbleMenu.configure({
key: new PluginKey('bubbleMenuTwo'),
element: document.querySelector('.menu-two'),
}),
],
})
```

View File

@@ -13,10 +13,12 @@ yarn add @tiptap/extension-floating-menu
``` ```
## Settings ## Settings
| Option | Type | Default | Description | | Option | Type | Default | Description |
| ------------ | ------------- | ------- | ----------------------------------------------------------------------- | | ------------ | -------------------- | ---------------- | ----------------------------------------------------------------------- |
| element | `HTMLElement` | `null` | The DOM element of your menu. | | element | `HTMLElement` | `null` | The DOM element of your menu. |
| tippyOptions | `Object` | `{}` | [Options for tippy.js](https://atomiks.github.io/tippyjs/v6/all-props/) | | tippyOptions | `Object` | `{}` | [Options for tippy.js](https://atomiks.github.io/tippyjs/v6/all-props/) |
| key | `string | PluginKey` | `'floatingMenu'` | The key for the underlying ProseMirror plugin. |
| shouldShow | `(props) => boolean` | | Controls whether the menu should be shown or not. |
## Source code ## Source code
[packages/extension-floating-menu/](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-floating-menu/) [packages/extension-floating-menu/](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-floating-menu/)
@@ -40,3 +42,57 @@ new Editor({
Vue: 'Extensions/FloatingMenu/Vue', Vue: 'Extensions/FloatingMenu/Vue',
React: 'Extensions/FloatingMenu/React', React: 'Extensions/FloatingMenu/React',
}" /> }" />
### Custom logic
Customize the logic for showing the menu with the `shouldShow` option. For components, `shouldShow` can be passed as a prop.
```js
FloatingMenu.configure({
shouldShow: ({ editor, view, state, oldState }) => {
// show the floating within any paragraph
return editor.isActive('paragraph')
},
})
```
### Multiple menus
Use multiple menus by setting an unique `key`.
```js
import { Editor } from '@tiptap/core'
import FloatingMenu from '@tiptap/extension-floating-menu'
new Editor({
extensions: [
FloatingMenu.configure({
key: 'floatingMenuOne',
element: document.querySelector('.menu-one'),
}),
FloatingMenu.configure({
key: 'floatingMenuTwo',
element: document.querySelector('.menu-two'),
}),
],
})
```
Alternatively you can pass a ProseMirror `PluginKey`.
```js
import { Editor } from '@tiptap/core'
import FloatingMenu from '@tiptap/extension-floating-menu'
import { PluginKey } from 'prosemirror-state'
new Editor({
extensions: [
FloatingMenu.configure({
key: new PluginKey('floatingMenuOne'),
element: document.querySelector('.menu-one'),
}),
FloatingMenu.configure({
key: new PluginKey('floatingMenuOne'),
element: document.querySelector('.menu-two'),
}),
],
})
```

View File

@@ -9,9 +9,18 @@ import { EditorView } from 'prosemirror-view'
import tippy, { Instance, Props } from 'tippy.js' import tippy, { Instance, Props } from 'tippy.js'
export interface BubbleMenuPluginProps { export interface BubbleMenuPluginProps {
key: PluginKey | string,
editor: Editor, editor: Editor,
element: HTMLElement, element: HTMLElement,
tippyOptions?: Partial<Props>, tippyOptions?: Partial<Props>,
shouldShow: ((props: {
editor: Editor,
view: EditorView,
state: EditorState,
oldState?: EditorState,
from: number,
to: number,
}) => boolean) | null,
} }
export type BubbleMenuViewProps = BubbleMenuPluginProps & { export type BubbleMenuViewProps = BubbleMenuPluginProps & {
@@ -29,15 +38,38 @@ export class BubbleMenuView {
public tippy!: Instance public tippy!: Instance
public shouldShow: Exclude<BubbleMenuPluginProps['shouldShow'], null> = ({ state, from, to }) => {
const { doc, selection } = state
const { empty } = selection
// Sometime check for `empty` is not enough.
// Doubleclick an empty paragraph returns a node size of 2.
// So we check also for an empty text size.
const isEmptyTextBlock = !doc.textBetween(from, to).length
&& isTextSelection(state.selection)
if (empty || isEmptyTextBlock) {
return false
}
return true
}
constructor({ constructor({
editor, editor,
element, element,
view, view,
tippyOptions, tippyOptions,
shouldShow,
}: BubbleMenuViewProps) { }: BubbleMenuViewProps) {
this.editor = editor this.editor = editor
this.element = element this.element = element
this.view = view this.view = view
if (shouldShow) {
this.shouldShow = shouldShow
}
this.element.addEventListener('mousedown', this.mousedownHandler, { capture: true }) this.element.addEventListener('mousedown', this.mousedownHandler, { capture: true })
this.view.dom.addEventListener('dragstart', this.dragstartHandler) this.view.dom.addEventListener('dragstart', this.dragstartHandler)
this.editor.on('focus', this.focusHandler) this.editor.on('focus', this.focusHandler)
@@ -98,19 +130,21 @@ export class BubbleMenuView {
return return
} }
const { empty, ranges } = selection
// support for CellSelections // support for CellSelections
const { ranges } = selection
const from = Math.min(...ranges.map(range => range.$from.pos)) const from = Math.min(...ranges.map(range => range.$from.pos))
const to = Math.max(...ranges.map(range => range.$to.pos)) const to = Math.max(...ranges.map(range => range.$to.pos))
// Sometime check for `empty` is not enough. const shouldShow = this.shouldShow({
// Doubleclick an empty paragraph returns a node size of 2. editor: this.editor,
// So we check also for an empty text size. view,
const isEmptyTextBlock = !doc.textBetween(from, to).length state,
&& isTextSelection(view.state.selection) oldState,
from,
to,
})
if (empty || isEmptyTextBlock) { if (!shouldShow) {
this.hide() this.hide()
return return
@@ -118,7 +152,7 @@ export class BubbleMenuView {
this.tippy.setProps({ this.tippy.setProps({
getReferenceClientRect: () => { getReferenceClientRect: () => {
if (isNodeSelection(view.state.selection)) { if (isNodeSelection(state.selection)) {
const node = view.nodeDOM(from) as HTMLElement const node = view.nodeDOM(from) as HTMLElement
if (node) { if (node) {
@@ -150,11 +184,11 @@ export class BubbleMenuView {
} }
} }
export const BubbleMenuPluginKey = new PluginKey('menuBubble')
export const BubbleMenuPlugin = (options: BubbleMenuPluginProps) => { export const BubbleMenuPlugin = (options: BubbleMenuPluginProps) => {
return new Plugin({ return new Plugin({
key: BubbleMenuPluginKey, key: typeof options.key === 'string'
? new PluginKey(options.key)
: options.key,
view: view => new BubbleMenuView({ view, ...options }), view: view => new BubbleMenuView({ view, ...options }),
}) })
} }

View File

@@ -11,6 +11,8 @@ export const BubbleMenu = Extension.create<BubbleMenuOptions>({
defaultOptions: { defaultOptions: {
element: null, element: null,
tippyOptions: {}, tippyOptions: {},
key: 'bubbleMenu',
shouldShow: null,
}, },
addProseMirrorPlugins() { addProseMirrorPlugins() {
@@ -20,9 +22,11 @@ export const BubbleMenu = Extension.create<BubbleMenuOptions>({
return [ return [
BubbleMenuPlugin({ BubbleMenuPlugin({
key: this.options.key,
editor: this.editor, editor: this.editor,
element: this.options.element, element: this.options.element,
tippyOptions: this.options.tippyOptions, tippyOptions: this.options.tippyOptions,
shouldShow: this.options.shouldShow,
}), }),
] ]
}, },

View File

@@ -4,9 +4,16 @@ import { EditorView } from 'prosemirror-view'
import tippy, { Instance, Props } from 'tippy.js' import tippy, { Instance, Props } from 'tippy.js'
export interface FloatingMenuPluginProps { export interface FloatingMenuPluginProps {
key: PluginKey | string,
editor: Editor, editor: Editor,
element: HTMLElement, element: HTMLElement,
tippyOptions?: Partial<Props>, tippyOptions?: Partial<Props>,
shouldShow: ((props: {
editor: Editor,
view: EditorView,
state: EditorState,
oldState?: EditorState,
}) => boolean) | null,
} }
export type FloatingMenuViewProps = FloatingMenuPluginProps & { export type FloatingMenuViewProps = FloatingMenuPluginProps & {
@@ -24,15 +31,36 @@ export class FloatingMenuView {
public tippy!: Instance public tippy!: Instance
public shouldShow: Exclude<FloatingMenuPluginProps['shouldShow'], null> = ({ state }) => {
const { selection } = state
const { $anchor, empty } = selection
const isRootDepth = $anchor.depth === 1
const isEmptyTextBlock = $anchor.parent.isTextblock
&& !$anchor.parent.type.spec.code
&& !$anchor.parent.textContent
if (!empty || !isRootDepth || !isEmptyTextBlock) {
return false
}
return true
}
constructor({ constructor({
editor, editor,
element, element,
view, view,
tippyOptions, tippyOptions,
shouldShow,
}: FloatingMenuViewProps) { }: FloatingMenuViewProps) {
this.editor = editor this.editor = editor
this.element = element this.element = element
this.view = view this.view = view
if (shouldShow) {
this.shouldShow = shouldShow
}
this.element.addEventListener('mousedown', this.mousedownHandler, { capture: true }) this.element.addEventListener('mousedown', this.mousedownHandler, { capture: true })
this.editor.on('focus', this.focusHandler) this.editor.on('focus', this.focusHandler)
this.editor.on('blur', this.blurHandler) this.editor.on('blur', this.blurHandler)
@@ -82,23 +110,21 @@ export class FloatingMenuView {
update(view: EditorView, oldState?: EditorState) { update(view: EditorView, oldState?: EditorState) {
const { state, composing } = view const { state, composing } = view
const { doc, selection } = state const { doc, selection } = state
const { from, to } = selection
const isSame = oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection) const isSame = oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection)
if (composing || isSame) { if (composing || isSame) {
return return
} }
const { const shouldShow = this.shouldShow({
$anchor, editor: this.editor,
empty, view,
from, state,
to, oldState,
} = selection })
const isRootDepth = $anchor.depth === 1
const isNodeEmpty = !selection.$anchor.parent.isLeaf && !selection.$anchor.parent.textContent
const isActive = isRootDepth && isNodeEmpty
if (!empty || !isActive) { if (!shouldShow) {
this.hide() this.hide()
return return
@@ -127,11 +153,11 @@ export class FloatingMenuView {
} }
} }
export const FloatingMenuPluginKey = new PluginKey('menuFloating')
export const FloatingMenuPlugin = (options: FloatingMenuPluginProps) => { export const FloatingMenuPlugin = (options: FloatingMenuPluginProps) => {
return new Plugin({ return new Plugin({
key: FloatingMenuPluginKey, key: typeof options.key === 'string'
? new PluginKey(options.key)
: options.key,
view: view => new FloatingMenuView({ view, ...options }), view: view => new FloatingMenuView({ view, ...options }),
}) })
} }

View File

@@ -11,6 +11,8 @@ export const FloatingMenu = Extension.create<FloatingMenuOptions>({
defaultOptions: { defaultOptions: {
element: null, element: null,
tippyOptions: {}, tippyOptions: {},
key: 'floatingMenu',
shouldShow: null,
}, },
addProseMirrorPlugins() { addProseMirrorPlugins() {
@@ -20,9 +22,11 @@ export const FloatingMenu = Extension.create<FloatingMenuOptions>({
return [ return [
FloatingMenuPlugin({ FloatingMenuPlugin({
key: this.options.key,
editor: this.editor, editor: this.editor,
element: this.options.element, element: this.options.element,
tippyOptions: this.options.tippyOptions, tippyOptions: this.options.tippyOptions,
shouldShow: this.options.shouldShow,
}), }),
] ]
}, },

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useRef } from 'react' import React, { useEffect, useRef } from 'react'
import { BubbleMenuPlugin, BubbleMenuPluginKey, BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu' import { BubbleMenuPlugin, BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu'
export type BubbleMenuProps = Omit<BubbleMenuPluginProps, 'element'> & { export type BubbleMenuProps = Omit<BubbleMenuPluginProps, 'element'> & {
className?: string, className?: string,
@@ -9,16 +9,23 @@ export const BubbleMenu: React.FC<BubbleMenuProps> = props => {
const element = useRef<HTMLDivElement>(null) const element = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
const { editor, tippyOptions } = props const {
key,
editor,
tippyOptions,
shouldShow,
} = props
editor.registerPlugin(BubbleMenuPlugin({ editor.registerPlugin(BubbleMenuPlugin({
key,
editor, editor,
element: element.current as HTMLElement, element: element.current as HTMLElement,
tippyOptions, tippyOptions,
shouldShow,
})) }))
return () => { return () => {
editor.unregisterPlugin(BubbleMenuPluginKey) editor.unregisterPlugin(key)
} }
}, []) }, [])

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useRef } from 'react' import React, { useEffect, useRef } from 'react'
import { FloatingMenuPlugin, FloatingMenuPluginKey, FloatingMenuPluginProps } from '@tiptap/extension-floating-menu' import { FloatingMenuPlugin, FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'
export type FloatingMenuProps = Omit<FloatingMenuPluginProps, 'element'> & { export type FloatingMenuProps = Omit<FloatingMenuPluginProps, 'element'> & {
className?: string, className?: string,
@@ -9,16 +9,23 @@ export const FloatingMenu: React.FC<FloatingMenuProps> = props => {
const element = useRef<HTMLDivElement>(null) const element = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
const { editor, tippyOptions } = props const {
key,
editor,
tippyOptions,
shouldShow,
} = props
editor.registerPlugin(FloatingMenuPlugin({ editor.registerPlugin(FloatingMenuPlugin({
key,
editor, editor,
element: element.current as HTMLElement, element: element.current as HTMLElement,
tippyOptions, tippyOptions,
shouldShow,
})) }))
return () => { return () => {
editor.unregisterPlugin(FloatingMenuPluginKey) editor.unregisterPlugin(key)
} }
}, []) }, [])

View File

@@ -1,15 +1,22 @@
import Vue, { Component, PropType } from 'vue' import Vue, { Component, PropType } from 'vue'
import { BubbleMenuPlugin, BubbleMenuPluginKey, BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu' import { BubbleMenuPlugin, BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu'
export interface BubbleMenuInterface extends Vue { export interface BubbleMenuInterface extends Vue {
tippyOptions: BubbleMenuPluginProps['tippyOptions'], pluginKey: BubbleMenuPluginProps['key'],
editor: BubbleMenuPluginProps['editor'], editor: BubbleMenuPluginProps['editor'],
tippyOptions: BubbleMenuPluginProps['tippyOptions'],
shouldShow: BubbleMenuPluginProps['shouldShow'],
} }
export const BubbleMenu: Component = { export const BubbleMenu: Component = {
name: 'BubbleMenu', name: 'BubbleMenu',
props: { props: {
pluginKey: {
type: [String, Object as PropType<Exclude<BubbleMenuPluginProps['key'], string>>],
default: 'bubbleMenu',
},
editor: { editor: {
type: Object as PropType<BubbleMenuPluginProps['editor']>, type: Object as PropType<BubbleMenuPluginProps['editor']>,
required: true, required: true,
@@ -19,6 +26,11 @@ export const BubbleMenu: Component = {
type: Object as PropType<BubbleMenuPluginProps['tippyOptions']>, type: Object as PropType<BubbleMenuPluginProps['tippyOptions']>,
default: () => ({}), default: () => ({}),
}, },
shouldShow: {
type: Function as PropType<Exclude<BubbleMenuPluginProps['shouldShow'], null>>,
default: null,
},
}, },
watch: { watch: {
@@ -31,9 +43,11 @@ export const BubbleMenu: Component = {
this.$nextTick(() => { this.$nextTick(() => {
editor.registerPlugin(BubbleMenuPlugin({ editor.registerPlugin(BubbleMenuPlugin({
key: this.pluginKey,
editor, editor,
element: this.$el as HTMLElement, element: this.$el as HTMLElement,
tippyOptions: this.tippyOptions, tippyOptions: this.tippyOptions,
shouldShow: this.shouldShow,
})) }))
}) })
}, },
@@ -45,6 +59,6 @@ export const BubbleMenu: Component = {
}, },
beforeDestroy(this: BubbleMenuInterface) { beforeDestroy(this: BubbleMenuInterface) {
this.editor.unregisterPlugin(BubbleMenuPluginKey) this.editor.unregisterPlugin(this.pluginKey)
}, },
} }

View File

@@ -1,15 +1,22 @@
import Vue, { Component, PropType } from 'vue' import Vue, { Component, PropType } from 'vue'
import { FloatingMenuPlugin, FloatingMenuPluginKey, FloatingMenuPluginProps } from '@tiptap/extension-floating-menu' import { FloatingMenuPlugin, FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'
export interface FloatingMenuInterface extends Vue { export interface FloatingMenuInterface extends Vue {
pluginKey: FloatingMenuPluginProps['key'],
tippyOptions: FloatingMenuPluginProps['tippyOptions'], tippyOptions: FloatingMenuPluginProps['tippyOptions'],
editor: FloatingMenuPluginProps['editor'], editor: FloatingMenuPluginProps['editor'],
shouldShow: FloatingMenuPluginProps['shouldShow'],
} }
export const FloatingMenu: Component = { export const FloatingMenu: Component = {
name: 'FloatingMenu', name: 'FloatingMenu',
props: { props: {
pluginKey: {
type: [String, Object as PropType<Exclude<FloatingMenuPluginProps['key'], string>>],
default: 'floatingMenu',
},
editor: { editor: {
type: Object as PropType<FloatingMenuPluginProps['editor']>, type: Object as PropType<FloatingMenuPluginProps['editor']>,
required: true, required: true,
@@ -19,6 +26,11 @@ export const FloatingMenu: Component = {
type: Object as PropType<FloatingMenuPluginProps['tippyOptions']>, type: Object as PropType<FloatingMenuPluginProps['tippyOptions']>,
default: () => ({}), default: () => ({}),
}, },
shouldShow: {
type: Function as PropType<Exclude<FloatingMenuPluginProps['shouldShow'], null>>,
default: null,
},
}, },
watch: { watch: {
@@ -31,9 +43,11 @@ export const FloatingMenu: Component = {
this.$nextTick(() => { this.$nextTick(() => {
editor.registerPlugin(FloatingMenuPlugin({ editor.registerPlugin(FloatingMenuPlugin({
key: this.pluginKey,
editor, editor,
element: this.$el as HTMLElement, element: this.$el as HTMLElement,
tippyOptions: this.tippyOptions, tippyOptions: this.tippyOptions,
shouldShow: this.shouldShow,
})) }))
}) })
}, },
@@ -45,6 +59,6 @@ export const FloatingMenu: Component = {
}, },
beforeDestroy(this: FloatingMenuInterface) { beforeDestroy(this: FloatingMenuInterface) {
this.editor.unregisterPlugin(FloatingMenuPluginKey) this.editor.unregisterPlugin(this.pluginKey)
}, },
} }

View File

@@ -6,16 +6,19 @@ import {
onBeforeUnmount, onBeforeUnmount,
defineComponent, defineComponent,
} from 'vue' } from 'vue'
import { import { BubbleMenuPlugin, BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu'
BubbleMenuPlugin,
BubbleMenuPluginKey,
BubbleMenuPluginProps,
} from '@tiptap/extension-bubble-menu'
export const BubbleMenu = defineComponent({ export const BubbleMenu = defineComponent({
name: 'BubbleMenu', name: 'BubbleMenu',
props: { props: {
pluginKey: {
// TODO: TypeScript breaks :(
// type: [String, Object as PropType<Exclude<BubbleMenuPluginProps['key'], string>>],
type: [String, Object],
default: 'bubbleMenu',
},
editor: { editor: {
type: Object as PropType<BubbleMenuPluginProps['editor']>, type: Object as PropType<BubbleMenuPluginProps['editor']>,
required: true, required: true,
@@ -25,25 +28,37 @@ export const BubbleMenu = defineComponent({
type: Object as PropType<BubbleMenuPluginProps['tippyOptions']>, type: Object as PropType<BubbleMenuPluginProps['tippyOptions']>,
default: () => ({}), default: () => ({}),
}, },
shouldShow: {
type: Function as PropType<Exclude<BubbleMenuPluginProps['shouldShow'], null>>,
default: null,
},
}, },
setup(props, { slots }) { setup(props, { slots }) {
const root = ref<HTMLElement | null>(null) const root = ref<HTMLElement | null>(null)
onMounted(() => { onMounted(() => {
const { editor, tippyOptions } = props const {
pluginKey,
editor,
tippyOptions,
shouldShow,
} = props
editor.registerPlugin(BubbleMenuPlugin({ editor.registerPlugin(BubbleMenuPlugin({
key: pluginKey,
editor, editor,
element: root.value as HTMLElement, element: root.value as HTMLElement,
tippyOptions, tippyOptions,
shouldShow,
})) }))
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
const { editor } = props const { pluginKey, editor } = props
editor.unregisterPlugin(BubbleMenuPluginKey) editor.unregisterPlugin(pluginKey)
}) })
return () => h('div', { ref: root }, slots.default?.()) return () => h('div', { ref: root }, slots.default?.())

View File

@@ -6,16 +6,19 @@ import {
onBeforeUnmount, onBeforeUnmount,
defineComponent, defineComponent,
} from 'vue' } from 'vue'
import { import { FloatingMenuPlugin, FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'
FloatingMenuPlugin,
FloatingMenuPluginKey,
FloatingMenuPluginProps,
} from '@tiptap/extension-floating-menu'
export const FloatingMenu = defineComponent({ export const FloatingMenu = defineComponent({
name: 'FloatingMenu', name: 'FloatingMenu',
props: { props: {
pluginKey: {
// TODO: TypeScript breaks :(
// type: [String, Object as PropType<Exclude<FloatingMenuPluginProps['key'], string>>],
type: [String, Object],
default: 'floatingMenu',
},
editor: { editor: {
type: Object as PropType<FloatingMenuPluginProps['editor']>, type: Object as PropType<FloatingMenuPluginProps['editor']>,
required: true, required: true,
@@ -25,25 +28,37 @@ export const FloatingMenu = defineComponent({
type: Object as PropType<FloatingMenuPluginProps['tippyOptions']>, type: Object as PropType<FloatingMenuPluginProps['tippyOptions']>,
default: () => ({}), default: () => ({}),
}, },
shouldShow: {
type: Function as PropType<Exclude<FloatingMenuPluginProps['shouldShow'], null>>,
default: null,
},
}, },
setup(props, { slots }) { setup(props, { slots }) {
const root = ref<HTMLElement | null>(null) const root = ref<HTMLElement | null>(null)
onMounted(() => { onMounted(() => {
const { editor, tippyOptions } = props const {
pluginKey,
editor,
tippyOptions,
shouldShow,
} = props
editor.registerPlugin(FloatingMenuPlugin({ editor.registerPlugin(FloatingMenuPlugin({
key: pluginKey,
editor, editor,
element: root.value as HTMLElement, element: root.value as HTMLElement,
tippyOptions, tippyOptions,
shouldShow,
})) }))
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
const { editor } = props const { pluginKey, editor } = props
editor.unregisterPlugin(FloatingMenuPluginKey) editor.unregisterPlugin(pluginKey)
}) })
return () => h('div', { ref: root }, slots.default?.()) return () => h('div', { ref: root }, slots.default?.())