diff --git a/apps/www/src/registry/default/plate-ui/table-cell-element.tsx b/apps/www/src/registry/default/plate-ui/table-cell-element.tsx index 81dd062cb0..7193449c57 100644 --- a/apps/www/src/registry/default/plate-ui/table-cell-element.tsx +++ b/apps/www/src/registry/default/plate-ui/table-cell-element.tsx @@ -6,12 +6,10 @@ import { cn, withProps, withRef } from '@udecode/cn'; import { useTableCellElement, useTableCellElementResizable, - useTableCellElementResizableState, useTableCellElementState, } from '@udecode/plate-table/react'; import { PlateElement } from './plate-element'; -import { ResizeHandle } from './resizable'; export const TableCellElement = withRef< typeof PlateElement, @@ -22,27 +20,11 @@ export const TableCellElement = withRef< >(({ children, className, hideBorder, isHeader, style, ...props }, ref) => { const { element } = props; - const { - borders, - colIndex, - colSpan, - hovered, - hoveredLeft, - isSelectingCell, - readOnly, - rowIndex, - rowSize, - selected, - } = useTableCellElementState(); + const { borders, isFirstCell, readOnly, selected } = + useTableCellElementState(); const { props: cellProps } = useTableCellElement({ element: props.element }); - const resizableState = useTableCellElementResizableState({ - colIndex, - colSpan, - rowIndex, - }); - const { bottomProps, hiddenLeft, leftProps, rightProps } = - useTableCellElementResizable(resizableState); + const { bottomProps, leftProps, rightProps } = useTableCellElementResizable(); return ( -
+
{children}
- {!isSelectingCell && ( + {!readOnly && (
- {!readOnly && ( - <> - - - {!hiddenLeft && ( - - )} - - {hovered && ( -
- )} - {hoveredLeft && ( -
- )} - + {isFirstCell && ( +
)} +
+
)} diff --git a/apps/www/src/registry/default/plate-ui/table-element.tsx b/apps/www/src/registry/default/plate-ui/table-element.tsx index 41912768ab..0d5e94fbd2 100644 --- a/apps/www/src/registry/default/plate-ui/table-element.tsx +++ b/apps/www/src/registry/default/plate-ui/table-element.tsx @@ -23,6 +23,8 @@ import { useTableElement, useTableElementState, useTableMergeState, + useTableResizableState, + useTableResize, } from '@udecode/plate-table/react'; import { type LucideProps, Combine, Trash2Icon, Ungroup } from 'lucide-react'; import { useReadOnly, useSelected } from 'slate-react'; @@ -38,6 +40,7 @@ import { } from './dropdown-menu'; import { PlateElement } from './plate-element'; import { Popover, PopoverContent, popoverVariants } from './popover'; +import { ResizeHandle } from './resizable'; export const TableBordersDropdownMenuContent = withRef< typeof DropdownMenuPrimitive.Content @@ -200,13 +203,34 @@ export const TableFloatingToolbar = withRef( export const TableElement = withHOC( TableProvider, withRef(({ children, className, ...props }, ref) => { - const { colSizes, isSelectingCell, marginLeft, minColumnWidth } = - useTableElementState(); + const { + colSizes, + containerRef, + element, + isSelectingCell, + marginLeft, + minColumnWidth, + } = useTableElementState(); const { colGroupProps, props: tableProps } = useTableElement(); + const tableResizableState = useTableResizableState({}); + + const { currentColIndex, currentRowIndex, resizeHandleProps, resizeStyle } = + useTableResize({ + containerRef, + element, + isSelectingCell, + minColumnWidth, + ...tableResizableState, + }); + return ( -
+
{children} + {!isSelectingCell && ( +
+ + + +
+ )}
); diff --git a/packages/table/src/lib/queries/getTableOverriddenRowSizes.ts b/packages/table/src/lib/queries/getTableOverriddenRowSizes.ts new file mode 100644 index 0000000000..d183211c19 --- /dev/null +++ b/packages/table/src/lib/queries/getTableOverriddenRowSizes.ts @@ -0,0 +1,19 @@ +import type { + TTableElement, + TTableRowElement, + TableStoreSizeOverrides, +} from '../types'; + +export const getTableOverriddenRowSizes = ( + tableNode: TTableElement, + rowSizeOverrides?: TableStoreSizeOverrides +): number[] => { + const rowSizes = Array.from({ length: tableNode.children.length }).map( + (_, index) => + rowSizeOverrides?.get?.(index) ?? + (tableNode.children?.[index] as TTableRowElement)?.size ?? + 0 + ); + + return rowSizes; +}; diff --git a/packages/table/src/lib/queries/index.ts b/packages/table/src/lib/queries/index.ts index 2b72d93510..391fc00a5c 100644 --- a/packages/table/src/lib/queries/index.ts +++ b/packages/table/src/lib/queries/index.ts @@ -13,5 +13,6 @@ export * from './getTableAbove'; export * from './getTableColumnCount'; export * from './getTableEntries'; export * from './getTableOverriddenColSizes'; +export * from './getTableOverriddenRowSizes'; export * from './getTopTableCell'; export * from './isTableBorderHidden'; diff --git a/packages/table/src/react/TablePlugin.tsx b/packages/table/src/react/TablePlugin.tsx index aa3b111228..e2168606a9 100644 --- a/packages/table/src/react/TablePlugin.tsx +++ b/packages/table/src/react/TablePlugin.tsx @@ -1,5 +1,7 @@ import { toPlatePlugin } from '@udecode/plate-common/react'; +import type { TTableRowElement } from '../lib'; + import { BaseTableCellHeaderPlugin, BaseTableCellPlugin, @@ -9,7 +11,18 @@ import { import { onKeyDownTable } from './onKeyDownTable'; import { withTable } from './withTable'; -export const TableRowPlugin = toPlatePlugin(BaseTableRowPlugin); +export const TableRowPlugin = toPlatePlugin(BaseTableRowPlugin, { + node: { + props: ({ element }: { element: TTableRowElement }) => ({ + nodeProps: { + style: { + height: + element.size === 0 || !element?.size ? 'auto' : `${element.size}px`, + }, + }, + }), + }, +}); export const TableCellPlugin = toPlatePlugin(BaseTableCellPlugin, { node: { diff --git a/packages/table/src/react/components/TableCellElement/useTableCellElementResizable.ts b/packages/table/src/react/components/TableCellElement/useTableCellElementResizable.ts index fc76f1c6b6..902f71730a 100644 --- a/packages/table/src/react/components/TableCellElement/useTableCellElementResizable.ts +++ b/packages/table/src/react/components/TableCellElement/useTableCellElementResizable.ts @@ -1,267 +1,92 @@ import React from 'react'; +import { findNode } from '@udecode/plate-common'; import { findNodePath, useEditorPlugin, useElement, } from '@udecode/plate-common/react'; -import { - type ResizeEvent, - type ResizeHandle, - resizeLengthClampStatic, -} from '@udecode/plate-resizable'; - -import type { TableCellElementState } from './useTableCellElementState'; +import debounce from 'lodash/debounce.js'; import { type TTableElement, - setTableColSize, - setTableMarginLeft, - setTableRowSize, + computeCellIndices, + getColSpan, + getRowSpan, } from '../../../lib'; import { TablePlugin } from '../../TablePlugin'; -import { - useOverrideColSize, - useOverrideMarginLeft, - useOverrideRowSize, - useTableStore, -} from '../../stores'; -import { useTableColSizes } from '../TableElement/useTableColSizes'; -import { roundCellSizeToStep } from './roundCellSizeToStep'; - -export type TableCellElementResizableOptions = { - /** Resize by step instead of by pixel. */ - step?: number; - - /** Overrides for X and Y axes. */ - stepX?: number; - stepY?: number; -} & Pick; - -export const useTableCellElementResizableState = ({ - colIndex, - colSpan, - rowIndex, - step, - stepX = step, - stepY = step, -}: TableCellElementResizableOptions) => { - const { getOptions } = useEditorPlugin(TablePlugin); - const { disableMarginLeft } = getOptions(); +import { getTableRowIndex } from '../../queries'; +import { useTableStore } from '../../stores'; - return { - colIndex, - colSpan, - disableMarginLeft, - rowIndex, - stepX, - stepY, - }; -}; - -export const useTableCellElementResizable = ({ - colIndex, - colSpan, - disableMarginLeft, - rowIndex, - stepX, - stepY, -}: ReturnType): { - bottomProps: React.ComponentPropsWithoutRef; - hiddenLeft: boolean; - leftProps: React.ComponentPropsWithoutRef; - rightProps: React.ComponentPropsWithoutRef; -} => { - const { editor, getOptions } = useEditorPlugin(TablePlugin); +export const useTableCellElementResizable = () => { + const { editor } = useEditorPlugin(TablePlugin); const element = useElement(); - const tableElement = useElement(TablePlugin.key); - const { minColumnWidth = 0 } = getOptions(); - - let initialWidth: number | undefined; - - if (colSpan > 1) { - initialWidth = tableElement.colSizes?.[colIndex]; - } - - const [hoveredColIndex, setHoveredColIndex] = - useTableStore().use.hoveredColIndex(); - - const colSizesWithoutOverrides = useTableColSizes(tableElement, { - disableOverrides: true, - }); - const { marginLeft = 0 } = tableElement; - - const overrideColSize = useOverrideColSize(); - const overrideRowSize = useOverrideRowSize(); - const overrideMarginLeft = useOverrideMarginLeft(); - - /* eslint-disable @typescript-eslint/no-shadow */ - const setColSize = React.useCallback( - (colIndex: number, width: number) => { - setTableColSize( - editor, - { colIndex, width }, - { at: findNodePath(editor, element)! } - ); - - // Prevent flickering - setTimeout(() => overrideColSize(colIndex, null), 0); - }, - [editor, element, overrideColSize] - ); - - /* eslint-disable @typescript-eslint/no-shadow */ - const setRowSize = React.useCallback( - (rowIndex: number, height: number) => { - setTableRowSize( - editor, - { height, rowIndex }, - { at: findNodePath(editor, element)! } - ); - - // Prevent flickering - setTimeout(() => overrideRowSize(rowIndex, null), 0); - }, - [editor, element, overrideRowSize] - ); - - const setMarginLeft = React.useCallback( - (marginLeft: number) => { - setTableMarginLeft( - editor, - { marginLeft }, - { at: findNodePath(editor, element)! } - ); - // Prevent flickering - setTimeout(() => overrideMarginLeft(null), 0); - }, - [editor, element, overrideMarginLeft] - ); - - const handleResizeRight = React.useCallback( - ({ delta, finished, initialSize: currentInitial }: ResizeEvent) => { - const nextInitial = colSizesWithoutOverrides[colIndex + 1]; - - const complement = (width: number) => - currentInitial + nextInitial - width; - - const currentNew = roundCellSizeToStep( - resizeLengthClampStatic(currentInitial + delta, { - max: nextInitial ? complement(minColumnWidth) : undefined, - min: minColumnWidth, - }), - stepX - ); - - const nextNew = nextInitial ? complement(currentNew) : undefined; - - const fn = finished ? setColSize : overrideColSize; - fn(colIndex, currentNew); - - if (nextNew) fn(colIndex + 1, nextNew); - }, - [ - colIndex, - colSizesWithoutOverrides, - minColumnWidth, - overrideColSize, - setColSize, - stepX, - ] - ); - - const handleResizeBottom = React.useCallback( - (event: ResizeEvent) => { - const newHeight = roundCellSizeToStep( - event.initialSize + event.delta, - stepY - ); - - if (event.finished) { - setRowSize(rowIndex, newHeight); - } else { - overrideRowSize(rowIndex, newHeight); - } - }, - [overrideRowSize, rowIndex, setRowSize, stepY] - ); - - const handleResizeLeft = React.useCallback( - (event: ResizeEvent) => { - const initial = colSizesWithoutOverrides[colIndex]; - - const complement = (width: number) => initial + marginLeft - width; - - const newMargin = roundCellSizeToStep( - resizeLengthClampStatic(marginLeft + event.delta, { - max: complement(minColumnWidth), - min: 0, - }), - stepX - ); - - const newWidth = complement(newMargin); - - if (event.finished) { - setMarginLeft(newMargin); - setColSize(colIndex, newWidth); - } else { - overrideMarginLeft(newMargin); - overrideColSize(colIndex, newWidth); - } - }, - [ - colIndex, - colSizesWithoutOverrides, - marginLeft, - minColumnWidth, - overrideColSize, - overrideMarginLeft, - setColSize, - setMarginLeft, - stepX, - ] - ); + const setHoveredColIndex = useTableStore().set.hoveredColIndex(); + const setHoveredRowIndex = useTableStore().set.hoveredRowIndex(); + + const handleColumnResize = debounce((colIndex?: number) => { + if (colIndex === -1) { + setHoveredColIndex(colIndex); + + return; + } + + const cellPath = findNodePath(editor, element)!; + const [tableElement] = findNode(editor, { + at: cellPath, + match: { type: TablePlugin.key }, + })!; + + const defaultColIndex = computeCellIndices( + editor, + tableElement, + element + )!.col; + const colSpan = getColSpan(element); + const endingColIndex = defaultColIndex + colSpan - 1; + + if (endingColIndex !== undefined) { + setHoveredColIndex(endingColIndex); + } + }, 150); + + const handleRowResize = debounce(() => { + const defaultRowIndex = getTableRowIndex(editor, element); + const rowSpan = getRowSpan(element); + const endingRowIndex = defaultRowIndex + rowSpan - 1; + setHoveredRowIndex(endingRowIndex); + }, 150); + + const handleColumnResizeCancel = () => { + handleColumnResize.cancel(); + setHoveredColIndex(null); + }; - /* eslint-disable @typescript-eslint/no-shadow */ - const getHandleHoverProps = (colIndex: number) => ({ - onHover: () => { - if (hoveredColIndex === null) { - setHoveredColIndex(colIndex); - } - }, - onHoverEnd: () => { - if (hoveredColIndex === colIndex) { - setHoveredColIndex(null); - } - }, - }); + const handleRowResizeCancel = () => { + handleRowResize.cancel(); + setHoveredRowIndex(null); + }; - const hasLeftHandle = colIndex === 0 && !disableMarginLeft; + React.useEffect(() => { + return () => { + handleColumnResize.cancel(); + handleRowResize.cancel(); + }; + }, [handleColumnResize, handleRowResize]); return { bottomProps: { - options: { - direction: 'bottom', - onResize: handleResizeBottom, - }, + onMouseEnter: handleRowResize, + onMouseLeave: handleRowResizeCancel, }, - hiddenLeft: !hasLeftHandle, leftProps: { - options: { - direction: 'left', - onResize: handleResizeLeft, - ...getHandleHoverProps(-1), - }, + onMouseEnter: () => handleColumnResize(-1), + onMouseLeave: handleColumnResizeCancel, }, rightProps: { - options: { - direction: 'right', - initialSize: initialWidth, - onResize: handleResizeRight, - ...getHandleHoverProps(colIndex), - }, + onMouseEnter: () => handleColumnResize(), + onMouseLeave: handleColumnResizeCancel, }, }; }; diff --git a/packages/table/src/react/components/TableCellElement/useTableCellElementState.ts b/packages/table/src/react/components/TableCellElement/useTableCellElementState.ts index e582aaa19d..dbd6ea6865 100644 --- a/packages/table/src/react/components/TableCellElement/useTableCellElementState.ts +++ b/packages/table/src/react/components/TableCellElement/useTableCellElementState.ts @@ -1,6 +1,11 @@ import React from 'react'; -import { useEditorRef, useElement } from '@udecode/plate-common/react'; +import { findNode } from '@udecode/plate-common'; +import { + findNodePath, + useEditorRef, + useElement, +} from '@udecode/plate-common/react'; import { useReadOnly } from 'slate-react'; import { @@ -13,7 +18,7 @@ import { getColSpan, getRowSpan, } from '../../../lib'; -import { TablePlugin } from '../../TablePlugin'; +import { TableCellPlugin, TablePlugin } from '../../TablePlugin'; import { getTableColumnIndex } from '../../merge'; import { getTableRowIndex } from '../../queries'; import { useTableStore } from '../../stores'; @@ -25,14 +30,8 @@ import { useIsCellSelected } from './useIsCellSelected'; export type TableCellElementState = { borders: BorderStylesDefault; - colIndex: number; - colSpan: number; - hovered: boolean; - hoveredLeft: boolean; - isSelectingCell: boolean; + isFirstCell: boolean; readOnly: boolean; - rowIndex: number; - rowSize: number | undefined; selected: boolean; }; @@ -43,29 +42,25 @@ export const useTableCellElementState = ({ ignoreReadOnly?: boolean; } = {}): TableCellElementState => { const editor = useEditorRef(); - const cellElement = useElement(); - - const colSpan = getColSpan(cellElement); - const rowSpan = getRowSpan(cellElement); + const cellElement = useElement(TableCellPlugin.key); + const cellPath = findNodePath(editor, cellElement); const readOnly = useReadOnly(); - const isCellSelected = useIsCellSelected(cellElement); - const hoveredColIndex = useTableStore().get.hoveredColIndex(); - const selectedCells = useTableStore().get.selectedCells(); - const tableElement = useElement(TablePlugin.key); - const rowElement = useElement(BaseTableRowPlugin.key); - const rowSizeOverrides = useTableStore().get.rowSizeOverrides(); + const [tableElement] = findNode(editor, { + at: cellPath, + match: { type: TablePlugin.key }, + })!; + const [rowElement] = findNode(editor, { + at: cellPath, + match: { type: BaseTableRowPlugin.key }, + })!; const { _cellIndices, enableMerging } = editor.getOptions(TablePlugin); if (!enableMerging) { const colIndex = getTableColumnIndex(editor, cellElement); - const rowIndex = getTableRowIndex(editor, cellElement); - - const rowSize = - rowSizeOverrides.get?.(rowIndex) ?? rowElement?.size ?? undefined; const isFirstCell = colIndex === 0; const isFirstRow = tableElement.children?.[0] === rowElement; @@ -77,14 +72,8 @@ export const useTableCellElementState = ({ return { borders, - colIndex, - colSpan, - hovered: hoveredColIndex === colIndex, - hoveredLeft: isFirstCell && hoveredColIndex === -1, - isSelectingCell: !!selectedCells, + isFirstCell, readOnly: !ignoreReadOnly && readOnly, - rowIndex, - rowSize, selected: isCellSelected, }; } @@ -104,13 +93,6 @@ export const useTableCellElementState = ({ } const colIndex = result.col; - const rowIndex = result.row; - - const endingRowIndex = rowIndex + rowSpan - 1; - const endingColIndex = colIndex + colSpan - 1; - - const rowSize = - rowSizeOverrides.get?.(endingRowIndex) ?? rowElement?.size ?? undefined; const isFirstCell = colIndex === 0; const isFirstRow = tableElement.children?.[0] === rowElement; @@ -122,14 +104,8 @@ export const useTableCellElementState = ({ return { borders, - colIndex: endingColIndex, - colSpan, - hovered: hoveredColIndex === endingColIndex, - hoveredLeft: isFirstCell && hoveredColIndex === -1, - isSelectingCell: !!selectedCells, + isFirstCell, readOnly: !ignoreReadOnly && readOnly, - rowIndex: endingRowIndex, - rowSize, selected: isCellSelected, }; }; diff --git a/packages/table/src/react/components/TableElement/index.ts b/packages/table/src/react/components/TableElement/index.ts index ccab74aaa4..46f76e0aeb 100644 --- a/packages/table/src/react/components/TableElement/index.ts +++ b/packages/table/src/react/components/TableElement/index.ts @@ -1,7 +1,8 @@ -/** - * @file Automatically generated by barrelsby. - */ +/** @file Automatically generated by barrelsby. */ export * from './useSelectedCells'; export * from './useTableColSizes'; +export * from './useTableRowSizes'; export * from './useTableElement'; +export * from './useTableResizable'; + diff --git a/packages/table/src/react/components/TableElement/useResizerOffset.ts b/packages/table/src/react/components/TableElement/useResizerOffset.ts new file mode 100644 index 0000000000..ccf1cbe023 --- /dev/null +++ b/packages/table/src/react/components/TableElement/useResizerOffset.ts @@ -0,0 +1,88 @@ +import React from 'react'; + +export const useReSizerOffset = ({ + containerRef, + currentColIndex, + currentRowIndex, + marginLeft, +}: { + containerRef: React.RefObject; + currentColIndex: number | null; + currentRowIndex: number | null; + marginLeft: number; +}) => { + const lastValidOffsets = React.useRef({ + col: 0, + colLeft: 0, + row: 0, + }); + + const { colElements, initialColSize, nextInitialColSize } = + React.useMemo(() => { + if (!containerRef.current || currentColIndex === null) { + return { + colElements: [] as HTMLTableColElement[], + initialColSize: 0, + nextInitialColSize: 0, + }; + } + + const cols = Array.from(containerRef.current.querySelectorAll('col')); + + return { + colElements: cols, + initialColSize: + currentColIndex === -1 + ? cols[0]?.getBoundingClientRect().width + : cols[currentColIndex]?.getBoundingClientRect().width, + nextInitialColSize: + cols[currentColIndex + 1]?.getBoundingClientRect().width, + }; + }, [containerRef, currentColIndex]); + + const { initialRowSize, rowElements } = React.useMemo(() => { + if (!containerRef.current || currentRowIndex === null) { + return { initialRowSize: 0, rowElements: [] as HTMLTableRowElement[] }; + } + + const rows = Array.from(containerRef.current.querySelectorAll('tr')); + + return { + initialRowSize: rows[currentRowIndex]?.getBoundingClientRect().height, + rowElements: rows, + }; + }, [currentRowIndex, containerRef]); + + if (currentColIndex !== null) { + if (currentColIndex === -1) { + const paddingLeft = + Number.parseInt( + window.getComputedStyle(containerRef.current!).paddingLeft + ) || 0; + const scrollLeft = containerRef.current?.scrollLeft ?? 0; + + lastValidOffsets.current.colLeft = paddingLeft - scrollLeft; + } else { + const totalWidth = colElements + .slice(0, currentColIndex + 1) + .reduce((sum, col) => sum + col.getBoundingClientRect().width, 0); + const scrollLeft = containerRef.current?.scrollLeft ?? 0; + + lastValidOffsets.current.col = totalWidth - scrollLeft + marginLeft; + } + } + if (currentRowIndex !== null) { + const totalHeight = rowElements + .slice(0, currentRowIndex + 1) + .reduce((sum, row) => sum + row.getBoundingClientRect().height, 0); + + lastValidOffsets.current.row = totalHeight; + } + + return { + initialColSize, + initialRowSize, + lastValidOffsets, + nextInitialColSize, + }; +}; diff --git a/packages/table/src/react/components/TableElement/useTableElement.ts b/packages/table/src/react/components/TableElement/useTableElement.ts index 1921b96e95..ee6f861d4d 100644 --- a/packages/table/src/react/components/TableElement/useTableElement.ts +++ b/packages/table/src/react/components/TableElement/useTableElement.ts @@ -8,9 +8,12 @@ import { TablePlugin } from '../../TablePlugin'; import { useTableStore } from '../../stores'; import { useSelectedCells } from './useSelectedCells'; import { useTableColSizes } from './useTableColSizes'; +import { useTableRowSizes } from './useTableRowSizes'; export interface TableElementState { colSizes: number[]; + containerRef: React.RefObject; + element: TTableElement; isSelectingCell: boolean; marginLeft: number; minColumnWidth: number; @@ -27,15 +30,19 @@ export const useTableElementState = ({ const { disableMarginLeft, enableMerging, minColumnWidth } = editor.getOptions(TablePlugin); - const element = useElement(); + const element = useElement(TablePlugin.key); + const containerRef = React.useRef(null); const selectedCells = useTableStore().get.selectedCells(); const marginLeftOverride = useTableStore().get.marginLeftOverride(); + const [isMounted, setIsMounted] = React.useState(false); + const marginLeft = disableMarginLeft ? 0 : (marginLeftOverride ?? element.marginLeft ?? 0); let colSizes = useTableColSizes(element); + const rowSizes = useTableRowSizes(element); React.useEffect(() => { if (enableMerging) { @@ -51,8 +58,24 @@ export const useTableElementState = ({ colSizes.push('100%' as any); } + // apply row sizes + React.useEffect(() => { + if (!isMounted || !containerRef.current) return; + + const trs = containerRef.current.querySelectorAll('tr'); + trs.forEach((tr, index) => { + tr.style.height = rowSizes[index] === 0 ? 'auto' : `${rowSizes[index]}px`; + }); + }, [rowSizes, isMounted]); + + React.useEffect(() => { + setIsMounted(true); + }, []); + return { colSizes, + containerRef, + element, isSelectingCell: !!selectedCells, marginLeft, minColumnWidth: minColumnWidth!, diff --git a/packages/table/src/react/components/TableElement/useTableResizable.ts b/packages/table/src/react/components/TableElement/useTableResizable.ts new file mode 100644 index 0000000000..da0916a75c --- /dev/null +++ b/packages/table/src/react/components/TableElement/useTableResizable.ts @@ -0,0 +1,291 @@ +import React from 'react'; + +import { findNodePath, useEditorRef } from '@udecode/plate-common/react'; +import { + type ResizeEvent, + resizeLengthClampStatic, +} from '@udecode/plate-resizable'; + +import { + type TTableElement, + setTableColSize, + setTableMarginLeft, + setTableRowSize, +} from '../../../lib'; +import { + useOverrideColSize, + useOverrideMarginLeft, + useOverrideRowSize, + useTableStore, +} from '../../stores'; +import { roundCellSizeToStep } from '../TableCellElement'; +import { useReSizerOffset } from './useResizerOffset'; + +export type TableElementResizableOptions = { + /** Resize by step instead of by pixel. */ + step?: number; + + /** Overrides for X and Y axes. */ + stepX?: number; + stepY?: number; +}; + +export const useTableResizableState = ({ + step, + stepX = step, + stepY = step, +}: TableElementResizableOptions) => { + const hoveredColIndex = useTableStore().get.hoveredColIndex(); + const hoveredRowIndex = useTableStore().get.hoveredRowIndex(); + + return { + hoveredColIndex, + hoveredRowIndex, + stepX, + stepY, + }; +}; + +export interface TableResizeOptions { + containerRef: React.RefObject; + element: TTableElement; + isSelectingCell: boolean; + minColumnWidth: number; + resizeOffset?: number; +} + +export const useTableResize = ({ + containerRef, + element, + hoveredColIndex, + hoveredRowIndex, + isSelectingCell, + minColumnWidth, + resizeOffset = 2, + stepX, + stepY, +}: ReturnType & TableResizeOptions) => { + const editor = useEditorRef(); + + const overrideColSize = useOverrideColSize(); + const overrideRowSize = useOverrideRowSize(); + const overrideMarginLeft = useOverrideMarginLeft(); + const setHoveredColIndex = useTableStore().set.hoveredColIndex(); + const setHoveredRowIndex = useTableStore().set.hoveredRowIndex(); + + const [currentColIndex, setCurrentColIndex] = React.useState( + null + ); + const [currentRowIndex, setCurrentRowIndex] = React.useState( + null + ); + + const { marginLeft = 0 } = element; + + React.useEffect(() => { + if (isSelectingCell) return; + if (currentColIndex === null && currentRowIndex === null) { + if (hoveredColIndex !== null) { + setCurrentColIndex(hoveredColIndex); + } + if (hoveredRowIndex !== null) { + setCurrentRowIndex(hoveredRowIndex); + } + } + }, [ + currentColIndex, + currentRowIndex, + hoveredColIndex, + hoveredRowIndex, + isSelectingCell, + ]); + + const { + initialColSize, + initialRowSize, + lastValidOffsets, + nextInitialColSize, + } = useReSizerOffset({ + containerRef, + currentColIndex, + currentRowIndex, + marginLeft, + }); + + const resetCol = React.useCallback(() => { + setCurrentColIndex(null); + setHoveredColIndex(null); + }, [setHoveredColIndex]); + + const resetRow = React.useCallback(() => { + setCurrentRowIndex(null); + setHoveredRowIndex(null); + }, [setHoveredRowIndex]); + + const setColSize = React.useCallback( + (colIndex: number, width: number) => { + setTableColSize( + editor, + { colIndex, width }, + { at: findNodePath(editor, element)! } + ); + }, + [editor, element] + ); + + const setRowSize = React.useCallback( + (rowIndex: number, height: number) => { + setTableRowSize( + editor, + { height, rowIndex }, + { at: findNodePath(editor, element)! } + ); + }, + [editor, element] + ); + + const setMarginLeftSize = React.useCallback( + (marginLeft: number) => { + setTableMarginLeft( + editor, + { marginLeft }, + { at: findNodePath(editor, element)! } + ); + }, + [editor, element] + ); + + const handleResizeRight = React.useCallback( + ({ delta, finished }: ResizeEvent) => { + if (currentColIndex === null) return; + if (initialColSize + delta < minColumnWidth) return; + + const complement = (width: number) => + initialColSize + nextInitialColSize - width; + + const currentNew = roundCellSizeToStep( + resizeLengthClampStatic(initialColSize + delta, { + max: nextInitialColSize ? complement(minColumnWidth) : undefined, + min: minColumnWidth, + }), + stepX + ); + + const nextNew = nextInitialColSize ? complement(currentNew) : undefined; + + const fn = finished ? setColSize : overrideColSize; + fn(currentColIndex, currentNew); + + if (nextNew) fn(currentColIndex + 1, nextNew); + }, + [ + currentColIndex, + initialColSize, + minColumnWidth, + nextInitialColSize, + overrideColSize, + setColSize, + stepX, + ] + ); + + const handleResizeRow = React.useCallback( + ({ delta, finished }: ResizeEvent) => { + if (currentRowIndex === null) return; + + const newHeight = roundCellSizeToStep(initialRowSize + delta, stepY); + + if (finished) { + setRowSize(currentRowIndex, newHeight); + } else { + overrideRowSize(currentRowIndex, newHeight); + } + }, + [currentRowIndex, initialRowSize, stepY, setRowSize, overrideRowSize] + ); + + const handleResizeLeft = React.useCallback( + ({ delta, finished }: ResizeEvent) => { + if (currentColIndex === null) return; + + const complement = (width: number) => initialColSize + marginLeft - width; + + const newMargin = roundCellSizeToStep( + resizeLengthClampStatic(marginLeft + delta, { + max: complement(minColumnWidth), + min: 0, + }), + stepX + ); + + const newWidth = complement(newMargin); + + if (finished) { + setMarginLeftSize(newMargin); + setColSize(currentColIndex + 1, newWidth); + } else { + overrideMarginLeft(newMargin); + overrideColSize(currentColIndex + 1, newWidth); + } + }, + [ + currentColIndex, + initialColSize, + marginLeft, + minColumnWidth, + overrideColSize, + overrideMarginLeft, + setColSize, + setMarginLeftSize, + stepX, + ] + ); + + const resizeHandleProps = { + bottomProps: { + options: { + direction: 'bottom' as const, + onHoverEnd: resetRow, + onResize: handleResizeRow, + }, + style: { + top: lastValidOffsets.current.row - resizeOffset, + }, + }, + leftProps: { + options: { + direction: 'left' as const, + onHoverEnd: () => { + if (currentColIndex !== -1) return; + + resetCol(); + }, + onResize: handleResizeLeft, + }, + style: { + left: lastValidOffsets.current.colLeft, + }, + }, + + rightProps: { + options: { + direction: 'right' as const, + onHoverEnd: resetCol, + onResize: handleResizeRight, + }, + style: { + left: lastValidOffsets.current.col - resizeOffset, + }, + }, + }; + + return { + currentColIndex, + currentRowIndex, + resizeHandleProps, + resizeStyle: { + height: containerRef.current?.querySelector('table')?.clientHeight, + width: '100%', + }, + }; +}; diff --git a/packages/table/src/react/components/TableElement/useTableRowSizes.ts b/packages/table/src/react/components/TableElement/useTableRowSizes.ts new file mode 100644 index 0000000000..737755d05b --- /dev/null +++ b/packages/table/src/react/components/TableElement/useTableRowSizes.ts @@ -0,0 +1,16 @@ +import { type TTableElement, getTableOverriddenRowSizes } from '../../../lib'; +import { useTableStore } from '../../stores'; + +export const useTableRowSizes = ( + tableNode: TTableElement, + { disableOverrides = false } = {} +): number[] => { + const rowSizeOverrides = useTableStore().get.rowSizeOverrides(); + + const overriddenRowSizes = getTableOverriddenRowSizes( + tableNode, + disableOverrides ? undefined : rowSizeOverrides + ); + + return overriddenRowSizes; +}; diff --git a/packages/table/src/react/stores/tableStore.ts b/packages/table/src/react/stores/tableStore.ts index 374682fccf..e41ea04166 100644 --- a/packages/table/src/react/stores/tableStore.ts +++ b/packages/table/src/react/stores/tableStore.ts @@ -10,6 +10,7 @@ export const { TableProvider, tableStore, useTableStore } = createAtomStore( { colSizeOverrides: atom(new Map() as TableStoreSizeOverrides), hoveredColIndex: null as number | null, + hoveredRowIndex: null as number | null, marginLeftOverride: null as number | null, rowSizeOverrides: atom(new Map() as TableStoreSizeOverrides), selectedCells: null as TElement[] | null,