diff --git a/packages/core/src/api/nodeConversions/blockToNode.ts b/packages/core/src/api/nodeConversions/blockToNode.ts index ec2c4582c..7ee9270ff 100644 --- a/packages/core/src/api/nodeConversions/blockToNode.ts +++ b/packages/core/src/api/nodeConversions/blockToNode.ts @@ -178,11 +178,7 @@ export function tableContentToNodes< // The colwidth array should have multiple values when the colspan of // a cell is greater than 1. However, this is not yet implemented so // we can always assume a length of 1. - colwidth: [ - tableContent.columnWidths - ? tableContent.columnWidths[i] || 100 - : 100, - ], + colwidth: [tableContent.columnWidths?.[i] || null], }, pNode ); diff --git a/packages/core/src/api/nodeConversions/nodeToBlock.ts b/packages/core/src/api/nodeConversions/nodeToBlock.ts index 45c6611be..780523947 100644 --- a/packages/core/src/api/nodeConversions/nodeToBlock.ts +++ b/packages/core/src/api/nodeConversions/nodeToBlock.ts @@ -44,7 +44,7 @@ export function contentNodeToTableContent< // The colwidth array should have multiple values when the colspan of a // cell is greater than 1. However, this is not yet implemented so we // can always assume a length of 1. - ret.columnWidths.push(cellNode.attrs.colwidth[0] || 100); + ret.columnWidths.push(cellNode.attrs.colwidth[0] || undefined); }); } diff --git a/packages/core/src/editor/editor.css b/packages/core/src/editor/editor.css index 17861efd3..e2a121a54 100644 --- a/packages/core/src/editor/editor.css +++ b/packages/core/src/editor/editor.css @@ -106,6 +106,7 @@ Tippy popups that are appended to document.body directly /* table related: */ .bn-editor table { + /* TODO: Do we want this? */ width: auto !important; margin-bottom: 2em; } diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index fbe0fb7fa..0a68cd7f1 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -2,7 +2,7 @@ import { Plugin, PluginKey, PluginView } from "prosemirror-state"; import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; import { nodeToBlock } from "../../api/nodeConversions/nodeToBlock.js"; import { checkBlockIsDefaultType } from "../../blocks/defaultBlockTypeGuards.js"; -import { Block, DefaultBlockSchema } from "../../blocks/defaultBlocks.js"; +import { DefaultBlockSchema } from "../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import { BlockFromConfigNoChildren, @@ -20,6 +20,8 @@ export type TableHandlesState< S extends StyleSchema > = { show: boolean; + showExtendButtonRow: boolean; + showExtendButtonCol: boolean; referencePosCell: DOMRect; referencePosTable: DOMRect; @@ -164,6 +166,8 @@ export class TableHandlesView< if (this.state?.show) { this.state.show = false; + this.state.showExtendButtonRow = false; + this.state.showExtendButtonCol = false; this.emitUpdate(); } } @@ -177,6 +181,8 @@ export class TableHandlesView< if (!target || !this.editor.isEditable) { if (this.state?.show) { this.state.show = false; + this.state.showExtendButtonRow = false; + this.state.showExtendButtonCol = false; this.emitUpdate(); } return; @@ -198,7 +204,9 @@ export class TableHandlesView< } this.tableElement = blockEl.node; - let tableBlock: Block | undefined = undefined; + let tableBlock: + | BlockFromConfigNoChildren + | undefined; // Copied from `getBlock`. We don't use `getBlock` since we also need the PM // node for the table, so we would effectively be doing the same work twice. @@ -245,6 +253,9 @@ export class TableHandlesView< this.state = { show: true, + showExtendButtonRow: + colIndex === tableBlock.content.rows[0].cells.length - 1, + showExtendButtonCol: rowIndex === tableBlock.content.rows.length - 1, referencePosCell: cellRect, referencePosTable: tableRect, @@ -418,30 +429,32 @@ export class TableHandlesView< return; } - // TODO: Can also just do down the DOM tree until we find it but this seems - // cleaner. const tableBody = this.tableElement!.querySelector("tbody"); if (!tableBody) { return; } + // If rows or columns are deleted in the update, the hovered indices for + // those may now be out of bounds. If this is the case, they are moved to + // the new last row or column. if (this.state.rowIndex >= tableBody.children.length) { this.state.rowIndex = tableBody.children.length - 1; this.emitUpdate(); return; } - const row = tableBody.children[this.state.rowIndex]; - if (this.state.colIndex >= tableBody.children[0].children.length) { this.state.colIndex = tableBody.children[0].children.length - 1; this.emitUpdate(); return; } + + const row = tableBody.children[this.state.rowIndex]; const cell = row.children[this.state.colIndex]; // TODO: Check if DOMRects changed first? + this.state.block = this.editor.getBlock(this.state.block.id)!; this.state.referencePosCell = cell.getBoundingClientRect(); this.state.referencePosTable = tableBody.getBoundingClientRect(); this.emitUpdate(); diff --git a/packages/core/src/schema/blocks/types.ts b/packages/core/src/schema/blocks/types.ts index 34565f1c6..a5e20ecdd 100644 --- a/packages/core/src/schema/blocks/types.ts +++ b/packages/core/src/schema/blocks/types.ts @@ -149,7 +149,7 @@ export type TableContent< S extends StyleSchema = StyleSchema > = { type: "tableContent"; - columnWidths: number[]; + columnWidths: (number | undefined)[]; rows: { cells: InlineContent[][]; }[]; diff --git a/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx b/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx index 4acdc3bf5..93bcede76 100644 --- a/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx +++ b/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx @@ -1,4 +1,6 @@ import { + BlockFromConfigNoChildren, + DefaultBlockSchema, DefaultInlineContentSchema, DefaultStyleSchema, InlineContentSchema, @@ -9,6 +11,7 @@ import { import { MouseEvent as ReactMouseEvent, ReactNode, + useCallback, useEffect, useState, } from "react"; @@ -17,6 +20,21 @@ import { RiAddFill } from "react-icons/ri"; import { useComponentsContext } from "../../../editor/ComponentsContext.js"; import { ExtendButtonProps } from "./ExtendButtonProps.js"; +// Rounds a number up or down, depending on whether the value past the decimal +// point is above or below a certain fraction. If no fraction is provided, it +// behaves like Math.round. +const roundUpAt = (num: number, fraction = 0.5) => { + if (fraction <= 0 || fraction >= 100) { + throw new Error("Percentage must be between 0 and 1"); + } + + if (num < fraction) { + return Math.floor(num); + } + + return Math.ceil(num); +}; + const getContentWithAddedRows = < I extends InlineContentSchema, S extends StyleSchema @@ -34,9 +52,7 @@ const getContentWithAddedRows = < return { type: "tableContent", - columnWidths: content.columnWidths - ? [...content.columnWidths, ...newRows.map(() => undefined)] - : undefined, + columnWidths: content.columnWidths, rows: [...content.rows, ...newRows], }; }; @@ -57,7 +73,9 @@ const getContentWithAddedCols = < return { type: "tableContent", - columnWidths: content.columnWidths || undefined, + columnWidths: content.columnWidths + ? [...content.columnWidths, ...newCells.map(() => undefined)] + : undefined, rows: content.rows.map((row) => ({ cells: [...row.cells, ...newCells], })), @@ -73,22 +91,23 @@ export const ExtendButton = < const Components = useComponentsContext()!; const [editingState, setEditingState] = useState<{ + originalBlock: BlockFromConfigNoChildren; startPos: number; - numOriginalCells: number; clickOnly: boolean; } | null>(null); - const mouseDownHandler = (event: ReactMouseEvent) => { - props.freezeHandles(); - setEditingState({ - startPos: props.orientation === "row" ? event.clientX : event.clientY, - numOriginalCells: - props.orientation === "row" - ? props.block.content.rows[0].cells.length - : props.block.content.rows.length, - clickOnly: true, - }); - }; + // Lets the user start extending columns/rows by moving the mouse. + const mouseDownHandler = useCallback( + (event: ReactMouseEvent) => { + props.freezeHandles(); + setEditingState({ + originalBlock: props.block, + startPos: props.orientation === "row" ? event.clientX : event.clientY, + clickOnly: true, + }); + }, + [props] + ); // Extends columns/rows on when moving the mouse. useEffect(() => { @@ -101,35 +120,36 @@ export const ExtendButton = < (props.orientation === "row" ? event.clientX : event.clientY) - editingState.startPos; - const numCells = - editingState.numOriginalCells + - Math.floor(diff / (props.orientation === "row" ? 100 : 31)); - const block = props.editor.getBlock(props.block)!; - const numCurrentCells = + const numOriginalCells = + props.orientation === "row" + ? editingState.originalBlock.content.rows[0].cells.length + : editingState.originalBlock.content.rows.length; + const oldNumCells = props.orientation === "row" - ? block.content.rows[0].cells.length - : block.content.rows.length; + ? props.block.content.rows[0].cells.length + : props.block.content.rows.length; + const newNumCells = + numOriginalCells + + roundUpAt(diff / (props.orientation === "row" ? 100 : 31), 0.3); - if ( - editingState.numOriginalCells <= numCells && - numCells !== numCurrentCells - ) { + if (numOriginalCells <= newNumCells && newNumCells !== oldNumCells) { props.editor.updateBlock(props.block, { type: "table", content: props.orientation === "row" ? getContentWithAddedCols( - props.block.content, - numCells - editingState.numOriginalCells + editingState.originalBlock.content, + newNumCells - numOriginalCells ) : getContentWithAddedRows( - props.block.content, - numCells - editingState.numOriginalCells + editingState.originalBlock.content, + newNumCells - numOriginalCells ), }); + // Edge case for updating block content as `updateBlock` causes the // selection to move into the next block, so we have to set it back. - if (block.content) { + if (editingState.originalBlock.content) { props.editor.setTextCursorPosition(props.block); } setEditingState({ ...editingState, clickOnly: false }); @@ -153,8 +173,8 @@ export const ExtendButton = < type: "table", content: props.orientation === "row" - ? getContentWithAddedCols(props.block.content) - : getContentWithAddedRows(props.block.content), + ? getContentWithAddedCols(editingState.originalBlock.content) + : getContentWithAddedRows(editingState.originalBlock.content), }); } @@ -167,12 +187,7 @@ export const ExtendButton = < return () => { document.body.removeEventListener("mouseup", callback); }; - }, [ - editingState?.clickOnly, - getContentWithAddedCols, - getContentWithAddedRows, - props, - ]); + }, [editingState?.clickOnly, editingState?.originalBlock.content, props]); return ( { const cells = [...row.cells]; diff --git a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/DeleteButton.tsx b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/DeleteButton.tsx index 8e6f776f7..dc71823af 100644 --- a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/DeleteButton.tsx +++ b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/DeleteButton.tsx @@ -31,9 +31,7 @@ export const DeleteRowButton = < onClick={() => { const content: PartialTableContent = { type: "tableContent", - columnWidths: props.block.content.columnWidths.filter( - (_, index) => index !== props.index - ), + columnWidths: props.block.content.columnWidths, rows: props.block.content.rows.filter( (_, index) => index !== props.index ), diff --git a/packages/react/src/components/TableHandles/TableHandlesController.tsx b/packages/react/src/components/TableHandles/TableHandlesController.tsx index ee43dd127..223a88857 100644 --- a/packages/react/src/components/TableHandles/TableHandlesController.tsx +++ b/packages/react/src/components/TableHandles/TableHandlesController.tsx @@ -8,10 +8,8 @@ import { import { FC, useMemo, useState } from "react"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js"; -import { - useExtendButtonsPositioning, - useTableHandlesPositioning, -} from "./hooks/useTableHandlesPositioning.js"; +import { useExtendButtonsPositioning } from "./hooks/useExtendButtonsPositioning.js"; +import { useTableHandlesPositioning } from "./hooks/useTableHandlesPositioning.js"; import { useUIPluginState } from "../../hooks/useUIPluginState.js"; import { ExtendButton } from "./ExtendButton/ExtendButton.js"; import { TableHandle } from "./TableHandle.js"; @@ -71,7 +69,8 @@ export const TableHandlesController = < ); const { rowExtendButton, colExtendButton } = useExtendButtonsPositioning( - state?.show || false, + state?.showExtendButtonRow || false, + state?.showExtendButtonCol || false, state?.referencePosCell || null, state?.referencePosTable || null, draggingState diff --git a/packages/react/src/components/TableHandles/hooks/useExtendButtonsPositioning.ts b/packages/react/src/components/TableHandles/hooks/useExtendButtonsPositioning.ts new file mode 100644 index 000000000..deb70251a --- /dev/null +++ b/packages/react/src/components/TableHandles/hooks/useExtendButtonsPositioning.ts @@ -0,0 +1,102 @@ +import { size, useFloating, useTransitionStyles } from "@floating-ui/react"; +import { useEffect, useMemo } from "react"; + +function useExtendButtonPosition( + orientation: "row" | "col", + show: boolean, + referencePosCell: DOMRect | null, + referencePosTable: DOMRect | null, + draggingState?: { + draggedCellOrientation: "row" | "col"; + mousePos: number; + } +) { + const { refs, update, context, floatingStyles } = useFloating({ + open: show, + placement: orientation === "row" ? "right" : "bottom", + middleware: [ + size({ + apply({ rects, elements }) { + Object.assign( + elements.floating.style, + orientation === "row" + ? { + height: `${rects.reference.height}px`, + } + : { + width: `${rects.reference.width}px`, + } + ); + }, + }), + ], + }); + + const { isMounted, styles } = useTransitionStyles(context); + + useEffect(() => { + update(); + }, [referencePosCell, referencePosTable, update]); + + useEffect(() => { + // Will be null on initial render when used in UI component controllers. + if (referencePosCell === null || referencePosTable === null) { + return; + } + + refs.setReference({ + getBoundingClientRect: () => referencePosTable, + }); + }, [draggingState, orientation, referencePosCell, referencePosTable, refs]); + + return useMemo( + () => ({ + isMounted: isMounted, + ref: refs.setFloating, + style: { + display: "flex", + ...styles, + ...floatingStyles, + zIndex: 10000, + }, + }), + [floatingStyles, isMounted, refs.setFloating, styles] + ); +} + +export function useExtendButtonsPositioning( + showExtendButtonRow: boolean, + showExtendButtonCol: boolean, + referencePosCell: DOMRect | null, + referencePosTable: DOMRect | null, + draggingState?: { + draggedCellOrientation: "row" | "col"; + mousePos: number; + } +): { + rowExtendButton: ReturnType; + colExtendButton: ReturnType; +} { + const rowExtendButton = useExtendButtonPosition( + "row", + showExtendButtonRow, + referencePosCell, + referencePosTable, + draggingState + ); + const colExtendButton = useExtendButtonPosition( + "col", + showExtendButtonCol, + referencePosCell, + referencePosTable, + draggingState + ); + + return useMemo( + () => ({ + rowExtendButton, + colExtendButton, + }), + [colExtendButton, rowExtendButton] + ); +} diff --git a/packages/react/src/components/TableHandles/hooks/useTableHandlesPositioning.ts b/packages/react/src/components/TableHandles/hooks/useTableHandlesPositioning.ts index 80284e53c..4bd7083fd 100644 --- a/packages/react/src/components/TableHandles/hooks/useTableHandlesPositioning.ts +++ b/packages/react/src/components/TableHandles/hooks/useTableHandlesPositioning.ts @@ -1,9 +1,4 @@ -import { - offset, - size, - useFloating, - useTransitionStyles, -} from "@floating-ui/react"; +import { offset, useFloating, useTransitionStyles } from "@floating-ui/react"; import { useEffect, useMemo } from "react"; function getBoundingClientRectRow( @@ -145,102 +140,3 @@ export function useTableHandlesPositioning( [colHandle, rowHandle] ); } - -function useExtendButtonPosition( - orientation: "row" | "col", - show: boolean, - referencePosCell: DOMRect | null, - referencePosTable: DOMRect | null, - draggingState?: { - draggedCellOrientation: "row" | "col"; - mousePos: number; - } -) { - const { refs, update, context, floatingStyles } = useFloating({ - open: show, - placement: orientation === "row" ? "right" : "bottom", - middleware: [ - size({ - apply({ rects, elements }) { - Object.assign( - elements.floating.style, - orientation === "row" - ? { - height: `${rects.reference.height}px`, - } - : { - width: `${rects.reference.width}px`, - } - ); - }, - }), - ], - }); - - const { isMounted, styles } = useTransitionStyles(context); - - useEffect(() => { - update(); - }, [referencePosCell, referencePosTable, update]); - - useEffect(() => { - // Will be null on initial render when used in UI component controllers. - if (referencePosCell === null || referencePosTable === null) { - return; - } - - refs.setReference({ - getBoundingClientRect: () => referencePosTable, - }); - }, [draggingState, orientation, referencePosCell, referencePosTable, refs]); - - return useMemo( - () => ({ - isMounted: isMounted, - ref: refs.setFloating, - style: { - display: "flex", - ...styles, - ...floatingStyles, - zIndex: 10000, - }, - }), - [floatingStyles, isMounted, refs.setFloating, styles] - ); -} - -export function useExtendButtonsPositioning( - show: boolean, - referencePosCell: DOMRect | null, - referencePosTable: DOMRect | null, - draggingState?: { - draggedCellOrientation: "row" | "col"; - mousePos: number; - } -): { - rowExtendButton: ReturnType; - colExtendButton: ReturnType; -} { - const rowExtendButton = useExtendButtonPosition( - "row", - show, - referencePosCell, - referencePosTable, - draggingState - ); - const colExtendButton = useExtendButtonPosition( - "col", - show, - referencePosCell, - referencePosTable, - draggingState - ); - - return useMemo( - () => ({ - rowExtendButton, - colExtendButton, - }), - [colExtendButton, rowExtendButton] - ); -} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 9e343458f..713325abf 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -70,6 +70,9 @@ export * from "./components/FilePanel/FilePanelProps.js"; export * from "./components/TableHandles/TableHandle.js"; export * from "./components/TableHandles/TableHandleProps.js"; export * from "./components/TableHandles/TableHandlesController.js"; +export * from "./components/TableHandles/ExtendButton/ExtendButton.js"; +export * from "./components/TableHandles/ExtendButton/ExtendButtonProps.js"; +export * from "./components/TableHandles/hooks/useExtendButtonsPositioning.js"; export * from "./components/TableHandles/hooks/useTableHandlesPositioning.js"; export * from "./components/TableHandles/TableHandleMenu/DefaultButtons/AddButton.js";