mirror of https://github.com/fantasticit/think.git
tiptap: improve table
This commit is contained in:
parent
b4764e11b7
commit
a06c795360
|
@ -1,20 +1,7 @@
|
|||
import { mergeAttributes } from '@tiptap/core';
|
||||
import { TableCell as BuiltInTableCell } from '@tiptap/extension-table-cell';
|
||||
import ReactDOM from 'react-dom';
|
||||
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 { Plugin } from 'prosemirror-state';
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||
import {
|
||||
getCellsInRow,
|
||||
getCellsInColumn,
|
||||
isRowSelected,
|
||||
isTableSelected,
|
||||
selectRow,
|
||||
selectTable,
|
||||
} from 'tiptap/prose-utils';
|
||||
import { FloatMenuView } from 'tiptap/views/float-menu';
|
||||
import { getCellsInColumn, selectTable, isRowSelected, isTableSelected, selectRow } from 'tiptap/prose-utils';
|
||||
|
||||
export const TableCell = BuiltInTableCell.extend({
|
||||
addAttributes() {
|
||||
|
@ -40,173 +27,68 @@ export const TableCell = BuiltInTableCell.extend({
|
|||
};
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
let totalWidth = 0;
|
||||
let fixedWidth = true;
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
props: {
|
||||
decorations: (state) => {
|
||||
const { doc, selection } = state;
|
||||
const decorations: Decoration[] = [];
|
||||
const cells = getCellsInColumn(0)(selection);
|
||||
|
||||
if (HTMLAttributes.colwidth) {
|
||||
HTMLAttributes.colwidth.forEach((col) => {
|
||||
if (!col) {
|
||||
fixedWidth = false;
|
||||
} else {
|
||||
totalWidth += col;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fixedWidth = false;
|
||||
}
|
||||
if (cells) {
|
||||
cells.forEach(({ pos }, index) => {
|
||||
if (index === 0) {
|
||||
decorations.push(
|
||||
Decoration.widget(pos + 1, () => {
|
||||
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);
|
||||
|
||||
if (fixedWidth && totalWidth > 0) {
|
||||
HTMLAttributes.style = `width: ${totalWidth}px;`;
|
||||
} else if (totalWidth && totalWidth > 0) {
|
||||
HTMLAttributes.style = `min-width: ${totalWidth}px`;
|
||||
} else {
|
||||
HTMLAttributes.style = null;
|
||||
}
|
||||
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;
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return ['td', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
return DecorationSet.create(doc, decorations);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
|
||||
// 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);
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// ];
|
||||
// },
|
||||
});
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import { mergeAttributes } from '@tiptap/core';
|
||||
import { TableHeader as BuiltInTableHeader } from '@tiptap/extension-table-header';
|
||||
import ReactDOM from 'react-dom';
|
||||
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 { Plugin } from 'prosemirror-state';
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||
import { getCellsInRow, isColumnSelected, isTableSelected, selectColumn } from 'tiptap/prose-utils';
|
||||
import { FloatMenuView } from '../views/float-menu';
|
||||
import { getCellsInRow, isColumnSelected, selectColumn } from 'tiptap/prose-utils';
|
||||
|
||||
export const TableHeader = BuiltInTableHeader.extend({
|
||||
addAttributes() {
|
||||
|
@ -33,144 +27,46 @@ export const TableHeader = BuiltInTableHeader.extend({
|
|||
};
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
let totalWidth = 0;
|
||||
let fixedWidth = true;
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
props: {
|
||||
decorations: (state) => {
|
||||
const { doc, selection } = state;
|
||||
const decorations: Decoration[] = [];
|
||||
const cells = getCellsInRow(0)(selection);
|
||||
|
||||
if (HTMLAttributes.colwidth) {
|
||||
HTMLAttributes.colwidth.forEach((col) => {
|
||||
if (!col) {
|
||||
fixedWidth = false;
|
||||
} else {
|
||||
totalWidth += col;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fixedWidth = false;
|
||||
}
|
||||
if (cells) {
|
||||
cells.forEach(({ pos }, index) => {
|
||||
decorations.push(
|
||||
Decoration.widget(pos + 1, () => {
|
||||
const colSelected = isColumnSelected(index)(selection);
|
||||
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;
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (fixedWidth && totalWidth > 0) {
|
||||
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];
|
||||
return DecorationSet.create(doc, decorations);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
|
||||
// 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);
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// ];
|
||||
// },
|
||||
});
|
||||
|
|
|
@ -1,3 +1,53 @@
|
|||
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 });
|
||||
|
|
|
@ -1,30 +1,62 @@
|
|||
.ProseMirror {
|
||||
.tableWrapper {
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
.scrollable-wrapper {
|
||||
position: relative;
|
||||
margin: 0.5em 0;
|
||||
scrollbar-width: thin;
|
||||
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 {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0.75em 0 0;
|
||||
overflow: hidden;
|
||||
margin-top: 1em;
|
||||
border-radius: 4px;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
box-sizing: border-box;
|
||||
border-color: var(--semi-color-fill-2);
|
||||
|
||||
td,
|
||||
th {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
min-width: 1em;
|
||||
padding: 3px 5px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--semi-color-fill-2);
|
||||
overflow: visible;
|
||||
min-width: 100px;
|
||||
padding: 4px 8px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
border: 1px solid rgb(232 235 237);
|
||||
border-color: var(--semi-color-fill-2);
|
||||
|
||||
> * {
|
||||
margin-bottom: 0;
|
||||
|
@ -42,6 +74,60 @@
|
|||
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 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -51,10 +137,6 @@
|
|||
pointer-events: none;
|
||||
background-color: #adf;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -43,11 +43,21 @@ export const TableBubbleMenu = ({ editor }) => {
|
|||
className={'bubble-menu bubble-menu-table'}
|
||||
editor={editor}
|
||||
pluginKey="table-bubble-menu"
|
||||
shouldShow={() => editor.isActive(Table.name)}
|
||||
tippyOptions={{
|
||||
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}>
|
||||
<Tooltip content="复制">
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -1,7 +1,15 @@
|
|||
import React from 'react';
|
||||
import { Editor } from 'tiptap/editor';
|
||||
import { TableBubbleMenu } from './bubble';
|
||||
import { TableRowBubbleMenu } from './row-bubble';
|
||||
import { TableColBubbleMenu } from './col-bubble';
|
||||
|
||||
export const Table: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||
return <TableBubbleMenu editor={editor} />;
|
||||
return (
|
||||
<>
|
||||
<TableBubbleMenu editor={editor} />
|
||||
<TableRowBubbleMenu editor={editor} />
|
||||
<TableColBubbleMenu editor={editor} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -11,6 +11,7 @@ export interface BubbleMenuPluginProps {
|
|||
shouldShow?:
|
||||
| ((props: {
|
||||
editor: Editor;
|
||||
node?: HTMLElement;
|
||||
view?: EditorView;
|
||||
state?: EditorState;
|
||||
oldState?: EditorState;
|
||||
|
@ -20,6 +21,7 @@ export interface BubbleMenuPluginProps {
|
|||
| null;
|
||||
renderContainerSelector?: string;
|
||||
matchRenderContainer?: (node: HTMLElement) => boolean;
|
||||
getRenderContainer?: (node: HTMLElement) => HTMLElement;
|
||||
}
|
||||
|
||||
export type BubbleMenuViewProps = BubbleMenuPluginProps & {
|
||||
|
@ -39,10 +41,9 @@ export class BubbleMenuView {
|
|||
|
||||
public tippyOptions?: Partial<Props>;
|
||||
|
||||
public renderContainerSelector?: string;
|
||||
|
||||
public matchRenderContainer?: BubbleMenuPluginProps['matchRenderContainer'];
|
||||
|
||||
// public renderContainerSelector?: string;
|
||||
// public matchRenderContainer?: BubbleMenuPluginProps['matchRenderContainer'];
|
||||
public getRenderContainer?: BubbleMenuPluginProps['getRenderContainer'];
|
||||
public shouldShow: Exclude<BubbleMenuPluginProps['shouldShow'], null> = ({ view, state, from, to }) => {
|
||||
const { doc, selection } = state;
|
||||
const { empty } = selection;
|
||||
|
@ -65,14 +66,17 @@ export class BubbleMenuView {
|
|||
view,
|
||||
tippyOptions = {},
|
||||
shouldShow,
|
||||
renderContainerSelector,
|
||||
matchRenderContainer,
|
||||
// renderContainerSelector,
|
||||
// matchRenderContainer,
|
||||
getRenderContainer,
|
||||
}: BubbleMenuViewProps) {
|
||||
this.editor = editor;
|
||||
this.element = element;
|
||||
this.view = view;
|
||||
this.renderContainerSelector = renderContainerSelector;
|
||||
this.matchRenderContainer = matchRenderContainer;
|
||||
// this.renderContainerSelector = renderContainerSelector;
|
||||
// this.matchRenderContainer = matchRenderContainer;
|
||||
|
||||
this.getRenderContainer = getRenderContainer;
|
||||
|
||||
if (shouldShow) {
|
||||
this.shouldShow = shouldShow;
|
||||
|
@ -82,8 +86,8 @@ export class BubbleMenuView {
|
|||
capture: true,
|
||||
});
|
||||
this.view.dom.addEventListener('dragstart', this.dragstartHandler);
|
||||
this.editor.on('focus', this.focusHandler);
|
||||
this.editor.on('blur', this.blurHandler);
|
||||
// this.editor.on('focus', this.focusHandler);
|
||||
// this.editor.on('blur', this.blurHandler);
|
||||
this.tippyOptions = tippyOptions || {};
|
||||
// Detaches menu content from its current parent
|
||||
this.element.remove();
|
||||
|
@ -167,12 +171,14 @@ export class BubbleMenuView {
|
|||
const { ranges } = selection;
|
||||
const from = Math.min(...ranges.map((range) => range.$from.pos));
|
||||
const to = Math.max(...ranges.map((range) => range.$to.pos));
|
||||
const node = view.domAtPos(from).node as HTMLElement;
|
||||
|
||||
const shouldShow =
|
||||
this.editor.isEditable &&
|
||||
this.shouldShow?.({
|
||||
editor: this.editor,
|
||||
view,
|
||||
node,
|
||||
state,
|
||||
oldState,
|
||||
from,
|
||||
|
@ -187,35 +193,44 @@ export class BubbleMenuView {
|
|||
this.tippy?.setProps({
|
||||
getReferenceClientRect: () => {
|
||||
if (isNodeSelection(state.selection)) {
|
||||
let node = view.nodeDOM(from) as HTMLElement;
|
||||
const node = view.nodeDOM(from) as HTMLElement;
|
||||
|
||||
if (this.matchRenderContainer) {
|
||||
while (node && !this.matchRenderContainer(node)) {
|
||||
node = node.firstElementChild as HTMLElement;
|
||||
}
|
||||
|
||||
if (node) {
|
||||
return node.getBoundingClientRect();
|
||||
}
|
||||
if (this.getRenderContainer) {
|
||||
return this.getRenderContainer(node).getBoundingClientRect();
|
||||
}
|
||||
|
||||
if (node) {
|
||||
return node.getBoundingClientRect();
|
||||
}
|
||||
// if (this.matchRenderContainer) {
|
||||
// while (node && !this.matchRenderContainer(node)) {
|
||||
// node = node.firstElementChild as HTMLElement;
|
||||
// }
|
||||
|
||||
// if (node) {
|
||||
// return node.getBoundingClientRect();
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (node) {
|
||||
// return node.getBoundingClientRect();
|
||||
// }
|
||||
}
|
||||
|
||||
if (this.matchRenderContainer) {
|
||||
let node = view.domAtPos(from).node as HTMLElement;
|
||||
|
||||
while (node && !this.matchRenderContainer(node)) {
|
||||
node = node.parentElement;
|
||||
}
|
||||
|
||||
if (node) {
|
||||
return node.getBoundingClientRect();
|
||||
}
|
||||
if (this.getRenderContainer) {
|
||||
const node = view.domAtPos(from).node as HTMLElement;
|
||||
return this.getRenderContainer(node).getBoundingClientRect();
|
||||
}
|
||||
|
||||
// if (this.matchRenderContainer) {
|
||||
// let node = view.domAtPos(from).node as HTMLElement;
|
||||
|
||||
// while (node && !this.matchRenderContainer(node)) {
|
||||
// node = node.parentElement;
|
||||
// }
|
||||
|
||||
// if (node) {
|
||||
// return node.getBoundingClientRect();
|
||||
// }
|
||||
// }
|
||||
|
||||
return posToDOMRect(view, from, to);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -26,8 +26,9 @@ export const BubbleMenu: React.FC<BubbleMenuProps> = (props) => {
|
|||
editor,
|
||||
tippyOptions = {},
|
||||
shouldShow = null,
|
||||
renderContainerSelector,
|
||||
matchRenderContainer,
|
||||
// renderContainerSelector,
|
||||
// matchRenderContainer,
|
||||
getRenderContainer,
|
||||
} = props;
|
||||
|
||||
const plugin = BubbleMenuPlugin({
|
||||
|
@ -36,8 +37,9 @@ export const BubbleMenu: React.FC<BubbleMenuProps> = (props) => {
|
|||
element,
|
||||
tippyOptions,
|
||||
shouldShow,
|
||||
renderContainerSelector,
|
||||
matchRenderContainer,
|
||||
// renderContainerSelector,
|
||||
// matchRenderContainer,
|
||||
getRenderContainer,
|
||||
});
|
||||
|
||||
editor.registerPlugin(plugin);
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
"constants/*": ["constants/*"],
|
||||
"helpers/*": ["helpers/*"],
|
||||
"tiptap/*": ["tiptap/*"],
|
||||
"tiptap-v2/*": ["tiptap-v2/*"],
|
||||
"event/*": ["event/*"]
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue