tiptap: improve table

pull/44/head
fantasticit 2022-05-09 15:07:23 +08:00
parent b4764e11b7
commit a06c795360
11 changed files with 434 additions and 386 deletions

View File

@ -1,20 +1,7 @@
import { mergeAttributes } from '@tiptap/core';
import { TableCell as BuiltInTableCell } from '@tiptap/extension-table-cell'; import { TableCell as BuiltInTableCell } from '@tiptap/extension-table-cell';
import ReactDOM from 'react-dom'; import { Plugin } from 'prosemirror-state';
import { Button } from '@douyinfe/semi-ui';
import { IconDelete, IconPlus } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { Plugin, PluginKey } from 'prosemirror-state';
import { Decoration, DecorationSet } from 'prosemirror-view'; import { Decoration, DecorationSet } from 'prosemirror-view';
import { import { getCellsInColumn, selectTable, isRowSelected, isTableSelected, selectRow } from 'tiptap/prose-utils';
getCellsInRow,
getCellsInColumn,
isRowSelected,
isTableSelected,
selectRow,
selectTable,
} from 'tiptap/prose-utils';
import { FloatMenuView } from 'tiptap/views/float-menu';
export const TableCell = BuiltInTableCell.extend({ export const TableCell = BuiltInTableCell.extend({
addAttributes() { addAttributes() {
@ -40,173 +27,68 @@ export const TableCell = BuiltInTableCell.extend({
}; };
}, },
renderHTML({ HTMLAttributes }) { addProseMirrorPlugins() {
let totalWidth = 0; return [
let fixedWidth = true; new Plugin({
props: {
decorations: (state) => {
const { doc, selection } = state;
const decorations: Decoration[] = [];
const cells = getCellsInColumn(0)(selection);
if (HTMLAttributes.colwidth) { if (cells) {
HTMLAttributes.colwidth.forEach((col) => { cells.forEach(({ pos }, index) => {
if (!col) { if (index === 0) {
fixedWidth = false; decorations.push(
} else { Decoration.widget(pos + 1, () => {
totalWidth += col; let className = 'grip-table';
const selected = isTableSelected(selection);
if (selected) {
className += ' selected';
} }
const grip = document.createElement('a');
grip.className = className;
grip.addEventListener('mousedown', (event) => {
event.preventDefault();
event.stopImmediatePropagation();
this.editor.view.dispatch(selectTable(this.editor.state.tr));
// this.options.onSelectTable(state);
});
return grip;
})
);
}
decorations.push(
Decoration.widget(pos + 1, () => {
const rowSelected = isRowSelected(index)(selection);
let className = 'grip-row';
if (rowSelected) {
className += ' selected';
}
if (index === 0) {
className += ' first';
}
if (index === cells.length - 1) {
className += ' last';
}
const grip = document.createElement('a');
grip.className = className;
grip.addEventListener('mousedown', (event) => {
event.preventDefault();
event.stopImmediatePropagation();
this.editor.view.dispatch(selectRow(index)(this.editor.state.tr));
});
return grip;
})
);
}); });
} else {
fixedWidth = false;
} }
if (fixedWidth && totalWidth > 0) { return DecorationSet.create(doc, decorations);
HTMLAttributes.style = `width: ${totalWidth}px;`; },
} else if (totalWidth && totalWidth > 0) { },
HTMLAttributes.style = `min-width: ${totalWidth}px`; }),
} else { ];
HTMLAttributes.style = null;
}
return ['td', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
}, },
// addProseMirrorPlugins() {
// const extensionThis = this;
// let selectedRowIndex = -1;
// return [
// new Plugin({
// key: new PluginKey(`${this.name}FloatMenu`),
// view: () =>
// new FloatMenuView({
// editor: this.editor,
// tippyOptions: {
// zIndex: 100,
// offset: [-28, 0],
// },
// shouldShow: ({ editor }, floatMenuView) => {
// if (!editor.isEditable) {
// return false;
// }
// if (isTableSelected(editor.state.selection)) {
// return false;
// }
// const cells = getCellsInColumn(0)(editor.state.selection);
// if (selectedRowIndex > -1) {
// // 获取当前行的第一个单元格的位置
// const rowCells = getCellsInRow(selectedRowIndex)(editor.state.selection);
// if (rowCells && rowCells[0]) {
// const node = editor.view.nodeDOM(rowCells[0].pos) as HTMLElement;
// if (node) {
// const el = node.querySelector('a.grip-row') as HTMLElement;
// if (el) {
// floatMenuView.parentNode = el;
// }
// }
// }
// }
// return !!cells?.some((cell, index) => isRowSelected(index)(editor.state.selection));
// },
// init: (dom, editor) => {
// dom.classList.add('bubble-memu-table-cell');
// dom.classList.add('row');
// ReactDOM.render(
// <>
// <Tooltip content="向前插入一行" position="left">
// <Button
// size="small"
// theme="borderless"
// type="tertiary"
// icon={<IconPlus />}
// onClick={() => {
// editor.chain().addRowBefore().run();
// }}
// />
// </Tooltip>
// <Tooltip content="删除当前行" position="left">
// <Button
// size="small"
// theme="borderless"
// type="tertiary"
// icon={<IconDelete />}
// onClick={() => {
// editor.chain().deleteRow().run();
// }}
// />
// </Tooltip>
// <Tooltip content="向后插入一行" position="left" hideOnClick>
// <Button
// size="small"
// theme="borderless"
// type="tertiary"
// icon={<IconPlus />}
// onClick={() => {
// editor.chain().addRowAfter().run();
// }}
// />
// </Tooltip>
// </>,
// dom
// );
// },
// }),
// props: {
// decorations: (state) => {
// if (!extensionThis.editor.isEditable) {
// return;
// }
// const { doc, selection } = state;
// const decorations: Decoration[] = [];
// const cells = getCellsInColumn(0)(selection);
// if (cells) {
// cells.forEach(({ pos }, index) => {
// if (index === 0) {
// decorations.push(
// Decoration.widget(pos + 1, () => {
// const grip = document.createElement('a');
// grip.classList.add('grip-table');
// if (isTableSelected(selection)) {
// grip.classList.add('selected');
// }
// grip.addEventListener('mousedown', (event) => {
// event.preventDefault();
// event.stopImmediatePropagation();
// selectedRowIndex = -1;
// this.editor.view.dispatch(selectTable(this.editor.state.tr));
// });
// return grip;
// })
// );
// }
// decorations.push(
// Decoration.widget(pos + 1, () => {
// const rowSelected = isRowSelected(index)(selection);
// const grip = document.createElement('a');
// grip.classList.add('grip-row');
// if (rowSelected) {
// grip.classList.add('selected');
// }
// if (index === 0) {
// grip.classList.add('first');
// }
// if (index === cells.length - 1) {
// grip.classList.add('last');
// }
// grip.addEventListener('mousedown', (event) => {
// event.preventDefault();
// event.stopImmediatePropagation();
// selectedRowIndex = index;
// this.editor.view.dispatch(selectRow(index)(this.editor.state.tr));
// });
// return grip;
// })
// );
// });
// }
// return DecorationSet.create(doc, decorations);
// },
// },
// }),
// ];
// },
}); });

View File

@ -1,13 +1,7 @@
import { mergeAttributes } from '@tiptap/core';
import { TableHeader as BuiltInTableHeader } from '@tiptap/extension-table-header'; import { TableHeader as BuiltInTableHeader } from '@tiptap/extension-table-header';
import ReactDOM from 'react-dom'; import { Plugin } from 'prosemirror-state';
import { Button, Space } from '@douyinfe/semi-ui';
import { IconDelete, IconPlus } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { Plugin, PluginKey } from 'prosemirror-state';
import { Decoration, DecorationSet } from 'prosemirror-view'; import { Decoration, DecorationSet } from 'prosemirror-view';
import { getCellsInRow, isColumnSelected, isTableSelected, selectColumn } from 'tiptap/prose-utils'; import { getCellsInRow, isColumnSelected, selectColumn } from 'tiptap/prose-utils';
import { FloatMenuView } from '../views/float-menu';
export const TableHeader = BuiltInTableHeader.extend({ export const TableHeader = BuiltInTableHeader.extend({
addAttributes() { addAttributes() {
@ -33,144 +27,46 @@ export const TableHeader = BuiltInTableHeader.extend({
}; };
}, },
renderHTML({ HTMLAttributes }) { addProseMirrorPlugins() {
let totalWidth = 0; return [
let fixedWidth = true; new Plugin({
props: {
decorations: (state) => {
const { doc, selection } = state;
const decorations: Decoration[] = [];
const cells = getCellsInRow(0)(selection);
if (HTMLAttributes.colwidth) { if (cells) {
HTMLAttributes.colwidth.forEach((col) => { cells.forEach(({ pos }, index) => {
if (!col) { decorations.push(
fixedWidth = false; Decoration.widget(pos + 1, () => {
} else { const colSelected = isColumnSelected(index)(selection);
totalWidth += col; let className = 'grip-column';
if (colSelected) {
className += ' selected';
} }
if (index === 0) {
className += ' first';
} else if (index === cells.length - 1) {
className += ' last';
}
const grip = document.createElement('a');
grip.className = className;
grip.addEventListener('mousedown', (event) => {
event.preventDefault();
event.stopImmediatePropagation();
this.editor.view.dispatch(selectColumn(index)(this.editor.state.tr));
});
return grip;
})
);
}); });
} else {
fixedWidth = false;
} }
if (fixedWidth && totalWidth > 0) { return DecorationSet.create(doc, decorations);
HTMLAttributes.style = `width: ${totalWidth}px;`; },
} else if (totalWidth && totalWidth > 0) { },
HTMLAttributes.style = `min-width: ${totalWidth}px`; }),
} else { ];
HTMLAttributes.style = null;
}
return ['th', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
}, },
// addProseMirrorPlugins() {
// const extensionThis = this;
// return [
// new Plugin({
// key: new PluginKey(`${this.name}FloatMenu`),
// view: () =>
// new FloatMenuView({
// editor: this.editor,
// tippyOptions: {
// zIndex: 100,
// },
// shouldShow: ({ editor }, floatMenuView) => {
// if (!editor.isEditable) {
// return false;
// }
// const selection = editor.state.selection;
// if (isTableSelected(selection)) {
// return false;
// }
// const cells = getCellsInRow(0)(selection);
// if (cells && cells[0]) {
// const node = editor.view.nodeDOM(cells[0].pos) as HTMLElement;
// floatMenuView.setConatiner(node.parentElement.parentElement.parentElement.parentElement);
// }
// return !!cells?.some((cell, index) => isColumnSelected(index)(selection));
// },
// init: (dom, editor) => {
// dom.classList.add('bubble-memu-table-cell');
// ReactDOM.render(
// <Space>
// <Tooltip content="向前插入一列">
// <Button
// size="small"
// theme="borderless"
// type="tertiary"
// icon={<IconPlus />}
// onClick={() => {
// editor.chain().addColumnBefore().run();
// }}
// />
// </Tooltip>
// <Tooltip content="删除当前列">
// <Button
// size="small"
// theme="borderless"
// type="tertiary"
// icon={<IconDelete />}
// onClick={() => {
// editor.chain().deleteColumn().run();
// }}
// />
// </Tooltip>
// <Tooltip content="向后插入一列" hideOnClick>
// <Button
// size="small"
// theme="borderless"
// type="tertiary"
// icon={<IconPlus />}
// onClick={() => {
// editor.chain().addColumnAfter().run();
// }}
// />
// </Tooltip>
// </Space>,
// dom
// );
// },
// }),
// props: {
// decorations: (state) => {
// if (!extensionThis.editor.isEditable) {
// return;
// }
// const { doc, selection } = state;
// const decorations: Decoration[] = [];
// const cells = getCellsInRow(0)(selection);
// if (cells) {
// cells.forEach(({ pos }, index) => {
// decorations.push(
// Decoration.widget(pos + 1, () => {
// const colSelected = isColumnSelected(index)(selection);
// const grip = document.createElement('a');
// grip.classList.add('grip-column');
// if (colSelected) {
// grip.classList.add('selected');
// }
// if (index === 0) {
// grip.classList.add('first');
// } else if (index === cells.length - 1) {
// grip.classList.add('last');
// }
// grip.addEventListener('mousedown', (event) => {
// event.preventDefault();
// event.stopImmediatePropagation();
// this.editor.view.dispatch(selectColumn(index)(this.editor.state.tr));
// });
// return grip;
// })
// );
// });
// }
// return DecorationSet.create(doc, decorations);
// },
// },
// }),
// ];
// },
}); });

View File

@ -1,3 +1,53 @@
import BuiltInTable from '@tiptap/extension-table'; import BuiltInTable from '@tiptap/extension-table';
import { Decoration, DecorationSet } from 'prosemirror-view';
import { tableEditing } from 'prosemirror-tables';
import { Plugin } from 'prosemirror-state';
export const Table = BuiltInTable.configure({ resizable: true }); export const Table = BuiltInTable.extend({
renderHTML() {
return [
'div',
{ class: 'scrollable-wrapper' },
['div', { class: 'scrollable' }, ['table', { class: 'rme-table' }, ['tbody', 0]]],
];
},
addProseMirrorPlugins() {
return [
tableEditing(),
new Plugin({
props: {
decorations: (state) => {
const { doc } = state;
const decorations: Decoration[] = [];
let index = 0;
doc.descendants((node, pos) => {
if (node.type.name !== this.name) return;
const elements = document.getElementsByClassName('rme-table');
const table = elements[index];
if (!table) return;
const element = table.parentElement;
const shadowRight = !!(element && element.scrollWidth > element.clientWidth);
if (shadowRight) {
decorations.push(
Decoration.widget(pos + 1, () => {
const shadow = document.createElement('div');
shadow.className = 'scrollable-shadow right';
return shadow;
})
);
}
index++;
});
return DecorationSet.create(doc, decorations);
},
},
}),
];
},
}).configure({ resizable: true });

View File

@ -1,30 +1,62 @@
.ProseMirror { .ProseMirror {
.tableWrapper { .scrollable-wrapper {
max-width: 100%; position: relative;
padding: 0; margin: 0.5em 0;
margin: 0; scrollbar-width: thin;
overflow: auto; scrollbar-color: transparent transparent;
}
.scrollable {
padding-left: 1em;
margin-left: -1em;
overflow: auto hidden;
border-left: 1px solid transparent;
border-right: 1px solid transparent;
transition: border 250ms ease-in-out 0s;
}
.scrollable-shadow {
position: absolute;
top: 0;
bottom: 0;
left: -1em;
width: 16px;
transition: box-shadow 250ms ease-in-out 0s;
border-width: 0 0 0 1em;
border-style: solid;
border-color: transparent;
border-image: initial;
pointer-events: none;
&.left {
box-shadow: 16px 0 16px -16px inset rgb(0 0 0 / 25%);
border-left: 1em solid red;
}
&.right {
right: 0;
left: auto;
box-shadow: rgb(0 0 0 / 25%) -16px 0 16px -16px inset;
}
} }
table { table {
width: 100%; width: 100%;
max-width: 100%; margin-top: 1em;
margin: 0.75em 0 0; border-radius: 4px;
overflow: hidden;
border-collapse: collapse; border-collapse: collapse;
table-layout: fixed; box-sizing: border-box;
border-color: var(--semi-color-fill-2);
td, td,
th { th {
position: relative; position: relative;
box-sizing: border-box; min-width: 100px;
min-width: 1em; padding: 4px 8px;
padding: 3px 5px; text-align: left;
border-width: 1px;
border-style: solid;
border-color: var(--semi-color-fill-2);
overflow: visible;
vertical-align: top; vertical-align: top;
border: 1px solid rgb(232 235 237);
border-color: var(--semi-color-fill-2);
> * { > * {
margin-bottom: 0; margin-bottom: 0;
@ -42,6 +74,60 @@
background: var(--semi-color-info-light-hover); background: var(--semi-color-info-light-hover);
} }
.grip-column {
position: absolute;
top: -1em;
left: 0;
z-index: 10;
display: block;
width: 100%;
height: 0.7em;
margin-bottom: 3px;
cursor: pointer;
background: #ced4da;
&:hover,
&.selected {
background: var(--semi-color-info);
}
}
.grip-row {
position: absolute;
top: 0;
left: -1em;
z-index: 10;
display: block;
width: 0.7em;
height: 100%;
margin-right: 3px;
cursor: pointer;
background: #ced4da;
&:hover,
&.selected {
background: var(--semi-color-info);
}
}
.grip-table {
position: absolute;
top: -1em;
left: -1em;
z-index: 10;
display: block;
width: 0.8em;
height: 0.8em;
cursor: pointer;
background: #ced4da;
border-radius: 50%;
&:hover,
&.selected {
background: var(--semi-color-info);
}
}
.column-resize-handle { .column-resize-handle {
position: absolute; position: absolute;
top: 0; top: 0;
@ -51,10 +137,6 @@
pointer-events: none; pointer-events: none;
background-color: #adf; background-color: #adf;
} }
p {
margin: 0;
}
} }
} }

View File

@ -43,11 +43,21 @@ export const TableBubbleMenu = ({ editor }) => {
className={'bubble-menu bubble-menu-table'} className={'bubble-menu bubble-menu-table'}
editor={editor} editor={editor}
pluginKey="table-bubble-menu" pluginKey="table-bubble-menu"
shouldShow={() => editor.isActive(Table.name)}
tippyOptions={{ tippyOptions={{
maxWidth: 'calc(100vw - 100px)', maxWidth: 'calc(100vw - 100px)',
placement: 'bottom',
offset: [0, 20],
}}
shouldShow={() => {
return editor.isActive(Table.name);
}}
getRenderContainer={(node) => {
let container = node;
while (container.tagName !== 'TABLE') {
container = container.parentElement;
}
return container;
}} }}
matchRenderContainer={(node: HTMLElement) => node && node.tagName === 'TABLE'}
> >
<Space spacing={4}> <Space spacing={4}>
<Tooltip content="复制"> <Tooltip content="复制">

View File

@ -0,0 +1,57 @@
import React, { useCallback } from 'react';
import { Space, Button } from '@douyinfe/semi-ui';
import { IconAddColumnBefore, IconAddColumnAfter, IconDeleteColumn } from 'components/icons';
import { Tooltip } from 'components/tooltip';
import { BubbleMenu } from 'tiptap/editor/views/bubble-menu';
import { TableRow } from 'tiptap/core/extensions/table-row';
import { isTableSelected } from 'tiptap/prose-utils';
export const TableColBubbleMenu = ({ editor }) => {
const addColumnBefore = useCallback(() => editor.chain().focus().addColumnBefore().run(), [editor]);
const addColumnAfter = useCallback(() => editor.chain().focus().addColumnAfter().run(), [editor]);
const deleteColumn = useCallback(() => editor.chain().focus().deleteColumn().run(), [editor]);
return (
<BubbleMenu
className={'bubble-menu bubble-menu-table'}
editor={editor}
pluginKey="table-col-bubble-menu"
tippyOptions={{
offset: [0, 20],
}}
shouldShow={({ node, state }) => {
if (!node || isTableSelected(state.selection)) return false;
const gripColumn = node.querySelector('a.grip-column.selected');
return editor.isActive(TableRow.name) && !!gripColumn;
}}
getRenderContainer={(node) => {
return node;
}}
>
<Space spacing={4}>
<Tooltip content="向前插入一列">
<Button
onClick={addColumnBefore}
icon={<IconAddColumnBefore />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip>
<Tooltip content="向后插入一列">
<Button
onClick={addColumnAfter}
icon={<IconAddColumnAfter />}
type="tertiary"
theme="borderless"
size="small"
/>
</Tooltip>
<Tooltip content="删除当前列" hideOnClick>
<Button onClick={deleteColumn} icon={<IconDeleteColumn />} type="tertiary" theme="borderless" size="small" />
</Tooltip>
</Space>
</BubbleMenu>
);
};

View File

@ -1,7 +1,15 @@
import React from 'react'; import React from 'react';
import { Editor } from 'tiptap/editor'; import { Editor } from 'tiptap/editor';
import { TableBubbleMenu } from './bubble'; import { TableBubbleMenu } from './bubble';
import { TableRowBubbleMenu } from './row-bubble';
import { TableColBubbleMenu } from './col-bubble';
export const Table: React.FC<{ editor: Editor }> = ({ editor }) => { export const Table: React.FC<{ editor: Editor }> = ({ editor }) => {
return <TableBubbleMenu editor={editor} />; return (
<>
<TableBubbleMenu editor={editor} />
<TableRowBubbleMenu editor={editor} />
<TableColBubbleMenu editor={editor} />
</>
);
}; };

View File

@ -0,0 +1,47 @@
import React, { useCallback } from 'react';
import { Space, Button } from '@douyinfe/semi-ui';
import { IconAddRowBefore, IconAddRowAfter, IconDeleteRow } from 'components/icons';
import { Tooltip } from 'components/tooltip';
import { BubbleMenu } from 'tiptap/editor/views/bubble-menu';
import { TableRow } from 'tiptap/core/extensions/table-row';
import { isTableSelected } from 'tiptap/prose-utils';
export const TableRowBubbleMenu = ({ editor }) => {
const addRowBefore = useCallback(() => editor.chain().focus().addRowBefore().run(), [editor]);
const addRowAfter = useCallback(() => editor.chain().focus().addRowAfter().run(), [editor]);
const deleteRow = useCallback(() => editor.chain().focus().deleteRow().run(), [editor]);
return (
<BubbleMenu
className={'bubble-menu bubble-menu-table'}
editor={editor}
pluginKey="table-row-bubble-menu"
tippyOptions={{
placement: 'left',
offset: [0, 20],
}}
shouldShow={({ node, state }) => {
if (!node || isTableSelected(state.selection)) return false;
const gripRow = node.querySelector('a.grip-row.selected');
return editor.isActive(TableRow.name) && !!gripRow;
}}
getRenderContainer={(node) => {
return node;
}}
>
<Space vertical spacing={4}>
<Tooltip content="向前插入一行">
<Button onClick={addRowBefore} icon={<IconAddRowBefore />} type="tertiary" theme="borderless" size="small" />
</Tooltip>
<Tooltip content="向后插入一行">
<Button onClick={addRowAfter} icon={<IconAddRowAfter />} type="tertiary" theme="borderless" size="small" />
</Tooltip>
<Tooltip content="删除当前行" hideOnClick>
<Button onClick={deleteRow} icon={<IconDeleteRow />} type="tertiary" theme="borderless" size="small" />
</Tooltip>
</Space>
</BubbleMenu>
);
};

View File

@ -11,6 +11,7 @@ export interface BubbleMenuPluginProps {
shouldShow?: shouldShow?:
| ((props: { | ((props: {
editor: Editor; editor: Editor;
node?: HTMLElement;
view?: EditorView; view?: EditorView;
state?: EditorState; state?: EditorState;
oldState?: EditorState; oldState?: EditorState;
@ -20,6 +21,7 @@ export interface BubbleMenuPluginProps {
| null; | null;
renderContainerSelector?: string; renderContainerSelector?: string;
matchRenderContainer?: (node: HTMLElement) => boolean; matchRenderContainer?: (node: HTMLElement) => boolean;
getRenderContainer?: (node: HTMLElement) => HTMLElement;
} }
export type BubbleMenuViewProps = BubbleMenuPluginProps & { export type BubbleMenuViewProps = BubbleMenuPluginProps & {
@ -39,10 +41,9 @@ export class BubbleMenuView {
public tippyOptions?: Partial<Props>; public tippyOptions?: Partial<Props>;
public renderContainerSelector?: string; // public renderContainerSelector?: string;
// public matchRenderContainer?: BubbleMenuPluginProps['matchRenderContainer'];
public matchRenderContainer?: BubbleMenuPluginProps['matchRenderContainer']; public getRenderContainer?: BubbleMenuPluginProps['getRenderContainer'];
public shouldShow: Exclude<BubbleMenuPluginProps['shouldShow'], null> = ({ view, state, from, to }) => { public shouldShow: Exclude<BubbleMenuPluginProps['shouldShow'], null> = ({ view, state, from, to }) => {
const { doc, selection } = state; const { doc, selection } = state;
const { empty } = selection; const { empty } = selection;
@ -65,14 +66,17 @@ export class BubbleMenuView {
view, view,
tippyOptions = {}, tippyOptions = {},
shouldShow, shouldShow,
renderContainerSelector, // renderContainerSelector,
matchRenderContainer, // matchRenderContainer,
getRenderContainer,
}: BubbleMenuViewProps) { }: BubbleMenuViewProps) {
this.editor = editor; this.editor = editor;
this.element = element; this.element = element;
this.view = view; this.view = view;
this.renderContainerSelector = renderContainerSelector; // this.renderContainerSelector = renderContainerSelector;
this.matchRenderContainer = matchRenderContainer; // this.matchRenderContainer = matchRenderContainer;
this.getRenderContainer = getRenderContainer;
if (shouldShow) { if (shouldShow) {
this.shouldShow = shouldShow; this.shouldShow = shouldShow;
@ -82,8 +86,8 @@ export class BubbleMenuView {
capture: true, 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);
this.editor.on('blur', this.blurHandler); // this.editor.on('blur', this.blurHandler);
this.tippyOptions = tippyOptions || {}; this.tippyOptions = tippyOptions || {};
// Detaches menu content from its current parent // Detaches menu content from its current parent
this.element.remove(); this.element.remove();
@ -167,12 +171,14 @@ export class BubbleMenuView {
const { ranges } = selection; 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));
const node = view.domAtPos(from).node as HTMLElement;
const shouldShow = const shouldShow =
this.editor.isEditable && this.editor.isEditable &&
this.shouldShow?.({ this.shouldShow?.({
editor: this.editor, editor: this.editor,
view, view,
node,
state, state,
oldState, oldState,
from, from,
@ -187,34 +193,43 @@ export class BubbleMenuView {
this.tippy?.setProps({ this.tippy?.setProps({
getReferenceClientRect: () => { getReferenceClientRect: () => {
if (isNodeSelection(state.selection)) { if (isNodeSelection(state.selection)) {
let node = view.nodeDOM(from) as HTMLElement; const node = view.nodeDOM(from) as HTMLElement;
if (this.matchRenderContainer) { if (this.getRenderContainer) {
while (node && !this.matchRenderContainer(node)) { return this.getRenderContainer(node).getBoundingClientRect();
node = node.firstElementChild as HTMLElement;
} }
if (node) { // if (this.matchRenderContainer) {
return node.getBoundingClientRect(); // while (node && !this.matchRenderContainer(node)) {
} // node = node.firstElementChild as HTMLElement;
// }
// if (node) {
// return node.getBoundingClientRect();
// }
// }
// if (node) {
// return node.getBoundingClientRect();
// }
} }
if (node) { if (this.getRenderContainer) {
return node.getBoundingClientRect(); const node = view.domAtPos(from).node as HTMLElement;
} return this.getRenderContainer(node).getBoundingClientRect();
} }
if (this.matchRenderContainer) { // if (this.matchRenderContainer) {
let node = view.domAtPos(from).node as HTMLElement; // let node = view.domAtPos(from).node as HTMLElement;
while (node && !this.matchRenderContainer(node)) { // while (node && !this.matchRenderContainer(node)) {
node = node.parentElement; // node = node.parentElement;
} // }
if (node) { // if (node) {
return node.getBoundingClientRect(); // return node.getBoundingClientRect();
} // }
} // }
return posToDOMRect(view, from, to); return posToDOMRect(view, from, to);
}, },

View File

@ -26,8 +26,9 @@ export const BubbleMenu: React.FC<BubbleMenuProps> = (props) => {
editor, editor,
tippyOptions = {}, tippyOptions = {},
shouldShow = null, shouldShow = null,
renderContainerSelector, // renderContainerSelector,
matchRenderContainer, // matchRenderContainer,
getRenderContainer,
} = props; } = props;
const plugin = BubbleMenuPlugin({ const plugin = BubbleMenuPlugin({
@ -36,8 +37,9 @@ export const BubbleMenu: React.FC<BubbleMenuProps> = (props) => {
element, element,
tippyOptions, tippyOptions,
shouldShow, shouldShow,
renderContainerSelector, // renderContainerSelector,
matchRenderContainer, // matchRenderContainer,
getRenderContainer,
}); });
editor.registerPlugin(plugin); editor.registerPlugin(plugin);

View File

@ -27,7 +27,6 @@
"constants/*": ["constants/*"], "constants/*": ["constants/*"],
"helpers/*": ["helpers/*"], "helpers/*": ["helpers/*"],
"tiptap/*": ["tiptap/*"], "tiptap/*": ["tiptap/*"],
"tiptap-v2/*": ["tiptap-v2/*"],
"event/*": ["event/*"] "event/*": ["event/*"]
} }
}, },