* 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:
@@ -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'),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|||||||
@@ -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 }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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?.())
|
||||||
|
|||||||
@@ -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?.())
|
||||||
|
|||||||
Reference in New Issue
Block a user