diff --git a/examples/03-ui-components/15-advanced-tables/.bnexample.json b/examples/03-ui-components/15-advanced-tables/.bnexample.json new file mode 100644 index 0000000000..9c4787320e --- /dev/null +++ b/examples/03-ui-components/15-advanced-tables/.bnexample.json @@ -0,0 +1,6 @@ +{ + "playground": true, + "docs": true, + "author": "nperez0111", + "tags": ["Intermediate", "UI Components", "Tables", "Appearance & Styling"] +} diff --git a/examples/03-ui-components/15-advanced-tables/App.tsx b/examples/03-ui-components/15-advanced-tables/App.tsx new file mode 100644 index 0000000000..dfe89812a9 --- /dev/null +++ b/examples/03-ui-components/15-advanced-tables/App.tsx @@ -0,0 +1,305 @@ +import "@blocknote/core/fonts/inter.css"; +import { BlockNoteView } from "@blocknote/mantine"; +import "@blocknote/mantine/style.css"; +import { useCreateBlockNote } from "@blocknote/react"; + +export default function App() { + // Creates a new editor instance. + const editor = useCreateBlockNote({ + // This enables the advanced table features + tables: { + splitCells: true, + cellBackgroundColor: true, + cellTextColor: true, + headers: true, + }, + initialContent: [ + { + id: "7e498b3d-d42e-4ade-9be0-054b292715ea", + type: "heading", + props: { + textColor: "default", + backgroundColor: "default", + textAlignment: "left", + level: 2, + }, + content: [ + { + type: "text", + text: "Advanced Tables", + styles: {}, + }, + ], + children: [], + }, + { + id: "cbf287c6-770b-413a-bff5-ad490a0b562a", + type: "table", + props: { + textColor: "default", + }, + content: { + type: "tableContent", + columnWidths: [199, 148, 201], + headerRows: 1, + rows: [ + { + cells: [ + { + type: "tableCell", + content: [ + { + type: "text", + text: "This row has headers", + styles: {}, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "default", + textColor: "default", + textAlignment: "center", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "This is ", + styles: {}, + }, + { + type: "text", + text: "RED", + styles: { + bold: true, + }, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "red", + textColor: "default", + textAlignment: "center", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "Text is Blue", + styles: {}, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "default", + textColor: "blue", + textAlignment: "center", + }, + }, + ], + }, + { + cells: [ + { + type: "tableCell", + content: [ + { + type: "text", + text: "This spans 2 columns\nand 2 rows", + styles: {}, + }, + ], + props: { + colspan: 2, + rowspan: 2, + backgroundColor: "yellow", + textColor: "default", + textAlignment: "left", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "Sooo many features", + styles: {}, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "gray", + textColor: "default", + textAlignment: "left", + }, + }, + ], + }, + { + cells: [ + { + type: "tableCell", + content: [], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "gray", + textColor: "purple", + textAlignment: "left", + }, + }, + ], + }, + { + cells: [ + { + type: "tableCell", + content: [ + { + type: "text", + text: "A cell", + styles: {}, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "Another Cell", + styles: {}, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "default", + textColor: "default", + textAlignment: "right", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "Aligned center", + styles: {}, + }, + ], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "default", + textColor: "default", + textAlignment: "center", + }, + }, + ], + }, + ], + }, + children: [], + }, + { + id: "16e76a94-74e5-42e2-b461-fc9da9f381f7", + type: "paragraph", + props: { + textColor: "default", + backgroundColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Featuring:", + styles: {}, + }, + ], + children: [ + { + id: "785fc9f7-8554-47f4-a4df-8fe2f1438cac", + type: "bulletListItem", + props: { + textColor: "default", + backgroundColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Cell background & foreground coloring", + styles: {}, + }, + ], + children: [], + }, + { + id: "1d0adf08-1b42-421a-b9ea-b3125dcc96d9", + type: "bulletListItem", + props: { + textColor: "default", + backgroundColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Splitting & merging cells", + styles: {}, + }, + ], + children: [], + }, + { + id: "99991aa7-9d86-4d06-9073-b1a9c0329062", + type: "bulletListItem", + props: { + textColor: "default", + backgroundColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Header row & column", + styles: {}, + }, + ], + children: [], + }, + ], + }, + { + id: "c7bf2a7c-8972-44f1-acd8-cf60fa734068", + type: "paragraph", + props: { + textColor: "default", + backgroundColor: "default", + textAlignment: "left", + }, + content: [], + children: [], + }, + ], + }); + + // Renders the editor instance using a React component. + return ; +} diff --git a/examples/03-ui-components/15-advanced-tables/README.md b/examples/03-ui-components/15-advanced-tables/README.md new file mode 100644 index 0000000000..0eb9e78c32 --- /dev/null +++ b/examples/03-ui-components/15-advanced-tables/README.md @@ -0,0 +1,13 @@ +# Advanced Tables + +This example enables the following features in tables: + +- Split cells +- Cell background color +- Cell text color +- Table row and column headers + +**Relevant Docs:** + +- [Tables](/docs/editor-basics/tables) +- [Editor Setup](/docs/editor-basics/setup) diff --git a/examples/03-ui-components/15-advanced-tables/index.html b/examples/03-ui-components/15-advanced-tables/index.html new file mode 100644 index 0000000000..b4bd86e618 --- /dev/null +++ b/examples/03-ui-components/15-advanced-tables/index.html @@ -0,0 +1,14 @@ + + + + + + Advanced Tables + + +
+ + + diff --git a/examples/03-ui-components/15-advanced-tables/main.tsx b/examples/03-ui-components/15-advanced-tables/main.tsx new file mode 100644 index 0000000000..f88b490fbd --- /dev/null +++ b/examples/03-ui-components/15-advanced-tables/main.tsx @@ -0,0 +1,11 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; + +const root = createRoot(document.getElementById("root")!); +root.render( + + + +); diff --git a/examples/03-ui-components/15-advanced-tables/package.json b/examples/03-ui-components/15-advanced-tables/package.json new file mode 100644 index 0000000000..9d7b623424 --- /dev/null +++ b/examples/03-ui-components/15-advanced-tables/package.json @@ -0,0 +1,37 @@ +{ + "name": "@blocknote/example-advanced-tables", + "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "private": true, + "version": "0.12.4", + "scripts": { + "start": "vite", + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --max-warnings 0" + }, + "dependencies": { + "@blocknote/core": "latest", + "@blocknote/react": "latest", + "@blocknote/ariakit": "latest", + "@blocknote/mantine": "latest", + "@blocknote/shadcn": "latest", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^8.10.0", + "vite": "^5.3.4" + }, + "eslintConfig": { + "extends": [ + "../../../.eslintrc.js" + ] + }, + "eslintIgnore": [ + "dist" + ] +} \ No newline at end of file diff --git a/examples/03-ui-components/15-advanced-tables/tsconfig.json b/examples/03-ui-components/15-advanced-tables/tsconfig.json new file mode 100644 index 0000000000..1bd8ab3c57 --- /dev/null +++ b/examples/03-ui-components/15-advanced-tables/tsconfig.json @@ -0,0 +1,36 @@ +{ + "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "composite": true + }, + "include": [ + "." + ], + "__ADD_FOR_LOCAL_DEV_references": [ + { + "path": "../../../packages/core/" + }, + { + "path": "../../../packages/react/" + } + ] +} \ No newline at end of file diff --git a/examples/03-ui-components/15-advanced-tables/vite.config.ts b/examples/03-ui-components/15-advanced-tables/vite.config.ts new file mode 100644 index 0000000000..f62ab20bc2 --- /dev/null +++ b/examples/03-ui-components/15-advanced-tables/vite.config.ts @@ -0,0 +1,32 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import react from "@vitejs/plugin-react"; +import * as fs from "fs"; +import * as path from "path"; +import { defineConfig } from "vite"; +// import eslintPlugin from "vite-plugin-eslint"; +// https://vitejs.dev/config/ +export default defineConfig((conf) => ({ + plugins: [react()], + optimizeDeps: {}, + build: { + sourcemap: true, + }, + resolve: { + alias: + conf.command === "build" || + !fs.existsSync(path.resolve(__dirname, "../../packages/core/src")) + ? {} + : ({ + // Comment out the lines below to load a built version of blocknote + // or, keep as is to load live from sources with live reload working + "@blocknote/core": path.resolve( + __dirname, + "../../packages/core/src/" + ), + "@blocknote/react": path.resolve( + __dirname, + "../../packages/react/src/" + ), + } as any), + }, +})); diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 51255ce302..efadeade12 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -268,6 +268,36 @@ export type BlockNoteEditorOptions< * @default "viewport" */ sideMenuDetection: "viewport" | "editor"; + + /** + * Allows enabling / disabling features of tables. + */ + tables?: { + /** + * Whether to allow splitting and merging cells within a table. + * + * @default false + */ + splitCells?: boolean; + /** + * Whether to allow changing the background color of cells. + * + * @default false + */ + cellBackgroundColor?: boolean; + /** + * Whether to allow changing the text color of cells. + * + * @default false + */ + cellTextColor?: boolean; + /** + * Whether to allow changing cells into headers. + * + * @default false + */ + headers?: boolean; + }; }; const blockNoteTipTapOptions = { @@ -281,7 +311,10 @@ export class BlockNoteEditor< ISchema extends InlineContentSchema = DefaultInlineContentSchema, SSchema extends StyleSchema = DefaultStyleSchema > { - private readonly _pmSchema: Schema; + /** + * The underlying prosemirror schema + */ + public readonly pmSchema: Schema; /** * extensions that are added to the editor, can be tiptap extensions or prosemirror plugins @@ -371,9 +404,17 @@ export class BlockNoteEditor< public readonly resolveFileUrl?: (url: string) => Promise; - public get pmSchema() { - return this._pmSchema; - } + /** + * Editor settings + */ + public readonly settings: { + tables: { + splitCells: boolean; + cellBackgroundColor: boolean; + cellTextColor: boolean; + headers: boolean; + }; + }; public static create< BSchema extends BlockSchema = DefaultBlockSchema, @@ -412,6 +453,14 @@ export class BlockNoteEditor< } this.dictionary = options.dictionary || en; + this.settings = { + tables: { + splitCells: options?.tables?.splitCells ?? false, + cellBackgroundColor: options?.tables?.cellBackgroundColor ?? false, + cellTextColor: options?.tables?.cellTextColor ?? false, + headers: options?.tables?.headers ?? false, + }, + }; // apply defaults const newOptions = { @@ -579,11 +628,11 @@ export class BlockNoteEditor< view: any; contentComponent: any; }; - this._pmSchema = this._tiptapEditor.schema; + this.pmSchema = this._tiptapEditor.schema; } else { // In headless mode, we don't instantiate an underlying TipTap editor, // but we still need the schema - this._pmSchema = getSchema(tiptapOptions.extensions!); + this.pmSchema = getSchema(tiptapOptions.extensions!); } } diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/TableCellMergeButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/TableCellMergeButton.tsx index 776d47f4e1..bb11326a08 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/TableCellMergeButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/TableCellMergeButton.tsx @@ -43,7 +43,11 @@ export const TableCellMergeButton = () => { editor.tableHandles?.mergeCells(); }, [editor]); - if (!editor.isEditable || mergeDirection === undefined) { + if ( + !editor.isEditable || + mergeDirection === undefined || + !editor.settings.tables.splitCells + ) { return null; } diff --git a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx index 53874aae4d..7f2e88fd86 100644 --- a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx +++ b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx @@ -31,7 +31,7 @@ export const TableRowHeaderItem = < S >(); - if (props.block.type !== "table") { + if (props.block.type !== "table" || !editor.settings.tables.headers) { return null; } @@ -79,7 +79,7 @@ export const TableColumnHeaderItem = < S >(); - if (props.block.type !== "table") { + if (props.block.type !== "table" || !editor.settings.tables.headers) { return null; } diff --git a/packages/react/src/components/TableHandles/TableCellButton.tsx b/packages/react/src/components/TableHandles/TableCellButton.tsx index f1ba77f211..00ef9f3a21 100644 --- a/packages/react/src/components/TableHandles/TableCellButton.tsx +++ b/packages/react/src/components/TableHandles/TableCellButton.tsx @@ -26,6 +26,15 @@ export const TableCellButton = < const Component = props.tableCellMenu || TableCellMenu; + if ( + !props.editor.settings.tables.splitCells && + !props.editor.settings.tables.cellBackgroundColor && + !props.editor.settings.tables.cellTextColor + ) { + // Hide the button altogether if all table cell settings are disabled + return null; + } + return ( { diff --git a/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/ColorPicker.tsx b/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/ColorPicker.tsx index c9f21f85e9..91b0951ec0 100644 --- a/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/ColorPicker.tsx +++ b/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/ColorPicker.tsx @@ -62,7 +62,11 @@ export const ColorPickerButton = < const currentCell = props.block.content.rows[props.rowIndex]?.cells?.[props.colIndex]; - if (!currentCell) { + if ( + !currentCell || + (editor.settings.tables.cellTextColor === false && + editor.settings.tables.cellBackgroundColor === false) + ) { return null; } @@ -72,7 +76,6 @@ export const ColorPickerButton = < - {/* TODO should I be using the dictionary here? */} {props.children || dict.drag_handle.colors_menuitem} @@ -82,18 +85,26 @@ export const ColorPickerButton = < className={"bn-menu-dropdown bn-color-picker-dropdown"}> updateColor(color, "text"), - }} - background={{ - color: isTableCell(currentCell) - ? currentCell.props.backgroundColor - : "default", - setColor: (color) => updateColor(color, "background"), - }} + text={ + editor.settings.tables.cellTextColor + ? { + color: isTableCell(currentCell) + ? currentCell.props.textColor + : "default", + setColor: (color) => updateColor(color, "text"), + } + : undefined + } + background={ + editor.settings.tables.cellBackgroundColor + ? { + color: isTableCell(currentCell) + ? currentCell.props.backgroundColor + : "default", + setColor: (color) => updateColor(color, "background"), + } + : undefined + } /> diff --git a/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/SplitButton.tsx b/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/SplitButton.tsx index 9d05c0ed65..1e8f5c9ab0 100644 --- a/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/SplitButton.tsx +++ b/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/SplitButton.tsx @@ -34,7 +34,8 @@ export const SplitButton = < if ( !currentCell || !isTableCell(currentCell) || - (getRowspan(currentCell) === 1 && getColspan(currentCell) === 1) + (getRowspan(currentCell) === 1 && getColspan(currentCell) === 1) || + !editor.settings.tables.splitCells ) { return null; } diff --git a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/ColorPicker.tsx b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/ColorPicker.tsx index 0b615a146e..1010b5c34d 100644 --- a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/ColorPicker.tsx +++ b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/ColorPicker.tsx @@ -73,7 +73,13 @@ export const ColorPickerButton = < editor.setTextCursorPosition(props.block); }; - if (!currentCells || !currentCells[0] || !tableHandles) { + if ( + !currentCells || + !currentCells[0] || + !tableHandles || + (editor.settings.tables.cellTextColor === false && + editor.settings.tables.cellBackgroundColor === false) + ) { return null; } @@ -95,29 +101,38 @@ export const ColorPickerButton = < className={"bn-menu-dropdown bn-color-picker-dropdown"}> - isTableCell(cell) && - cell.props.textColor === firstCell.props.textColor - ) - ? firstCell.props.textColor - : "default", - setColor: (color) => { - updateColor(color, "text"); - }, - }} - background={{ - color: currentCells.every( - ({ cell }) => - isTableCell(cell) && - cell.props.backgroundColor === firstCell.props.backgroundColor - ) - ? firstCell.props.backgroundColor - : "default", - setColor: (color) => updateColor(color, "background"), - }} + text={ + editor.settings.tables.cellTextColor + ? { + // All cells have the same text color + color: currentCells.every( + ({ cell }) => + isTableCell(cell) && + cell.props.textColor === firstCell.props.textColor + ) + ? firstCell.props.textColor + : "default", + setColor: (color) => { + updateColor(color, "text"); + }, + } + : undefined + } + background={ + editor.settings.tables.cellBackgroundColor + ? { + color: currentCells.every( + ({ cell }) => + isTableCell(cell) && + cell.props.backgroundColor === + firstCell.props.backgroundColor + ) + ? firstCell.props.backgroundColor + : "default", + setColor: (color) => updateColor(color, "background"), + } + : undefined + } /> diff --git a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/TableHeaderButton.tsx b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/TableHeaderButton.tsx index 2efad1b0ae..1860127f64 100644 --- a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/TableHeaderButton.tsx +++ b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/TableHeaderButton.tsx @@ -27,7 +27,12 @@ export const TableHeaderRowButton = < >(); const tableHandles = editor.tableHandles; - if (!tableHandles || props.index !== 0 || props.orientation !== "row") { + if ( + !tableHandles || + props.index !== 0 || + props.orientation !== "row" || + !editor.settings.tables.headers + ) { return null; } @@ -73,7 +78,12 @@ export const TableHeaderColumnButton = < >(); const tableHandles = editor.tableHandles; - if (!tableHandles || props.index !== 0 || props.orientation !== "column") { + if ( + !tableHandles || + props.index !== 0 || + props.orientation !== "column" || + !editor.settings.tables.headers + ) { return null; } diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index 1aa17a29dc..1952dca6da 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -657,6 +657,27 @@ "slug": "ui-components" } }, + { + "projectSlug": "advanced-tables", + "fullSlug": "ui-components/advanced-tables", + "pathFromRoot": "examples/03-ui-components/15-advanced-tables", + "config": { + "playground": true, + "docs": true, + "author": "nperez0111", + "tags": [ + "Intermediate", + "UI Components", + "Tables", + "Appearance & Styling" + ] + }, + "title": "Advanced Tables", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + }, { "projectSlug": "link-toolbar-buttons", "fullSlug": "ui-components/link-toolbar-buttons",