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..9c4787320e
--- /dev/null
+++ b/examples/03-ui-components/17-advanced-tables-2/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "nperez0111",
+ "tags": ["Intermediate", "UI Components", "Tables", "Appearance & Styling"]
+}
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..8c3e1128ab
--- /dev/null
+++ b/examples/03-ui-components/17-advanced-tables-2/README.md
@@ -0,0 +1,56 @@
+# 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
+
+```typescript
+// Listen for changes and update calculations
+useEffect(() => {
+ const cleanup = editor.onChange((editor, { getChanges }) => {
+ 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,
+ },
+ });
+ }
+ }
+ });
+ });
+
+ return cleanup;
+}, [editor]);
+```
+
+**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..83da19d2d7
--- /dev/null
+++ b/examples/03-ui-components/17-advanced-tables-2/src/App.tsx
@@ -0,0 +1,667 @@
+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";
+
+export default function App() {
+ // 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;
+ };
+
+ // Renders the editor instance using a React component.
+ return (
+ {
+ const changes = getChanges();
+
+ 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 as any,
+ } as any,
+ });
+ }
+ }
+ });
+ }}
+ >
+ );
+}
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..e6794446b1 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";
@@ -56,7 +56,7 @@ export function updateBlockTr<
I extends InlineContentSchema,
S extends StyleSchema,
>(
- tr: Transform,
+ tr: Transaction,
posBeforeBlock: number,
block: PartialBlock,
replaceFromPos?: number,
@@ -64,6 +64,32 @@ export function updateBlockTr<
) {
const blockInfo = getBlockInfoFromResolvedPos(tr.doc.resolve(posBeforeBlock));
+ const originalSelection = tr.selection;
+ // Capturing current selection info for restoring it after the block is updated.
+ let anchorOffset: number | undefined = undefined;
+ let headOffset: number | undefined = undefined;
+
+ if (
+ originalSelection instanceof TextSelection &&
+ blockInfo.isBlockContainer &&
+ blockInfo.blockContent
+ ) {
+ // Ensure both anchor and head are within the block's content before proceeding.
+ const isAnchorInContent =
+ originalSelection.anchor >= blockInfo.blockContent.beforePos + 1 &&
+ originalSelection.anchor <= blockInfo.blockContent.afterPos - 1;
+ const isHeadInContent =
+ originalSelection.head >= blockInfo.blockContent.beforePos + 1 &&
+ originalSelection.head <= blockInfo.blockContent.afterPos - 1;
+
+ if (isAnchorInContent && isHeadInContent) {
+ anchorOffset =
+ originalSelection.anchor - (blockInfo.blockContent.beforePos + 1);
+ headOffset =
+ originalSelection.head - (blockInfo.blockContent.beforePos + 1);
+ }
+ }
+
const pmSchema = getPmSchema(tr);
if (
@@ -143,6 +169,34 @@ export function updateBlockTr<
...blockInfo.bnBlock.node.attrs,
...block.props,
});
+
+ // Restore selection
+ const newBlockInfo = getBlockInfoFromResolvedPos(
+ tr.doc.resolve(tr.mapping.map(posBeforeBlock)),
+ );
+
+ // If we captured relative offsets, try to restore the selection using them.
+ if (
+ anchorOffset !== undefined &&
+ headOffset !== undefined &&
+ newBlockInfo.isBlockContainer &&
+ newBlockInfo.blockContent
+ ) {
+ const contentNode = newBlockInfo.blockContent.node;
+ const contentStartPos = newBlockInfo.blockContent.beforePos + 1;
+ const contentEndPos = contentStartPos + contentNode.content.size;
+
+ const newAnchorPos = Math.min(
+ contentStartPos + anchorOffset,
+ contentEndPos,
+ );
+ const newHeadPos = Math.min(contentStartPos + headOffset, contentEndPos);
+
+ tr.setSelection(TextSelection.create(tr.doc, newAnchorPos, newHeadPos));
+ } else {
+ // Fallback to the default mapping if we couldn't use the offset method.
+ tr.setSelection(originalSelection.map(tr.doc, tr.mapping));
+ }
}
function updateBlockContentNode<
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..0ac754695a 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": "nperez0111",
+ "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```typescript\n// Listen for changes and update calculations\nuseEffect(() => {\n const cleanup = editor.onChange((editor, { getChanges }) => {\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,\n },\n });\n }\n }\n });\n });\n\n return cleanup;\n}, [editor]);\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':