refactor(extension/bubble-menu): add debounce to bubble menu updates (#3385)

* refactor(extension/bubble-menu): add debounce to bubble menu updates

* fix: change default duration in react bubble menu demo
This commit is contained in:
Dominik 2022-11-04 16:39:41 +01:00 committed by GitHub
parent 1f549c0773
commit cd5fd606d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 94 additions and 27 deletions

View File

@ -4,7 +4,11 @@
<input type="checkbox" :checked="isEditable" @change="() => isEditable = !isEditable">
Editable
</div>
<bubble-menu :editor="editor" :tippy-options="{ duration: 100 }" v-if="editor">
<bubble-menu
:editor="editor"
:tippy-options="{ duration: 100 }"
v-if="editor"
>
<button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
bold
</button>

View File

@ -25,6 +25,14 @@ Type: `HTMLElement`
Default: `null`
### delay
The `BubbleMenu` debounces the `update` method to allow the bubble menu to not be updated on every selection update. This can be controlled in milliseconds.
The BubbleMenuPlugin will come with a default delay of 250ms. This can be deactivated, by setting the delay to `0` which deactivates the debounce.
Type: `Number`
Default: `undefined`
### tippyOptions
Under the hood, the `BubbleMenu` uses [tippy.js](https://atomiks.github.io/tippyjs/v6/all-props/). You can directly pass options to it.

25
package-lock.json generated
View File

@ -6790,6 +6790,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.14.187",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.187.tgz",
"integrity": "sha512-MrO/xLXCaUgZy3y96C/iOsaIqZSeupyTImKClHunL5GrmaiII2VwvWmLBu2hwa0Kp0sV19CsyjtrTc/Fx8rg/A==",
"dev": true
},
"node_modules/@types/minimatch": {
"version": "3.0.5",
"dev": true,
@ -13603,8 +13609,9 @@
},
"node_modules/lodash": {
"version": "4.17.21",
"dev": true,
"license": "MIT"
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
@ -19356,6 +19363,10 @@
"prosemirror-view": "^1.28.2",
"tippy.js": "^6.3.7"
},
"devDependencies": {
"@types/lodash": "^4.14.187",
"lodash": "^4.17.21"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@ -24872,6 +24883,8 @@
"@tiptap/extension-bubble-menu": {
"version": "file:packages/extension-bubble-menu",
"requires": {
"@types/lodash": "*",
"lodash": "^4.17.21",
"prosemirror-state": "^1.4.1",
"prosemirror-view": "^1.28.2",
"tippy.js": "^6.3.7"
@ -25207,6 +25220,12 @@
"version": "0.0.29",
"dev": true
},
"@types/lodash": {
"version": "4.14.187",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.187.tgz",
"integrity": "sha512-MrO/xLXCaUgZy3y96C/iOsaIqZSeupyTImKClHunL5GrmaiII2VwvWmLBu2hwa0Kp0sV19CsyjtrTc/Fx8rg/A==",
"dev": true
},
"@types/minimatch": {
"version": "3.0.5",
"dev": true
@ -29770,6 +29789,8 @@
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"lodash.debounce": {

View File

@ -33,5 +33,9 @@
"url": "https://github.com/ueberdosis/tiptap",
"directory": "packages/extension-bubble-menu"
},
"sideEffects": false
"sideEffects": false,
"devDependencies": {
"@types/lodash": "^4.14.187",
"lodash": "^4.17.21"
}
}

View File

@ -4,6 +4,7 @@ import {
isTextSelection,
posToDOMRect,
} from '@tiptap/core'
import debounce from 'lodash/debounce'
import { EditorState, Plugin, PluginKey } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view'
import tippy, { Instance, Props } from 'tippy.js'
@ -13,6 +14,7 @@ export interface BubbleMenuPluginProps {
editor: Editor,
element: HTMLElement,
tippyOptions?: Partial<Props>,
delay?: number,
shouldShow?: ((props: {
editor: Editor,
view: EditorView,
@ -40,6 +42,8 @@ export class BubbleMenuView {
public tippyOptions?: Partial<Props>
public delay: number
public shouldShow: Exclude<BubbleMenuPluginProps['shouldShow'], null> = ({
view,
state,
@ -79,11 +83,13 @@ export class BubbleMenuView {
element,
view,
tippyOptions = {},
delay = 250,
shouldShow,
}: BubbleMenuViewProps) {
this.editor = editor
this.element = element
this.view = view
this.delay = delay
if (shouldShow) {
this.shouldShow = shouldShow
@ -157,6 +163,21 @@ export class BubbleMenuView {
}
update(view: EditorView, oldState?: EditorState) {
const { state } = view
const hasValidSelection = state.selection.$from.pos !== state.selection.$to.pos
if (hasValidSelection) {
if (this.delay > 0) {
debounce(this.updateHandler, this.delay)(view, oldState)
} else {
this.updateHandler(view, oldState)
}
} else {
this.hide()
}
}
updateHandler = (view: EditorView, oldState?: EditorState) => {
const { state, composing } = view
const { doc, selection } = state
const isSame = oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection)

View File

@ -14,6 +14,7 @@ export const BubbleMenu = Extension.create<BubbleMenuOptions>({
element: null,
tippyOptions: {},
pluginKey: 'bubbleMenu',
delay: undefined,
shouldShow: null,
}
},
@ -29,6 +30,7 @@ export const BubbleMenu = Extension.create<BubbleMenuOptions>({
editor: this.editor,
element: this.options.element,
tippyOptions: this.options.tippyOptions,
delay: this.options.delay,
shouldShow: this.options.shouldShow,
}),
]

View File

@ -1,14 +1,13 @@
import { BubbleMenuPlugin, BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu'
import React, {
useEffect, useState,
} from 'react'
import React, { useEffect, useState } from 'react'
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
export type BubbleMenuProps = Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'> & {
className?: string,
children: React.ReactNode
}
className?: string;
children: React.ReactNode;
delay?: number;
};
export const BubbleMenu = (props: BubbleMenuProps) => {
const [element, setElement] = useState<HTMLDivElement | null>(null)
@ -23,26 +22,21 @@ export const BubbleMenu = (props: BubbleMenuProps) => {
}
const {
pluginKey = 'bubbleMenu',
editor,
tippyOptions = {},
shouldShow = null,
pluginKey = 'bubbleMenu', editor, tippyOptions = {}, delay, shouldShow = null,
} = props
const plugin = BubbleMenuPlugin({
pluginKey,
delay,
editor,
element,
tippyOptions,
pluginKey,
shouldShow,
tippyOptions,
})
editor.registerPlugin(plugin)
return () => editor.unregisterPlugin(pluginKey)
}, [
props.editor,
element,
])
}, [props.editor, element])
return (
<div ref={setElement} className={props.className} style={{ visibility: 'hidden' }}>

View File

@ -5,6 +5,7 @@ export interface BubbleMenuInterface extends Vue {
pluginKey: BubbleMenuPluginProps['pluginKey'],
editor: BubbleMenuPluginProps['editor'],
tippyOptions: BubbleMenuPluginProps['tippyOptions'],
delay: BubbleMenuPluginProps['delay'],
shouldShow: BubbleMenuPluginProps['shouldShow'],
}
@ -22,6 +23,10 @@ export const BubbleMenu: Component = {
required: true,
},
delay: {
type: Number as PropType<BubbleMenuPluginProps['delay']>,
},
tippyOptions: {
type: Object as PropType<BubbleMenuPluginProps['tippyOptions']>,
default: () => ({}),
@ -43,11 +48,12 @@ export const BubbleMenu: Component = {
this.$nextTick(() => {
editor.registerPlugin(BubbleMenuPlugin({
pluginKey: this.pluginKey,
delay: this.delay,
editor,
element: this.$el as HTMLElement,
tippyOptions: this.tippyOptions,
pluginKey: this.pluginKey,
shouldShow: this.shouldShow,
tippyOptions: this.tippyOptions,
}))
})
},

View File

@ -24,6 +24,11 @@ export const BubbleMenu = defineComponent({
required: true,
},
delay: {
type: Number as PropType<BubbleMenuPluginProps['delay']>,
default: undefined,
},
tippyOptions: {
type: Object as PropType<BubbleMenuPluginProps['tippyOptions']>,
default: () => ({}),
@ -40,18 +45,20 @@ export const BubbleMenu = defineComponent({
onMounted(() => {
const {
pluginKey,
delay,
editor,
tippyOptions,
pluginKey,
shouldShow,
tippyOptions,
} = props
editor.registerPlugin(BubbleMenuPlugin({
pluginKey,
delay,
editor,
element: root.value as HTMLElement,
tippyOptions,
pluginKey,
shouldShow,
tippyOptions,
}))
})