feat: revert table for basic user experience

pull/29/head
fantasticit 2022-04-24 20:57:24 +08:00
parent 3f5d5be67a
commit ab1dd6f05d
5 changed files with 270 additions and 380 deletions

View File

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

View File

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

View File

@ -1,57 +1,3 @@
import { mergeAttributes } from '@tiptap/core'; import BuiltInTable from '@tiptap/extension-table';
import { Table as BuiltInTable } from '@tiptap/extension-table';
export const Table = BuiltInTable.extend({ export const Table = BuiltInTable.configure({ resizable: true });
addAttributes() {
return {
style: {
default: null,
},
};
},
renderHTML({ node, HTMLAttributes }) {
let totalWidth = 0;
let fixedWidth = true;
try {
// use first row to determine width of table;
// @ts-ignore
const tr = node.content.content[0];
tr.content.content.forEach((td) => {
if (td.attrs.colwidth) {
td.attrs.colwidth.forEach((col) => {
if (!col) {
fixedWidth = false;
totalWidth += this.options.cellMinWidth;
} else {
totalWidth += col;
}
});
} else {
fixedWidth = false;
const colspan = td.attrs.colspan ? td.attrs.colspan : 1;
totalWidth += this.options.cellMinWidth * colspan;
}
});
} catch (error) {
fixedWidth = false;
}
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 [
'div',
{ class: 'tableWrapper' },
['table', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), ['tbody', 0]],
];
},
}).configure({
resizable: true,
cellMinWidth: 50,
});

View File

@ -29,9 +29,7 @@ export const TableBubbleMenu = ({ editor }) => {
maxWidth: 486, maxWidth: 486,
placement: 'bottom', placement: 'bottom',
}} }}
matchRenderContainer={(node: HTMLElement) => matchRenderContainer={(node: HTMLElement) => node && node.tagName === 'TABLE'}
node && node.classList && node.classList.contains('tableWrapper') && node.tagName === 'DIV'
}
> >
<Space> <Space>
<Tooltip content="向前插入一列"> <Tooltip content="向前插入一列">

View File

@ -1,20 +1,10 @@
.ProseMirror { .ProseMirror {
.tableWrapper {
max-width: 100%;
margin-top: 0.75em;
overflow: auto;
&.has-focus {
padding: 1em 0 0 1em;
}
}
table { table {
table-layout: fixed;
border-collapse: collapse; border-collapse: collapse;
border-width: 1px; overflow: hidden;
border-style: solid; table-layout: fixed;
border-color: var(--semi-color-fill-2); width: 100%;
margin: 0.75em 0 0;
td, td,
th { th {
@ -44,60 +34,6 @@
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;
@ -107,5 +43,19 @@
pointer-events: none; pointer-events: none;
background-color: #adf; background-color: #adf;
} }
p {
margin: 0;
}
} }
} }
.tableWrapper {
padding: 1rem 0;
overflow-x: auto;
}
.resize-cursor {
cursor: ew-resize;
cursor: col-resize;
}