uui-editor/src/plugins/tablePlugin/tablePlugin.tsx (175 lines of code) (raw):
import React from 'react';
import { Dropdown } from '@epam/uui-components';
import { useFocused, useReadOnly, useSelected } from 'slate-react';
import { useIsPluginActive, isTextSelected } from '../../helpers';
import { ReactComponent as TableIcon } from '../../icons/table-add.svg';
import { FloatingToolbar } from '../../implementation/PositionedToolbar';
import { ToolbarButton } from '../../implementation/ToolbarButton';
import {
DeserializeHtml,
PlateEditor,
getPluginType,
insertNodes,
someNode,
useEditorRef,
withoutNormalizing,
isElement,
PlatePlugin,
setNodes,
} from '@udecode/plate-common';
import {
ELEMENT_TABLE,
ELEMENT_TD,
ELEMENT_TH,
ELEMENT_TR,
TTableElement,
TablePlugin,
createTablePlugin,
getTableColumnCount,
getTableGridAbove,
useTableMergeState,
withTable,
} from '@udecode/plate-table';
import { MergeToolbarContent } from './MergeToolbarContent';
import { TableToolbarContent } from './ToolbarContent';
import { createInitialTable, selectFirstCell } from './utils';
import { TableRowElement } from './TableRowElement';
import { TableCellElement } from './TableCellElement';
import { TableElement } from './TableElement';
import { WithToolbarButton } from '../../implementation/Toolbars';
import { TABLE_CELL_TYPE, TABLE_HEADER_CELL_TYPE, TABLE_TYPE, TABLE_ROW_TYPE, DEFAULT_COL_WIDTH } from './constants';
import { normalizeTableCellElement, normalizeTableElement } from '../../migrations/normalizers';
import { DeprecatedTTableCellElement } from '../../migrations/types';
const noop = () => {};
function TableRenderer(props: any) {
const editor = useEditorRef();
const isReadonly = useReadOnly();
const isFocused = useFocused();
const isSelected = useSelected();
const cellEntries = getTableGridAbove(editor, { format: 'cell' });
const hasEntries = !!cellEntries?.length;
const showToolbar = !isReadonly && isSelected && isFocused && hasEntries;
const { canMerge, canUnmerge } = useTableMergeState();
return (
<Dropdown
renderTarget={ (innerProps: any) => (
<div ref={ innerProps.ref }>
<TableElement { ...props } />
</div>
) }
renderBody={ () => (
<FloatingToolbar
placement="bottom"
children={
canMerge ? (
<MergeToolbarContent />
) : (
<TableToolbarContent canUnmerge={ canUnmerge } />
)
}
editor={ editor }
isTable
/>
) }
onValueChange={ noop }
value={ showToolbar }
placement="top"
/>
);
}
const getDefaultColWidths = (columnsNumber: number) =>
Array.from({ length: columnsNumber }, () => DEFAULT_COL_WIDTH);
const initDefaultTableColWidth = (tableNode: DeprecatedTTableCellElement): TTableElement => {
if (!tableNode.colSizes) {
return {
...tableNode,
colSizes: getDefaultColWidths(getTableColumnCount(tableNode)),
};
}
return tableNode;
};
// TODO: move to plate
const createGetNodeFunc = (type: string) => {
const getNode: DeserializeHtml['getNode'] = (element) => {
const background = element.style.background || element.style.backgroundColor;
if (background) {
return {
type,
background,
};
}
return { type };
};
return getNode;
};
type TablePLuginOptions = WithToolbarButton & TablePlugin;
export const tablePlugin = (): PlatePlugin<TablePLuginOptions> =>
createTablePlugin<TablePLuginOptions>({
overrideByKey: {
[ELEMENT_TABLE]: {
type: TABLE_TYPE,
component: TableRenderer,
},
[ELEMENT_TR]: {
type: TABLE_ROW_TYPE,
component: TableRowElement,
},
[ELEMENT_TD]: {
type: TABLE_CELL_TYPE,
component: TableCellElement,
deserializeHtml: {
getNode: createGetNodeFunc(TABLE_CELL_TYPE),
},
},
[ELEMENT_TH]: {
type: TABLE_HEADER_CELL_TYPE,
component: TableCellElement,
deserializeHtml: {
getNode: createGetNodeFunc(TABLE_HEADER_CELL_TYPE),
},
},
},
options: {
enableMerging: true,
bottomBarButton: TableButton,
},
withOverrides: (editor, plugin) => {
// eslint-disable-next-line no-param-reassign
editor = withTable(editor, plugin);
const { normalizeNode } = editor;
editor.normalizeNode = (entry) => {
const [node, path] = entry;
if (isElement(node) && node.type === TABLE_TYPE) {
const normalized = initDefaultTableColWidth(normalizeTableElement(entry) as DeprecatedTTableCellElement);
setNodes(
editor,
normalized,
{ at: path },
);
return;
}
if (isElement(node) && (TABLE_CELL_TYPE === node.type || TABLE_CELL_TYPE === node.type)) {
normalizeTableCellElement(editor, entry);
}
normalizeNode(entry);
};
return editor;
},
});
export function TableButton({ editor }: { editor: PlateEditor }) {
if (!useIsPluginActive(ELEMENT_TABLE)) return null;
const onCreateTable = async () => {
if (!editor) return;
withoutNormalizing(editor, () => {
const isCurrentTableSelection = !!someNode(editor, {
match: { type: getPluginType(editor, ELEMENT_TABLE) },
});
if (!isCurrentTableSelection) {
insertNodes(editor, createInitialTable(editor));
selectFirstCell(editor);
}
});
};
return (
<ToolbarButton
isDisabled={ isTextSelected(editor, true) }
onClick={ onCreateTable }
icon={ TableIcon }
/>
);
}