improve table cell

This commit is contained in:
fantasticit 2022-12-19 19:14:06 +08:00
parent 922ecdf98f
commit c6abfab7c8
6 changed files with 418 additions and 168 deletions

View File

@ -1,13 +1,18 @@
import { IconPlus } from '@douyinfe/semi-icons';
import { mergeAttributes, Node } from '@tiptap/core';
import { Plugin } from 'prosemirror-state';
import { Tooltip } from 'components/tooltip';
import { Plugin, PluginKey } from 'prosemirror-state';
import { addRowAfter } from 'prosemirror-tables';
import { Decoration, DecorationSet } from 'prosemirror-view';
import React from 'react';
import ReactDOM from 'react-dom';
import { getCellsInColumn, isRowSelected, isTableSelected, selectRow, selectTable } from 'tiptap/prose-utils';
export interface TableCellOptions {
HTMLAttributes: Record<string, any>;
}
export const TableCell = Node.create<TableCellOptions>({
export const TableCell = Node.create<TableCellOptions, { clearCallbacks: Array<() => void> }>({
name: 'tableCell',
content: 'block+',
tableRole: 'cell',
@ -19,14 +24,6 @@ export const TableCell = Node.create<TableCellOptions>({
};
},
parseHTML() {
return [{ tag: 'td' }];
},
renderHTML({ HTMLAttributes }) {
return ['td', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
},
addAttributes() {
return {
colspan: {
@ -46,7 +43,7 @@ export const TableCell = Node.create<TableCellOptions>({
},
},
colwidth: {
default: [150],
default: [100],
parseHTML: (element) => {
const colwidth = element.getAttribute('colwidth');
const value = colwidth ? [parseInt(colwidth, 10)] : null;
@ -59,22 +56,44 @@ export const TableCell = Node.create<TableCellOptions>({
};
},
parseHTML() {
return [{ tag: 'td' }];
},
renderHTML({ HTMLAttributes }) {
return ['td', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
},
addStorage() {
return {
clearCallbacks: [],
};
},
onDestroy() {
this.storage.clearCallbacks.forEach((cb) => cb());
this.storage.clearCallbacks.length = 0;
},
// @ts-ignore
addProseMirrorPlugins() {
const { isEditable } = this.editor;
return [
new Plugin({
key: new PluginKey('table-cell-control'),
props: {
decorations: (state) => {
if (!isEditable) {
return DecorationSet.empty;
}
const { doc, selection } = state;
const decorations: Decoration[] = [];
const cells = getCellsInColumn(0)(selection);
if (cells) {
this.storage.clearCallbacks.forEach((cb) => cb());
this.storage.clearCallbacks.length = 0;
cells.forEach(({ pos }, index) => {
if (index === 0) {
decorations.push(
@ -89,8 +108,10 @@ export const TableCell = Node.create<TableCellOptions>({
grip.addEventListener('mousedown', (event) => {
event.preventDefault();
event.stopImmediatePropagation();
this.editor.view.dispatch(selectTable(this.editor.state.tr));
// this.options.onSelectTable(state);
this.editor.view.dispatch(
// @ts-ignore
selectTable(this.editor.state.tr)
);
});
return grip;
})
@ -99,7 +120,6 @@ export const TableCell = Node.create<TableCellOptions>({
decorations.push(
Decoration.widget(pos + 1, () => {
const rowSelected = isRowSelected(index)(selection);
let className = 'grip-row';
if (rowSelected) {
className += ' selected';
@ -111,18 +131,41 @@ export const TableCell = Node.create<TableCellOptions>({
className += ' last';
}
const grip = document.createElement('a');
ReactDOM.render(
<Tooltip content="向后增加一行">
<IconPlus />
</Tooltip>,
grip
);
this.storage.clearCallbacks.push(() => {
ReactDOM.unmountComponentAtNode(grip);
});
grip.className = className;
grip.addEventListener('mousedown', (event) => {
grip.addEventListener(
'mousedown',
(event) => {
event.preventDefault();
event.stopImmediatePropagation();
this.editor.view.dispatch(selectRow(index)(this.editor.state.tr));
});
this.editor.view.dispatch(
// @ts-ignore
selectRow(index)(this.editor.state.tr)
);
if (event.target !== grip) {
addRowAfter(this.editor.state, this.editor.view.dispatch);
}
},
true
);
return grip;
})
);
});
}
return DecorationSet.create(doc, decorations);
},
},

View File

@ -1,9 +1,29 @@
import { TableHeader as BuiltInTableHeader } from '@tiptap/extension-table-header';
import { Plugin } from 'prosemirror-state';
import { IconPlus } from '@douyinfe/semi-icons';
import { mergeAttributes, Node } from '@tiptap/core';
import { Tooltip } from 'components/tooltip';
import { Plugin, PluginKey } from 'prosemirror-state';
import { addColumnAfter } from 'prosemirror-tables';
import { Decoration, DecorationSet } from 'prosemirror-view';
import React from 'react';
import ReactDOM from 'react-dom';
import { getCellsInRow, isColumnSelected, selectColumn } from 'tiptap/prose-utils';
export const TableHeader = BuiltInTableHeader.extend({
export interface TableHeaderOptions {
HTMLAttributes: Record<string, any>;
}
export const TableHeader = Node.create<TableHeaderOptions, { clearCallbacks: Array<() => void> }>({
name: 'tableHeader',
content: 'block+',
tableRole: 'header_cell',
isolating: true,
addOptions() {
return {
HTMLAttributes: {},
};
},
addAttributes() {
return {
colspan: {
@ -13,10 +33,10 @@ export const TableHeader = BuiltInTableHeader.extend({
default: 1,
},
colwidth: {
default: [150],
default: [100],
parseHTML: (element) => {
const colwidth = element.getAttribute('colwidth');
const value = colwidth ? colwidth.split(',').map((item) => parseInt(item, 10)) : null;
const value = colwidth ? [parseInt(colwidth, 10)] : null;
return value;
},
@ -27,22 +47,44 @@ export const TableHeader = BuiltInTableHeader.extend({
};
},
parseHTML() {
return [{ tag: 'th' }];
},
renderHTML({ HTMLAttributes }) {
return ['th', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
},
addStorage() {
return {
clearCallbacks: [],
};
},
onDestroy() {
this.storage.clearCallbacks.forEach((cb) => cb());
this.storage.clearCallbacks.length = 0;
},
// @ts-ignore
addProseMirrorPlugins() {
const { isEditable } = this.editor;
return [
new Plugin({
key: new PluginKey('table-header-control'),
props: {
decorations: (state) => {
if (!isEditable) {
return DecorationSet.empty;
}
const { doc, selection } = state;
const decorations: Decoration[] = [];
const cells = getCellsInRow(0)(selection);
if (cells) {
this.storage.clearCallbacks.forEach((cb) => cb());
this.storage.clearCallbacks.length = 0;
cells.forEach(({ pos }, index) => {
decorations.push(
Decoration.widget(pos + 1, () => {
@ -58,17 +100,33 @@ export const TableHeader = BuiltInTableHeader.extend({
}
const grip = document.createElement('a');
grip.className = className;
ReactDOM.render(
<Tooltip content="向后增加一列">
<IconPlus />
</Tooltip>,
grip
);
this.storage.clearCallbacks.push(() => {
ReactDOM.unmountComponentAtNode(grip);
});
grip.addEventListener('mousedown', (event) => {
event.preventDefault();
event.stopImmediatePropagation();
this.editor.view.dispatch(selectColumn(index)(this.editor.state.tr));
if (event.target !== grip) {
addColumnAfter(this.editor.state, this.editor.view.dispatch);
}
});
return grip;
})
);
});
}
return DecorationSet.create(doc, decorations);
},
},

View File

@ -1,5 +1,7 @@
import BuiltInTable from '@tiptap/extension-table';
import { Node as ProseMirrorNode } from 'prosemirror-model';
import { Plugin, PluginKey } from 'prosemirror-state';
import { tableNodeTypes } from 'prosemirror-tables';
import { NodeView } from 'prosemirror-view';
function updateColumns(
@ -105,6 +107,26 @@ class TableView implements NodeView {
}
}
export function readonlyTableView({ cellMinWidth = 25, View = TableView } = {}) {
const plugin = new Plugin({
key: new PluginKey('readonlyTableView'),
state: {
init(_, state) {
this.spec.props.nodeViews[tableNodeTypes(state.schema).table.name] = (node, view) =>
new View(node, cellMinWidth);
return {};
},
apply(tr, prev) {
return prev;
},
},
props: {
nodeViews: {},
},
});
return plugin;
}
export const Table = BuiltInTable.extend({
// @ts-ignore
addOptions() {
@ -118,4 +140,8 @@ export const Table = BuiltInTable.extend({
allowTableNodeSelection: false,
};
},
addProseMirrorPlugins() {
return [...this.parent(), !this.editor.isEditable && readonlyTableView()].filter(Boolean);
},
}).configure({ resizable: true });

View File

@ -25,13 +25,14 @@ export const TableBubbleMenu = ({ editor }) => {
const shouldShow = useCallback(() => {
return editor.isActive(Table.name);
}, [editor]);
const getRenderContainer = useCallback((node) => {
let container = node;
// 文本节点
if (container && !container.tag) {
container = node.parentElement;
}
while (container && container.tagName !== 'TABLE') {
while (container && !container.classList.contains('tableWrapper')) {
container = container.parentElement;
}
return container;
@ -40,6 +41,7 @@ export const TableBubbleMenu = ({ editor }) => {
const deleteMe = useCallback(() => {
deleteNode(Table.name, editor);
}, [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]);
@ -59,7 +61,6 @@ export const TableBubbleMenu = ({ editor }) => {
pluginKey="table-bubble-menu"
tippyOptions={{
maxWidth: 'calc(100vw - 100px)',
placement: 'bottom',
}}
shouldShow={shouldShow}
getRenderContainer={getRenderContainer}

View File

@ -9,8 +9,8 @@ export const Table: React.FC<{ editor: Editor }> = ({ editor }) => {
return (
<>
<TableBubbleMenu editor={editor} />
<TableRowBubbleMenu editor={editor} />
<TableColBubbleMenu editor={editor} />
{/* <TableRowBubbleMenu editor={editor} /> */}
{/* <TableColBubbleMenu editor={editor} /> */}
</>
);
};

View File

@ -1,147 +1,271 @@
.ProseMirror {
.node-table {
/* stylelint-disable */
$tableBorderColor: var(--semi-color-border);
$tableHeaderBgColor: var(--semi-color-fill-0);
$tableSelectedBorderColor: rgb(0 101 255);
$tableSelectedCellBgColor: transparent;
$tableSelectedControlBgColor: #2584ff;
$tableResizeHandleBgColor: #adf;
.tableWrapper {
position: relative;
margin-top: 0.75em;
scrollbar-width: thin;
scrollbar-color: transparent transparent;
margin: 0.5em 0px;
&.has-focus {
.scrollWrapper {
margin-top: -20px;
}
}
}
.scrollable {
padding-left: 1em;
margin-left: -1em;
overflow: auto hidden;
.scrollWrapper {
overflow-y: hidden;
overflow-x: auto;
padding-left: 28px;
padding-top: 28px;
padding-bottom: 8px;
margin-left: -28px;
margin-top: -20px;
margin-bottom: -8px;
border-left: 1px solid transparent;
border-right: 1px solid transparent;
-webkit-transition: border 250ms ease-in-out 0s;
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%);
}
&.right {
right: 0;
left: auto;
box-shadow: rgb(0 0 0 / 25%) -16px 0 16px -16px inset;
&.is-editable {
&::after {
position: absolute;
top: 0;
right: 0;
width: 1em;
height: 1em;
background-color: var(--semi-color-nav-bg);
content: '';
}
}
}
}
table {
width: 100%;
margin-top: 1em;
border-radius: 4px;
border-collapse: collapse;
box-sizing: border-box;
border-color: var(--semi-color-fill-2);
border-radius: 4px;
overflow: auto;
&.is-readonly {
margin-top: 0;
box-sizing: border-box;
* {
box-sizing: border-box;
}
tr {
position: relative;
border-bottom: 1px solid $tableBorderColor;
}
th {
background: $tableHeaderBgColor;
}
td,
th {
position: relative;
min-width: 100px;
vertical-align: top;
border: 1px solid $tableBorderColor;
position: relative;
padding: 4px 8px;
text-align: left;
vertical-align: top;
border: 1px solid rgb(232 235 237);
border-color: var(--semi-color-fill-2);
:not(a) {
&:first-of-type {
margin-top: 0;
}
}
}
th {
font-weight: bold;
background-color: var(--semi-color-fill-0);
min-width: 100px;
}
.selectedCell {
border-style: double;
border-color: rgb(0 101 255);
background: var(--semi-color-info-light-hover);
position: relative;
border: 1px solid $tableSelectedBorderColor;
background-color: $tableSelectedCellBgColor;
&::after {
box-sizing: content-box;
height: 100%;
width: 100%;
border: 1px solid $tableSelectedBorderColor;
content: '';
position: absolute;
left: -1px;
top: -1px;
bottom: 0px;
z-index: 12;
display: inline-block;
pointer-events: none;
}
}
.grip-column {
position: absolute;
top: -1em;
left: 0;
z-index: 10;
display: block;
top: -12px;
left: -1px;
width: 100%;
height: 0.7em;
margin-bottom: 3px;
cursor: pointer;
background: #ced4da;
&:hover,
&.selected {
background: var(--semi-color-info);
> span {
position: absolute;
top: -18px;
left: 100%;
transform: translateX(-8px);
display: inline-block;
width: 16px;
height: 16px;
font-size: 0;
cursor: pointer;
.semi-icon-default {
font-size: inherit;
}
}
&::before {
content: '';
position: absolute;
left: 100%;
bottom: 4px;
transform: translateX(-1px);
width: 4px;
height: 4px;
background-color: $tableBorderColor;
border-radius: 50%;
display: block;
}
&::after {
box-sizing: content-box;
content: '';
cursor: pointer;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 10px;
background: $tableHeaderBgColor;
border: 1px solid $tableBorderColor;
display: block;
}
&:hover {
color: $tableSelectedBorderColor;
> span {
font-size: 14px;
}
&::before {
display: none;
}
&::after {
background: $tableSelectedControlBgColor;
border-color: $tableSelectedControlBgColor;
}
}
&.last::after {
border-top-right-radius: 3px;
}
&.selected::after {
background: $tableSelectedControlBgColor;
border-color: $tableSelectedControlBgColor;
}
}
.grip-row {
position: absolute;
top: 0;
left: -1em;
z-index: 10;
display: block;
width: 0.7em;
left: -12px;
top: -1px;
height: 100%;
margin-right: 3px;
cursor: pointer;
background: #ced4da;
&:hover,
&.selected {
background: var(--semi-color-info);
> span {
transform: translateY(8px);
position: absolute;
left: -16px;
bottom: 4px;
display: inline-block;
width: 16px;
height: 16px;
font-size: 0;
cursor: pointer;
.semi-icon-default {
font-size: inherit;
}
}
&::before {
content: '';
position: absolute;
left: -10px;
bottom: -2px;
width: 4px;
height: 4px;
background-color: $tableBorderColor;
border-radius: 50%;
display: block;
}
&::after {
box-sizing: content-box;
content: '';
cursor: pointer;
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 10px;
background: $tableHeaderBgColor;
border: 1px solid $tableBorderColor;
display: block;
}
&:hover {
color: $tableSelectedBorderColor;
> span {
font-size: 14px;
}
&::before {
display: none;
}
&::after {
background: $tableSelectedControlBgColor;
border-color: $tableSelectedBorderColor;
}
}
&.last::after {
border-bottom-left-radius: 3px;
}
&.selected::after {
background: $tableSelectedControlBgColor;
border-color: $tableSelectedBorderColor;
}
}
.grip-table {
position: absolute;
top: -1em;
left: -1em;
z-index: 10;
display: block;
width: 0.8em;
height: 0.8em;
&::after {
box-sizing: content-box;
content: '';
cursor: pointer;
background: #ced4da;
border-radius: 50%;
position: absolute;
top: -12px;
left: -12px;
display: block;
background: $tableHeaderBgColor;
width: 10px;
height: 10px;
border: 1px solid $tableBorderColor;
border-top-left-radius: 3px;
}
&:hover,
&.selected {
background: var(--semi-color-info);
&:hover::after {
background: $tableSelectedControlBgColor;
border-color: $tableSelectedBorderColor;
}
&.selected::after {
background: $tableSelectedControlBgColor;
border-color: $tableSelectedBorderColor;
}
}
}
@ -152,9 +276,7 @@
bottom: -2px;
width: 4px;
pointer-events: none;
background-color: #adf;
}
}
background-color: $tableResizeHandleBgColor;
}
.resize-cursor {