tiptap: fix bubble menu

This commit is contained in:
fantasticit 2022-05-09 15:26:47 +08:00
parent a06c795360
commit 7e0d145ce9
7 changed files with 33 additions and 181 deletions

View File

@ -16,7 +16,7 @@ export const CalloutWrapper = ({ editor, node, updateAttributes }) => {
); );
return ( return (
<NodeViewWrapper id="js-bannber-container" className={cls(styles.wrap)}> <NodeViewWrapper id="js-callout-container" className={cls(styles.wrap)}>
<div <div
className={cls(styles.innerWrap, 'render-wrapper')} className={cls(styles.innerWrap, 'render-wrapper')}
style={{ style={{

View File

@ -39,9 +39,15 @@ export const CalloutBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
<BubbleMenu <BubbleMenu
className={'bubble-menu'} className={'bubble-menu'}
editor={editor} editor={editor}
pluginKey="calloyt-bubble-menu" pluginKey="callout-bubble-menu"
shouldShow={() => editor.isActive(Callout.name)} shouldShow={() => editor.isActive(Callout.name)}
matchRenderContainer={(node) => node && node.id === 'js-bannber-container'} getRenderContainer={(node) => {
let container = node;
while (container && container.id !== 'js-callout-container') {
container = container.parentElement;
}
return container;
}}
> >
<Space spacing={4}> <Space spacing={4}>
<Tooltip content="复制"> <Tooltip content="复制">

View File

@ -18,7 +18,13 @@ export const CodeBlockBubbleMenu = ({ editor }) => {
pluginKey="code-block-bubble-menu" pluginKey="code-block-bubble-menu"
shouldShow={() => editor.isActive(CodeBlock.name)} shouldShow={() => editor.isActive(CodeBlock.name)}
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }} tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
matchRenderContainer={(node: HTMLElement) => node && node.classList && node.classList.contains('node-codeBlock')} getRenderContainer={(node) => {
let container = node;
while (container && container.classList && !container.classList.contains('node-codeBlock')) {
container = container.parentElement;
}
return container;
}}
> >
<Space spacing={4}> <Space spacing={4}>
<Tooltip content="复制"> <Tooltip content="复制">

View File

@ -79,7 +79,14 @@ export const ImageBubbleMenu = ({ editor }) => {
tippyOptions={{ tippyOptions={{
maxWidth: 'calc(100vw - 100px)', maxWidth: 'calc(100vw - 100px)',
}} }}
matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'} getRenderContainer={(node) => {
try {
const inner = node.querySelector('#js-resizeable-container');
return inner as HTMLElement;
} catch (e) {
return node;
}
}}
> >
<Space spacing={4}> <Space spacing={4}>
<Tooltip content="复制"> <Tooltip content="复制">

View File

@ -30,7 +30,14 @@ export const MindBubbleMenu = ({ editor }) => {
pluginKey="mind-bubble-menu" pluginKey="mind-bubble-menu"
shouldShow={() => editor.isActive(Mind.name)} shouldShow={() => editor.isActive(Mind.name)}
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }} tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'} getRenderContainer={(node) => {
try {
const inner = node.querySelector('#js-resizeable-container');
return inner as HTMLElement;
} catch (e) {
return node;
}
}}
> >
<Space spacing={4}> <Space spacing={4}>
<Tooltip content="复制"> <Tooltip content="复制">

View File

@ -46,7 +46,6 @@ export const TableBubbleMenu = ({ editor }) => {
tippyOptions={{ tippyOptions={{
maxWidth: 'calc(100vw - 100px)', maxWidth: 'calc(100vw - 100px)',
placement: 'bottom', placement: 'bottom',
offset: [0, 20],
}} }}
shouldShow={() => { shouldShow={() => {
return editor.isActive(Table.name); return editor.isActive(Table.name);
@ -56,7 +55,7 @@ export const TableBubbleMenu = ({ editor }) => {
while (container.tagName !== 'TABLE') { while (container.tagName !== 'TABLE') {
container = container.parentElement; container = container.parentElement;
} }
return container; return container.parentElement;
}} }}
> >
<Space spacing={4}> <Space spacing={4}>

View File

@ -1,173 +0,0 @@
import { Editor, isNodeSelection, posToDOMRect, Range } from '@tiptap/core';
import tippy, { Instance, Props } from 'tippy.js';
import { EditorView } from 'prosemirror-view';
import { EditorState } from 'prosemirror-state';
export type FloatMenuViewOptions = {
editor: Editor;
getReferenceClientRect?: (props: { editor: Editor; range: Range; oldState?: EditorState }) => DOMRect;
shouldShow: (props: { editor: Editor; range: Range; oldState?: EditorState }, instance: FloatMenuView) => boolean;
init: (dom: HTMLElement, editor: Editor) => void;
update?: (
dom: HTMLElement,
props: {
editor: Editor;
oldState?: EditorState;
range: Range;
show: () => void;
hide: () => void;
}
) => void;
tippyOptions?: Partial<Props>;
};
export class FloatMenuView {
public editor: Editor;
public parentNode: null | HTMLElement;
public container: null | HTMLElement;
private dom: HTMLElement;
private popup: Instance;
private _update: FloatMenuViewOptions['update'];
private shouldShow: FloatMenuViewOptions['shouldShow'];
private tippyOptions: FloatMenuViewOptions['tippyOptions'];
private getReferenceClientRect: NonNullable<FloatMenuViewOptions['getReferenceClientRect']> = ({ editor, range }) => {
const { view, state } = editor;
if (this.parentNode) {
return this.parentNode.getBoundingClientRect();
}
if (isNodeSelection(state.selection)) {
const node = view.nodeDOM(range.from) as HTMLElement;
if (node) {
return node.getBoundingClientRect();
}
}
const rangeRect = posToDOMRect(view, range.from, range.to);
if (this.container) {
const containerRect = this.container.getBoundingClientRect();
if (rangeRect.width > containerRect.width) {
return containerRect;
}
}
return rangeRect;
};
constructor(props: FloatMenuViewOptions) {
this.editor = props.editor;
this.shouldShow = props.shouldShow;
this.tippyOptions = props.tippyOptions || {};
if (props.getReferenceClientRect) {
this.getReferenceClientRect = props.getReferenceClientRect;
}
this._update = props.update;
this.dom = document.createElement('div');
// init
props.init(this.dom, this.editor);
// popup
this.createPopup();
}
setConatiner(el) {
this.container = el;
// this.popup?.setProps({
// appendTo: el,
// });
// this.popup?.
}
createPopup() {
const { element: editorElement } = this.editor.options;
const editorIsAttached = !!editorElement.parentElement;
if (this.popup || !editorIsAttached) {
return;
}
this.popup = tippy(editorElement, {
getReferenceClientRect: null,
content: this.dom,
interactive: true,
trigger: 'manual',
placement: 'top',
hideOnClick: 'toggle',
...Object.assign({ zIndex: 99 }, this.tippyOptions),
});
}
public update(view: EditorView, oldState?: EditorState) {
const { state, composing } = view;
const { doc, selection } = state;
const isSame = oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection);
if (composing || isSame) {
return;
}
this.createPopup();
const { ranges } = selection;
const from = Math.min(...ranges.map((range) => range.$from.pos));
const to = Math.max(...ranges.map((range) => range.$to.pos));
const shouldShow = this.shouldShow?.(
{
editor: this.editor,
oldState,
range: {
from,
to,
},
},
this
);
if (!shouldShow) {
this.hide();
return;
}
this._update?.(this.dom, {
editor: this.editor,
oldState,
range: {
from,
to,
},
show: this.show.bind(this),
hide: this.hide.bind(this),
});
this.popup.setProps({
getReferenceClientRect: () => {
return this.getReferenceClientRect({
editor: this.editor,
oldState,
range: {
from,
to,
},
});
},
});
this.show();
}
show() {
this.popup?.show();
}
hide() {
this.popup?.hide();
}
public destroy() {
this.popup?.destroy();
}
}