From ca7552306e216ea70e7be456dc9bcb1b35d82b5a Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Mon, 21 Oct 2024 17:30:16 +0200 Subject: [PATCH 01/15] Added buttons to extend table rows/columns --- packages/ariakit/src/index.tsx | 5 + .../src/api/nodeConversions/blockToNode.ts | 17 +- .../src/api/nodeConversions/nodeToBlock.ts | 12 +- .../TableBlockContent/TableExtension.ts | 27 +++ packages/core/src/editor/editor.css | 5 +- .../TableHandles/TableHandlesPlugin.ts | 37 ++++ packages/core/src/schema/blocks/types.ts | 2 + packages/mantine/src/index.tsx | 5 + .../ExtendButton/ExtendButton.tsx | 181 ++++++++++++++++++ .../DefaultButtons/AddButton.tsx | 4 +- .../DefaultButtons/DeleteButton.tsx | 6 +- .../TableHandles/TableHandlesController.tsx | 48 ++++- .../hooks/useTableHandlesPositioning.ts | 106 +++++++++- .../react/src/editor/ComponentsContext.tsx | 5 + packages/shadcn/src/index.tsx | 5 + 15 files changed, 448 insertions(+), 17 deletions(-) create mode 100644 packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx diff --git a/packages/ariakit/src/index.tsx b/packages/ariakit/src/index.tsx index d651dde9a..cf000eec3 100644 --- a/packages/ariakit/src/index.tsx +++ b/packages/ariakit/src/index.tsx @@ -81,6 +81,11 @@ export const components: Components = { }, TableHandle: { Root: TableHandle, + ExtendButton: (props) => ( + + ), }, Generic: { Form: { diff --git a/packages/core/src/api/nodeConversions/blockToNode.ts b/packages/core/src/api/nodeConversions/blockToNode.ts index f63ee0997..ec2c4582c 100644 --- a/packages/core/src/api/nodeConversions/blockToNode.ts +++ b/packages/core/src/api/nodeConversions/blockToNode.ts @@ -161,7 +161,8 @@ export function tableContentToNodes< for (const row of tableContent.rows) { const columnNodes: Node[] = []; - for (const cell of row.cells) { + for (let i = 0; i < row.cells.length; i++) { + const cell = row.cells[i]; let pNode: Node; if (!cell) { pNode = schema.nodes["tableParagraph"].create({}); @@ -172,7 +173,19 @@ export function tableContentToNodes< pNode = schema.nodes["tableParagraph"].create({}, textNodes); } - const cellNode = schema.nodes["tableCell"].create({}, pNode); + const cellNode = schema.nodes["tableCell"].create( + { + // 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, + ], + }, + pNode + ); columnNodes.push(cellNode); } const rowNode = schema.nodes["tableRow"].create({}, columnNodes); diff --git a/packages/core/src/api/nodeConversions/nodeToBlock.ts b/packages/core/src/api/nodeConversions/nodeToBlock.ts index 55cfa045a..45c6611be 100644 --- a/packages/core/src/api/nodeConversions/nodeToBlock.ts +++ b/packages/core/src/api/nodeConversions/nodeToBlock.ts @@ -30,14 +30,24 @@ export function contentNodeToTableContent< >(contentNode: Node, inlineContentSchema: I, styleSchema: S) { const ret: TableContent = { type: "tableContent", + columnWidths: [], rows: [], }; - contentNode.content.forEach((rowNode) => { + contentNode.content.forEach((rowNode, _offset, index) => { const row: TableContent["rows"][0] = { cells: [], }; + if (index === 0) { + rowNode.content.forEach((cellNode) => { + // 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); + }); + } + rowNode.content.forEach((cellNode) => { row.cells.push( contentNodeToInlineContent( diff --git a/packages/core/src/blocks/TableBlockContent/TableExtension.ts b/packages/core/src/blocks/TableBlockContent/TableExtension.ts index fb0147d7a..23e767705 100644 --- a/packages/core/src/blocks/TableBlockContent/TableExtension.ts +++ b/packages/core/src/blocks/TableBlockContent/TableExtension.ts @@ -1,13 +1,40 @@ import { callOrReturn, Extension, getExtensionField } from "@tiptap/core"; import { columnResizing, tableEditing } from "prosemirror-tables"; +import { TableView } from "prosemirror-tables"; +import { Node as PMNode } from "prosemirror-model"; +import { mergeCSSClasses } from "../../util/browser.js"; export const TableExtension = Extension.create({ name: "BlockNoteTableExtension", addProseMirrorPlugins: () => { + class CustomTableView extends TableView { + constructor(public node: PMNode, public cellMinWidth: number) { + super(node, cellMinWidth); + + const blockContent = document.createElement("div"); + blockContent.className = mergeCSSClasses( + "bn-block-content" + // blockContentHTMLAttributes.class + ); + blockContent.setAttribute("data-content-type", "table"); + // for (const [attribute, value] of Object.entries(blockContentHTMLAttributes)) { + // if (attribute !== "class") { + // blockContent.setAttribute(attribute, value); + // } + // } + + const tableWrapper = this.dom; + blockContent.appendChild(tableWrapper); + + this.dom = blockContent; + } + } + return [ columnResizing({ cellMinWidth: 100, + View: CustomTableView, }), tableEditing(), ]; diff --git a/packages/core/src/editor/editor.css b/packages/core/src/editor/editor.css index f2040b7db..17861efd3 100644 --- a/packages/core/src/editor/editor.css +++ b/packages/core/src/editor/editor.css @@ -107,6 +107,7 @@ Tippy popups that are appended to document.body directly /* table related: */ .bn-editor table { width: auto !important; + margin-bottom: 2em; } .bn-editor th, .bn-editor td { @@ -115,10 +116,6 @@ Tippy popups that are appended to document.body directly padding: 3px 5px; } -.bn-editor .tableWrapper { - margin: 1em 0; -} - .bn-editor th { font-weight: bold; text-align: left; diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index b72c76b93..fbe0fb7fa 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -104,6 +104,7 @@ export class TableHandlesView< public tableId: string | undefined; public tablePos: number | undefined; + public tableElement: HTMLElement | undefined; public menuFrozen = false; @@ -195,6 +196,7 @@ export class TableHandlesView< if (!blockEl) { return; } + this.tableElement = blockEl.node; let tableBlock: Block | undefined = undefined; @@ -410,6 +412,41 @@ export class TableHandlesView< } }; + // Updates drag handle positions on table content updates. + update() { + if (!this.state || !this.state.show) { + 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 (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 cell = row.children[this.state.colIndex]; + + // TODO: Check if DOMRects changed first? + this.state.referencePosCell = cell.getBoundingClientRect(); + this.state.referencePosTable = tableBody.getBoundingClientRect(); + this.emitUpdate(); + } + destroy() { this.pmView.dom.removeEventListener("mousemove", this.mouseMoveHandler); this.pmView.root.removeEventListener( diff --git a/packages/core/src/schema/blocks/types.ts b/packages/core/src/schema/blocks/types.ts index 7fc70ea88..34565f1c6 100644 --- a/packages/core/src/schema/blocks/types.ts +++ b/packages/core/src/schema/blocks/types.ts @@ -149,6 +149,7 @@ export type TableContent< S extends StyleSchema = StyleSchema > = { type: "tableContent"; + columnWidths: number[]; rows: { cells: InlineContent[][]; }[]; @@ -224,6 +225,7 @@ export type PartialTableContent< S extends StyleSchema = StyleSchema > = { type: "tableContent"; + columnWidths?: (number | undefined)[]; rows: { cells: PartialInlineContent[]; }[]; diff --git a/packages/mantine/src/index.tsx b/packages/mantine/src/index.tsx index 3a3799eae..da1925674 100644 --- a/packages/mantine/src/index.tsx +++ b/packages/mantine/src/index.tsx @@ -90,6 +90,11 @@ export const components: Components = { }, TableHandle: { Root: TableHandle, + ExtendButton: (props) => ( + + ), }, Generic: { Form: { diff --git a/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx b/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx new file mode 100644 index 000000000..6d934073d --- /dev/null +++ b/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx @@ -0,0 +1,181 @@ +import { + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + PartialTableContent, + StyleSchema, +} from "@blocknote/core"; +import { MouseEvent as ReactMouseEvent, useEffect, useState } from "react"; + +import { useComponentsContext } from "../../../editor/ComponentsContext.js"; +import { TableHandleProps } from "../TableHandleProps.js"; + +const getContentWithAddedRows = < + I extends InlineContentSchema, + S extends StyleSchema +>( + content: PartialTableContent, + rowsToAdd = 1 +): PartialTableContent => { + const newRow: PartialTableContent["rows"][number] = { + cells: content.rows[0].cells.map(() => []), + }; + const newRows: PartialTableContent["rows"] = []; + for (let i = 0; i < rowsToAdd; i++) { + newRows.push(newRow); + } + + return { + type: "tableContent", + columnWidths: content.columnWidths + ? [...content.columnWidths, ...newRows.map(() => undefined)] + : undefined, + rows: [...content.rows, ...newRows], + }; +}; + +const getContentWithAddedCols = < + I extends InlineContentSchema, + S extends StyleSchema +>( + content: PartialTableContent, + colsToAdd = 1 +): PartialTableContent => { + const newCell: PartialTableContent["rows"][number]["cells"][number] = + []; + const newCells: PartialTableContent["rows"][number]["cells"] = []; + for (let i = 0; i < colsToAdd; i++) { + newCells.push(newCell); + } + + return { + type: "tableContent", + columnWidths: content.columnWidths || undefined, + rows: content.rows.map((row) => ({ + cells: [...row.cells, ...newCells], + })), + }; +}; + +export const ExtendButton = < + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +>( + props: Pick< + TableHandleProps, + "block" | "editor" | "orientation" | "freezeHandles" | "unfreezeHandles" + > +) => { + const Components = useComponentsContext()!; + + const [editingState, setEditingState] = useState<{ + 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, + }); + }; + + // Extends columns/rows on when moving the mouse. + useEffect(() => { + const callback = (event: MouseEvent) => { + if (editingState === null) { + return; + } + + const diff = + (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 = + props.orientation === "row" + ? block.content.rows[0].cells.length + : block.content.rows.length; + + if ( + editingState.numOriginalCells <= numCells && + numCells !== numCurrentCells + ) { + props.editor.updateBlock(props.block, { + type: "table", + content: + props.orientation === "row" + ? getContentWithAddedCols( + props.block.content, + numCells - editingState.numOriginalCells + ) + : getContentWithAddedRows( + props.block.content, + numCells - editingState.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) { + props.editor.setTextCursorPosition(props.block); + } + setEditingState({ ...editingState, clickOnly: false }); + } + }; + + document.body.addEventListener("mousemove", callback); + + return () => { + document.body.removeEventListener("mousemove", callback); + }; + }, [editingState, props.block, props.editor, props.orientation]); + + // Stops mouse movements from extending columns/rows when the mouse is + // released. Also extends columns/rows by 1 if the mouse wasn't moved enough + // to add any, imitating a click. + useEffect(() => { + const callback = () => { + if (editingState?.clickOnly) { + props.editor.updateBlock(props.block, { + type: "table", + content: + props.orientation === "row" + ? getContentWithAddedCols(props.block.content) + : getContentWithAddedRows(props.block.content), + }); + } + + setEditingState(null); + props.unfreezeHandles(); + }; + + document.body.addEventListener("mouseup", callback); + + return () => { + document.body.removeEventListener("mouseup", callback); + }; + }, [ + editingState?.clickOnly, + getContentWithAddedCols, + getContentWithAddedRows, + props, + ]); + + return ( + { + event.preventDefault(); + }} + onMouseDown={mouseDownHandler} + /> + ); +}; diff --git a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/AddButton.tsx b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/AddButton.tsx index 5f86a6839..b54a4438c 100644 --- a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/AddButton.tsx +++ b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/AddButton.tsx @@ -3,8 +3,8 @@ import { DefaultInlineContentSchema, DefaultStyleSchema, InlineContentSchema, + PartialTableContent, StyleSchema, - TableContent, } from "@blocknote/core"; import { useComponentsContext } from "../../../../editor/ComponentsContext.js"; @@ -73,7 +73,7 @@ export const AddColumnButton = < return ( { - const content: TableContent = { + const content: PartialTableContent = { type: "tableContent", rows: props.block.content.rows.map((row) => { 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 f5bd9ae7c..301017f23 100644 --- a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/DeleteButton.tsx +++ b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/DeleteButton.tsx @@ -3,8 +3,8 @@ import { DefaultInlineContentSchema, DefaultStyleSchema, InlineContentSchema, + PartialTableContent, StyleSchema, - TableContent, } from "@blocknote/core"; import { useComponentsContext } from "../../../../editor/ComponentsContext.js"; @@ -29,7 +29,7 @@ export const DeleteRowButton = < return ( { - const content: TableContent = { + const content: PartialTableContent = { type: "tableContent", rows: props.block.content.rows.filter( (_, index) => index !== props.index @@ -68,7 +68,7 @@ export const DeleteColumnButton = < return ( { - const content: TableContent = { + const content: PartialTableContent = { type: "tableContent", rows: props.block.content.rows.map((row) => ({ cells: row.cells.filter((_, index) => index !== props.index), diff --git a/packages/react/src/components/TableHandles/TableHandlesController.tsx b/packages/react/src/components/TableHandles/TableHandlesController.tsx index fac165485..ee43dd127 100644 --- a/packages/react/src/components/TableHandles/TableHandlesController.tsx +++ b/packages/react/src/components/TableHandles/TableHandlesController.tsx @@ -8,16 +8,26 @@ import { import { FC, useMemo, useState } from "react"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js"; +import { + useExtendButtonsPositioning, + useTableHandlesPositioning, +} from "./hooks/useTableHandlesPositioning.js"; import { useUIPluginState } from "../../hooks/useUIPluginState.js"; +import { ExtendButton } from "./ExtendButton/ExtendButton.js"; import { TableHandle } from "./TableHandle.js"; import { TableHandleProps } from "./TableHandleProps.js"; -import { useTableHandlesPositioning } from "./hooks/useTableHandlesPositioning.js"; export const TableHandlesController = < I extends InlineContentSchema = DefaultInlineContentSchema, S extends StyleSchema = DefaultStyleSchema >(props: { tableHandle?: FC>; + extendButton?: FC< + Pick< + TableHandleProps, + "block" | "editor" | "orientation" | "freezeHandles" | "unfreezeHandles" + > + >; }) => { const editor = useBlockNoteEditor(); @@ -60,6 +70,13 @@ export const TableHandlesController = < draggingState ); + const { rowExtendButton, colExtendButton } = useExtendButtonsPositioning( + state?.show || false, + state?.referencePosCell || null, + state?.referencePosTable || null, + draggingState + ); + const [hideRow, setHideRow] = useState(false); const [hideCol, setHideCol] = useState(false); @@ -67,13 +84,14 @@ export const TableHandlesController = < return null; } - const Component = props.tableHandle || TableHandle; + const TableHandleComponent = props.tableHandle || TableHandle; + const ExtendButtonComponent = props.extendButton || ExtendButton; return ( <> {!hideRow && (
- - setHideRow(false)} @@ -104,6 +122,28 @@ export const TableHandlesController = < />
)} + {!draggingState && !hideCol && !hideRow && ( + <> +
+ +
+
+ +
+ + )} ); }; diff --git a/packages/react/src/components/TableHandles/hooks/useTableHandlesPositioning.ts b/packages/react/src/components/TableHandles/hooks/useTableHandlesPositioning.ts index 4bd7083fd..80284e53c 100644 --- a/packages/react/src/components/TableHandles/hooks/useTableHandlesPositioning.ts +++ b/packages/react/src/components/TableHandles/hooks/useTableHandlesPositioning.ts @@ -1,4 +1,9 @@ -import { offset, useFloating, useTransitionStyles } from "@floating-ui/react"; +import { + offset, + size, + useFloating, + useTransitionStyles, +} from "@floating-ui/react"; import { useEffect, useMemo } from "react"; function getBoundingClientRectRow( @@ -140,3 +145,102 @@ 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/editor/ComponentsContext.tsx b/packages/react/src/editor/ComponentsContext.tsx index 735d36f29..d79cf1dd1 100644 --- a/packages/react/src/editor/ComponentsContext.tsx +++ b/packages/react/src/editor/ComponentsContext.tsx @@ -183,6 +183,11 @@ export type ComponentProps = { | { children: ReactNode; label?: string } | { children?: undefined; label: string } ); + ExtendButton: { + className?: string; + onDragStart: (e: React.DragEvent) => void; + onMouseDown: (e: React.MouseEvent) => void; + }; }; // TODO: We should try to make everything as generic as we can Generic: { diff --git a/packages/shadcn/src/index.tsx b/packages/shadcn/src/index.tsx index a799d0c75..6ddd1f664 100644 --- a/packages/shadcn/src/index.tsx +++ b/packages/shadcn/src/index.tsx @@ -84,6 +84,11 @@ export const components: Components = { }, TableHandle: { Root: TableHandle, + ExtendButton: (props) => ( + + ), }, Generic: { Form: { From ec4f300becc07887c051f743087da0f57b48f271 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Tue, 22 Oct 2024 18:06:45 +0200 Subject: [PATCH 02/15] Added proper extend button implementations --- packages/ariakit/src/index.tsx | 7 +--- packages/ariakit/src/style.css | 25 +++++++++++- .../ariakit/src/tableHandle/ExtendButton.tsx | 30 +++++++++++++++ packages/mantine/src/index.tsx | 7 +--- packages/mantine/src/style.css | 19 +++++++++- .../mantine/src/tableHandle/ExtendButton.tsx | 27 +++++++++++++ .../ExtendButton/ExtendButton.tsx | 28 ++++++++++---- .../ExtendButton/ExtendButtonProps.ts | 16 ++++++++ .../react/src/editor/ComponentsContext.tsx | 1 + packages/shadcn/src/index.tsx | 7 +--- .../shadcn/src/tableHandle/ExtendButton.tsx | 38 +++++++++++++++++++ 11 files changed, 179 insertions(+), 26 deletions(-) create mode 100644 packages/ariakit/src/tableHandle/ExtendButton.tsx create mode 100644 packages/mantine/src/tableHandle/ExtendButton.tsx create mode 100644 packages/react/src/components/TableHandles/ExtendButton/ExtendButtonProps.ts create mode 100644 packages/shadcn/src/tableHandle/ExtendButton.tsx diff --git a/packages/ariakit/src/index.tsx b/packages/ariakit/src/index.tsx index cf000eec3..cd7b217a6 100644 --- a/packages/ariakit/src/index.tsx +++ b/packages/ariakit/src/index.tsx @@ -39,6 +39,7 @@ import { SuggestionMenuItem } from "./suggestionMenu/SuggestionMenuItem.js"; import { SuggestionMenuLabel } from "./suggestionMenu/SuggestionMenuLabel.js"; import { SuggestionMenuLoader } from "./suggestionMenu/SuggestionMenuLoader.js"; import { TableHandle } from "./tableHandle/TableHandle.js"; +import { ExtendButton } from "./tableHandle/ExtendButton.js"; import { Toolbar } from "./toolbar/Toolbar.js"; import { ToolbarButton } from "./toolbar/ToolbarButton.js"; import { ToolbarSelect } from "./toolbar/ToolbarSelect.js"; @@ -81,11 +82,7 @@ export const components: Components = { }, TableHandle: { Root: TableHandle, - ExtendButton: (props) => ( - - ), + ExtendButton: ExtendButton, }, Generic: { Form: { diff --git a/packages/ariakit/src/style.css b/packages/ariakit/src/style.css index 213f4d5cd..26505702d 100644 --- a/packages/ariakit/src/style.css +++ b/packages/ariakit/src/style.css @@ -160,10 +160,33 @@ } .bn-ariakit .bn-side-menu, -.bn-ariakit .bn-table-handle { +.bn-ariakit .bn-table-handle, +.bn-ariakit .bn-extend-button { color: gray; } +.bn-ariakit .bn-extend-button-editing { + background-color: hsl(204 4% 0% / 0.05); +} + +.bn-ariakit .bn-extend-button-editing:where(.dark, .dark *) { + background-color: hsl(204 20% 100% / 0.05); +} + +.bn-ariakit .bn-extend-button-row { + height: 100%; + width: 18px; + padding: 0; + margin-left: 4px; +} + +.bn-ariakit .bn-extend-button-column { + height: 18px; + width: 100%; + padding: 0; + margin-top: 4px; +} + .bn-ak-button:where(.dark, .dark *) { color: hsl(204 20% 100%); } diff --git a/packages/ariakit/src/tableHandle/ExtendButton.tsx b/packages/ariakit/src/tableHandle/ExtendButton.tsx new file mode 100644 index 000000000..a295a1321 --- /dev/null +++ b/packages/ariakit/src/tableHandle/ExtendButton.tsx @@ -0,0 +1,30 @@ +import { Button as AriakitButton } from "@ariakit/react"; + +import { forwardRef } from "react"; +import { ComponentProps } from "@blocknote/react"; +import { assertEmpty, mergeCSSClasses } from "@blocknote/core"; + +export const ExtendButton = forwardRef< + HTMLButtonElement, + ComponentProps["TableHandle"]["ExtendButton"] +>((props, ref) => { + const { children, className, onDragStart, onMouseDown, ...rest } = props; + + // false, because rest props can be added by mantine when button is used as a trigger + // assertEmpty in this case is only used at typescript level, not runtime level + assertEmpty(rest, false); + + return ( + + {children} + + ); +}); diff --git a/packages/mantine/src/index.tsx b/packages/mantine/src/index.tsx index da1925674..3941d640d 100644 --- a/packages/mantine/src/index.tsx +++ b/packages/mantine/src/index.tsx @@ -47,6 +47,7 @@ import { SuggestionMenuItem } from "./suggestionMenu/SuggestionMenuItem.js"; import { SuggestionMenuLabel } from "./suggestionMenu/SuggestionMenuLabel.js"; import { SuggestionMenuLoader } from "./suggestionMenu/SuggestionMenuLoader.js"; import { TableHandle } from "./tableHandle/TableHandle.js"; +import { ExtendButton } from "./tableHandle/ExtendButton.js"; import { Toolbar } from "./toolbar/Toolbar.js"; import { ToolbarButton } from "./toolbar/ToolbarButton.js"; import { ToolbarSelect } from "./toolbar/ToolbarSelect.js"; @@ -90,11 +91,7 @@ export const components: Components = { }, TableHandle: { Root: TableHandle, - ExtendButton: (props) => ( - - ), + ExtendButton: ExtendButton, }, Generic: { Form: { diff --git a/packages/mantine/src/style.css b/packages/mantine/src/style.css index ee8b2e60f..4e1038871 100644 --- a/packages/mantine/src/style.css +++ b/packages/mantine/src/style.css @@ -488,7 +488,8 @@ } /* Table Handle styling */ -.bn-mantine .bn-table-handle { +.bn-mantine .bn-table-handle, +.bn-mantine .bn-extend-button { align-items: center; background-color: var(--bn-colors-menu-background); border: var(--bn-border); @@ -508,10 +509,24 @@ } .bn-mantine .bn-table-handle:hover, -.bn-mantine .bn-table-handle-dragging { +.bn-mantine .bn-table-handle-dragging, +.bn-mantine .bn-extend-button:hover, +.bn-mantine .bn-extend-button-editing { background-color: var(--bn-colors-hovered-background); } +.bn-mantine .bn-extend-button-row { + height: 100%; + width: 18px; + margin-left: 4px; +} + +.bn-mantine .bn-extend-button-column { + height: 18px; + width: 100%; + margin-top: 4px; +} + /* Drag Handle & Table Handle Menu styling */ .bn-mantine .bn-drag-handle-menu { overflow: visible; diff --git a/packages/mantine/src/tableHandle/ExtendButton.tsx b/packages/mantine/src/tableHandle/ExtendButton.tsx new file mode 100644 index 000000000..6b2ece76a --- /dev/null +++ b/packages/mantine/src/tableHandle/ExtendButton.tsx @@ -0,0 +1,27 @@ +import { Button as MantineButton } from "@mantine/core"; + +import { forwardRef } from "react"; +import { ComponentProps } from "@blocknote/react"; +import { assertEmpty } from "@blocknote/core"; + +export const ExtendButton = forwardRef< + HTMLButtonElement, + ComponentProps["TableHandle"]["ExtendButton"] +>((props, ref) => { + const { children, className, onDragStart, onMouseDown, ...rest } = props; + + // false, because rest props can be added by mantine when button is used as a trigger + // assertEmpty in this case is only used at typescript level, not runtime level + assertEmpty(rest, false); + + return ( + + {children} + + ); +}); diff --git a/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx b/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx index 6d934073d..4acdc3bf5 100644 --- a/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx +++ b/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx @@ -2,13 +2,20 @@ import { DefaultInlineContentSchema, DefaultStyleSchema, InlineContentSchema, + mergeCSSClasses, PartialTableContent, StyleSchema, } from "@blocknote/core"; -import { MouseEvent as ReactMouseEvent, useEffect, useState } from "react"; +import { + MouseEvent as ReactMouseEvent, + ReactNode, + useEffect, + useState, +} from "react"; +import { RiAddFill } from "react-icons/ri"; import { useComponentsContext } from "../../../editor/ComponentsContext.js"; -import { TableHandleProps } from "../TableHandleProps.js"; +import { ExtendButtonProps } from "./ExtendButtonProps.js"; const getContentWithAddedRows = < I extends InlineContentSchema, @@ -61,10 +68,7 @@ export const ExtendButton = < I extends InlineContentSchema = DefaultInlineContentSchema, S extends StyleSchema = DefaultStyleSchema >( - props: Pick< - TableHandleProps, - "block" | "editor" | "orientation" | "freezeHandles" | "unfreezeHandles" - > + props: ExtendButtonProps & { children?: ReactNode } ) => { const Components = useComponentsContext()!; @@ -172,10 +176,18 @@ export const ExtendButton = < return ( { event.preventDefault(); }} - onMouseDown={mouseDownHandler} - /> + onMouseDown={mouseDownHandler}> + {props.children || } + ); }; diff --git a/packages/react/src/components/TableHandles/ExtendButton/ExtendButtonProps.ts b/packages/react/src/components/TableHandles/ExtendButton/ExtendButtonProps.ts new file mode 100644 index 000000000..8bb84d56e --- /dev/null +++ b/packages/react/src/components/TableHandles/ExtendButton/ExtendButtonProps.ts @@ -0,0 +1,16 @@ +import { + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; + +import { TableHandleProps } from "../TableHandleProps.js"; + +export type ExtendButtonProps< + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +> = Pick< + TableHandleProps, + "block" | "editor" | "orientation" | "freezeHandles" | "unfreezeHandles" +>; diff --git a/packages/react/src/editor/ComponentsContext.tsx b/packages/react/src/editor/ComponentsContext.tsx index d79cf1dd1..1ff35a71d 100644 --- a/packages/react/src/editor/ComponentsContext.tsx +++ b/packages/react/src/editor/ComponentsContext.tsx @@ -187,6 +187,7 @@ export type ComponentProps = { className?: string; onDragStart: (e: React.DragEvent) => void; onMouseDown: (e: React.MouseEvent) => void; + children: ReactNode; }; }; // TODO: We should try to make everything as generic as we can diff --git a/packages/shadcn/src/index.tsx b/packages/shadcn/src/index.tsx index 6ddd1f664..f5dc69eec 100644 --- a/packages/shadcn/src/index.tsx +++ b/packages/shadcn/src/index.tsx @@ -40,6 +40,7 @@ import { SuggestionMenuItem } from "./suggestionMenu/SuggestionMenuItem.js"; import { SuggestionMenuLabel } from "./suggestionMenu/SuggestionMenuLabel.js"; import { SuggestionMenuLoader } from "./suggestionMenu/SuggestionMenuLoader.js"; import { TableHandle } from "./tableHandle/TableHandle.js"; +import { ExtendButton } from "./tableHandle/ExtendButton.js"; import { Toolbar, ToolbarButton, ToolbarSelect } from "./toolbar/Toolbar.js"; import { PanelButton } from "./panel/PanelButton.js"; @@ -84,11 +85,7 @@ export const components: Components = { }, TableHandle: { Root: TableHandle, - ExtendButton: (props) => ( - - ), + ExtendButton: ExtendButton, }, Generic: { Form: { diff --git a/packages/shadcn/src/tableHandle/ExtendButton.tsx b/packages/shadcn/src/tableHandle/ExtendButton.tsx new file mode 100644 index 000000000..c46e740a1 --- /dev/null +++ b/packages/shadcn/src/tableHandle/ExtendButton.tsx @@ -0,0 +1,38 @@ +import { assertEmpty } from "@blocknote/core"; +import { ComponentProps } from "@blocknote/react"; +import { forwardRef } from "react"; + +import { cn } from "../lib/utils.js"; +import { useShadCNComponentsContext } from "../ShadCNComponentsContext.js"; + +export const ExtendButton = forwardRef< + HTMLButtonElement, + ComponentProps["TableHandle"]["ExtendButton"] +>((props, ref) => { + const { className, children, onDragStart, onMouseDown, ...rest } = props; + + // false, because rest props can be added by shadcn when button is used as a trigger + // assertEmpty in this case is only used at typescript level, not runtime level + assertEmpty(rest, false); + + const ShadCNComponents = useShadCNComponentsContext()!; + + return ( + + {children} + + ); +}); From 41fa26f40b74408532aae10772de53c32f39f169 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Tue, 22 Oct 2024 18:07:12 +0200 Subject: [PATCH 03/15] Fixed table handle menu buttons not preserving column widths --- .../TableHandleMenu/DefaultButtons/AddButton.tsx | 6 ++++++ .../TableHandleMenu/DefaultButtons/DeleteButton.tsx | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/AddButton.tsx b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/AddButton.tsx index b54a4438c..b400281c4 100644 --- a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/AddButton.tsx +++ b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/AddButton.tsx @@ -42,6 +42,7 @@ export const AddRowButton = < type: "table", content: { type: "tableContent", + columnWidths: props.block.content.columnWidths, rows, }, }); @@ -75,6 +76,11 @@ export const AddColumnButton = < onClick={() => { const content: PartialTableContent = { type: "tableContent", + columnWidths: props.block.content.columnWidths.toSpliced( + props.index + (props.side === "right" ? 1 : 0), + 0, + 100 + ), rows: props.block.content.rows.map((row) => { const cells = [...row.cells]; cells.splice(props.index + (props.side === "right" ? 1 : 0), 0, []); diff --git a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/DeleteButton.tsx b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/DeleteButton.tsx index 301017f23..8e6f776f7 100644 --- a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/DeleteButton.tsx +++ b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/DeleteButton.tsx @@ -31,6 +31,9 @@ export const DeleteRowButton = < onClick={() => { const content: PartialTableContent = { type: "tableContent", + columnWidths: props.block.content.columnWidths.filter( + (_, index) => index !== props.index + ), rows: props.block.content.rows.filter( (_, index) => index !== props.index ), @@ -70,6 +73,9 @@ export const DeleteColumnButton = < onClick={() => { const content: PartialTableContent = { type: "tableContent", + columnWidths: props.block.content.columnWidths.filter( + (_, index) => index !== props.index + ), rows: props.block.content.rows.map((row) => ({ cells: row.cells.filter((_, index) => index !== props.index), })), From cfb0168d931ca1a0cb6701cac39b731766b09b03 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Wed, 23 Oct 2024 15:54:27 +0200 Subject: [PATCH 04/15] Implemented PR feedback --- .../src/api/nodeConversions/blockToNode.ts | 6 +- .../src/api/nodeConversions/nodeToBlock.ts | 2 +- packages/core/src/editor/editor.css | 1 + .../TableHandles/TableHandlesPlugin.ts | 25 ++++- packages/core/src/schema/blocks/types.ts | 2 +- .../ExtendButton/ExtendButton.tsx | 95 +++++++++------- .../DefaultButtons/AddButton.tsx | 2 +- .../DefaultButtons/DeleteButton.tsx | 4 +- .../TableHandles/TableHandlesController.tsx | 9 +- .../hooks/useExtendButtonsPositioning.ts | 102 +++++++++++++++++ .../hooks/useTableHandlesPositioning.ts | 106 +----------------- packages/react/src/index.ts | 3 + 12 files changed, 190 insertions(+), 167 deletions(-) create mode 100644 packages/react/src/components/TableHandles/hooks/useExtendButtonsPositioning.ts 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"; From 2647bfca1185cf15c9a24160ac6632dacaca0467 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Wed, 23 Oct 2024 16:01:36 +0200 Subject: [PATCH 05/15] Implemented PR feedback --- packages/ariakit/src/tableHandle/ExtendButton.tsx | 3 +-- packages/mantine/src/tableHandle/ExtendButton.tsx | 3 +-- .../src/components/TableHandles/ExtendButton/ExtendButton.tsx | 3 --- packages/react/src/editor/ComponentsContext.tsx | 1 - packages/shadcn/src/tableHandle/ExtendButton.tsx | 3 +-- 5 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/ariakit/src/tableHandle/ExtendButton.tsx b/packages/ariakit/src/tableHandle/ExtendButton.tsx index a295a1321..0a522b8ef 100644 --- a/packages/ariakit/src/tableHandle/ExtendButton.tsx +++ b/packages/ariakit/src/tableHandle/ExtendButton.tsx @@ -8,7 +8,7 @@ export const ExtendButton = forwardRef< HTMLButtonElement, ComponentProps["TableHandle"]["ExtendButton"] >((props, ref) => { - const { children, className, onDragStart, onMouseDown, ...rest } = props; + const { children, className, onMouseDown, ...rest } = props; // false, because rest props can be added by mantine when button is used as a trigger // assertEmpty in this case is only used at typescript level, not runtime level @@ -21,7 +21,6 @@ export const ExtendButton = forwardRef< className || "" )} ref={ref} - onDragStart={onDragStart} onMouseDown={onMouseDown} {...rest}> {children} diff --git a/packages/mantine/src/tableHandle/ExtendButton.tsx b/packages/mantine/src/tableHandle/ExtendButton.tsx index 6b2ece76a..a37023062 100644 --- a/packages/mantine/src/tableHandle/ExtendButton.tsx +++ b/packages/mantine/src/tableHandle/ExtendButton.tsx @@ -8,7 +8,7 @@ export const ExtendButton = forwardRef< HTMLButtonElement, ComponentProps["TableHandle"]["ExtendButton"] >((props, ref) => { - const { children, className, onDragStart, onMouseDown, ...rest } = props; + const { children, className, onMouseDown, ...rest } = props; // false, because rest props can be added by mantine when button is used as a trigger // assertEmpty in this case is only used at typescript level, not runtime level @@ -18,7 +18,6 @@ export const ExtendButton = forwardRef< {children} diff --git a/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx b/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx index 93bcede76..06a9f5295 100644 --- a/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx +++ b/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx @@ -198,9 +198,6 @@ export const ExtendButton = < : "bn-extend-button-column", editingState !== null ? "bn-extend-button-editing" : "" )} - onDragStart={(event) => { - event.preventDefault(); - }} onMouseDown={mouseDownHandler}> {props.children || } diff --git a/packages/react/src/editor/ComponentsContext.tsx b/packages/react/src/editor/ComponentsContext.tsx index 1ff35a71d..aa63f3132 100644 --- a/packages/react/src/editor/ComponentsContext.tsx +++ b/packages/react/src/editor/ComponentsContext.tsx @@ -185,7 +185,6 @@ export type ComponentProps = { ); ExtendButton: { className?: string; - onDragStart: (e: React.DragEvent) => void; onMouseDown: (e: React.MouseEvent) => void; children: ReactNode; }; diff --git a/packages/shadcn/src/tableHandle/ExtendButton.tsx b/packages/shadcn/src/tableHandle/ExtendButton.tsx index c46e740a1..81fa61805 100644 --- a/packages/shadcn/src/tableHandle/ExtendButton.tsx +++ b/packages/shadcn/src/tableHandle/ExtendButton.tsx @@ -9,7 +9,7 @@ export const ExtendButton = forwardRef< HTMLButtonElement, ComponentProps["TableHandle"]["ExtendButton"] >((props, ref) => { - const { className, children, onDragStart, onMouseDown, ...rest } = props; + const { className, children, onMouseDown, ...rest } = props; // false, because rest props can be added by shadcn when button is used as a trigger // assertEmpty in this case is only used at typescript level, not runtime level @@ -29,7 +29,6 @@ export const ExtendButton = forwardRef< : "" )} ref={ref} - onDragStart={onDragStart} onMouseDown={onMouseDown} {...rest}> {children} From e4398155972be69b599e66652cbddc9469c3e8e9 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Wed, 23 Oct 2024 17:25:12 +0200 Subject: [PATCH 06/15] Cleaned up code --- .../TableBlockContent/TableBlockContent.ts | 59 ++++++++++++++++--- .../TableBlockContent/TableExtension.ts | 36 +++-------- packages/core/src/index.ts | 4 ++ .../ExtendButton/ExtendButton.tsx | 10 +++- 4 files changed, 73 insertions(+), 36 deletions(-) diff --git a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts index ac3afa4d5..43d34fa09 100644 --- a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts +++ b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts @@ -1,14 +1,18 @@ -import { mergeAttributes, Node } from "@tiptap/core"; +import { Node } from "@tiptap/core"; import { TableCell } from "@tiptap/extension-table-cell"; import { TableHeader } from "@tiptap/extension-table-header"; import { TableRow } from "@tiptap/extension-table-row"; +import { Node as PMNode } from "prosemirror-model"; +import { TableView } from "prosemirror-tables"; + import { createBlockSpecFromStronglyTypedTiptapNode, createStronglyTypedTiptapNode, } from "../../schema/index.js"; +import { mergeCSSClasses } from "../../util/browser.js"; import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers.js"; import { defaultProps } from "../defaultProps.js"; -import { TableExtension } from "./TableExtension.js"; +import { EMPTY_CELL_WIDTH, TableExtension } from "./TableExtension.js"; export const tablePropSchema = { ...defaultProps, @@ -37,6 +41,51 @@ export const TableBlockContent = createStronglyTypedTiptapNode({ this.options.domAttributes?.inlineContent || {} ); }, + + // This node view is needed for the `columnResizing` plugin. By default, the + // plugin adds its own node view, which overrides how the node is rendered vs + // `renderHTML`. This means that the wrapping `blockContent` HTML element is + // no longer rendered. The `columnResizing` plugin uses the `TableView` as its + // default node view. `BlockNoteTableView` extends it by wrapping it in a + // `blockContent` element, so the DOM structure is consistent with other block + // types. + addNodeView() { + return ({ node, HTMLAttributes }) => { + class BlockNoteTableView extends TableView { + constructor( + public node: PMNode, + public cellMinWidth: number, + public blockContentHTMLAttributes: Record + ) { + super(node, cellMinWidth); + + const blockContent = document.createElement("div"); + blockContent.className = mergeCSSClasses( + "bn-block-content", + blockContentHTMLAttributes.class + ); + blockContent.setAttribute("data-content-type", "table"); + for (const [attribute, value] of Object.entries( + blockContentHTMLAttributes + )) { + if (attribute !== "class") { + blockContent.setAttribute(attribute, value); + } + } + + const tableWrapper = this.dom; + blockContent.appendChild(tableWrapper); + + this.dom = blockContent; + } + } + + return new BlockNoteTableView(node, EMPTY_CELL_WIDTH, { + ...(this.options.domAttributes?.blockContent || {}), + ...HTMLAttributes, + }); + }; + }, }); const TableParagraph = Node.create({ @@ -70,11 +119,7 @@ const TableParagraph = Node.create({ }, renderHTML({ HTMLAttributes }) { - return [ - "p", - mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), - 0, - ]; + return ["p", HTMLAttributes, 0]; }, }); diff --git a/packages/core/src/blocks/TableBlockContent/TableExtension.ts b/packages/core/src/blocks/TableBlockContent/TableExtension.ts index 23e767705..58de6c8c9 100644 --- a/packages/core/src/blocks/TableBlockContent/TableExtension.ts +++ b/packages/core/src/blocks/TableBlockContent/TableExtension.ts @@ -1,40 +1,20 @@ import { callOrReturn, Extension, getExtensionField } from "@tiptap/core"; import { columnResizing, tableEditing } from "prosemirror-tables"; -import { TableView } from "prosemirror-tables"; -import { Node as PMNode } from "prosemirror-model"; -import { mergeCSSClasses } from "../../util/browser.js"; + +export const EMPTY_CELL_WIDTH = 100; +export const EMPTY_CELL_HEIGHT = 31; export const TableExtension = Extension.create({ name: "BlockNoteTableExtension", addProseMirrorPlugins: () => { - class CustomTableView extends TableView { - constructor(public node: PMNode, public cellMinWidth: number) { - super(node, cellMinWidth); - - const blockContent = document.createElement("div"); - blockContent.className = mergeCSSClasses( - "bn-block-content" - // blockContentHTMLAttributes.class - ); - blockContent.setAttribute("data-content-type", "table"); - // for (const [attribute, value] of Object.entries(blockContentHTMLAttributes)) { - // if (attribute !== "class") { - // blockContent.setAttribute(attribute, value); - // } - // } - - const tableWrapper = this.dom; - blockContent.appendChild(tableWrapper); - - this.dom = blockContent; - } - } - return [ columnResizing({ - cellMinWidth: 100, - View: CustomTableView, + cellMinWidth: EMPTY_CELL_WIDTH, + // We set this to null as we implement our own node view in the table + // block content. This node view is the same as what's used by default, + // but is wrapped in a `blockContent` HTML element. + View: null, }), tableEditing(), ]; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ae98dff89..cae4aefe2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,6 +8,10 @@ export * from "./blocks/FileBlockContent/FileBlockContent.js"; export * from "./blocks/FileBlockContent/fileBlockHelpers.js"; export * from "./blocks/FileBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.js"; export * from "./blocks/ImageBlockContent/ImageBlockContent.js"; +export { + EMPTY_CELL_WIDTH, + EMPTY_CELL_HEIGHT, +} from "./blocks/TableBlockContent/TableExtension.js"; export { parseImageElement } from "./blocks/ImageBlockContent/imageBlockHelpers.js"; export * from "./blocks/VideoBlockContent/VideoBlockContent.js"; export * from "./blocks/defaultBlockHelpers.js"; diff --git a/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx b/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx index 06a9f5295..65e69943a 100644 --- a/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx +++ b/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx @@ -3,6 +3,8 @@ import { DefaultBlockSchema, DefaultInlineContentSchema, DefaultStyleSchema, + EMPTY_CELL_HEIGHT, + EMPTY_CELL_WIDTH, InlineContentSchema, mergeCSSClasses, PartialTableContent, @@ -130,7 +132,13 @@ export const ExtendButton = < : props.block.content.rows.length; const newNumCells = numOriginalCells + - roundUpAt(diff / (props.orientation === "row" ? 100 : 31), 0.3); + roundUpAt( + diff / + (props.orientation === "row" + ? EMPTY_CELL_WIDTH + : EMPTY_CELL_HEIGHT), + 0.3 + ); if (numOriginalCells <= newNumCells && newNumCells !== oldNumCells) { props.editor.updateBlock(props.block, { From b9c01e409819adfe9ea72764f4d51b1a9fec5aa4 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Wed, 23 Oct 2024 18:00:33 +0200 Subject: [PATCH 07/15] Updated test snapshots & fixes --- .../__snapshots__/insertBlocks.test.ts.snap | 30 +++++++ .../__snapshots__/mergeBlocks.test.ts.snap | 25 ++++++ .../__snapshots__/moveBlock.test.ts.snap | 40 +++++++++ .../__snapshots__/removeBlocks.test.ts.snap | 10 +++ .../__snapshots__/replaceBlocks.test.ts.snap | 40 +++++++++ .../__snapshots__/splitBlock.test.ts.snap | 30 +++++++ .../__snapshots__/updateBlock.test.ts.snap | 87 +++++++++++++++++++ .../core/src/api/clipboard/clipboard.test.ts | 2 +- .../src/api/nodeConversions/blockToNode.ts | 4 +- .../src/api/nodeConversions/nodeToBlock.ts | 2 +- .../html/__snapshots__/parse-notion-html.json | 5 ++ 11 files changed, 272 insertions(+), 3 deletions(-) diff --git a/packages/core/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap b/packages/core/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap index 4bac28e44..34aebcb1b 100644 --- a/packages/core/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap @@ -309,6 +309,11 @@ exports[`Test insertBlocks > Insert multiple blocks after 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -831,6 +836,11 @@ exports[`Test insertBlocks > Insert multiple blocks before 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -1313,6 +1323,11 @@ exports[`Test insertBlocks > Insert single basic block after 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -1795,6 +1810,11 @@ exports[`Test insertBlocks > Insert single basic block before 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -2334,6 +2354,11 @@ exports[`Test insertBlocks > Insert single complex block after 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -2873,6 +2898,11 @@ exports[`Test insertBlocks > Insert single complex block before 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ diff --git a/packages/core/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap b/packages/core/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap index 6b2592770..233e8e85f 100644 --- a/packages/core/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap @@ -241,6 +241,11 @@ exports[`Test mergeBlocks > Basic 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -695,6 +700,11 @@ exports[`Test mergeBlocks > Blocks have different types 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -1149,6 +1159,11 @@ exports[`Test mergeBlocks > First block has children 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -1602,6 +1617,11 @@ exports[`Test mergeBlocks > Second block has children 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -2073,6 +2093,11 @@ exports[`Test mergeBlocks > Second block is empty 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ diff --git a/packages/core/src/api/blockManipulation/commands/moveBlock/__snapshots__/moveBlock.test.ts.snap b/packages/core/src/api/blockManipulation/commands/moveBlock/__snapshots__/moveBlock.test.ts.snap index 558e863c7..64a53c658 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlock/__snapshots__/moveBlock.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/moveBlock/__snapshots__/moveBlock.test.ts.snap @@ -258,6 +258,11 @@ exports[`Test moveBlockDown > Basic 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -729,6 +734,11 @@ exports[`Test moveBlockDown > Into children 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -1200,6 +1210,11 @@ exports[`Test moveBlockDown > Last block 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -1671,6 +1686,11 @@ exports[`Test moveBlockDown > Out of children 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -2141,6 +2161,11 @@ exports[`Test moveBlockUp > Basic 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -2612,6 +2637,11 @@ exports[`Test moveBlockUp > First block 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -3083,6 +3113,11 @@ exports[`Test moveBlockUp > Into children 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -3554,6 +3589,11 @@ exports[`Test moveBlockUp > Out of children 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ diff --git a/packages/core/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap b/packages/core/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap index 68c3cbfa1..27ed2158a 100644 --- a/packages/core/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap @@ -171,6 +171,11 @@ exports[`Test removeBlocks > Remove multiple consecutive blocks 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -922,6 +927,11 @@ exports[`Test removeBlocks > Remove single block 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap b/packages/core/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap index add46c028..200a384ea 100644 --- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap @@ -171,6 +171,11 @@ exports[`Test replaceBlocks > Remove multiple consecutive blocks 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -922,6 +927,11 @@ exports[`Test replaceBlocks > Remove single block 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -1357,6 +1367,11 @@ exports[`Test replaceBlocks > Replace multiple consecutive blocks with multiple { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -1752,6 +1767,11 @@ exports[`Test replaceBlocks > Replace multiple consecutive blocks with single ba { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -2204,6 +2224,11 @@ exports[`Test replaceBlocks > Replace multiple consecutive blocks with single co { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -3730,6 +3755,11 @@ exports[`Test replaceBlocks > Replace single block with multiple 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -4195,6 +4225,11 @@ exports[`Test replaceBlocks > Replace single block with single basic 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -4717,6 +4752,11 @@ exports[`Test replaceBlocks > Replace single block with single complex 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ diff --git a/packages/core/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap b/packages/core/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap index d308e0046..492fe3330 100644 --- a/packages/core/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap @@ -275,6 +275,11 @@ exports[`Test splitBlocks > Basic 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -763,6 +768,11 @@ exports[`Test splitBlocks > Block has children 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -1251,6 +1261,11 @@ exports[`Test splitBlocks > Don't keep props 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -1739,6 +1754,11 @@ exports[`Test splitBlocks > Don't keep type 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -2221,6 +2241,11 @@ exports[`Test splitBlocks > End of content 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -2710,6 +2735,11 @@ exports[`Test splitBlocks > Keep type 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap b/packages/core/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap index a069e613a..6138dbc1c 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap @@ -258,6 +258,11 @@ exports[`Test updateBlock > Revert all props 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -729,6 +734,11 @@ exports[`Test updateBlock > Revert single prop 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -1200,6 +1210,11 @@ exports[`Test updateBlock > Update all props 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -1671,6 +1686,11 @@ exports[`Test updateBlock > Update children 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -1889,6 +1909,7 @@ exports[`Test updateBlock > Update inline content to empty table content 1`] = ` { "children": [], "content": { + "columnWidths": [], "rows": [], "type": "tableContent", }, @@ -2138,6 +2159,11 @@ exports[`Test updateBlock > Update inline content to empty table content 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -2607,6 +2633,11 @@ exports[`Test updateBlock > Update inline content to no content 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -2825,6 +2856,11 @@ exports[`Test updateBlock > Update inline content to table content 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -3150,6 +3186,11 @@ exports[`Test updateBlock > Update inline content to table content 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -3617,6 +3658,11 @@ exports[`Test updateBlock > Update no content to empty inline content 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -4056,6 +4102,7 @@ exports[`Test updateBlock > Update no content to empty table content 1`] = ` { "children": [], "content": { + "columnWidths": [], "rows": [], "type": "tableContent", }, @@ -4086,6 +4133,11 @@ exports[`Test updateBlock > Update no content to empty table content 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -4559,6 +4611,11 @@ exports[`Test updateBlock > Update no content to inline content 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -4998,6 +5055,11 @@ exports[`Test updateBlock > Update no content to table content 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -5104,6 +5166,11 @@ exports[`Test updateBlock > Update no content to table content 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -5575,6 +5642,11 @@ exports[`Test updateBlock > Update single prop 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -7235,6 +7307,11 @@ exports[`Test updateBlock > Update type 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -7705,6 +7782,11 @@ exports[`Test updateBlock > Update with plain content 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ @@ -8162,6 +8244,11 @@ exports[`Test updateBlock > Update with styled content 1`] = ` { "children": [], "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], "rows": [ { "cells": [ diff --git a/packages/core/src/api/clipboard/clipboard.test.ts b/packages/core/src/api/clipboard/clipboard.test.ts index a5b8f71db..416076aa0 100644 --- a/packages/core/src/api/clipboard/clipboard.test.ts +++ b/packages/core/src/api/clipboard/clipboard.test.ts @@ -173,7 +173,7 @@ describe("Test ProseMirror selection clipboard HTML", () => { ) ); - const { clipboardHTML, externalHTML } = await selectedFragmentToHTML( + const { clipboardHTML, externalHTML } = selectedFragmentToHTML( editor._tiptapEditor.view, editor ); diff --git a/packages/core/src/api/nodeConversions/blockToNode.ts b/packages/core/src/api/nodeConversions/blockToNode.ts index 7ee9270ff..b4df390c1 100644 --- a/packages/core/src/api/nodeConversions/blockToNode.ts +++ b/packages/core/src/api/nodeConversions/blockToNode.ts @@ -178,7 +178,9 @@ 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?.[i] || null], + colwidth: tableContent.columnWidths?.[i] + ? [tableContent.columnWidths[i]] + : null, }, pNode ); diff --git a/packages/core/src/api/nodeConversions/nodeToBlock.ts b/packages/core/src/api/nodeConversions/nodeToBlock.ts index 780523947..79889cb54 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] || undefined); + ret.columnWidths.push(cellNode.attrs.colwidth?.[0] || undefined); }); } diff --git a/packages/core/src/api/parsers/html/__snapshots__/parse-notion-html.json b/packages/core/src/api/parsers/html/__snapshots__/parse-notion-html.json index d79fe0964..8b9a11c50 100644 --- a/packages/core/src/api/parsers/html/__snapshots__/parse-notion-html.json +++ b/packages/core/src/api/parsers/html/__snapshots__/parse-notion-html.json @@ -370,6 +370,11 @@ }, "content": { "type": "tableContent", + "columnWidths": [ + null, + null, + null + ], "rows": [ { "cells": [ From 6e3487e12a17148dafc7120c51ac64dde5de437e Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Wed, 23 Oct 2024 18:42:28 +0200 Subject: [PATCH 08/15] Added unit tests for column widths --- .../table/allColWidths/external.html | 1 + .../table/allColWidths/internal.html | 1 + .../__snapshots__/table/basic/external.html | 1 + .../__snapshots__/table/basic/internal.html | 1 + .../table/mixedColWidths/external.html | 1 + .../table/mixedColWidths/internal.html | 1 + .../table/allColWidths/markdown.md | 5 + .../__snapshots__/table/basic/markdown.md | 5 + .../table/mixedColWidths/markdown.md | 5 + .../nodeConversions.test.ts.snap | 642 ++++++++++++++++++ .../src/api/testUtil/cases/defaultSchema.ts | 68 ++ .../src/api/testUtil/partialBlockTestUtil.ts | 27 +- .../TableBlockContent/TableBlockContent.ts | 3 +- 13 files changed, 759 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/api/exporters/html/__snapshots__/table/allColWidths/external.html create mode 100644 packages/core/src/api/exporters/html/__snapshots__/table/allColWidths/internal.html create mode 100644 packages/core/src/api/exporters/html/__snapshots__/table/basic/external.html create mode 100644 packages/core/src/api/exporters/html/__snapshots__/table/basic/internal.html create mode 100644 packages/core/src/api/exporters/html/__snapshots__/table/mixedColWidths/external.html create mode 100644 packages/core/src/api/exporters/html/__snapshots__/table/mixedColWidths/internal.html create mode 100644 packages/core/src/api/exporters/markdown/__snapshots__/table/allColWidths/markdown.md create mode 100644 packages/core/src/api/exporters/markdown/__snapshots__/table/basic/markdown.md create mode 100644 packages/core/src/api/exporters/markdown/__snapshots__/table/mixedColWidths/markdown.md diff --git a/packages/core/src/api/exporters/html/__snapshots__/table/allColWidths/external.html b/packages/core/src/api/exporters/html/__snapshots__/table/allColWidths/external.html new file mode 100644 index 000000000..fca273021 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/table/allColWidths/external.html @@ -0,0 +1 @@ +

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/table/allColWidths/internal.html b/packages/core/src/api/exporters/html/__snapshots__/table/allColWidths/internal.html new file mode 100644 index 000000000..7247411a1 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/table/allColWidths/internal.html @@ -0,0 +1 @@ +

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/table/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/table/basic/external.html new file mode 100644 index 000000000..ce73c75aa --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/table/basic/external.html @@ -0,0 +1 @@ +

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/table/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/table/basic/internal.html new file mode 100644 index 000000000..f00383b3d --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/table/basic/internal.html @@ -0,0 +1 @@ +

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/table/mixedColWidths/external.html b/packages/core/src/api/exporters/html/__snapshots__/table/mixedColWidths/external.html new file mode 100644 index 000000000..c7018c749 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/table/mixedColWidths/external.html @@ -0,0 +1 @@ +

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/table/mixedColWidths/internal.html b/packages/core/src/api/exporters/html/__snapshots__/table/mixedColWidths/internal.html new file mode 100644 index 000000000..b2b6765da --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots__/table/mixedColWidths/internal.html @@ -0,0 +1 @@ +

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/markdown/__snapshots__/table/allColWidths/markdown.md b/packages/core/src/api/exporters/markdown/__snapshots__/table/allColWidths/markdown.md new file mode 100644 index 000000000..3e52272fe --- /dev/null +++ b/packages/core/src/api/exporters/markdown/__snapshots__/table/allColWidths/markdown.md @@ -0,0 +1,5 @@ +| | | | +| ---------- | ---------- | ---------- | +| Table Cell | Table Cell | Table Cell | +| Table Cell | Table Cell | Table Cell | +| Table Cell | Table Cell | Table Cell | diff --git a/packages/core/src/api/exporters/markdown/__snapshots__/table/basic/markdown.md b/packages/core/src/api/exporters/markdown/__snapshots__/table/basic/markdown.md new file mode 100644 index 000000000..3e52272fe --- /dev/null +++ b/packages/core/src/api/exporters/markdown/__snapshots__/table/basic/markdown.md @@ -0,0 +1,5 @@ +| | | | +| ---------- | ---------- | ---------- | +| Table Cell | Table Cell | Table Cell | +| Table Cell | Table Cell | Table Cell | +| Table Cell | Table Cell | Table Cell | diff --git a/packages/core/src/api/exporters/markdown/__snapshots__/table/mixedColWidths/markdown.md b/packages/core/src/api/exporters/markdown/__snapshots__/table/mixedColWidths/markdown.md new file mode 100644 index 000000000..3e52272fe --- /dev/null +++ b/packages/core/src/api/exporters/markdown/__snapshots__/table/mixedColWidths/markdown.md @@ -0,0 +1,5 @@ +| | | | +| ---------- | ---------- | ---------- | +| Table Cell | Table Cell | Table Cell | +| Table Cell | Table Cell | Table Cell | +| Table Cell | Table Cell | Table Cell | diff --git a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap index fe98c2415..942a525b4 100644 --- a/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +++ b/packages/core/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap @@ -1752,3 +1752,645 @@ exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert "type": "blockContainer", } `; + +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert table/allColWidths to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "content": [ + { + "attrs": { + "colspan": 1, + "colwidth": [ + 100, + ], + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": [ + 200, + ], + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": [ + 300, + ], + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + { + "content": [ + { + "attrs": { + "colspan": 1, + "colwidth": [ + 100, + ], + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": [ + 200, + ], + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": [ + 300, + ], + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + { + "content": [ + { + "attrs": { + "colspan": 1, + "colwidth": [ + 100, + ], + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": [ + 200, + ], + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": [ + 300, + ], + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + ], + "type": "table", + }, + ], + "type": "blockContainer", +} +`; + +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert table/basic to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "content": [ + { + "attrs": { + "colspan": 1, + "colwidth": null, + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": null, + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": null, + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + { + "content": [ + { + "attrs": { + "colspan": 1, + "colwidth": null, + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": null, + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": null, + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + { + "content": [ + { + "attrs": { + "colspan": 1, + "colwidth": null, + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": null, + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": null, + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + ], + "type": "table", + }, + ], + "type": "blockContainer", +} +`; + +exports[`Test BlockNote-Prosemirror conversion > Case: default schema > Convert table/mixedColWidths to/from prosemirror 1`] = ` +{ + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "content": [ + { + "attrs": { + "colspan": 1, + "colwidth": [ + 100, + ], + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": null, + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": [ + 300, + ], + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + { + "content": [ + { + "attrs": { + "colspan": 1, + "colwidth": [ + 100, + ], + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": null, + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": [ + 300, + ], + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + { + "content": [ + { + "attrs": { + "colspan": 1, + "colwidth": [ + 100, + ], + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": null, + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "colspan": 1, + "colwidth": [ + 300, + ], + "rowspan": 1, + }, + "content": [ + { + "content": [ + { + "text": "Table Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + ], + "type": "table", + }, + ], + "type": "blockContainer", +} +`; diff --git a/packages/core/src/api/testUtil/cases/defaultSchema.ts b/packages/core/src/api/testUtil/cases/defaultSchema.ts index 66a84ab28..da8c790e8 100644 --- a/packages/core/src/api/testUtil/cases/defaultSchema.ts +++ b/packages/core/src/api/testUtil/cases/defaultSchema.ts @@ -332,6 +332,74 @@ export const defaultSchemaTestCases: EditorTestCases< }, ], }, + { + name: "table/basic", + blocks: [ + { + type: "table", + content: { + type: "tableContent", + rows: [ + { + cells: ["Table Cell", "Table Cell", "Table Cell"], + }, + { + cells: ["Table Cell", "Table Cell", "Table Cell"], + }, + { + cells: ["Table Cell", "Table Cell", "Table Cell"], + }, + ], + }, + }, + ], + }, + { + name: "table/allColWidths", + blocks: [ + { + type: "table", + content: { + type: "tableContent", + columnWidths: [100, 200, 300], + rows: [ + { + cells: ["Table Cell", "Table Cell", "Table Cell"], + }, + { + cells: ["Table Cell", "Table Cell", "Table Cell"], + }, + { + cells: ["Table Cell", "Table Cell", "Table Cell"], + }, + ], + }, + }, + ], + }, + { + name: "table/mixedColWidths", + blocks: [ + { + type: "table", + content: { + type: "tableContent", + columnWidths: [100, undefined, 300], + rows: [ + { + cells: ["Table Cell", "Table Cell", "Table Cell"], + }, + { + cells: ["Table Cell", "Table Cell", "Table Cell"], + }, + { + cells: ["Table Cell", "Table Cell", "Table Cell"], + }, + ], + }, + }, + ], + }, { name: "link/basic", blocks: [ diff --git a/packages/core/src/api/testUtil/partialBlockTestUtil.ts b/packages/core/src/api/testUtil/partialBlockTestUtil.ts index fa5eb8b77..efd685ab3 100644 --- a/packages/core/src/api/testUtil/partialBlockTestUtil.ts +++ b/packages/core/src/api/testUtil/partialBlockTestUtil.ts @@ -80,12 +80,19 @@ export function partialBlockToBlockForTesting< schema: BSchema, partialBlock: PartialBlock ): Block { + const contentType: "inline" | "table" | "none" = + schema[partialBlock.type!].content; + const withDefaults: Block = { id: "", type: partialBlock.type!, props: {} as any, content: - schema[partialBlock.type!].content === "inline" ? [] : (undefined as any), + contentType === "inline" + ? [] + : contentType === "table" + ? { type: "tableContent", columnWidths: [], rows: [] } + : (undefined as any), children: [] as any, ...partialBlock, }; @@ -98,6 +105,24 @@ export function partialBlockToBlockForTesting< } ); + if (contentType === "inline") { + const content = withDefaults.content as InlineContent[] | undefined; + withDefaults.content = partialContentToInlineContent(content) as any; + } else if (contentType === "table") { + const content = withDefaults.content as TableContent | undefined; + withDefaults.content = { + type: "tableContent", + columnWidths: + content?.columnWidths || + content?.rows[0]?.cells.map(() => undefined) || + [], + rows: + content?.rows.map((row) => ({ + cells: row.cells.map((cell) => partialContentToInlineContent(cell)), + })) || [], + } as any; + } + return { ...withDefaults, content: partialContentToInlineContent(withDefaults.content), diff --git a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts index 43d34fa09..b1dde675f 100644 --- a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts +++ b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts @@ -15,7 +15,8 @@ import { defaultProps } from "../defaultProps.js"; import { EMPTY_CELL_WIDTH, TableExtension } from "./TableExtension.js"; export const tablePropSchema = { - ...defaultProps, + backgroundColor: defaultProps.backgroundColor, + textColor: defaultProps.textColor, }; export const TableBlockContent = createStronglyTypedTiptapNode({ From 195e6c81d877694f81c263a3883f53f389493435 Mon Sep 17 00:00:00 2001 From: yousefed Date: Fri, 25 Oct 2024 10:45:49 +0200 Subject: [PATCH 09/15] remaining todos --- packages/ariakit/src/style.css | 186 ++++++++-------- .../ariakit/src/tableHandle/ExtendButton.tsx | 7 +- .../TableBlockContent/TableBlockContent.ts | 22 +- .../TableBlockContent/TableExtension.ts | 5 +- packages/core/src/editor/editor.css | 30 ++- .../TableHandles/TableHandlesPlugin.ts | 82 +++---- packages/mantine/src/style.css | 8 +- .../mantine/src/tableHandle/ExtendButton.tsx | 7 +- .../ExtendButton/ExtendButton.tsx | 202 ++++++++++++------ .../ExtendButton/ExtendButtonProps.ts | 21 +- .../components/TableHandles/TableHandle.tsx | 15 +- .../TableHandles/TableHandleProps.ts | 1 + .../TableHandles/TableHandlesController.tsx | 122 ++++++----- .../hooks/useExtendButtonsPositioning.ts | 1 - .../hooks/useTableHandlesPositioning.ts | 1 - .../react/src/editor/ComponentsContext.tsx | 1 + .../shadcn/src/tableHandle/ExtendButton.tsx | 7 +- 17 files changed, 448 insertions(+), 270 deletions(-) diff --git a/packages/ariakit/src/style.css b/packages/ariakit/src/style.css index 26505702d..bf1dcd4ce 100644 --- a/packages/ariakit/src/style.css +++ b/packages/ariakit/src/style.css @@ -6,204 +6,206 @@ @import "./ariakitStyles.css"; .bn-ak-input-wrapper { - align-items: center; - display: flex; - gap: 0.5rem; + align-items: center; + display: flex; + gap: 0.5rem; } .bn-toolbar .bn-ak-button { - width: unset; + width: unset; } .bn-toolbar .bn-ak-button[data-selected] { - padding-top: 0.125rem; - box-shadow: inset 0 0 0 1px var(--border), inset 0 2px 0 var(--border); + padding-top: 0.125rem; + box-shadow: inset 0 0 0 1px var(--border), inset 0 2px 0 var(--border); } .bn-toolbar .bn-ak-button[data-selected]:where(.dark, .dark *) { - box-shadow: inset 0 0 0 1px var(--border), inset 0 1px 1px 1px var(--shadow); + box-shadow: inset 0 0 0 1px var(--border), inset 0 1px 1px 1px var(--shadow); } .bn-toolbar .bn-ak-popover { - gap: 0.5rem; + gap: 0.5rem; } .bn-ariakit .bn-tab-panel { - align-items: center; - display: flex; - flex-direction: column; - gap: 0.5rem; + align-items: center; + display: flex; + flex-direction: column; + gap: 0.5rem; } .bn-ariakit .bn-file-input { - max-width: 100%; + max-width: 100%; } .bn-ak-button { - outline-style: none; + outline-style: none; } .bn-ak-menu-item[aria-selected="true"], .bn-ak-menu-item:hover { - background-color: hsl(204 100% 40%); - color: hsl(204 20% 100%); + background-color: hsl(204 100% 40%); + color: hsl(204 20% 100%); } .bn-ak-menu-item { - display: flex; + display: flex; } .bn-ariakit .bn-dropdown { - overflow: visible; + overflow: visible; } .bn-ariakit .bn-suggestion-menu { - height: fit-content; - max-height: 100%; + height: fit-content; + max-height: 100%; } .bn-ariakit .bn-color-picker-dropdown { - overflow: scroll; + overflow: scroll; } .bn-ak-suggestion-menu-item-body { - flex: 1; + flex: 1; } .bn-ak-suggestion-menu-item-subtitle { - font-size: 0.7rem; + font-size: 0.7rem; } .bn-ak-suggestion-menu-item-section[data-position="left"] { - padding: 8px; + padding: 8px; } .bn-ak-suggestion-menu-item-section[data-position="right"] { - --border: rgb(0 0 0/13%); - --highlight: rgb(255 255 255/20%); - --shadow: rgb(0 0 0/10%); - box-shadow: inset 0 0 0 1px var(--border), inset 0 2px 0 var(--highlight), + --border: rgb(0 0 0/13%); + --highlight: rgb(255 255 255/20%); + --shadow: rgb(0 0 0/10%); + box-shadow: inset 0 0 0 1px var(--border), inset 0 2px 0 var(--highlight), inset 0 -1px 0 var(--shadow), 0 1px 1px var(--shadow); - font-size: 0.7rem; - border-radius: 4px; - padding-inline: 4px; + font-size: 0.7rem; + border-radius: 4px; + padding-inline: 4px; } .bn-ariakit .bn-grid-suggestion-menu { - background: var(--bn-colors-menu-background); - border-radius: var(--bn-border-radius-large); - box-shadow: var(--bn-shadow-medium); - display: grid; - gap: 7px; - height: fit-content; - justify-items: center; - max-height: min(500px, 100%); - overflow-y: auto; - padding: 20px; + background: var(--bn-colors-menu-background); + border-radius: var(--bn-border-radius-large); + box-shadow: var(--bn-shadow-medium); + display: grid; + gap: 7px; + height: fit-content; + justify-items: center; + max-height: min(500px, 100%); + overflow-y: auto; + padding: 20px; } .bn-ariakit .bn-grid-suggestion-menu-item { - align-items: center; - border-radius: var(--bn-border-radius-large); - cursor: pointer; - display: flex; - font-size: 24px; - height: 32px; - justify-content: center; - margin: 2px; - padding: 4px; - width: 32px; + align-items: center; + border-radius: var(--bn-border-radius-large); + cursor: pointer; + display: flex; + font-size: 24px; + height: 32px; + justify-content: center; + margin: 2px; + padding: 4px; + width: 32px; } .bn-ariakit .bn-grid-suggestion-menu-item[aria-selected="true"], .bn-ariakit .bn-grid-suggestion-menu-item:hover { - background-color: var(--bn-colors-hovered-background); + background-color: var(--bn-colors-hovered-background); } .bn-ariakit .bn-grid-suggestion-menu-empty-item, .bn-ariakit .bn-grid-suggestion-menu-loader { - align-items: center; - color: var(--bn-colors-menu-text); - display: flex; - font-size: 14px; - font-weight: 500; - height: 32px; - justify-content: center; + align-items: center; + color: var(--bn-colors-menu-text); + display: flex; + font-size: 14px; + font-weight: 500; + height: 32px; + justify-content: center; } .bn-ariakit .bn-grid-suggestion-menu-loader span { - background-color: var(--bn-colors-side-menu); + background-color: var(--bn-colors-side-menu); } .bn-ariakit .bn-side-menu { - align-items: center; - display: flex; - justify-content: center; + align-items: center; + display: flex; + justify-content: center; } .bn-side-menu .bn-ak-button { - height: fit-content; - padding: 0; - width: fit-content; + height: fit-content; + padding: 0; + width: fit-content; } .bn-ariakit .bn-panel-popover { - background-color: transparent; - border: none; - box-shadow: none; + background-color: transparent; + border: none; + box-shadow: none; } .bn-ariakit .bn-table-handle { - height: fit-content; - padding: 0; - width: fit-content; + height: fit-content; + padding: 0; + width: fit-content; } .bn-ariakit .bn-side-menu, .bn-ariakit .bn-table-handle, .bn-ariakit .bn-extend-button { - color: gray; + color: gray; } .bn-ariakit .bn-extend-button-editing { - background-color: hsl(204 4% 0% / 0.05); + background-color: hsl(204 4% 0% / 0.05); } .bn-ariakit .bn-extend-button-editing:where(.dark, .dark *) { - background-color: hsl(204 20% 100% / 0.05); + background-color: hsl(204 20% 100% / 0.05); } -.bn-ariakit .bn-extend-button-row { - height: 100%; - width: 18px; - padding: 0; - margin-left: 4px; +.bn-ariakit .bn-extend-button-add-remove-columns { + height: 100%; + width: 18px; + padding: 0; + margin-left: 4px; + cursor: col-resize; } -.bn-ariakit .bn-extend-button-column { - height: 18px; - width: 100%; - padding: 0; - margin-top: 4px; +.bn-ariakit .bn-extend-button-add-remove-rows { + height: 18px; + width: 100%; + padding: 0; + margin-top: 4px; + cursor: row-resize; } .bn-ak-button:where(.dark, .dark *) { - color: hsl(204 20% 100%); + color: hsl(204 20% 100%); } .bn-ak-tab, .bn-ariakit .bn-file-input { - background-color: transparent; - color: black; + background-color: transparent; + color: black; } .bn-ak-tab:where(.dark, .dark *), .bn-ariakit .bn-file-input:where(.dark, .dark *) { - color: white; + color: white; } .bn-ak-tooltip { - align-items: center; - display: flex; - flex-direction: column; + align-items: center; + display: flex; + flex-direction: column; } diff --git a/packages/ariakit/src/tableHandle/ExtendButton.tsx b/packages/ariakit/src/tableHandle/ExtendButton.tsx index 0a522b8ef..67b163905 100644 --- a/packages/ariakit/src/tableHandle/ExtendButton.tsx +++ b/packages/ariakit/src/tableHandle/ExtendButton.tsx @@ -1,14 +1,14 @@ import { Button as AriakitButton } from "@ariakit/react"; -import { forwardRef } from "react"; -import { ComponentProps } from "@blocknote/react"; import { assertEmpty, mergeCSSClasses } from "@blocknote/core"; +import { ComponentProps } from "@blocknote/react"; +import { forwardRef } from "react"; export const ExtendButton = forwardRef< HTMLButtonElement, ComponentProps["TableHandle"]["ExtendButton"] >((props, ref) => { - const { children, className, onMouseDown, ...rest } = props; + const { children, className, onMouseDown, onClick, ...rest } = props; // false, because rest props can be added by mantine when button is used as a trigger // assertEmpty in this case is only used at typescript level, not runtime level @@ -22,6 +22,7 @@ export const ExtendButton = forwardRef< )} ref={ref} onMouseDown={onMouseDown} + onClick={onClick} {...rest}> {children} diff --git a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts index b1dde675f..ca234f4d9 100644 --- a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts +++ b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts @@ -12,7 +12,7 @@ import { import { mergeCSSClasses } from "../../util/browser.js"; import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers.js"; import { defaultProps } from "../defaultProps.js"; -import { EMPTY_CELL_WIDTH, TableExtension } from "./TableExtension.js"; +import { MIN_CELL_WIDTH, TableExtension } from "./TableExtension.js"; export const tablePropSchema = { backgroundColor: defaultProps.backgroundColor, @@ -75,13 +75,31 @@ export const TableBlockContent = createStronglyTypedTiptapNode({ } const tableWrapper = this.dom; + + const tableWrapperInner = document.createElement("div"); + tableWrapperInner.className = "tableWrapper-inner"; + tableWrapperInner.appendChild(tableWrapper.firstChild!); + + tableWrapper.appendChild(tableWrapperInner); + blockContent.appendChild(tableWrapper); + const floatingContainer = document.createElement("div"); + floatingContainer.className = "table-widgets-container"; + floatingContainer.style.position = "relative"; + tableWrapper.appendChild(floatingContainer); this.dom = blockContent; } + + ignoreMutation(record: MutationRecord): boolean { + return ( + !(record.target as HTMLElement).closest(".tableWrapper-inner") || + super.ignoreMutation(record) + ); + } } - return new BlockNoteTableView(node, EMPTY_CELL_WIDTH, { + return new BlockNoteTableView(node, MIN_CELL_WIDTH, { ...(this.options.domAttributes?.blockContent || {}), ...HTMLAttributes, }); diff --git a/packages/core/src/blocks/TableBlockContent/TableExtension.ts b/packages/core/src/blocks/TableBlockContent/TableExtension.ts index 58de6c8c9..793be3ed0 100644 --- a/packages/core/src/blocks/TableBlockContent/TableExtension.ts +++ b/packages/core/src/blocks/TableBlockContent/TableExtension.ts @@ -1,7 +1,8 @@ import { callOrReturn, Extension, getExtensionField } from "@tiptap/core"; import { columnResizing, tableEditing } from "prosemirror-tables"; -export const EMPTY_CELL_WIDTH = 100; +export const MIN_CELL_WIDTH = 35; +export const EMPTY_CELL_WIDTH = 120; export const EMPTY_CELL_HEIGHT = 31; export const TableExtension = Extension.create({ @@ -10,7 +11,7 @@ export const TableExtension = Extension.create({ addProseMirrorPlugins: () => { return [ columnResizing({ - cellMinWidth: EMPTY_CELL_WIDTH, + cellMinWidth: MIN_CELL_WIDTH, // We set this to null as we implement our own node view in the table // block content. This node view is the same as what's used by default, // but is wrapped in a `blockContent` HTML element. diff --git a/packages/core/src/editor/editor.css b/packages/core/src/editor/editor.css index e2a121a54..4bacb619a 100644 --- a/packages/core/src/editor/editor.css +++ b/packages/core/src/editor/editor.css @@ -104,19 +104,45 @@ Tippy popups that are appended to document.body directly white-space: nowrap; } +/* .tableWrapper { + padding +} */ + +.ProseMirror .tableWrapper { + position: relative; + top: -16px; + left: -16px; + /* padding: 16px; */ + min-width: calc(100% + 16px); + padding-bottom: 16px; + overflow-y: hidden; +} + +.ProseMirror .tableWrapper-inner { + /* position: relative; */ + /* top: -16px; + left: -16px; */ + padding: 16px; +} + /* table related: */ .bn-editor table { /* TODO: Do we want this? */ width: auto !important; - margin-bottom: 2em; + word-break: break-word; } .bn-editor th, .bn-editor td { - min-width: 1em; border: 1px solid #ddd; padding: 3px 5px; } +/* .bn-editor th:not([colwidth]), +.bn-editor td:not([colwidth]) { + min-width: 120px; + max-width: 240px; +} */ + .bn-editor th { font-weight: bold; text-align: left; diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index 0a68cd7f1..4952ec9ab 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -36,6 +36,8 @@ export type TableHandlesState< mousePos: number; } | undefined; + + widgetContainer: HTMLElement | undefined; }; function setHiddenDragImage(rootEl: Document | ShadowRoot) { @@ -72,14 +74,30 @@ function getChildIndex(node: Element) { // Finds the DOM element corresponding to the table cell that the target element // is currently in. If the target element is not in a table cell, returns null. -function domCellAround(target: Element | null): Element | null { - while (target && target.nodeName !== "TD" && target.nodeName !== "TH") { +function domCellAround(target: Element | undefined) { + while ( + target && + target.nodeName !== "TD" && + target.nodeName !== "TH" && + !target.classList.contains("tableWrapper") + ) { target = target.classList && target.classList.contains("ProseMirror") - ? null + ? undefined : (target.parentNode as Element); } - return target; + if (!target) { + return undefined; + } + return target?.nodeName === "TD" || target?.nodeName === "TH" + ? { + type: "cell", + domNode: target, + } + : { + type: "wrapper", + domNode: target, + }; } // Hides elements in the DOMwith the provided class names. @@ -140,11 +158,6 @@ export class TableHandlesView< this.dragOverHandler as EventListener ); pmView.root.addEventListener("drop", this.dropHandler as EventListener); - - // Setting capture=true ensures that any parent container of the editor that - // gets scrolled will trigger the scroll event. Scroll events do not bubble - // and so won't propagate to the document by default. - pmView.root.addEventListener("scroll", this.scrollHandler, true); } viewMousedownHandler = () => { @@ -161,7 +174,14 @@ export class TableHandlesView< return; } - if (this.mouseState === "down") { + const target = domCellAround(event.target as HTMLElement); + + if ( + target?.type === "cell" && + this.mouseState === "down" && + !this.state?.draggingState + ) { + // hide draghandles when selecting text as they could be in the way of the user this.mouseState = "selecting"; if (this.state?.show) { @@ -170,13 +190,16 @@ export class TableHandlesView< this.state.showExtendButtonCol = false; this.emitUpdate(); } + return; } if (this.mouseState === "selecting") { return; } - const target = domCellAround(event.target as HTMLElement); + if (target?.type === "wrapper") { + return; + } if (!target || !this.editor.isEditable) { if (this.state?.show) { @@ -188,21 +211,21 @@ export class TableHandlesView< return; } - const colIndex = getChildIndex(target); - const rowIndex = getChildIndex(target.parentElement!); - const cellRect = target.getBoundingClientRect(); - const tableRect = - target.parentElement?.parentElement?.getBoundingClientRect(); + const colIndex = getChildIndex(target.domNode); + const rowIndex = getChildIndex(target.domNode.parentElement!); + const cellRect = target.domNode.getBoundingClientRect(); + + const tableRect = target.domNode.closest("tbody")!.getBoundingClientRect(); if (!tableRect) { return; } - const blockEl = getDraggableBlockFromElement(target, this.pmView); + const blockEl = getDraggableBlockFromElement(target.domNode, this.pmView); if (!blockEl) { return; } - this.tableElement = blockEl.node; + this.tableElement = blockEl.node; // TODO: needed? let tableBlock: | BlockFromConfigNoChildren @@ -264,6 +287,10 @@ export class TableHandlesView< rowIndex: rowIndex, draggingState: undefined, + widgetContainer: + target.domNode + .closest(".tableWrapper") + ?.querySelector(".table-widgets-container") || undefined, }; this.emitUpdate(); @@ -405,24 +432,6 @@ export class TableHandlesView< // the existing selection out of the block. this.editor.setTextCursorPosition(this.state.block.id); }; - - scrollHandler = () => { - if (this.state?.show) { - const tableElement = this.pmView.root.querySelector( - `[data-node-type="blockContainer"][data-id="${this.tableId}"] table` - )!; - const cellElement = tableElement.querySelector( - `tr:nth-child(${this.state.rowIndex + 1}) > td:nth-child(${ - this.state.colIndex + 1 - })` - )!; - - this.state.referencePosTable = tableElement.getBoundingClientRect(); - this.state.referencePosCell = cellElement.getBoundingClientRect(); - this.emitUpdate(); - } - }; - // Updates drag handle positions on table content updates. update() { if (!this.state || !this.state.show) { @@ -470,7 +479,6 @@ export class TableHandlesView< "drop", this.dropHandler as EventListener ); - this.pmView.root.removeEventListener("scroll", this.scrollHandler, true); } } diff --git a/packages/mantine/src/style.css b/packages/mantine/src/style.css index 4e1038871..55b5301fb 100644 --- a/packages/mantine/src/style.css +++ b/packages/mantine/src/style.css @@ -396,7 +396,7 @@ } .bn-mantine .bn-grid-suggestion-menu-empty-item, -.bn-mantine .bn-grid-suggestion-menu-loader{ +.bn-mantine .bn-grid-suggestion-menu-loader { align-items: center; color: var(--bn-colors-menu-text); display: flex; @@ -515,16 +515,18 @@ background-color: var(--bn-colors-hovered-background); } -.bn-mantine .bn-extend-button-row { +.bn-mantine .bn-extend-button-add-remove-columns { height: 100%; width: 18px; margin-left: 4px; + cursor: col-resize; } -.bn-mantine .bn-extend-button-column { +.bn-mantine .bn-extend-button-add-remove-rows { height: 18px; width: 100%; margin-top: 4px; + cursor: row-resize; } /* Drag Handle & Table Handle Menu styling */ diff --git a/packages/mantine/src/tableHandle/ExtendButton.tsx b/packages/mantine/src/tableHandle/ExtendButton.tsx index a37023062..a145106ae 100644 --- a/packages/mantine/src/tableHandle/ExtendButton.tsx +++ b/packages/mantine/src/tableHandle/ExtendButton.tsx @@ -1,14 +1,14 @@ import { Button as MantineButton } from "@mantine/core"; -import { forwardRef } from "react"; -import { ComponentProps } from "@blocknote/react"; import { assertEmpty } from "@blocknote/core"; +import { ComponentProps } from "@blocknote/react"; +import { forwardRef } from "react"; export const ExtendButton = forwardRef< HTMLButtonElement, ComponentProps["TableHandle"]["ExtendButton"] >((props, ref) => { - const { children, className, onMouseDown, ...rest } = props; + const { children, className, onMouseDown, onClick, ...rest } = props; // false, because rest props can be added by mantine when button is used as a trigger // assertEmpty in this case is only used at typescript level, not runtime level @@ -19,6 +19,7 @@ export const ExtendButton = forwardRef< className={className} ref={ref} onMouseDown={onMouseDown} + onClick={onClick} {...rest}> {children} diff --git a/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx b/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx index 65e69943a..4ded8413a 100644 --- a/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx +++ b/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx @@ -1,6 +1,4 @@ import { - BlockFromConfigNoChildren, - DefaultBlockSchema, DefaultInlineContentSchema, DefaultStyleSchema, EMPTY_CELL_HEIGHT, @@ -15,6 +13,7 @@ import { ReactNode, useCallback, useEffect, + useRef, useState, } from "react"; import { RiAddFill } from "react-icons/ri"; @@ -22,19 +21,65 @@ 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"); +function cropEmptyRowsOrColumns< + I extends InlineContentSchema, + S extends StyleSchema +>( + content: PartialTableContent, + removeEmpty: "columns" | "rows" +): PartialTableContent { + let emptyColsOnRight = 0; + + if (removeEmpty === "columns") { + // strips empty columns to the right and empty rows at the bottom + for (let i = content.rows[0].cells.length - 1; i >= 0; i--) { + const isEmpty = content.rows.every((row) => row.cells[i].length === 0); + if (!isEmpty) { + break; + } + + emptyColsOnRight++; + } } - if (num < fraction) { - return Math.floor(num); + const rows: PartialTableContent["rows"] = []; + for (let i = content.rows.length - 1; i >= 0; i--) { + if (removeEmpty === "rows") { + if ( + rows.length === 0 && + content.rows[i].cells.every((cell) => cell.length === 0) + ) { + // empty row at bottom + continue; + } + } + + rows.unshift({ + cells: content.rows[i].cells.slice( + 0, + content.rows[0].cells.length - emptyColsOnRight + ), + }); } - return Math.ceil(num); + return { + ...content, + rows, + }; +} +// Rounds a number up or down, depending on whether we're close (as defined by +// `margin`) to the next integer. +const marginRound = (num: number, margin = 0.3) => { + const lowerBound = Math.floor(num) + margin; + const upperBound = Math.ceil(num) - margin; + + if (num >= lowerBound && num <= upperBound) { + return Math.round(num); + } else if (num < lowerBound) { + return Math.floor(num); + } else { + return Math.ceil(num); + } }; const getContentWithAddedRows = < @@ -51,7 +96,6 @@ const getContentWithAddedRows = < for (let i = 0; i < rowsToAdd; i++) { newRows.push(newRow); } - return { type: "tableContent", columnWidths: content.columnWidths, @@ -92,82 +136,123 @@ export const ExtendButton = < ) => { const Components = useComponentsContext()!; - const [editingState, setEditingState] = useState<{ - originalBlock: BlockFromConfigNoChildren; - startPos: number; - clickOnly: boolean; - } | null>(null); + // needs to be a ref because it's used immediately in the onClick handler + // (state would be async and only have effect after the next render + const movedMouse = useRef(false); + + const [editingState, setEditingState] = useState< + | { + originalContent: PartialTableContent; + originalCroppedContent: PartialTableContent; + startPos: number; + } + | undefined + >(); // Lets the user start extending columns/rows by moving the mouse. const mouseDownHandler = useCallback( (event: ReactMouseEvent) => { - props.freezeHandles(); + props.onMouseDown(); setEditingState({ - originalBlock: props.block, - startPos: props.orientation === "row" ? event.clientX : event.clientY, - clickOnly: true, + originalContent: props.block.content, + originalCroppedContent: cropEmptyRowsOrColumns( + props.block.content, + props.orientation === "addOrRemoveColumns" ? "columns" : "rows" + ), + startPos: + props.orientation === "addOrRemoveColumns" + ? event.clientX + : event.clientY, }); + movedMouse.current = false; + + // preventdefault, otherwise text in the table might be selected + event.preventDefault(); }, [props] ); + const onClickHandler = useCallback(() => { + if (movedMouse.current) { + return; + } + props.editor.updateBlock(props.block, { + type: "table", + content: + props.orientation === "addOrRemoveColumns" + ? getContentWithAddedCols(props.block.content) + : getContentWithAddedRows(props.block.content), + }); + }, [props.block, props.orientation, props.editor]); + // Extends columns/rows on when moving the mouse. useEffect(() => { const callback = (event: MouseEvent) => { - if (editingState === null) { - return; + // console.log("callback", event); + if (!editingState) { + throw new Error("editingState is undefined"); } + movedMouse.current = true; + const diff = - (props.orientation === "row" ? event.clientX : event.clientY) - - editingState.startPos; + (props.orientation === "addOrRemoveColumns" + ? event.clientX + : event.clientY) - editingState.startPos; + + const numCroppedCells = + props.orientation === "addOrRemoveColumns" + ? editingState.originalCroppedContent.rows[0].cells.length + : editingState.originalCroppedContent.rows.length; const numOriginalCells = - props.orientation === "row" - ? editingState.originalBlock.content.rows[0].cells.length - : editingState.originalBlock.content.rows.length; - const oldNumCells = - props.orientation === "row" + props.orientation === "addOrRemoveColumns" + ? editingState.originalContent.rows[0].cells.length + : editingState.originalContent.rows.length; + + const currentNumCells = + props.orientation === "addOrRemoveColumns" ? props.block.content.rows[0].cells.length : props.block.content.rows.length; + const newNumCells = numOriginalCells + - roundUpAt( + marginRound( diff / - (props.orientation === "row" + (props.orientation === "addOrRemoveColumns" ? EMPTY_CELL_WIDTH : EMPTY_CELL_HEIGHT), 0.3 ); - if (numOriginalCells <= newNumCells && newNumCells !== oldNumCells) { + if (newNumCells >= numCroppedCells && newNumCells !== currentNumCells) { props.editor.updateBlock(props.block, { type: "table", content: - props.orientation === "row" + props.orientation === "addOrRemoveColumns" ? getContentWithAddedCols( - editingState.originalBlock.content, - newNumCells - numOriginalCells + editingState.originalCroppedContent, + newNumCells - numCroppedCells ) : getContentWithAddedRows( - editingState.originalBlock.content, - newNumCells - numOriginalCells + editingState.originalCroppedContent, + newNumCells - numCroppedCells ), }); // 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 (editingState.originalBlock.content) { + if (props.block.content) { props.editor.setTextCursorPosition(props.block); } - setEditingState({ ...editingState, clickOnly: false }); } }; - document.body.addEventListener("mousemove", callback); - + if (editingState) { + window.addEventListener("mousemove", callback); + } return () => { - document.body.removeEventListener("mousemove", callback); + window.removeEventListener("mousemove", callback); }; }, [editingState, props.block, props.editor, props.orientation]); @@ -175,37 +260,32 @@ export const ExtendButton = < // released. Also extends columns/rows by 1 if the mouse wasn't moved enough // to add any, imitating a click. useEffect(() => { - const callback = () => { - if (editingState?.clickOnly) { - props.editor.updateBlock(props.block, { - type: "table", - content: - props.orientation === "row" - ? getContentWithAddedCols(editingState.originalBlock.content) - : getContentWithAddedRows(editingState.originalBlock.content), - }); - } + const onMouseUp = props.onMouseUp; - setEditingState(null); - props.unfreezeHandles(); + const callback = () => { + setEditingState(undefined); + onMouseUp(); }; - document.body.addEventListener("mouseup", callback); + if (editingState) { + window.addEventListener("mouseup", callback); + } return () => { - document.body.removeEventListener("mouseup", callback); + window.removeEventListener("mouseup", callback); }; - }, [editingState?.clickOnly, editingState?.originalBlock.content, props]); + }, [editingState, props.onMouseUp]); return ( {props.children || } diff --git a/packages/react/src/components/TableHandles/ExtendButton/ExtendButtonProps.ts b/packages/react/src/components/TableHandles/ExtendButton/ExtendButtonProps.ts index 8bb84d56e..cc473c289 100644 --- a/packages/react/src/components/TableHandles/ExtendButton/ExtendButtonProps.ts +++ b/packages/react/src/components/TableHandles/ExtendButton/ExtendButtonProps.ts @@ -1,16 +1,25 @@ import { + BlockNoteEditor, + DefaultBlockSchema, DefaultInlineContentSchema, DefaultStyleSchema, InlineContentSchema, StyleSchema, + TableHandlesState, } from "@blocknote/core"; -import { TableHandleProps } from "../TableHandleProps.js"; - export type ExtendButtonProps< I extends InlineContentSchema = DefaultInlineContentSchema, S extends StyleSchema = DefaultStyleSchema -> = Pick< - TableHandleProps, - "block" | "editor" | "orientation" | "freezeHandles" | "unfreezeHandles" ->; +> = { + editor: BlockNoteEditor< + { + table: DefaultBlockSchema["table"]; + }, + I, + S + >; + onMouseDown: () => void; + onMouseUp: () => void; + orientation: "addOrRemoveRows" | "addOrRemoveColumns"; +} & Pick, "block">; diff --git a/packages/react/src/components/TableHandles/TableHandle.tsx b/packages/react/src/components/TableHandles/TableHandle.tsx index ef1599f83..451de4b84 100644 --- a/packages/react/src/components/TableHandles/TableHandle.tsx +++ b/packages/react/src/components/TableHandles/TableHandle.tsx @@ -7,6 +7,7 @@ import { } from "@blocknote/core"; import { ReactNode, useState } from "react"; +import { createPortal } from "react-dom"; import { MdDragIndicator } from "react-icons/md"; import { useComponentsContext } from "../../editor/ComponentsContext.js"; import { TableHandleMenu } from "./TableHandleMenu/TableHandleMenu.js"; @@ -66,11 +67,15 @@ export const TableHandle = < )} - + {/* the menu can extend outside of the table, so we use a portal to prevent clipping */} + {createPortal( + , + props.menuContainer + )} ); }; diff --git a/packages/react/src/components/TableHandles/TableHandleProps.ts b/packages/react/src/components/TableHandles/TableHandleProps.ts index e961ddb53..c7c2272df 100644 --- a/packages/react/src/components/TableHandles/TableHandleProps.ts +++ b/packages/react/src/components/TableHandles/TableHandleProps.ts @@ -29,6 +29,7 @@ export type TableHandleProps< dragStart: (e: DragEvent) => void; showOtherSide: () => void; hideOtherSide: () => void; + menuContainer: HTMLDivElement; tableHandleMenu?: FC< DragHandleMenuProps< { diff --git a/packages/react/src/components/TableHandles/TableHandlesController.tsx b/packages/react/src/components/TableHandles/TableHandlesController.tsx index 223a88857..d5eba7310 100644 --- a/packages/react/src/components/TableHandles/TableHandlesController.tsx +++ b/packages/react/src/components/TableHandles/TableHandlesController.tsx @@ -5,30 +5,30 @@ import { InlineContentSchema, StyleSchema, } from "@blocknote/core"; -import { FC, useMemo, useState } from "react"; +import { FC, useCallback, useMemo, useState } from "react"; +import { FloatingPortal } from "@floating-ui/react"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.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 { ExtendButtonProps } from "./ExtendButton/ExtendButtonProps.js"; import { TableHandle } from "./TableHandle.js"; import { TableHandleProps } from "./TableHandleProps.js"; +import { useExtendButtonsPositioning } from "./hooks/useExtendButtonsPositioning.js"; +import { useTableHandlesPositioning } from "./hooks/useTableHandlesPositioning.js"; export const TableHandlesController = < I extends InlineContentSchema = DefaultInlineContentSchema, S extends StyleSchema = DefaultStyleSchema >(props: { tableHandle?: FC>; - extendButton?: FC< - Pick< - TableHandleProps, - "block" | "editor" | "orientation" | "freezeHandles" | "unfreezeHandles" - > - >; + extendButton?: FC>; }) => { const editor = useBlockNoteEditor(); + const [menuContainerRef, setMenuContainerRef] = + useState(null); + if (!editor.tableHandles) { throw new Error( "TableHandlesController can only be used when BlockNote editor schema contains table block" @@ -43,6 +43,20 @@ export const TableHandlesController = < unfreezeHandles: editor.tableHandles.unfreezeHandles, }; + const { freezeHandles, unfreezeHandles } = callbacks; + + const onStartExtend = useCallback(() => { + freezeHandles(); + setHideCol(true); + setHideRow(true); + }, [freezeHandles]); + + const onEndExtend = useCallback(() => { + unfreezeHandles(); + setHideCol(false); + setHideRow(false); + }, [unfreezeHandles]); + const state = useUIPluginState( editor.tableHandles.onUpdate.bind(editor.tableHandles) ); @@ -88,61 +102,69 @@ export const TableHandlesController = < return ( <> - {!hideRow && ( -
- setHideCol(false)} - hideOtherSide={() => setHideCol(true)} - index={state.rowIndex} - block={state.block} - dragStart={callbacks.rowDragStart} - dragEnd={callbacks.dragEnd} - freezeHandles={callbacks.freezeHandles} - unfreezeHandles={callbacks.unfreezeHandles} - /> -
- )} - {!hideCol && ( -
- setHideRow(false)} - hideOtherSide={() => setHideRow(true)} - index={state.colIndex} - block={state.block} - dragStart={callbacks.colDragStart} - dragEnd={callbacks.dragEnd} - freezeHandles={callbacks.freezeHandles} - unfreezeHandles={callbacks.unfreezeHandles} - /> -
- )} - {!draggingState && !hideCol && !hideRow && ( - <> -
-
+ {/* we want to make sure the elements are clipped by the .tableWrapper element (so that we scroll the table, widgets also dissappear) + we do this by rendering in a portal into the table's widget container (defined in TableBlockContent.ts) + */} + + {!hideRow && menuContainerRef && ( +
+ setHideCol(false)} + hideOtherSide={() => setHideCol(true)} + index={state.rowIndex} block={state.block} + dragStart={callbacks.rowDragStart} + dragEnd={callbacks.dragEnd} freezeHandles={callbacks.freezeHandles} unfreezeHandles={callbacks.unfreezeHandles} + menuContainer={menuContainerRef} />
-
- + setHideRow(false)} + hideOtherSide={() => setHideRow(true)} + index={state.colIndex} block={state.block} + dragStart={callbacks.colDragStart} + dragEnd={callbacks.dragEnd} freezeHandles={callbacks.freezeHandles} unfreezeHandles={callbacks.unfreezeHandles} + menuContainer={menuContainerRef} />
- - )} + )} + {!draggingState && ( + <> +
+ +
+
+ +
+ + )} +
); }; diff --git a/packages/react/src/components/TableHandles/hooks/useExtendButtonsPositioning.ts b/packages/react/src/components/TableHandles/hooks/useExtendButtonsPositioning.ts index deb70251a..40d61d76e 100644 --- a/packages/react/src/components/TableHandles/hooks/useExtendButtonsPositioning.ts +++ b/packages/react/src/components/TableHandles/hooks/useExtendButtonsPositioning.ts @@ -57,7 +57,6 @@ function useExtendButtonPosition( display: "flex", ...styles, ...floatingStyles, - zIndex: 10000, }, }), [floatingStyles, isMounted, refs.setFloating, styles] diff --git a/packages/react/src/components/TableHandles/hooks/useTableHandlesPositioning.ts b/packages/react/src/components/TableHandles/hooks/useTableHandlesPositioning.ts index 4bd7083fd..44cea9116 100644 --- a/packages/react/src/components/TableHandles/hooks/useTableHandlesPositioning.ts +++ b/packages/react/src/components/TableHandles/hooks/useTableHandlesPositioning.ts @@ -98,7 +98,6 @@ function useTableHandlePosition( display: "flex", ...styles, ...floatingStyles, - zIndex: 10000, }, }), [floatingStyles, isMounted, refs.setFloating, styles] diff --git a/packages/react/src/editor/ComponentsContext.tsx b/packages/react/src/editor/ComponentsContext.tsx index aa63f3132..123264a19 100644 --- a/packages/react/src/editor/ComponentsContext.tsx +++ b/packages/react/src/editor/ComponentsContext.tsx @@ -185,6 +185,7 @@ export type ComponentProps = { ); ExtendButton: { className?: string; + onClick: (e: React.MouseEvent) => void; onMouseDown: (e: React.MouseEvent) => void; children: ReactNode; }; diff --git a/packages/shadcn/src/tableHandle/ExtendButton.tsx b/packages/shadcn/src/tableHandle/ExtendButton.tsx index 81fa61805..76b9eca6a 100644 --- a/packages/shadcn/src/tableHandle/ExtendButton.tsx +++ b/packages/shadcn/src/tableHandle/ExtendButton.tsx @@ -9,7 +9,7 @@ export const ExtendButton = forwardRef< HTMLButtonElement, ComponentProps["TableHandle"]["ExtendButton"] >((props, ref) => { - const { className, children, onMouseDown, ...rest } = props; + const { className, children, onMouseDown, onClick, ...rest } = props; // false, because rest props can be added by shadcn when button is used as a trigger // assertEmpty in this case is only used at typescript level, not runtime level @@ -23,12 +23,15 @@ export const ExtendButton = forwardRef< className={cn( className, "bn-p-0 bn-h-full bn-w-full bn-text-gray-400", - className?.includes("bn-extend-button-row") ? "bn-ml-1" : "bn-mt-1", + className?.includes("bn-extend-button-add-remove-columns") + ? "bn-ml-1" + : "bn-mt-1", className?.includes("bn-extend-button-editing") ? "bn-bg-accent bn-text-accent-foreground" : "" )} ref={ref} + onClick={onClick} onMouseDown={onMouseDown} {...rest}> {children} From 8352718ec9dde8dfad08d719a936adf4610a408f Mon Sep 17 00:00:00 2001 From: yousefed Date: Fri, 25 Oct 2024 13:51:38 +0200 Subject: [PATCH 10/15] prosemirror-tables upgrade --- package-lock.json | 8 ++++---- packages/core/package.json | 2 +- .../src/blocks/TableBlockContent/TableBlockContent.ts | 4 ++-- .../core/src/blocks/TableBlockContent/TableExtension.ts | 5 +++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index db74f1351..42cb35750 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23294,9 +23294,9 @@ } }, "node_modules/prosemirror-tables": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.5.0.tgz", - "integrity": "sha512-VMx4zlYWm7aBlZ5xtfJHpqa3Xgu3b7srV54fXYnXgsAcIGRqKSrhiK3f89omzzgaAgAtDOV4ImXnLKhVfheVNQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.6.0.tgz", + "integrity": "sha512-eirSS2fwVYzKhvM2qeXSn9ix/SBn7QOLDftPQ4ImEQIevFDiSKAB6Lbrmm/WEgrbTDbCm+xhSq4gOD9w7wT59Q==", "dependencies": { "prosemirror-keymap": "^1.1.2", "prosemirror-model": "^1.8.1", @@ -28554,7 +28554,7 @@ "hast-util-from-dom": "^4.2.0", "prosemirror-model": "^1.21.0", "prosemirror-state": "^1.4.3", - "prosemirror-tables": "^1.3.7", + "prosemirror-tables": "^1.6.0", "prosemirror-transform": "^1.9.0", "prosemirror-view": "^1.33.7", "rehype-format": "^5.0.0", diff --git a/packages/core/package.json b/packages/core/package.json index 62667072a..a453afab1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -82,7 +82,7 @@ "hast-util-from-dom": "^4.2.0", "prosemirror-model": "^1.21.0", "prosemirror-state": "^1.4.3", - "prosemirror-tables": "^1.3.7", + "prosemirror-tables": "^1.6.0", "prosemirror-transform": "^1.9.0", "prosemirror-view": "^1.33.7", "rehype-format": "^5.0.0", diff --git a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts index ca234f4d9..ca9190297 100644 --- a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts +++ b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts @@ -12,7 +12,7 @@ import { import { mergeCSSClasses } from "../../util/browser.js"; import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers.js"; import { defaultProps } from "../defaultProps.js"; -import { MIN_CELL_WIDTH, TableExtension } from "./TableExtension.js"; +import { EMPTY_CELL_WIDTH, TableExtension } from "./TableExtension.js"; export const tablePropSchema = { backgroundColor: defaultProps.backgroundColor, @@ -99,7 +99,7 @@ export const TableBlockContent = createStronglyTypedTiptapNode({ } } - return new BlockNoteTableView(node, MIN_CELL_WIDTH, { + return new BlockNoteTableView(node, EMPTY_CELL_WIDTH, { ...(this.options.domAttributes?.blockContent || {}), ...HTMLAttributes, }); diff --git a/packages/core/src/blocks/TableBlockContent/TableExtension.ts b/packages/core/src/blocks/TableBlockContent/TableExtension.ts index 793be3ed0..cd197a139 100644 --- a/packages/core/src/blocks/TableBlockContent/TableExtension.ts +++ b/packages/core/src/blocks/TableBlockContent/TableExtension.ts @@ -1,7 +1,7 @@ import { callOrReturn, Extension, getExtensionField } from "@tiptap/core"; import { columnResizing, tableEditing } from "prosemirror-tables"; -export const MIN_CELL_WIDTH = 35; +export const RESIZE_MIN_WIDTH = 35; export const EMPTY_CELL_WIDTH = 120; export const EMPTY_CELL_HEIGHT = 31; @@ -11,7 +11,8 @@ export const TableExtension = Extension.create({ addProseMirrorPlugins: () => { return [ columnResizing({ - cellMinWidth: MIN_CELL_WIDTH, + cellMinWidth: RESIZE_MIN_WIDTH, + defaultCellMinWidth: EMPTY_CELL_WIDTH, // We set this to null as we implement our own node view in the table // block content. This node view is the same as what's used by default, // but is wrapped in a `blockContent` HTML element. From 12270e3e50ab9dd4768c778bc4129cbb8be25e81 Mon Sep 17 00:00:00 2001 From: yousefed Date: Fri, 25 Oct 2024 13:51:49 +0200 Subject: [PATCH 11/15] show buttons when to right / bottom of table --- .../TableHandles/TableHandlesPlugin.ts | 224 ++++++++++-------- .../TableHandles/TableHandlesController.tsx | 8 +- .../hooks/useExtendButtonsPositioning.ts | 22 +- 3 files changed, 136 insertions(+), 118 deletions(-) diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index 4952ec9ab..90640e3c2 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -1,6 +1,7 @@ import { Plugin, PluginKey, PluginView } from "prosemirror-state"; import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; import { nodeToBlock } from "../../api/nodeConversions/nodeToBlock.js"; +import { getNodeById } from "../../api/nodeUtil.js"; import { checkBlockIsDefaultType } from "../../blocks/defaultBlockTypeGuards.js"; import { DefaultBlockSchema } from "../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; @@ -22,12 +23,12 @@ export type TableHandlesState< show: boolean; showExtendButtonRow: boolean; showExtendButtonCol: boolean; - referencePosCell: DOMRect; + referencePosCell: DOMRect | undefined; referencePosTable: DOMRect; block: BlockFromConfigNoChildren; - colIndex: number; - rowIndex: number; + colIndex: number | undefined; + rowIndex: number | undefined; draggingState: | { @@ -93,10 +94,12 @@ function domCellAround(target: Element | undefined) { ? { type: "cell", domNode: target, + tbodyNode: target.closest("tbody"), } : { type: "wrapper", domNode: target, + tbodyNode: target.querySelector("tbody"), }; } @@ -174,6 +177,10 @@ export class TableHandlesView< return; } + if (this.mouseState === "selecting") { + return; + } + const target = domCellAround(event.target as HTMLElement); if ( @@ -193,14 +200,6 @@ export class TableHandlesView< return; } - if (this.mouseState === "selecting") { - return; - } - - if (target?.type === "wrapper") { - return; - } - if (!target || !this.editor.isEditable) { if (this.state?.show) { this.state.show = false; @@ -211,87 +210,99 @@ export class TableHandlesView< return; } - const colIndex = getChildIndex(target.domNode); - const rowIndex = getChildIndex(target.domNode.parentElement!); - const cellRect = target.domNode.getBoundingClientRect(); - - const tableRect = target.domNode.closest("tbody")!.getBoundingClientRect(); - - if (!tableRect) { + if (!target.tbodyNode) { return; } + const tableRect = target.tbodyNode.getBoundingClientRect(); + const blockEl = getDraggableBlockFromElement(target.domNode, this.pmView); if (!blockEl) { return; } - this.tableElement = blockEl.node; // TODO: needed? + this.tableElement = blockEl.node; 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. - this.editor._tiptapEditor.state.doc.descendants((node, pos) => { - if (typeof tableBlock !== "undefined") { - return false; - } - - if (node.type.name !== "blockContainer" || node.attrs.id !== blockEl.id) { - return true; - } - - const block = nodeToBlock( - node, - this.editor.schema.blockSchema, - this.editor.schema.inlineContentSchema, - this.editor.schema.styleSchema, - this.editor.blockCache - ); + const pmNodeInfo = getNodeById( + blockEl.id, + this.editor._tiptapEditor.state.doc + ); - if (checkBlockIsDefaultType("table", block, this.editor)) { - this.tablePos = pos + 1; - tableBlock = block; - } + const block = nodeToBlock( + pmNodeInfo.node, + this.editor.schema.blockSchema, + this.editor.schema.inlineContentSchema, + this.editor.schema.styleSchema, + this.editor.blockCache + ); - return false; - }); + if (checkBlockIsDefaultType("table", block, this.editor)) { + this.tablePos = pmNodeInfo.posBeforeNode + 1; + tableBlock = block; + } if (!tableBlock) { return; } this.tableId = blockEl.id; + const widgetContainer = target.domNode + .closest(".tableWrapper") + ?.querySelector(".table-widgets-container") as HTMLElement; - if ( - this.state !== undefined && - this.state.show && - this.tableId === blockEl.id && - this.state.rowIndex === rowIndex && - this.state.colIndex === colIndex - ) { - return; - } + if (target?.type === "wrapper") { + // if we're just to the right or below the table, show the extend buttons + // (this is a bit hacky) + const belowTable = + event.clientY > tableRect.bottom && + event.clientY < tableRect.bottom + 20; + const toRightOfTable = + event.clientX > tableRect.right && event.clientX < tableRect.right + 20; + + this.state = { + ...this.state!, + show: true, + showExtendButtonRow: toRightOfTable, + showExtendButtonCol: belowTable, + referencePosTable: tableRect, + block: tableBlock, + widgetContainer, + }; + } else { + const colIndex = getChildIndex(target.domNode); + const rowIndex = getChildIndex(target.domNode.parentElement!); + const cellRect = target.domNode.getBoundingClientRect(); + + if ( + this.state !== undefined && + this.state.show && + this.tableId === blockEl.id && + this.state.rowIndex === rowIndex && + this.state.colIndex === colIndex + ) { + // no update needed + return; + } - this.state = { - show: true, - showExtendButtonRow: - colIndex === tableBlock.content.rows[0].cells.length - 1, - showExtendButtonCol: rowIndex === tableBlock.content.rows.length - 1, - referencePosCell: cellRect, - referencePosTable: tableRect, - - block: tableBlock, - colIndex: colIndex, - rowIndex: rowIndex, - - draggingState: undefined, - widgetContainer: - target.domNode - .closest(".tableWrapper") - ?.querySelector(".table-widgets-container") || undefined, - }; + this.state = { + show: true, + showExtendButtonRow: + colIndex === tableBlock.content.rows[0].cells.length - 1, + showExtendButtonCol: rowIndex === tableBlock.content.rows.length - 1, + referencePosTable: tableRect, + + block: tableBlock, + draggingState: undefined, + referencePosCell: cellRect, + colIndex: colIndex, + rowIndex: rowIndex, + + widgetContainer, + }; + } this.emitUpdate(); return false; @@ -402,21 +413,32 @@ export class TableHandlesView< return; } + if ( + this.state.rowIndex === undefined || + this.state.colIndex === undefined + ) { + throw new Error( + "Attempted to drop table row or column, but no table block was hovered prior." + ); + } + event.preventDefault(); + const { draggingState, colIndex, rowIndex } = this.state; + const rows = this.state.block.content.rows; - if (this.state.draggingState.draggedCellOrientation === "row") { - const rowToMove = rows[this.state.draggingState.originalIndex]; - rows.splice(this.state.draggingState.originalIndex, 1); - rows.splice(this.state.rowIndex, 0, rowToMove); + if (draggingState.draggedCellOrientation === "row") { + const rowToMove = rows[draggingState.originalIndex]; + rows.splice(draggingState.originalIndex, 1); + rows.splice(rowIndex, 0, rowToMove); } else { const cellsToMove = rows.map( - (row) => row.cells[this.state!.draggingState!.originalIndex] + (row) => row.cells[draggingState.originalIndex] ); rows.forEach((row, rowIndex) => { - row.cells.splice(this.state!.draggingState!.originalIndex, 1); - row.cells.splice(this.state!.colIndex, 0, cellsToMove[rowIndex]); + row.cells.splice(draggingState.originalIndex, 1); + row.cells.splice(colIndex, 0, cellsToMove[rowIndex]); }); } @@ -443,28 +465,26 @@ export class TableHandlesView< 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; - } - if (this.state.colIndex >= tableBody.children[0].children.length) { - this.state.colIndex = tableBody.children[0].children.length - 1; - this.emitUpdate(); + if ( + this.state.rowIndex !== undefined && + this.state.colIndex !== undefined + ) { + // 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; + } + if (this.state.colIndex >= tableBody.children[0].children.length) { + this.state.colIndex = tableBody.children[0].children.length - 1; + } - return; + const row = tableBody.children[this.state.rowIndex]; + const cell = row.children[this.state.colIndex]; + this.state.referencePosCell = cell.getBoundingClientRect(); } - 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(); } @@ -525,6 +545,10 @@ export class TableHandlesProsemirrorPlugin< ? this.view.state.rowIndex : this.view.state.colIndex; + if (!newIndex) { + return; + } + const decorations: Decoration[] = []; if (newIndex === this.view.state.draggingState.originalIndex) { @@ -650,7 +674,10 @@ export class TableHandlesProsemirrorPlugin< dataTransfer: DataTransfer | null; clientX: number; }) => { - if (this.view!.state === undefined) { + if ( + this.view!.state === undefined || + this.view!.state.colIndex === undefined + ) { throw new Error( "Attempted to drag table column, but no table block was hovered prior." ); @@ -686,7 +713,10 @@ export class TableHandlesProsemirrorPlugin< dataTransfer: DataTransfer | null; clientY: number; }) => { - if (this.view!.state === undefined) { + if ( + this.view!.state === undefined || + this.view!.state.rowIndex === undefined + ) { throw new Error( "Attempted to drag table row, but no table block was hovered prior." ); diff --git a/packages/react/src/components/TableHandles/TableHandlesController.tsx b/packages/react/src/components/TableHandles/TableHandlesController.tsx index d5eba7310..7636f82c5 100644 --- a/packages/react/src/components/TableHandles/TableHandlesController.tsx +++ b/packages/react/src/components/TableHandles/TableHandlesController.tsx @@ -85,9 +85,7 @@ export const TableHandlesController = < const { rowExtendButton, colExtendButton } = useExtendButtonsPositioning( state?.showExtendButtonRow || false, state?.showExtendButtonCol || false, - state?.referencePosCell || null, - state?.referencePosTable || null, - draggingState + state?.referencePosTable || null ); const [hideRow, setHideRow] = useState(false); @@ -107,7 +105,7 @@ export const TableHandlesController = < we do this by rendering in a portal into the table's widget container (defined in TableBlockContent.ts) */} - {!hideRow && menuContainerRef && ( + {!hideRow && menuContainerRef && state.rowIndex !== undefined && (
)} - {!hideCol && menuContainerRef && ( + {!hideCol && menuContainerRef && state.colIndex !== undefined && (
{ update(); - }, [referencePosCell, referencePosTable, update]); + }, [referencePosTable, update]); useEffect(() => { // Will be null on initial render when used in UI component controllers. - if (referencePosCell === null || referencePosTable === null) { + if (referencePosTable === null) { return; } refs.setReference({ getBoundingClientRect: () => referencePosTable, }); - }, [draggingState, orientation, referencePosCell, referencePosTable, refs]); + }, [orientation, referencePosTable, refs]); return useMemo( () => ({ @@ -66,7 +61,6 @@ function useExtendButtonPosition( export function useExtendButtonsPositioning( showExtendButtonRow: boolean, showExtendButtonCol: boolean, - referencePosCell: DOMRect | null, referencePosTable: DOMRect | null, draggingState?: { draggedCellOrientation: "row" | "col"; @@ -79,16 +73,12 @@ export function useExtendButtonsPositioning( const rowExtendButton = useExtendButtonPosition( "row", showExtendButtonRow, - referencePosCell, - referencePosTable, - draggingState + referencePosTable ); const colExtendButton = useExtendButtonPosition( "col", showExtendButtonCol, - referencePosCell, - referencePosTable, - draggingState + referencePosTable ); return useMemo( From ade2067b69470fa252560f35844870bb58444333 Mon Sep 17 00:00:00 2001 From: yousefed Date: Fri, 25 Oct 2024 14:02:34 +0200 Subject: [PATCH 12/15] fix lint --- .../TableHandles/hooks/useExtendButtonsPositioning.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/react/src/components/TableHandles/hooks/useExtendButtonsPositioning.ts b/packages/react/src/components/TableHandles/hooks/useExtendButtonsPositioning.ts index 35d705b0a..a41da7959 100644 --- a/packages/react/src/components/TableHandles/hooks/useExtendButtonsPositioning.ts +++ b/packages/react/src/components/TableHandles/hooks/useExtendButtonsPositioning.ts @@ -61,11 +61,7 @@ function useExtendButtonPosition( export function useExtendButtonsPositioning( showExtendButtonRow: boolean, showExtendButtonCol: boolean, - referencePosTable: DOMRect | null, - draggingState?: { - draggedCellOrientation: "row" | "col"; - mousePos: number; - } + referencePosTable: DOMRect | null ): { rowExtendButton: ReturnType; colExtendButton: ReturnType; From 0eb643b05f01f58bbbc473b5cdcc586846a272e1 Mon Sep 17 00:00:00 2001 From: yousefed Date: Fri, 25 Oct 2024 21:03:31 +0200 Subject: [PATCH 13/15] fix names --- .../TableHandles/TableHandlesPlugin.ts | 21 +-- .../TableHandles/TableHandlesController.tsx | 121 ++++++++++-------- .../hooks/useExtendButtonsPositioning.ts | 32 ++--- 3 files changed, 95 insertions(+), 79 deletions(-) diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index 90640e3c2..032c7f2f5 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -21,8 +21,8 @@ export type TableHandlesState< S extends StyleSchema > = { show: boolean; - showExtendButtonRow: boolean; - showExtendButtonCol: boolean; + showAddOrRemoveRowsButton: boolean; + showAddOrRemoveColumnsButton: boolean; referencePosCell: DOMRect | undefined; referencePosTable: DOMRect; @@ -193,8 +193,8 @@ export class TableHandlesView< if (this.state?.show) { this.state.show = false; - this.state.showExtendButtonRow = false; - this.state.showExtendButtonCol = false; + this.state.showAddOrRemoveRowsButton = false; + this.state.showAddOrRemoveColumnsButton = false; this.emitUpdate(); } return; @@ -203,8 +203,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.state.showAddOrRemoveRowsButton = false; + this.state.showAddOrRemoveColumnsButton = false; this.emitUpdate(); } return; @@ -265,8 +265,8 @@ export class TableHandlesView< this.state = { ...this.state!, show: true, - showExtendButtonRow: toRightOfTable, - showExtendButtonCol: belowTable, + showAddOrRemoveRowsButton: belowTable, + showAddOrRemoveColumnsButton: toRightOfTable, referencePosTable: tableRect, block: tableBlock, widgetContainer, @@ -289,9 +289,10 @@ export class TableHandlesView< this.state = { show: true, - showExtendButtonRow: + showAddOrRemoveColumnsButton: colIndex === tableBlock.content.rows[0].cells.length - 1, - showExtendButtonCol: rowIndex === tableBlock.content.rows.length - 1, + showAddOrRemoveRowsButton: + rowIndex === tableBlock.content.rows.length - 1, referencePosTable: tableRect, block: tableBlock, diff --git a/packages/react/src/components/TableHandles/TableHandlesController.tsx b/packages/react/src/components/TableHandles/TableHandlesController.tsx index 7636f82c5..3de88080d 100644 --- a/packages/react/src/components/TableHandles/TableHandlesController.tsx +++ b/packages/react/src/components/TableHandles/TableHandlesController.tsx @@ -82,16 +82,17 @@ export const TableHandlesController = < draggingState ); - const { rowExtendButton, colExtendButton } = useExtendButtonsPositioning( - state?.showExtendButtonRow || false, - state?.showExtendButtonCol || false, - state?.referencePosTable || null - ); + const { addOrRemoveColumnsButton, addOrRemoveRowsButton } = + useExtendButtonsPositioning( + state?.showAddOrRemoveColumnsButton || false, + state?.showAddOrRemoveRowsButton || false, + state?.referencePosTable || null + ); const [hideRow, setHideRow] = useState(false); const [hideCol, setHideCol] = useState(false); - if (!rowHandle.isMounted || !colHandle.isMounted || !state) { + if (!state) { return null; } @@ -105,61 +106,75 @@ export const TableHandlesController = < we do this by rendering in a portal into the table's widget container (defined in TableBlockContent.ts) */} - {!hideRow && menuContainerRef && state.rowIndex !== undefined && ( -
- setHideCol(false)} - hideOtherSide={() => setHideCol(true)} - index={state.rowIndex} - block={state.block} - dragStart={callbacks.rowDragStart} - dragEnd={callbacks.dragEnd} - freezeHandles={callbacks.freezeHandles} - unfreezeHandles={callbacks.unfreezeHandles} - menuContainer={menuContainerRef} - /> -
- )} - {!hideCol && menuContainerRef && state.colIndex !== undefined && ( -
- setHideRow(false)} - hideOtherSide={() => setHideRow(true)} - index={state.colIndex} - block={state.block} - dragStart={callbacks.colDragStart} - dragEnd={callbacks.dragEnd} - freezeHandles={callbacks.freezeHandles} - unfreezeHandles={callbacks.unfreezeHandles} - menuContainer={menuContainerRef} - /> -
- )} - {!draggingState && ( - <> -
- + setHideCol(false)} + hideOtherSide={() => setHideCol(true)} + index={state.rowIndex} block={state.block} - onMouseDown={onStartExtend} - onMouseUp={onEndExtend} + dragStart={callbacks.rowDragStart} + dragEnd={callbacks.dragEnd} + freezeHandles={callbacks.freezeHandles} + unfreezeHandles={callbacks.unfreezeHandles} + menuContainer={menuContainerRef} />
-
- + setHideRow(false)} + hideOtherSide={() => setHideRow(true)} + index={state.colIndex} block={state.block} - onMouseDown={onStartExtend} - onMouseUp={onEndExtend} + dragStart={callbacks.colDragStart} + dragEnd={callbacks.dragEnd} + freezeHandles={callbacks.freezeHandles} + unfreezeHandles={callbacks.unfreezeHandles} + menuContainer={menuContainerRef} />
+ )} + {!draggingState && ( + <> + {addOrRemoveRowsButton.isMounted && ( +
+ +
+ )} + {addOrRemoveColumnsButton.isMounted && ( +
+ +
+ )} )}
diff --git a/packages/react/src/components/TableHandles/hooks/useExtendButtonsPositioning.ts b/packages/react/src/components/TableHandles/hooks/useExtendButtonsPositioning.ts index a41da7959..f102b6c64 100644 --- a/packages/react/src/components/TableHandles/hooks/useExtendButtonsPositioning.ts +++ b/packages/react/src/components/TableHandles/hooks/useExtendButtonsPositioning.ts @@ -2,19 +2,19 @@ import { size, useFloating, useTransitionStyles } from "@floating-ui/react"; import { useEffect, useMemo } from "react"; function useExtendButtonPosition( - orientation: "row" | "col", + orientation: "addOrRemoveRows" | "addOrRemoveColumns", show: boolean, referencePosTable: DOMRect | null ) { const { refs, update, context, floatingStyles } = useFloating({ open: show, - placement: orientation === "row" ? "right" : "bottom", + placement: orientation === "addOrRemoveColumns" ? "right" : "bottom", middleware: [ size({ apply({ rects, elements }) { Object.assign( elements.floating.style, - orientation === "row" + orientation === "addOrRemoveColumns" ? { height: `${rects.reference.height}px`, } @@ -59,29 +59,29 @@ function useExtendButtonPosition( } export function useExtendButtonsPositioning( - showExtendButtonRow: boolean, - showExtendButtonCol: boolean, + showAddOrRemoveColumnsButton: boolean, + showAddOrRemoveRowsButton: boolean, referencePosTable: DOMRect | null ): { - rowExtendButton: ReturnType; - colExtendButton: ReturnType; + addOrRemoveRowsButton: ReturnType; + addOrRemoveColumnsButton: ReturnType; } { - const rowExtendButton = useExtendButtonPosition( - "row", - showExtendButtonRow, + const addOrRemoveRowsButton = useExtendButtonPosition( + "addOrRemoveRows", + showAddOrRemoveRowsButton, referencePosTable ); - const colExtendButton = useExtendButtonPosition( - "col", - showExtendButtonCol, + const addOrRemoveColumnsButton = useExtendButtonPosition( + "addOrRemoveColumns", + showAddOrRemoveColumnsButton, referencePosTable ); return useMemo( () => ({ - rowExtendButton, - colExtendButton, + addOrRemoveRowsButton, + addOrRemoveColumnsButton, }), - [colExtendButton, rowExtendButton] + [addOrRemoveColumnsButton, addOrRemoveRowsButton] ); } From 6b74eb85428ce592ea89a148cdbcb95d701d9247 Mon Sep 17 00:00:00 2001 From: yousefed Date: Fri, 25 Oct 2024 21:13:22 +0200 Subject: [PATCH 14/15] fix small bugs --- .../TableHandles/TableHandlesPlugin.ts | 6 +- .../TableHandles/TableHandlesController.tsx | 55 +++++++++---------- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index 032c7f2f5..9693fc6b5 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -154,7 +154,7 @@ export class TableHandlesView< pmView.dom.addEventListener("mousemove", this.mouseMoveHandler); pmView.dom.addEventListener("mousedown", this.viewMousedownHandler); - pmView.dom.addEventListener("mouseup", this.viewMouseupHandler); + window.addEventListener("mouseup", this.mouseUpHandler); pmView.root.addEventListener( "dragover", @@ -167,7 +167,7 @@ export class TableHandlesView< this.mouseState = "down"; }; - viewMouseupHandler = (event: MouseEvent) => { + mouseUpHandler = (event: MouseEvent) => { this.mouseState = "up"; this.mouseMoveHandler(event); }; @@ -492,6 +492,8 @@ export class TableHandlesView< destroy() { this.pmView.dom.removeEventListener("mousemove", this.mouseMoveHandler); + window.removeEventListener("mouseup", this.mouseUpHandler); + this.pmView.dom.removeEventListener("mousedown", this.viewMousedownHandler); this.pmView.root.removeEventListener( "dragover", this.dragOverHandler as EventListener diff --git a/packages/react/src/components/TableHandles/TableHandlesController.tsx b/packages/react/src/components/TableHandles/TableHandlesController.tsx index 3de88080d..31ccecf4d 100644 --- a/packages/react/src/components/TableHandles/TableHandlesController.tsx +++ b/packages/react/src/components/TableHandles/TableHandlesController.tsx @@ -147,36 +147,31 @@ export const TableHandlesController = < />
)} - {!draggingState && ( - <> - {addOrRemoveRowsButton.isMounted && ( -
- -
- )} - {addOrRemoveColumnsButton.isMounted && ( -
- -
- )} - - )} + + {/* note that the extend buttons are always shown (we don't look at isMounted etc, + because otherwise the table slightly shifts when they unmount */} +
+ +
+
+ +
); From d750891f7b3809085555e4f4150973a1149bec1b Mon Sep 17 00:00:00 2001 From: yousefed Date: Fri, 25 Oct 2024 21:58:19 +0200 Subject: [PATCH 15/15] fix drag handle --- .../TableHandles/TableHandlesPlugin.ts | 78 ++++++++++--------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index 9693fc6b5..a563a6e98 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -75,46 +75,42 @@ function getChildIndex(node: Element) { // Finds the DOM element corresponding to the table cell that the target element // is currently in. If the target element is not in a table cell, returns null. -function domCellAround(target: Element | undefined) { +function domCellAround(target: Element) { + let currentTarget: Element | undefined = target; while ( - target && - target.nodeName !== "TD" && - target.nodeName !== "TH" && - !target.classList.contains("tableWrapper") + currentTarget && + currentTarget.nodeName !== "TD" && + currentTarget.nodeName !== "TH" && + !currentTarget.classList.contains("tableWrapper") ) { - target = - target.classList && target.classList.contains("ProseMirror") + currentTarget = + currentTarget.classList && currentTarget.classList.contains("ProseMirror") ? undefined - : (target.parentNode as Element); + : (currentTarget.parentNode as Element); } - if (!target) { + if (!currentTarget) { return undefined; } - return target?.nodeName === "TD" || target?.nodeName === "TH" + return currentTarget?.nodeName === "TD" || currentTarget?.nodeName === "TH" ? { type: "cell", - domNode: target, - tbodyNode: target.closest("tbody"), + domNode: currentTarget, + tbodyNode: currentTarget.closest("tbody"), } : { type: "wrapper", - domNode: target, - tbodyNode: target.querySelector("tbody"), + domNode: currentTarget, + tbodyNode: currentTarget.querySelector("tbody"), }; } // Hides elements in the DOMwith the provided class names. -function hideElementsWithClassNames( - classNames: string[], - rootEl: Document | ShadowRoot -) { - classNames.forEach((className) => { - const elementsToHide = rootEl.querySelectorAll(className); - - for (let i = 0; i < elementsToHide.length; i++) { - (elementsToHide[i] as HTMLElement).style.visibility = "hidden"; - } - }); +function hideElements(selector: string, rootEl: Document | ShadowRoot) { + const elementsToHide = rootEl.querySelectorAll(selector); + + for (let i = 0; i < elementsToHide.length; i++) { + (elementsToHide[i] as HTMLElement).style.visibility = "hidden"; + } } export class TableHandlesView< @@ -181,7 +177,11 @@ export class TableHandlesView< return; } - const target = domCellAround(event.target as HTMLElement); + if (!(event.target instanceof Element)) { + return; + } + + const target = domCellAround(event.target); if ( target?.type === "cell" && @@ -257,10 +257,15 @@ export class TableHandlesView< // if we're just to the right or below the table, show the extend buttons // (this is a bit hacky) const belowTable = - event.clientY > tableRect.bottom && + event.clientY >= tableRect.bottom - 1 && // -1 to account for fractions of pixels in "bottom" event.clientY < tableRect.bottom + 20; const toRightOfTable = - event.clientX > tableRect.right && event.clientX < tableRect.right + 20; + event.clientX >= tableRect.right - 1 && + event.clientX < tableRect.right + 20; + + // without this check, we'd also hide draghandles when hovering over them + const hideHandles = + event.clientX > tableRect.right || event.clientY > tableRect.bottom; this.state = { ...this.state!, @@ -270,6 +275,11 @@ export class TableHandlesView< referencePosTable: tableRect, block: tableBlock, widgetContainer, + colIndex: hideHandles ? undefined : this.state!.colIndex, + rowIndex: hideHandles ? undefined : this.state!.rowIndex, + referencePosCell: hideHandles + ? undefined + : this.state!.referencePosCell, }; } else { const colIndex = getChildIndex(target.domNode); @@ -317,12 +327,8 @@ export class TableHandlesView< event.preventDefault(); event.dataTransfer!.dropEffect = "move"; - hideElementsWithClassNames( - [ - "column-resize-handle", - "prosemirror-dropcursor-block", - "prosemirror-dropcursor-inline", - ], + hideElements( + ".prosemirror-dropcursor-block, .prosemirror-dropcursor-inline", this.pmView.root ); @@ -410,6 +416,7 @@ export class TableHandlesView< }; dropHandler = (event: DragEvent) => { + this.mouseState = "up"; if (this.state === undefined || this.state.draggingState === undefined) { return; } @@ -548,7 +555,7 @@ export class TableHandlesProsemirrorPlugin< ? this.view.state.rowIndex : this.view.state.colIndex; - if (!newIndex) { + if (newIndex === undefined) { return; } @@ -632,6 +639,7 @@ export class TableHandlesProsemirrorPlugin< (newIndex > this.view.state.draggingState.originalIndex ? cellNode.nodeSize - 2 : 0); + decorations.push( // The widget is a small bar which spans the height of the cell. Decoration.widget(decorationPos, () => {