Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Menu improvements #1714

Merged
merged 8 commits into from
Aug 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 60 additions & 4 deletions docs/src/docPages/api/extensions/bubble-menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ yarn add @tiptap/extension-bubble-menu
```

## Settings
| Option | Type | Default | Description |
| ------------ | ------------- | ------- | ----------------------------------------------------------------------- |
| element | `HTMLElement` | `null` | The DOM element that contains your menu. |
| tippyOptions | `Object` | `{}` | [Options for tippy.js](https://atomiks.github.io/tippyjs/v6/all-props/) |
| Option | Type | Default | Description |
| ------------ | -------------------- | -------------- | ----------------------------------------------------------------------- |
| element | `HTMLElement` | `null` | The DOM element that contains your menu. |
| 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
[packages/extension-bubble-menu/](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-bubble-menu/)
Expand All @@ -44,3 +46,57 @@ new Editor({
Vue: 'Extensions/BubbleMenu/Vue',
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'),
}),
],
})
```
64 changes: 60 additions & 4 deletions docs/src/docPages/api/extensions/floating-menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ yarn add @tiptap/extension-floating-menu
```

## Settings
| Option | Type | Default | Description |
| ------------ | ------------- | ------- | ----------------------------------------------------------------------- |
| element | `HTMLElement` | `null` | The DOM element of your menu. |
| tippyOptions | `Object` | `{}` | [Options for tippy.js](https://atomiks.github.io/tippyjs/v6/all-props/) |
| Option | Type | Default | Description |
| ------------ | -------------------- | ---------------- | ----------------------------------------------------------------------- |
| element | `HTMLElement` | `null` | The DOM element of your menu. |
| 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
[packages/extension-floating-menu/](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-floating-menu/)
Expand All @@ -40,3 +42,57 @@ new Editor({
Vue: 'Extensions/FloatingMenu/Vue',
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'),
}),
],
})
```
58 changes: 46 additions & 12 deletions packages/extension-bubble-menu/src/bubble-menu-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,18 @@ import { EditorView } from 'prosemirror-view'
import tippy, { Instance, Props } from 'tippy.js'

export interface BubbleMenuPluginProps {
key: PluginKey | string,
editor: Editor,
element: HTMLElement,
tippyOptions?: Partial<Props>,
shouldShow: ((props: {
editor: Editor,
view: EditorView,
state: EditorState,
oldState?: EditorState,
from: number,
to: number,
}) => boolean) | null,
}

export type BubbleMenuViewProps = BubbleMenuPluginProps & {
Expand All @@ -29,15 +38,38 @@ export class BubbleMenuView {

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({
editor,
element,
view,
tippyOptions,
shouldShow,
}: BubbleMenuViewProps) {
this.editor = editor
this.element = element
this.view = view

if (shouldShow) {
this.shouldShow = shouldShow
}

this.element.addEventListener('mousedown', this.mousedownHandler, { capture: true })
this.view.dom.addEventListener('dragstart', this.dragstartHandler)
this.editor.on('focus', this.focusHandler)
Expand Down Expand Up @@ -98,27 +130,29 @@ export class BubbleMenuView {
return
}

const { empty, ranges } = selection

// support for CellSelections
const { ranges } = selection
const from = Math.min(...ranges.map(range => range.$from.pos))
const to = Math.max(...ranges.map(range => range.$to.pos))

// 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(view.state.selection)
const shouldShow = this.shouldShow({
editor: this.editor,
view,
state,
oldState,
from,
to,
})

if (empty || isEmptyTextBlock) {
if (!shouldShow) {
this.hide()

return
}

this.tippy.setProps({
getReferenceClientRect: () => {
if (isNodeSelection(view.state.selection)) {
if (isNodeSelection(state.selection)) {
const node = view.nodeDOM(from) as HTMLElement

if (node) {
Expand Down Expand Up @@ -150,11 +184,11 @@ export class BubbleMenuView {
}
}

export const BubbleMenuPluginKey = new PluginKey('menuBubble')

export const BubbleMenuPlugin = (options: BubbleMenuPluginProps) => {
return new Plugin({
key: BubbleMenuPluginKey,
key: typeof options.key === 'string'
? new PluginKey(options.key)
: options.key,
view: view => new BubbleMenuView({ view, ...options }),
})
}
4 changes: 4 additions & 0 deletions packages/extension-bubble-menu/src/bubble-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export const BubbleMenu = Extension.create<BubbleMenuOptions>({
defaultOptions: {
element: null,
tippyOptions: {},
key: 'bubbleMenu',
shouldShow: null,
},

addProseMirrorPlugins() {
Expand All @@ -20,9 +22,11 @@ export const BubbleMenu = Extension.create<BubbleMenuOptions>({

return [
BubbleMenuPlugin({
key: this.options.key,
editor: this.editor,
element: this.options.element,
tippyOptions: this.options.tippyOptions,
shouldShow: this.options.shouldShow,
}),
]
},
Expand Down
52 changes: 39 additions & 13 deletions packages/extension-floating-menu/src/floating-menu-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ import { EditorView } from 'prosemirror-view'
import tippy, { Instance, Props } from 'tippy.js'

export interface FloatingMenuPluginProps {
key: PluginKey | string,
editor: Editor,
element: HTMLElement,
tippyOptions?: Partial<Props>,
shouldShow: ((props: {
editor: Editor,
view: EditorView,
state: EditorState,
oldState?: EditorState,
}) => boolean) | null,
}

export type FloatingMenuViewProps = FloatingMenuPluginProps & {
Expand All @@ -24,15 +31,36 @@ export class FloatingMenuView {

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({
editor,
element,
view,
tippyOptions,
shouldShow,
}: FloatingMenuViewProps) {
this.editor = editor
this.element = element
this.view = view

if (shouldShow) {
this.shouldShow = shouldShow
}

this.element.addEventListener('mousedown', this.mousedownHandler, { capture: true })
this.editor.on('focus', this.focusHandler)
this.editor.on('blur', this.blurHandler)
Expand Down Expand Up @@ -82,23 +110,21 @@ export class FloatingMenuView {
update(view: EditorView, oldState?: EditorState) {
const { state, composing } = view
const { doc, selection } = state
const { from, to } = selection
const isSame = oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection)

if (composing || isSame) {
return
}

const {
$anchor,
empty,
from,
to,
} = selection
const isRootDepth = $anchor.depth === 1
const isNodeEmpty = !selection.$anchor.parent.isLeaf && !selection.$anchor.parent.textContent
const isActive = isRootDepth && isNodeEmpty
const shouldShow = this.shouldShow({
editor: this.editor,
view,
state,
oldState,
})

if (!empty || !isActive) {
if (!shouldShow) {
this.hide()

return
Expand Down Expand Up @@ -127,11 +153,11 @@ export class FloatingMenuView {
}
}

export const FloatingMenuPluginKey = new PluginKey('menuFloating')

export const FloatingMenuPlugin = (options: FloatingMenuPluginProps) => {
return new Plugin({
key: FloatingMenuPluginKey,
key: typeof options.key === 'string'
? new PluginKey(options.key)
: options.key,
view: view => new FloatingMenuView({ view, ...options }),
})
}
Loading