diff --git a/examples/03-ui-components/17-advanced-tables-2/.bnexample.json b/examples/03-ui-components/17-advanced-tables-2/.bnexample.json new file mode 100644 index 0000000000..52ae0e6e39 --- /dev/null +++ b/examples/03-ui-components/17-advanced-tables-2/.bnexample.json @@ -0,0 +1,11 @@ +{ + "playground": true, + "docs": true, + "author": "must", + "tags": [ + "Intermediate", + "UI Components", + "Tables", + "Appearance & Styling" + ] +} \ No newline at end of file diff --git a/examples/03-ui-components/17-advanced-tables-2/README.md b/examples/03-ui-components/17-advanced-tables-2/README.md new file mode 100644 index 0000000000..827e0c1428 --- /dev/null +++ b/examples/03-ui-components/17-advanced-tables-2/README.md @@ -0,0 +1,54 @@ +# Advanced Tables with Calculated Columns + +This example demonstrates advanced table features including automatic calculations. It shows how to create a table with calculated columns that automatically update when values change. + +## Features + +- **Automatic Calculations**: Quantity × Price = Total for each row +- **Grand Total**: Automatically calculated sum of all totals +- **Real-time Updates**: Calculations update immediately when you change quantity or price values +- **Split cells**: Merge and split table cells +- **Cell background color**: Color individual cells +- **Cell text color**: Change text color in cells +- **Table row and column headers**: Use headers for better organization + +## How It Works + +The example uses the `onChange` event listener to detect when table content changes. When a table is updated, it automatically: + +1. Extracts quantity and price values from each data row +2. Calculates the total (quantity × price) for each row +3. Updates the total column with the calculated values +4. Calculates and updates the grand total + +## Code Highlights + +```tsx + { + const changes = getChanges(); + + changes.forEach((change) => { + if (change.type === "update" && change.block.type === "table") { + const updatedRows = calculateTableTotals(change.block); + if (updatedRows) { + editor.updateBlock(change.block, { + type: "table", + content: { + ...change.block.content, + rows: updatedRows as any, + } as any, + }); + } + } + }); + }} +> +``` + +**Relevant Docs:** + +- [Tables](/docs/features/blocks/tables) +- [Editor Setup](/docs/getting-started/editor-setup) +- [Events](/docs/reference/editor/events) diff --git a/examples/03-ui-components/17-advanced-tables-2/index.html b/examples/03-ui-components/17-advanced-tables-2/index.html new file mode 100644 index 0000000000..8c97f71cc4 --- /dev/null +++ b/examples/03-ui-components/17-advanced-tables-2/index.html @@ -0,0 +1,14 @@ + + + + + Advanced Tables with Calculated Columns + + + +
+ + + diff --git a/examples/03-ui-components/17-advanced-tables-2/main.tsx b/examples/03-ui-components/17-advanced-tables-2/main.tsx new file mode 100644 index 0000000000..677c7f7eed --- /dev/null +++ b/examples/03-ui-components/17-advanced-tables-2/main.tsx @@ -0,0 +1,11 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./src/App.jsx"; + +const root = createRoot(document.getElementById("root")!); +root.render( + + + +); diff --git a/examples/03-ui-components/17-advanced-tables-2/package.json b/examples/03-ui-components/17-advanced-tables-2/package.json new file mode 100644 index 0000000000..7da6d78b42 --- /dev/null +++ b/examples/03-ui-components/17-advanced-tables-2/package.json @@ -0,0 +1,27 @@ +{ + "name": "@blocknote/example-ui-components-advanced-tables-2", + "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "private": true, + "version": "0.12.4", + "scripts": { + "start": "vite", + "dev": "vite", + "build:prod": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@blocknote/core": "latest", + "@blocknote/react": "latest", + "@blocknote/ariakit": "latest", + "@blocknote/mantine": "latest", + "@blocknote/shadcn": "latest", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.3.4" + } +} \ No newline at end of file diff --git a/examples/03-ui-components/17-advanced-tables-2/src/App.tsx b/examples/03-ui-components/17-advanced-tables-2/src/App.tsx new file mode 100644 index 0000000000..7d5f6fe531 --- /dev/null +++ b/examples/03-ui-components/17-advanced-tables-2/src/App.tsx @@ -0,0 +1,677 @@ +import "@blocknote/core/fonts/inter.css"; +import { BlockNoteView } from "@blocknote/mantine"; +import "@blocknote/mantine/style.css"; +import { useCreateBlockNote } from "@blocknote/react"; +import type { Block, DefaultBlockSchema } from "@blocknote/core"; +import { useRef } from "react"; + +export default function App() { + const applying = useRef(false); + + // Creates a new editor instance. + const editor = useCreateBlockNote({ + // This enables the advanced table features + tables: { + splitCells: true, + cellBackgroundColor: true, + cellTextColor: true, + headers: true, + }, + initialContent: [ + { + id: "7e498b3d-d42e-4ade-9be0-054b292715ea", + type: "heading", + props: { + textColor: "default", + backgroundColor: "default", + textAlignment: "left", + level: 2, + }, + content: [ + { + type: "text", + text: "Advanced Tables with Calculated Columns", + styles: {}, + }, + ], + children: [], + }, + { + id: "cbf287c6-770b-413a-bff5-ad490a0b562a", + type: "table", + props: { + textColor: "default", + }, + content: { + type: "tableContent", + columnWidths: [150, 120, 120, 120], + headerRows: 1, + rows: [ + { + cells: [ + { + type: "tableCell", + content: [ + { + type: "text", + text: "Item", + styles: { bold: true }, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "gray", + textColor: "default", + textAlignment: "center", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "Quantity", + styles: { bold: true }, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "gray", + textColor: "default", + textAlignment: "center", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "Price ($)", + styles: { bold: true }, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "gray", + textColor: "default", + textAlignment: "center", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "Total ($)", + styles: { bold: true }, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "blue", + textColor: "white", + textAlignment: "center", + }, + }, + ], + }, + { + cells: [ + { + type: "tableCell", + content: [ + { + type: "text", + text: "Laptop", + styles: {}, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "2", + styles: {}, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "default", + textColor: "default", + textAlignment: "center", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "1200", + styles: {}, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "default", + textColor: "default", + textAlignment: "center", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "2400", + styles: { bold: true }, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "green", + textColor: "white", + textAlignment: "center", + }, + }, + ], + }, + { + cells: [ + { + type: "tableCell", + content: [ + { + type: "text", + text: "Mouse", + styles: {}, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "5", + styles: {}, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "default", + textColor: "default", + textAlignment: "center", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "25", + styles: {}, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "default", + textColor: "default", + textAlignment: "center", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "125", + styles: { bold: true }, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "green", + textColor: "white", + textAlignment: "center", + }, + }, + ], + }, + { + cells: [ + { + type: "tableCell", + content: [ + { + type: "text", + text: "Keyboard", + styles: {}, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "3", + styles: {}, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "default", + textColor: "default", + textAlignment: "center", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "80", + styles: {}, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "default", + textColor: "default", + textAlignment: "center", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "240", + styles: { bold: true }, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "green", + textColor: "white", + textAlignment: "center", + }, + }, + ], + }, + { + cells: [ + { + type: "tableCell", + content: [ + { + type: "text", + text: "Grand Total", + styles: { bold: true }, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "yellow", + textColor: "default", + textAlignment: "center", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "", + styles: {}, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "yellow", + textColor: "default", + textAlignment: "center", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "", + styles: {}, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "yellow", + textColor: "default", + textAlignment: "center", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "2765", + styles: { bold: true }, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "red", + textColor: "white", + textAlignment: "center", + }, + }, + ], + }, + ], + }, + children: [], + }, + { + id: "16e76a94-74e5-42e2-b461-fc9da9f381f7", + type: "paragraph", + props: { + textColor: "default", + backgroundColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Features:", + styles: {}, + }, + ], + children: [ + { + id: "785fc9f7-8554-47f4-a4df-8fe2f1438cac", + type: "bulletListItem", + props: { + textColor: "default", + backgroundColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Automatic calculation of totals (Quantity × Price)", + styles: {}, + }, + ], + children: [], + }, + { + id: "1d0adf08-1b42-421a-b9ea-b3125dcc96d9", + type: "bulletListItem", + props: { + textColor: "default", + backgroundColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Grand total calculation", + styles: {}, + }, + ], + children: [], + }, + { + id: "99991aa7-9d86-4d06-9073-b1a9c0329062", + type: "bulletListItem", + props: { + textColor: "default", + backgroundColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Cell background & foreground coloring", + styles: {}, + }, + ], + children: [], + }, + { + id: "c7bf2a7c-8972-44f1-acd8-cf60fa734068", + type: "bulletListItem", + props: { + textColor: "default", + backgroundColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Splitting & merging cells", + styles: {}, + }, + ], + children: [], + }, + { + id: "785fc9f7-8554-47f4-a4df-8fe2f1438cac", + type: "bulletListItem", + props: { + textColor: "default", + backgroundColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Header rows & columns", + styles: {}, + }, + ], + children: [], + }, + ], + }, + { + id: "c7bf2a7c-8972-44f1-acd8-cf60fa734068", + type: "paragraph", + props: { + textColor: "default", + backgroundColor: "default", + textAlignment: "left", + }, + content: [], + children: [], + }, + ], + }); + + // Function to calculate totals for a table + const calculateTableTotals = (tableBlock: Block) => { + if (tableBlock.type !== "table") return; + + const rows = tableBlock.content.rows; + if (rows.length < 2) return; // Need at least header + 1 data row + + let grandTotal = 0; + const updatedRows = rows.map((row, rowIndex: number) => { + if (rowIndex === 0) return row; // Skip header row + if (rowIndex === rows.length - 1) return row; // Skip grand total row + + // Helper function to extract text from a cell + const getCellText = (cell: any): string => { + if (typeof cell === "string") return cell; + if (cell && typeof cell === "object" && "content" in cell) { + return cell.content?.[0]?.text || "0"; + } + return "0"; + }; + + const itemText = getCellText(row.cells[0]); + const quantityText = getCellText(row.cells[1]); + const priceText = getCellText(row.cells[2]); + + const quantity = parseFloat(quantityText) || 0; + const price = parseFloat(priceText) || 0; + const total = quantity * price; + + grandTotal += total; + + // Update the total cell + const updatedCells = [...row.cells]; + updatedCells[3] = { + type: "tableCell", + content: [ + { + type: "text", + text: total.toString(), + styles: { bold: true }, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "green", + textColor: "white", + textAlignment: "center", + }, + }; + + // Update item label if total is above 4k + const baseItemText = itemText.replace(" (eligible for discount)", ""); + if (total >= 4000) { + updatedCells[0] = { + ...row.cells[0], + content: [ + { + type: "text", + text: baseItemText + " (eligible for discount)", + styles: {}, + }, + ], + }; + } else { + updatedCells[0] = { + ...row.cells[0], + content: [ + { + type: "text", + text: baseItemText, + styles: {}, + }, + ], + }; + } + + return { + ...row, + cells: updatedCells, + }; + }); + + // Update grand total row + const grandTotalRow = updatedRows[rows.length - 1]; + if (grandTotalRow) { + const updatedGrandTotalCells = [...grandTotalRow.cells]; + updatedGrandTotalCells[3] = { + type: "tableCell", + content: [ + { + type: "text", + text: grandTotal.toString(), + styles: { bold: true }, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "red", + textColor: "white", + textAlignment: "center", + }, + }; + + updatedRows[rows.length - 1] = { + ...grandTotalRow, + cells: updatedGrandTotalCells, + }; + } + + return updatedRows as typeof tableBlock.content.rows; + }; + + // Renders the editor instance using a React component. + return ( + { + const changes = getChanges(); + + if (changes.length === 0 || applying.current) return; + + // prevents a double onChange because we're updating the block here + applying.current = true; + + changes.forEach((change) => { + if (change.type === "update" && change.block.type === "table") { + const updatedRows = calculateTableTotals(change.block); + if (updatedRows) { + // Use any type to bypass complex type checking for this demo + editor.updateBlock(change.block, { + type: "table", + content: { + ...change.block.content, + rows: updatedRows, + }, + }); + } + } + }); + + requestAnimationFrame(() => (applying.current = false)); + }} + > + ); +} diff --git a/examples/03-ui-components/17-advanced-tables-2/tsconfig.json b/examples/03-ui-components/17-advanced-tables-2/tsconfig.json new file mode 100644 index 0000000000..dbe3e6f62d --- /dev/null +++ b/examples/03-ui-components/17-advanced-tables-2/tsconfig.json @@ -0,0 +1,36 @@ +{ + "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "composite": true + }, + "include": [ + "." + ], + "__ADD_FOR_LOCAL_DEV_references": [ + { + "path": "../../../packages/core/" + }, + { + "path": "../../../packages/react/" + } + ] +} \ No newline at end of file diff --git a/examples/03-ui-components/17-advanced-tables-2/vite.config.ts b/examples/03-ui-components/17-advanced-tables-2/vite.config.ts new file mode 100644 index 0000000000..f62ab20bc2 --- /dev/null +++ b/examples/03-ui-components/17-advanced-tables-2/vite.config.ts @@ -0,0 +1,32 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import react from "@vitejs/plugin-react"; +import * as fs from "fs"; +import * as path from "path"; +import { defineConfig } from "vite"; +// import eslintPlugin from "vite-plugin-eslint"; +// https://vitejs.dev/config/ +export default defineConfig((conf) => ({ + plugins: [react()], + optimizeDeps: {}, + build: { + sourcemap: true, + }, + resolve: { + alias: + conf.command === "build" || + !fs.existsSync(path.resolve(__dirname, "../../packages/core/src")) + ? {} + : ({ + // Comment out the lines below to load a built version of blocknote + // or, keep as is to load live from sources with live reload working + "@blocknote/core": path.resolve( + __dirname, + "../../packages/core/src/" + ), + "@blocknote/react": path.resolve( + __dirname, + "../../packages/react/src/" + ), + } as any), + }, +})); diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts index 663d63a542..f4e99a572a 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts @@ -4,7 +4,7 @@ import { type Node as PMNode, Slice, } from "prosemirror-model"; -import type { Transaction } from "prosemirror-state"; +import { TextSelection, Transaction } from "prosemirror-state"; import { ReplaceStep, Transform } from "prosemirror-transform"; import type { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; @@ -27,6 +27,7 @@ import { import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../../nodeUtil.js"; import { getPmSchema } from "../../../pmUtil.js"; +import { TableMap } from "prosemirror-tables"; // for compatibility with tiptap. TODO: remove as we want to remove dependency on tiptap command interface export const updateBlockCommand = < @@ -56,7 +57,7 @@ export function updateBlockTr< I extends InlineContentSchema, S extends StyleSchema, >( - tr: Transform, + tr: Transaction, posBeforeBlock: number, block: PartialBlock, replaceFromPos?: number, @@ -64,6 +65,11 @@ export function updateBlockTr< ) { const blockInfo = getBlockInfoFromResolvedPos(tr.doc.resolve(posBeforeBlock)); + let cellAnchor: CellAnchor | null = null; + if (blockInfo.blockNoteType === "table") { + cellAnchor = captureCellAnchor(tr); + } + const pmSchema = getPmSchema(tr); if ( @@ -143,6 +149,10 @@ export function updateBlockTr< ...blockInfo.bnBlock.node.attrs, ...block.props, }); + + if (cellAnchor) { + restoreCellAnchor(tr, blockInfo, cellAnchor); + } } function updateBlockContentNode< @@ -329,3 +339,105 @@ export function updateBlock< const pmSchema = getPmSchema(tr); return nodeToBlock(blockContainerNode, pmSchema); } + +type CellAnchor = { row: number; col: number; offset: number }; + +/** + * Captures the cell anchor from the current selection. + * @param tr - The transaction to capture the cell anchor from. + * + * @returns The cell anchor, or null if no cell is selected. + */ +export function captureCellAnchor(tr: Transaction): CellAnchor | null { + const sel = tr.selection; + if (!(sel instanceof TextSelection)) return null; + + const $head = tr.doc.resolve(sel.head); + // Find enclosing cell and table + let cellDepth = -1; + let tableDepth = -1; + for (let d = $head.depth; d >= 0; d--) { + const name = $head.node(d).type.name; + if (cellDepth < 0 && (name === "tableCell" || name === "tableHeader")) { + cellDepth = d; + } + if (name === "table") { + tableDepth = d; + break; + } + } + if (cellDepth < 0 || tableDepth < 0) return null; + + // Absolute positions (before the cell) + const cellPos = $head.before(cellDepth); + const tablePos = $head.before(tableDepth); + const table = tr.doc.nodeAt(tablePos); + if (!table || table.type.name !== "table") return null; + + // Visual grid position via TableMap (handles spans) + const map = TableMap.get(table); + const rel = cellPos - (tablePos + 1); // relative to inside table + const idx = map.map.indexOf(rel); + if (idx < 0) return null; + + const row = Math.floor(idx / map.width); + const col = idx % map.width; + + // Caret offset relative to the start of paragraph text + const paraPos = cellPos + 1; // pos BEFORE tableParagraph + const textStart = paraPos + 1; // start of paragraph text + const offset = Math.max(0, sel.head - textStart); + + return { row, col, offset }; +} + +function restoreCellAnchor( + tr: Transaction, + blockInfo: BlockInfo, + a: CellAnchor, +): boolean { + if (blockInfo.blockNoteType !== "table") return false; + + // 1) Resolve the table node in the current document + let tablePos = -1; + + if (blockInfo.isBlockContainer) { + // Prefer the blockContent position when available (points directly at the PM table node) + tablePos = tr.mapping.map(blockInfo.blockContent.beforePos); + } else { + // Fallback: scan within the mapped bnBlock range to find the inner table node + const start = tr.mapping.map(blockInfo.bnBlock.beforePos); + const end = start + (tr.doc.nodeAt(start)?.nodeSize || 0); + tr.doc.nodesBetween(start, end, (node, pos) => { + if (node.type.name === "table") { + tablePos = pos; + return false; + } + return true; + }); + } + + const table = tablePos >= 0 ? tr.doc.nodeAt(tablePos) : null; + if (!table || table.type.name !== "table") return false; + + // 2) Clamp row/col to the table’s current grid + const map = TableMap.get(table); + const row = Math.max(0, Math.min(a.row, map.height - 1)); + const col = Math.max(0, Math.min(a.col, map.width - 1)); + + // 3) Compute the absolute position of the target cell (pos BEFORE the cell) + const cellIndex = row * map.width + col; + const relCellPos = map.map[cellIndex]; // relative to (tablePos + 1) + if (relCellPos == null) return false; + const cellPos = tablePos + 1 + relCellPos; + + // 4) Place the caret inside the cell, clamping the text offset + const textPos = cellPos + 1; + const textNode = tr.doc.nodeAt(textPos); + const textStart = textPos + 1; + const max = textNode ? textNode.content.size : 0; + const head = textStart + Math.max(0, Math.min(a.offset, max)); + + tr.setSelection(TextSelection.create(tr.doc, head)); + return true; +} diff --git a/packages/xl-ai/src/prosemirror/changeset.ts b/packages/xl-ai/src/prosemirror/changeset.ts index 26e3099a89..2bcdbee3fc 100644 --- a/packages/xl-ai/src/prosemirror/changeset.ts +++ b/packages/xl-ai/src/prosemirror/changeset.ts @@ -6,6 +6,7 @@ import { TokenEncoder, } from "prosemirror-changeset"; import { Node } from "prosemirror-model"; +import { Transaction } from "prosemirror-state"; import { ReplaceStep, Transform } from "prosemirror-transform"; type CustomChange = Change & { @@ -198,7 +199,7 @@ export function updateToReplaceSteps( updateToPos?: number, ) { const blockPos = getNodeById(op.id, doc)!; - const updatedTr = new Transform(doc); + const updatedTr = new Transaction(doc); updateBlockTr( updatedTr, blockPos.posBeforeNode, diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index 0a0122c56f..4964fbb6a6 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -750,6 +750,28 @@ "slug": "ui-components" }, "readme": "In this example, we add a button to the Link Toolbar which opens a browser alert.\n\n**Try it out:** Hover the link open the Link Toolbar, and click the new \"Open Alert\" button!\n\n**Relevant Docs:**\n\n- [Changing the Link Toolbar](/docs/react/components/link-toolbar)\n- [Editor Setup](/docs/getting-started/editor-setup)" + }, + { + "projectSlug": "advanced-tables-2", + "fullSlug": "ui-components/advanced-tables-2", + "pathFromRoot": "examples/03-ui-components/17-advanced-tables-2", + "config": { + "playground": true, + "docs": true, + "author": "must", + "tags": [ + "Intermediate", + "UI Components", + "Tables", + "Appearance & Styling" + ] + }, + "title": "Advanced Tables with Calculated Columns", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + }, + "readme": "This example demonstrates advanced table features including automatic calculations. It shows how to create a table with calculated columns that automatically update when values change.\n\n## Features\n\n- **Automatic Calculations**: Quantity × Price = Total for each row\n- **Grand Total**: Automatically calculated sum of all totals\n- **Real-time Updates**: Calculations update immediately when you change quantity or price values\n- **Split cells**: Merge and split table cells\n- **Cell background color**: Color individual cells\n- **Cell text color**: Change text color in cells\n- **Table row and column headers**: Use headers for better organization\n\n## How It Works\n\nThe example uses the `onChange` event listener to detect when table content changes. When a table is updated, it automatically:\n\n1. Extracts quantity and price values from each data row\n2. Calculates the total (quantity × price) for each row\n3. Updates the total column with the calculated values\n4. Calculates and updates the grand total\n\n## Code Highlights\n\n```tsx\n {\n const changes = getChanges();\n\n changes.forEach((change) => {\n if (change.type === \"update\" && change.block.type === \"table\") {\n const updatedRows = calculateTableTotals(change.block);\n if (updatedRows) {\n editor.updateBlock(change.block, {\n type: \"table\",\n content: {\n ...change.block.content,\n rows: updatedRows as any,\n } as any,\n });\n }\n }\n });\n }}\n>\n```\n\n**Relevant Docs:**\n\n- [Tables](/docs/features/blocks/tables)\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Events](/docs/reference/editor/events)" } ] }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bec8df27c4..9e2394ecdd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1721,6 +1721,43 @@ importers: specifier: ^5.3.4 version: 5.4.15(@types/node@22.15.2)(lightningcss@1.30.1)(terser@5.43.1) + examples/03-ui-components/17-advanced-tables-2: + dependencies: + '@blocknote/ariakit': + specifier: latest + version: link:../../../packages/ariakit + '@blocknote/core': + specifier: latest + version: link:../../../packages/core + '@blocknote/mantine': + specifier: latest + version: link:../../../packages/mantine + '@blocknote/react': + specifier: latest + version: link:../../../packages/react + '@blocknote/shadcn': + specifier: latest + version: link:../../../packages/shadcn + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) + devDependencies: + '@types/react': + specifier: ^19.1.0 + version: 19.1.8 + '@types/react-dom': + specifier: ^19.1.0 + version: 19.1.6(@types/react@19.1.8) + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.4.1(vite@5.4.15(@types/node@22.15.2)(lightningcss@1.30.1)(terser@5.43.1)) + vite: + specifier: ^5.3.4 + version: 5.4.15(@types/node@22.15.2)(lightningcss@1.30.1)(terser@5.43.1) + examples/04-theming/01-theming-dom-attributes: dependencies: '@blocknote/ariakit':