mirror of https://github.com/fantasticit/think.git
tiptap: improve table
parent
b4764e11b7
commit
a06c795360
|
@ -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);
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// }),
|
|
||||||
// ];
|
|
||||||
// },
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// }),
|
|
||||||
// ];
|
|
||||||
// },
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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="复制">
|
||||||
|
|
|
@ -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 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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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?:
|
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);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -27,7 +27,6 @@
|
||||||
"constants/*": ["constants/*"],
|
"constants/*": ["constants/*"],
|
||||||
"helpers/*": ["helpers/*"],
|
"helpers/*": ["helpers/*"],
|
||||||
"tiptap/*": ["tiptap/*"],
|
"tiptap/*": ["tiptap/*"],
|
||||||
"tiptap-v2/*": ["tiptap-v2/*"],
|
|
||||||
"event/*": ["event/*"]
|
"event/*": ["event/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue