diff --git a/package-lock.json b/package-lock.json
index db74f1351..78c617d34 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.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.6.1.tgz",
+ "integrity": "sha512-p8WRJNA96jaNQjhJolmbxTzd6M4huRE5xQ8OxjvMhQUP0Nzpo4zz6TztEiwk6aoqGBhz9lxRWR1yRZLlpQN98w==",
"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.1",
"prosemirror-transform": "^1.9.0",
"prosemirror-view": "^1.33.7",
"rehype-format": "^5.0.0",
diff --git a/packages/ariakit/src/index.tsx b/packages/ariakit/src/index.tsx
index d651dde9a..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,6 +82,7 @@ export const components: Components = {
},
TableHandle: {
Root: TableHandle,
+ ExtendButton: ExtendButton,
},
Generic: {
Form: {
diff --git a/packages/ariakit/src/style.css b/packages/ariakit/src/style.css
index 213f4d5cd..bf1dcd4ce 100644
--- a/packages/ariakit/src/style.css
+++ b/packages/ariakit/src/style.css
@@ -6,181 +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 {
- color: gray;
+.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-add-remove-columns {
+ height: 100%;
+ width: 18px;
+ padding: 0;
+ margin-left: 4px;
+ cursor: col-resize;
+}
+
+.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
new file mode 100644
index 000000000..67b163905
--- /dev/null
+++ b/packages/ariakit/src/tableHandle/ExtendButton.tsx
@@ -0,0 +1,30 @@
+import { Button as AriakitButton } from "@ariakit/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, 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
+ assertEmpty(rest, false);
+
+ return (
+
+ {children}
+
+ );
+});
diff --git a/packages/core/package.json b/packages/core/package.json
index 62667072a..b116b4d6f 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.1",
"prosemirror-transform": "^1.9.0",
"prosemirror-view": "^1.33.7",
"rehype-format": "^5.0.0",
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/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/nodeConversions/blockToNode.ts b/packages/core/src/api/nodeConversions/blockToNode.ts
index f63ee0997..b4df390c1 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,17 @@ 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?.[i]
+ ? [tableContent.columnWidths[i]]
+ : null,
+ },
+ 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..79889cb54 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] || undefined);
+ });
+ }
+
rowNode.content.forEach((cellNode) => {
row.cells.push(
contentNodeToInlineContent(
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": [
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 ac3afa4d5..ca9190297 100644
--- a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts
+++ b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts
@@ -1,17 +1,22 @@
-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,
+ backgroundColor: defaultProps.backgroundColor,
+ textColor: defaultProps.textColor,
};
export const TableBlockContent = createStronglyTypedTiptapNode({
@@ -37,6 +42,69 @@ 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;
+
+ 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, {
+ ...(this.options.domAttributes?.blockContent || {}),
+ ...HTMLAttributes,
+ });
+ };
+ },
});
const TableParagraph = Node.create({
@@ -70,11 +138,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 fb0147d7a..cd197a139 100644
--- a/packages/core/src/blocks/TableBlockContent/TableExtension.ts
+++ b/packages/core/src/blocks/TableBlockContent/TableExtension.ts
@@ -1,13 +1,22 @@
import { callOrReturn, Extension, getExtensionField } from "@tiptap/core";
import { columnResizing, tableEditing } from "prosemirror-tables";
+export const RESIZE_MIN_WIDTH = 35;
+export const EMPTY_CELL_WIDTH = 120;
+export const EMPTY_CELL_HEIGHT = 31;
+
export const TableExtension = Extension.create({
name: "BlockNoteTableExtension",
addProseMirrorPlugins: () => {
return [
columnResizing({
- cellMinWidth: 100,
+ 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.
+ View: null,
}),
tableEditing(),
];
diff --git a/packages/core/src/editor/editor.css b/packages/core/src/editor/editor.css
index f2040b7db..9ee98f1dc 100644
--- a/packages/core/src/editor/editor.css
+++ b/packages/core/src/editor/editor.css
@@ -104,22 +104,50 @@ 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 {
width: auto !important;
+ word-break: break-word;
}
.bn-editor th,
.bn-editor td {
- min-width: 1em;
border: 1px solid #ddd;
padding: 3px 5px;
}
-.bn-editor .tableWrapper {
- margin: 1em 0;
-}
-
.bn-editor th {
font-weight: bold;
text-align: left;
}
+
+/* tiptap uses colwidth instead of data-colwidth, se we need to adjust this style from prosemirror-tables */
+.ProseMirror td,
+.ProseMirror th {
+ min-width: auto !important;
+}
+.ProseMirror td:not([colwidth]):not(.column-resize-dragging),
+.ProseMirror th:not([colwidth]):not(.column-resize-dragging) {
+ /* if there's no explicit width set and the column is not being resized, set a default width */
+ min-width: var(--default-cell-min-width) !important;
+}
diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts
index 94fd95cf7..4b7519e72 100644
--- a/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts
+++ b/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts
@@ -32,7 +32,8 @@ export async function getDefaultEmojiPickerItems<
if (!data) {
// use a dynamic import to encourage bundle-splitting
// and a smaller initial client bundle size
- data = import("@emoji-mart/data", { assert: { type: "json" } }) as any;
+
+ data = import("@emoji-mart/data") as any;
// load dynamically because emoji-mart doesn't specify type: module and breaks in nodejs
emojiMart = await import("emoji-mart");
diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts
index b72c76b93..af49098a8 100644
--- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts
+++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts
@@ -1,8 +1,9 @@
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 { Block, DefaultBlockSchema } from "../../blocks/defaultBlocks.js";
+import { DefaultBlockSchema } from "../../blocks/defaultBlocks.js";
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
import {
BlockFromConfigNoChildren,
@@ -20,12 +21,14 @@ export type TableHandlesState<
S extends StyleSchema
> = {
show: boolean;
- referencePosCell: DOMRect;
+ showAddOrRemoveRowsButton: boolean;
+ showAddOrRemoveColumnsButton: boolean;
+ referencePosCell: DOMRect | undefined;
referencePosTable: DOMRect;
block: BlockFromConfigNoChildren;
- colIndex: number;
- rowIndex: number;
+ colIndex: number | undefined;
+ rowIndex: number | undefined;
draggingState:
| {
@@ -34,6 +37,8 @@ export type TableHandlesState<
mousePos: number;
}
| undefined;
+
+ widgetContainer: HTMLElement | undefined;
};
function setHiddenDragImage(rootEl: Document | ShadowRoot) {
@@ -70,28 +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 | null): Element | null {
- while (target && target.nodeName !== "TD" && target.nodeName !== "TH") {
- target =
- target.classList && target.classList.contains("ProseMirror")
- ? null
- : (target.parentNode as Element);
+function domCellAround(target: Element) {
+ let currentTarget: Element | undefined = target;
+ while (
+ currentTarget &&
+ currentTarget.nodeName !== "TD" &&
+ currentTarget.nodeName !== "TH" &&
+ !currentTarget.classList.contains("tableWrapper")
+ ) {
+ currentTarget =
+ currentTarget.classList && currentTarget.classList.contains("ProseMirror")
+ ? undefined
+ : (currentTarget.parentNode as Element);
+ }
+ if (!currentTarget) {
+ return undefined;
}
- return target;
+ return currentTarget?.nodeName === "TD" || currentTarget?.nodeName === "TH"
+ ? {
+ type: "cell",
+ domNode: currentTarget,
+ tbodyNode: currentTarget.closest("tbody"),
+ }
+ : {
+ type: "wrapper",
+ 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<
@@ -104,6 +123,7 @@ export class TableHandlesView<
public tableId: string | undefined;
public tablePos: number | undefined;
+ public tableElement: HTMLElement | undefined;
public menuFrozen = false;
@@ -130,25 +150,20 @@ 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",
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 = () => {
this.mouseState = "down";
};
- viewMouseupHandler = (event: MouseEvent) => {
+ mouseUpHandler = (event: MouseEvent) => {
this.mouseState = "up";
this.mouseMoveHandler(event);
};
@@ -158,100 +173,147 @@ export class TableHandlesView<
return;
}
- if (this.mouseState === "down") {
+ if (this.mouseState === "selecting") {
+ return;
+ }
+
+ if (!(event.target instanceof Element)) {
+ return;
+ }
+
+ const target = domCellAround(event.target);
+
+ 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) {
this.state.show = false;
+ this.state.showAddOrRemoveRowsButton = false;
+ this.state.showAddOrRemoveColumnsButton = false;
this.emitUpdate();
}
- }
-
- if (this.mouseState === "selecting") {
return;
}
- const target = domCellAround(event.target as HTMLElement);
-
if (!target || !this.editor.isEditable) {
if (this.state?.show) {
this.state.show = false;
+ this.state.showAddOrRemoveRowsButton = false;
+ this.state.showAddOrRemoveColumnsButton = false;
this.emitUpdate();
}
return;
}
- const colIndex = getChildIndex(target);
- const rowIndex = getChildIndex(target.parentElement!);
- const cellRect = target.getBoundingClientRect();
- const tableRect =
- target.parentElement?.parentElement?.getBoundingClientRect();
-
- if (!tableRect) {
+ if (!target.tbodyNode) {
return;
}
- const blockEl = getDraggableBlockFromElement(target, this.pmView);
+ const tableRect = target.tbodyNode.getBoundingClientRect();
+
+ const blockEl = getDraggableBlockFromElement(target.domNode, this.pmView);
if (!blockEl) {
return;
}
+ 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.
- 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 (target?.type === "wrapper") {
+ // if we're just to the right or below the table, show the extend buttons
+ // (this is a bit hacky. It would probably be cleaner to render the extend buttons in the Table NodeView instead)
+ const belowTable =
+ 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 - 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!,
+ show: true,
+ showAddOrRemoveRowsButton: belowTable,
+ showAddOrRemoveColumnsButton: toRightOfTable,
+ 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);
+ 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;
+ }
- if (
- this.state !== undefined &&
- this.state.show &&
- this.tableId === blockEl.id &&
- this.state.rowIndex === rowIndex &&
- this.state.colIndex === colIndex
- ) {
- return;
+ this.state = {
+ show: true,
+ showAddOrRemoveColumnsButton:
+ colIndex === tableBlock.content.rows[0].cells.length - 1,
+ showAddOrRemoveRowsButton:
+ rowIndex === tableBlock.content.rows.length - 1,
+ referencePosTable: tableRect,
+
+ block: tableBlock,
+ draggingState: undefined,
+ referencePosCell: cellRect,
+ colIndex: colIndex,
+ rowIndex: rowIndex,
+
+ widgetContainer,
+ };
}
-
- this.state = {
- show: true,
- referencePosCell: cellRect,
- referencePosTable: tableRect,
-
- block: tableBlock,
- colIndex: colIndex,
- rowIndex: rowIndex,
-
- draggingState: undefined,
- };
this.emitUpdate();
return false;
@@ -265,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
);
@@ -358,25 +416,37 @@ export class TableHandlesView<
};
dropHandler = (event: DragEvent) => {
+ this.mouseState = "up";
if (this.state === undefined || this.state.draggingState === undefined) {
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]);
});
}
@@ -392,26 +462,45 @@ export class TableHandlesView<
// the existing selection out of the block.
this.editor.setTextCursorPosition(this.state.block.id);
};
+ // Updates drag handle positions on table content updates.
+ update() {
+ if (!this.state || !this.state.show) {
+ return;
+ }
- 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();
+ const tableBody = this.tableElement!.querySelector("tbody");
+ if (!tableBody) {
+ return;
+ }
+
+ 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;
+ }
+
+ const row = tableBody.children[this.state.rowIndex];
+ const cell = row.children[this.state.colIndex];
+ this.state.referencePosCell = cell.getBoundingClientRect();
}
- };
+
+ this.state.block = this.editor.getBlock(this.state.block.id)!;
+ this.state.referencePosTable = tableBody.getBoundingClientRect();
+ this.emitUpdate();
+ }
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
@@ -420,7 +509,6 @@ export class TableHandlesView<
"drop",
this.dropHandler as EventListener
);
- this.pmView.root.removeEventListener("scroll", this.scrollHandler, true);
}
}
@@ -467,6 +555,10 @@ export class TableHandlesProsemirrorPlugin<
? this.view.state.rowIndex
: this.view.state.colIndex;
+ if (newIndex === undefined) {
+ return;
+ }
+
const decorations: Decoration[] = [];
if (newIndex === this.view.state.draggingState.originalIndex) {
@@ -547,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, () => {
@@ -592,7 +685,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."
);
@@ -628,7 +724,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/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/core/src/schema/blocks/types.ts b/packages/core/src/schema/blocks/types.ts
index 7fc70ea88..a5e20ecdd 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 | undefined)[];
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..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,6 +91,7 @@ export const components: Components = {
},
TableHandle: {
Root: TableHandle,
+ ExtendButton: ExtendButton,
},
Generic: {
Form: {
diff --git a/packages/mantine/src/style.css b/packages/mantine/src/style.css
index ee8b2e60f..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;
@@ -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,26 @@
}
.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-add-remove-columns {
+ height: 100%;
+ width: 18px;
+ margin-left: 4px;
+ cursor: col-resize;
+}
+
+.bn-mantine .bn-extend-button-add-remove-rows {
+ height: 18px;
+ width: 100%;
+ margin-top: 4px;
+ cursor: row-resize;
+}
+
/* 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..a145106ae
--- /dev/null
+++ b/packages/mantine/src/tableHandle/ExtendButton.tsx
@@ -0,0 +1,27 @@
+import { Button as MantineButton } from "@mantine/core";
+
+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, 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
+ 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
new file mode 100644
index 000000000..60aaaeabe
--- /dev/null
+++ b/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx
@@ -0,0 +1,304 @@
+import {
+ DefaultInlineContentSchema,
+ DefaultStyleSchema,
+ EMPTY_CELL_HEIGHT,
+ EMPTY_CELL_WIDTH,
+ InlineContentSchema,
+ mergeCSSClasses,
+ PartialTableContent,
+ StyleSchema,
+} from "@blocknote/core";
+import {
+ MouseEvent as ReactMouseEvent,
+ ReactNode,
+ useCallback,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+import { RiAddFill } from "react-icons/ri";
+
+import { useComponentsContext } from "../../../editor/ComponentsContext.js";
+import { ExtendButtonProps } from "./ExtendButtonProps.js";
+
+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++;
+ }
+ }
+
+ 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 {
+ ...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 = <
+ I extends InlineContentSchema,
+ S extends StyleSchema
+>(
+ content: PartialTableContent,
+ rowsToAdd: number,
+ numCols: number
+): PartialTableContent => {
+ const newRow: PartialTableContent["rows"][number] = {
+ cells: Array(numCols).fill([]),
+ };
+
+ const newRows: PartialTableContent["rows"] = [];
+ for (let i = 0; i < rowsToAdd; i++) {
+ newRows.push(newRow);
+ }
+ return {
+ type: "tableContent",
+ columnWidths: content.columnWidths,
+ rows: [...content.rows, ...newRows],
+ };
+};
+
+const getContentWithAddedCols = <
+ I extends InlineContentSchema,
+ S extends StyleSchema
+>(
+ content: PartialTableContent,
+ colsToAdd: number
+): 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
+ ? [...content.columnWidths, ...newCells.map(() => undefined)]
+ : undefined,
+ rows: content.rows.map((row) => ({
+ cells: [...row.cells, ...newCells],
+ })),
+ };
+};
+
+export const ExtendButton = <
+ I extends InlineContentSchema = DefaultInlineContentSchema,
+ S extends StyleSchema = DefaultStyleSchema
+>(
+ props: ExtendButtonProps & { children?: ReactNode }
+) => {
+ const Components = useComponentsContext()!;
+
+ // 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.onMouseDown();
+ setEditingState({
+ 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, 1)
+ : getContentWithAddedRows(
+ props.block.content,
+ 1,
+ props.block.content.rows[0].cells.length
+ ),
+ });
+ }, [props.block, props.orientation, props.editor]);
+
+ // Extends columns/rows on when moving the mouse.
+ useEffect(() => {
+ const callback = (event: MouseEvent) => {
+ // console.log("callback", event);
+ if (!editingState) {
+ throw new Error("editingState is undefined");
+ }
+
+ movedMouse.current = true;
+
+ const diff =
+ (props.orientation === "addOrRemoveColumns"
+ ? event.clientX
+ : event.clientY) - editingState.startPos;
+
+ const numCroppedCells =
+ props.orientation === "addOrRemoveColumns"
+ ? editingState.originalCroppedContent.rows[0]?.cells.length ?? 0
+ : editingState.originalCroppedContent.rows.length;
+
+ const numOriginalCells =
+ props.orientation === "addOrRemoveColumns"
+ ? editingState.originalContent.rows[0]?.cells.length ?? 0
+ : editingState.originalContent.rows.length;
+
+ const currentNumCells =
+ props.orientation === "addOrRemoveColumns"
+ ? props.block.content.rows[0].cells.length
+ : props.block.content.rows.length;
+
+ const newNumCells =
+ numOriginalCells +
+ marginRound(
+ diff /
+ (props.orientation === "addOrRemoveColumns"
+ ? EMPTY_CELL_WIDTH
+ : EMPTY_CELL_HEIGHT),
+ 0.3
+ );
+
+ if (
+ newNumCells >= numCroppedCells &&
+ newNumCells > 0 &&
+ newNumCells !== currentNumCells
+ ) {
+ props.editor.updateBlock(props.block, {
+ type: "table",
+ content:
+ props.orientation === "addOrRemoveColumns"
+ ? getContentWithAddedCols(
+ editingState.originalCroppedContent,
+ newNumCells - numCroppedCells
+ )
+ : getContentWithAddedRows(
+ editingState.originalCroppedContent,
+ newNumCells - numCroppedCells,
+ editingState.originalContent.rows[0].cells.length
+ ),
+ });
+
+ // 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 (props.block.content) {
+ props.editor.setTextCursorPosition(props.block);
+ }
+ }
+ };
+
+ if (editingState) {
+ window.addEventListener("mousemove", callback);
+ }
+ return () => {
+ window.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 onMouseUp = props.onMouseUp;
+
+ const callback = () => {
+ setEditingState(undefined);
+ onMouseUp();
+ };
+
+ if (editingState) {
+ window.addEventListener("mouseup", callback);
+ }
+
+ return () => {
+ window.removeEventListener("mouseup", callback);
+ };
+ }, [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
new file mode 100644
index 000000000..cc473c289
--- /dev/null
+++ b/packages/react/src/components/TableHandles/ExtendButton/ExtendButtonProps.ts
@@ -0,0 +1,25 @@
+import {
+ BlockNoteEditor,
+ DefaultBlockSchema,
+ DefaultInlineContentSchema,
+ DefaultStyleSchema,
+ InlineContentSchema,
+ StyleSchema,
+ TableHandlesState,
+} from "@blocknote/core";
+
+export type ExtendButtonProps<
+ I extends InlineContentSchema = DefaultInlineContentSchema,
+ S extends StyleSchema = DefaultStyleSchema
+> = {
+ 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/TableHandleMenu/DefaultButtons/AddButton.tsx b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/AddButton.tsx
index 5f86a6839..e90289647 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";
@@ -42,6 +42,7 @@ export const AddRowButton = <
type: "table",
content: {
type: "tableContent",
+ columnWidths: props.block.content.columnWidths,
rows,
},
});
@@ -73,8 +74,15 @@ export const AddColumnButton = <
return (
{
- const content: TableContent = {
+ const columnWidths = [...props.block.content.columnWidths];
+ columnWidths.splice(
+ props.index + (props.side === "right" ? 1 : 0),
+ 0,
+ undefined
+ );
+ const content: PartialTableContent = {
type: "tableContent",
+ columnWidths,
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 f5bd9ae7c..dc71823af 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,8 +29,9 @@ export const DeleteRowButton = <
return (
{
- const content: TableContent = {
+ const content: PartialTableContent = {
type: "tableContent",
+ columnWidths: props.block.content.columnWidths,
rows: props.block.content.rows.filter(
(_, index) => index !== props.index
),
@@ -68,8 +69,11 @@ export const DeleteColumnButton = <
return (
{
- const content: TableContent = {
+ 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),
})),
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 fac165485..31ccecf4d 100644
--- a/packages/react/src/components/TableHandles/TableHandlesController.tsx
+++ b/packages/react/src/components/TableHandles/TableHandlesController.tsx
@@ -5,12 +5,16 @@ 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 { 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 = <
@@ -18,9 +22,13 @@ export const TableHandlesController = <
S extends StyleSchema = DefaultStyleSchema
>(props: {
tableHandle?: FC>;
+ 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"
@@ -35,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)
);
@@ -60,50 +82,97 @@ export const TableHandlesController = <
draggingState
);
+ 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;
}
- const Component = props.tableHandle || TableHandle;
+ const TableHandleComponent = props.tableHandle || TableHandle;
+ const ExtendButtonComponent = props.extendButton || ExtendButton;
return (
<>
- {!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 &&
+ rowHandle.isMounted &&
+ 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 &&
+ colHandle.isMounted &&
+ 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}
+ />
+
+ )}
+
+ {/* note that the extend buttons are always shown (we don't look at isMounted etc,
+ because otherwise the table slightly shifts when they unmount */}
+
+ setHideCol(false)}
- hideOtherSide={() => setHideCol(true)}
- index={state.rowIndex}
+ orientation={"addOrRemoveRows"}
block={state.block}
- dragStart={callbacks.rowDragStart}
- dragEnd={callbacks.dragEnd}
- freezeHandles={callbacks.freezeHandles}
- unfreezeHandles={callbacks.unfreezeHandles}
+ onMouseDown={onStartExtend}
+ onMouseUp={onEndExtend}
/>
- )}
- {!hideCol && (
-
-
+ setHideRow(false)}
- hideOtherSide={() => setHideRow(true)}
- index={state.colIndex}
+ orientation={"addOrRemoveColumns"}
block={state.block}
- dragStart={callbacks.colDragStart}
- dragEnd={callbacks.dragEnd}
- freezeHandles={callbacks.freezeHandles}
- unfreezeHandles={callbacks.unfreezeHandles}
+ onMouseDown={onStartExtend}
+ onMouseUp={onEndExtend}
/>
- )}
+
>
);
};
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..f102b6c64
--- /dev/null
+++ b/packages/react/src/components/TableHandles/hooks/useExtendButtonsPositioning.ts
@@ -0,0 +1,87 @@
+import { size, useFloating, useTransitionStyles } from "@floating-ui/react";
+import { useEffect, useMemo } from "react";
+
+function useExtendButtonPosition(
+ orientation: "addOrRemoveRows" | "addOrRemoveColumns",
+ show: boolean,
+ referencePosTable: DOMRect | null
+) {
+ const { refs, update, context, floatingStyles } = useFloating({
+ open: show,
+ placement: orientation === "addOrRemoveColumns" ? "right" : "bottom",
+ middleware: [
+ size({
+ apply({ rects, elements }) {
+ Object.assign(
+ elements.floating.style,
+ orientation === "addOrRemoveColumns"
+ ? {
+ height: `${rects.reference.height}px`,
+ }
+ : {
+ width: `${rects.reference.width}px`,
+ }
+ );
+ },
+ }),
+ ],
+ });
+
+ const { isMounted, styles } = useTransitionStyles(context);
+
+ useEffect(() => {
+ update();
+ }, [referencePosTable, update]);
+
+ useEffect(() => {
+ // Will be null on initial render when used in UI component controllers.
+ if (referencePosTable === null) {
+ return;
+ }
+
+ refs.setReference({
+ getBoundingClientRect: () => referencePosTable,
+ });
+ }, [orientation, referencePosTable, refs]);
+
+ return useMemo(
+ () => ({
+ isMounted: isMounted,
+ ref: refs.setFloating,
+ style: {
+ display: "flex",
+ ...styles,
+ ...floatingStyles,
+ },
+ }),
+ [floatingStyles, isMounted, refs.setFloating, styles]
+ );
+}
+
+export function useExtendButtonsPositioning(
+ showAddOrRemoveColumnsButton: boolean,
+ showAddOrRemoveRowsButton: boolean,
+ referencePosTable: DOMRect | null
+): {
+ addOrRemoveRowsButton: ReturnType;
+ addOrRemoveColumnsButton: ReturnType;
+} {
+ const addOrRemoveRowsButton = useExtendButtonPosition(
+ "addOrRemoveRows",
+ showAddOrRemoveRowsButton,
+ referencePosTable
+ );
+ const addOrRemoveColumnsButton = useExtendButtonPosition(
+ "addOrRemoveColumns",
+ showAddOrRemoveColumnsButton,
+ referencePosTable
+ );
+
+ return useMemo(
+ () => ({
+ addOrRemoveRowsButton,
+ addOrRemoveColumnsButton,
+ }),
+ [addOrRemoveColumnsButton, addOrRemoveRowsButton]
+ );
+}
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 735d36f29..123264a19 100644
--- a/packages/react/src/editor/ComponentsContext.tsx
+++ b/packages/react/src/editor/ComponentsContext.tsx
@@ -183,6 +183,12 @@ export type ComponentProps = {
| { children: ReactNode; label?: string }
| { children?: undefined; label: string }
);
+ ExtendButton: {
+ className?: string;
+ onClick: (e: React.MouseEvent) => void;
+ onMouseDown: (e: React.MouseEvent) => void;
+ children: ReactNode;
+ };
};
// TODO: We should try to make everything as generic as we can
Generic: {
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";
diff --git a/packages/shadcn/src/index.tsx b/packages/shadcn/src/index.tsx
index a799d0c75..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,6 +85,7 @@ export const components: Components = {
},
TableHandle: {
Root: TableHandle,
+ ExtendButton: ExtendButton,
},
Generic: {
Form: {
diff --git a/packages/shadcn/src/style.css b/packages/shadcn/src/style.css
index bd507e628..23271f05f 100644
--- a/packages/shadcn/src/style.css
+++ b/packages/shadcn/src/style.css
@@ -147,7 +147,7 @@
}
.bn-shadcn .bn-grid-suggestion-menu-empty-item,
-.bn-shadcn .bn-grid-suggestion-menu-loader{
+.bn-shadcn .bn-grid-suggestion-menu-loader {
align-items: center;
color: var(--bn-colors-menu-text);
display: flex;
@@ -160,3 +160,11 @@
.bn-shadcn .bn-grid-suggestion-menu-loader span {
background-color: hsl(var(--accent));
}
+
+.bn-shadcn .bn-extend-button-add-remove-columns {
+ cursor: col-resize;
+}
+
+.bn-shadcn .bn-extend-button-add-remove-rows {
+ cursor: row-resize;
+}
diff --git a/packages/shadcn/src/tableHandle/ExtendButton.tsx b/packages/shadcn/src/tableHandle/ExtendButton.tsx
new file mode 100644
index 000000000..76b9eca6a
--- /dev/null
+++ b/packages/shadcn/src/tableHandle/ExtendButton.tsx
@@ -0,0 +1,40 @@
+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, 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
+ assertEmpty(rest, false);
+
+ const ShadCNComponents = useShadCNComponentsContext()!;
+
+ return (
+
+ {children}
+
+ );
+});
diff --git a/playground/src/main.tsx b/playground/src/main.tsx
index 653e7c868..14271f4d6 100644
--- a/playground/src/main.tsx
+++ b/playground/src/main.tsx
@@ -96,9 +96,9 @@ const App = (props: { project: (typeof examples.basic)["projects"][0] }) => {
React.useEffect(() => {
(async () => {
// load app async
- const c: any = await modules[
- "../../" + props.project.pathFromRoot + "/App.tsx"
- ]();
+ const moduleName = "../../" + props.project.pathFromRoot + "/App.tsx";
+ const module = modules[moduleName];
+ const c: any = await module();
setExampleComponent(c);
})();
}, [props.project.pathFromRoot]);