@blocknote/core 0.24.2 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/dist/blocknote.cjs +12 -0
  2. package/dist/blocknote.cjs.map +1 -0
  3. package/dist/blocknote.js +4754 -3514
  4. package/dist/blocknote.js.map +1 -1
  5. package/dist/comments.cjs +2 -0
  6. package/dist/comments.cjs.map +1 -0
  7. package/dist/comments.js +593 -0
  8. package/dist/comments.js.map +1 -0
  9. package/dist/style.css +1 -1
  10. package/dist/tsconfig.tsbuildinfo +1 -1
  11. package/dist/webpack-stats.json +1 -1
  12. package/package.json +39 -26
  13. package/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap +1022 -378
  14. package/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap +730 -270
  15. package/src/api/blockManipulation/commands/moveBlocks/__snapshots__/moveBlocks.test.ts.snap +3100 -1260
  16. package/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap +438 -162
  17. package/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap +1168 -432
  18. package/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap +930 -378
  19. package/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap +2485 -1015
  20. package/src/api/blockManipulation/commands/updateBlock/updateBlock.test.ts +28 -1
  21. package/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +1 -1
  22. package/src/api/blockManipulation/selections/__snapshots__/selection.test.ts.snap +292 -108
  23. package/src/api/blockManipulation/setupTestEnv.ts +14 -1
  24. package/src/api/blockManipulation/tables/tables.test.ts +1987 -0
  25. package/src/api/blockManipulation/tables/tables.ts +887 -0
  26. package/src/api/clipboard/__snapshots__/external/pasteEndOfParagraph.html +66 -24
  27. package/src/api/clipboard/__snapshots__/external/pasteEndOfParagraphText.html +66 -24
  28. package/src/api/clipboard/__snapshots__/external/pasteImage.html +66 -24
  29. package/src/api/clipboard/__snapshots__/external/pasteParagraphInCustomBlock.html +66 -24
  30. package/src/api/clipboard/__snapshots__/external/pasteTable.html +132 -48
  31. package/src/api/clipboard/__snapshots__/external/pasteTableInExistingTable.html +136 -44
  32. package/src/api/clipboard/toClipboard/copyExtension.ts +2 -3
  33. package/src/api/exporters/html/__snapshots__/table/headerCols/external.html +1 -0
  34. package/src/api/exporters/html/__snapshots__/table/headerCols/internal.html +1 -0
  35. package/src/api/exporters/html/__snapshots__/table/headerRows/external.html +1 -0
  36. package/src/api/exporters/html/__snapshots__/table/headerRows/internal.html +1 -0
  37. package/src/api/exporters/html/__snapshots__/table/headersRows/external.html +1 -0
  38. package/src/api/exporters/html/__snapshots__/table/headersRows/internal.html +1 -0
  39. package/src/api/exporters/html/__snapshots__/table/mixedCellColors/external.html +1 -0
  40. package/src/api/exporters/html/__snapshots__/table/mixedCellColors/internal.html +1 -0
  41. package/src/api/exporters/html/__snapshots__/table/mixedRowspansAndColspans/external.html +1 -0
  42. package/src/api/exporters/html/__snapshots__/table/mixedRowspansAndColspans/internal.html +1 -0
  43. package/src/api/exporters/markdown/__snapshots__/table/headerCols/markdown.md +4 -0
  44. package/src/api/exporters/markdown/__snapshots__/table/headerRows/markdown.md +4 -0
  45. package/src/api/exporters/markdown/__snapshots__/table/mixedCellColors/markdown.md +5 -0
  46. package/src/api/exporters/markdown/__snapshots__/table/mixedRowspansAndColspans/markdown.md +5 -0
  47. package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +985 -20
  48. package/src/api/nodeConversions/blockToNode.ts +63 -20
  49. package/src/api/nodeConversions/nodeToBlock.ts +75 -13
  50. package/src/api/parsers/html/__snapshots__/parse-notion-html.json +145 -54
  51. package/src/api/testUtil/cases/defaultSchema.ts +782 -9
  52. package/src/api/testUtil/partialBlockTestUtil.ts +39 -4
  53. package/src/blocks/TableBlockContent/TableBlockContent.ts +11 -5
  54. package/src/blocks/defaultBlockTypeGuards.ts +8 -0
  55. package/src/comments/index.ts +9 -0
  56. package/src/comments/models/User.ts +8 -0
  57. package/src/comments/threadstore/DefaultThreadStoreAuth.ts +106 -0
  58. package/src/comments/threadstore/ThreadStore.ts +134 -0
  59. package/src/comments/threadstore/ThreadStoreAuth.ts +13 -0
  60. package/src/comments/threadstore/TipTapThreadStore.ts +292 -0
  61. package/src/comments/threadstore/yjs/RESTYjsThreadStore.ts +144 -0
  62. package/src/comments/threadstore/yjs/YjsThreadStore.test.ts +294 -0
  63. package/src/comments/threadstore/yjs/YjsThreadStore.ts +340 -0
  64. package/src/comments/threadstore/yjs/YjsThreadStoreBase.ts +48 -0
  65. package/src/comments/threadstore/yjs/yjsHelpers.ts +121 -0
  66. package/src/comments/types.ts +117 -0
  67. package/src/editor/Block.css +16 -8
  68. package/src/editor/BlockNoteEditor.ts +269 -92
  69. package/src/editor/BlockNoteExtensions.ts +24 -1
  70. package/src/editor/BlockNoteTipTapEditor.ts +5 -1
  71. package/src/editor/editor.css +17 -0
  72. package/src/extensions/BackgroundColor/BackgroundColorExtension.ts +1 -1
  73. package/src/extensions/Comments/CommentMark.ts +61 -0
  74. package/src/extensions/Comments/CommentsPlugin.ts +301 -0
  75. package/src/extensions/Comments/userstore/UserStore.ts +72 -0
  76. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +9 -5
  77. package/src/extensions/LinkToolbar/LinkToolbarPlugin.ts +3 -3
  78. package/src/extensions/ShowSelection/ShowSelectionPlugin.ts +52 -0
  79. package/src/extensions/TableHandles/TableHandlesPlugin.ts +409 -57
  80. package/src/extensions/TextAlignment/TextAlignmentExtension.ts +2 -0
  81. package/src/extensions/TextColor/TextColorExtension.ts +1 -1
  82. package/src/i18n/locales/ar.ts +23 -0
  83. package/src/i18n/locales/de.ts +15 -0
  84. package/src/i18n/locales/en.ts +25 -1
  85. package/src/i18n/locales/es.ts +16 -1
  86. package/src/i18n/locales/fr.ts +23 -0
  87. package/src/i18n/locales/hr.ts +18 -0
  88. package/src/i18n/locales/is.ts +24 -1
  89. package/src/i18n/locales/it.ts +15 -0
  90. package/src/i18n/locales/ja.ts +23 -0
  91. package/src/i18n/locales/ko.ts +23 -0
  92. package/src/i18n/locales/nl.ts +23 -0
  93. package/src/i18n/locales/no.ts +23 -0
  94. package/src/i18n/locales/pl.ts +23 -0
  95. package/src/i18n/locales/pt.ts +23 -0
  96. package/src/i18n/locales/ru.ts +23 -0
  97. package/src/i18n/locales/uk.ts +23 -0
  98. package/src/i18n/locales/vi.ts +23 -0
  99. package/src/i18n/locales/zh.ts +23 -0
  100. package/src/index.ts +6 -4
  101. package/src/schema/blocks/types.ts +32 -2
  102. package/src/util/browser.ts +1 -1
  103. package/src/util/table.ts +107 -0
  104. package/types/src/api/blockManipulation/tables/tables.d.ts +343 -0
  105. package/types/src/api/blockManipulation/tables/tables.test.d.ts +1 -0
  106. package/types/src/api/clipboard/toClipboard/copyExtension.d.ts +1 -1
  107. package/types/src/blocks/TableBlockContent/TableBlockContent.d.ts +1 -2
  108. package/types/src/blocks/defaultBlockTypeGuards.d.ts +3 -0
  109. package/types/src/comments/index.d.ts +9 -0
  110. package/types/src/comments/models/User.d.ts +8 -0
  111. package/types/src/comments/threadstore/DefaultThreadStoreAuth.d.ts +47 -0
  112. package/types/src/comments/threadstore/ThreadStore.d.ts +121 -0
  113. package/types/src/comments/threadstore/ThreadStoreAuth.d.ts +12 -0
  114. package/types/src/comments/threadstore/TipTapThreadStore.d.ts +97 -0
  115. package/types/src/comments/threadstore/yjs/RESTYjsThreadStore.d.ts +83 -0
  116. package/types/src/comments/threadstore/yjs/YjsThreadStore.d.ts +79 -0
  117. package/types/src/comments/threadstore/yjs/YjsThreadStore.test.d.ts +1 -0
  118. package/types/src/comments/threadstore/yjs/YjsThreadStoreBase.d.ts +15 -0
  119. package/types/src/comments/threadstore/yjs/yjsHelpers.d.ts +13 -0
  120. package/types/src/comments/types.d.ts +109 -0
  121. package/types/src/editor/BlockNoteEditor.d.ts +146 -66
  122. package/types/src/editor/BlockNoteExtensions.d.ts +4 -0
  123. package/types/src/extensions/Collaboration/createCollaborationExtensions.d.ts +1 -1
  124. package/types/src/extensions/Comments/CommentMark.d.ts +2 -0
  125. package/types/src/extensions/Comments/CommentsPlugin.d.ts +49 -0
  126. package/types/src/extensions/Comments/userstore/UserStore.d.ts +31 -0
  127. package/types/src/extensions/ShowSelection/ShowSelectionPlugin.d.ts +15 -0
  128. package/types/src/extensions/TableHandles/TableHandlesPlugin.d.ts +66 -1
  129. package/types/src/i18n/locales/de.d.ts +15 -0
  130. package/types/src/i18n/locales/en.d.ts +20 -0
  131. package/types/src/i18n/locales/es.d.ts +15 -0
  132. package/types/src/i18n/locales/hr.d.ts +18 -0
  133. package/types/src/i18n/locales/it.d.ts +15 -0
  134. package/types/src/index.d.ts +5 -4
  135. package/types/src/pm-nodes/BlockContainer.d.ts +2 -2
  136. package/types/src/pm-nodes/BlockGroup.d.ts +2 -2
  137. package/types/src/schema/blocks/types.d.ts +23 -2
  138. package/types/src/util/browser.d.ts +1 -1
  139. package/types/src/util/table.d.ts +12 -0
  140. package/dist/blocknote.umd.cjs +0 -11
  141. package/dist/blocknote.umd.cjs.map +0 -1
@@ -0,0 +1,887 @@
1
+ import { DefaultBlockSchema } from "../../../blocks/defaultBlocks.js";
2
+ import {
3
+ BlockFromConfigNoChildren,
4
+ PartialTableContent,
5
+ TableCell,
6
+ TableContent,
7
+ } from "../../../schema/blocks/types.js";
8
+ import {
9
+ isPartialLinkInlineContent,
10
+ isStyledTextInlineContent,
11
+ } from "../../../schema/index.js";
12
+ import {
13
+ getColspan,
14
+ getRowspan,
15
+ isPartialTableCell,
16
+ mapTableCell,
17
+ } from "../../../util/table.js";
18
+
19
+ /**
20
+ * Here be dragons.
21
+ *
22
+ * Tables are complex because of rowspan and colspan behavior.
23
+ * The majority of this file is concerned with translating between "relative" and "absolute" indices.
24
+ *
25
+ * The following diagram may help explain the relationship between the different indices:
26
+ *
27
+ * One-based indexing of rows and columns in a table:
28
+ * | 1-1 | 1-2 | 1-3 |
29
+ * | 2-1 | 2-2 | 2-3 |
30
+ * | 3-1 | 3-2 | 3-3 |
31
+ *
32
+ * A complicated table with colspans and rowspans:
33
+ * | 1-1 | 1-2 | 1-2 |
34
+ * | 2-1 | 2-1 | 2-2 |
35
+ * | 2-1 | 2-1 | 3-1 |
36
+ *
37
+ * You can see here that we have:
38
+ * - two cells that contain the value "1-2", because it has a colspan of 2.
39
+ * - four cells that contain the value "2-1", because it has a rowspan of 2 and a colspan of 2.
40
+ *
41
+ * This would be represented in block note json (roughly) as:
42
+ * [
43
+ * {
44
+ * "cells": [
45
+ * {
46
+ * "type": "tableCell",
47
+ * "content": ["1,1"],
48
+ * "props": {
49
+ * "colspan": 1,
50
+ * "rowspan": 1
51
+ * },
52
+ * },
53
+ * {
54
+ * "type": "tableCell",
55
+ * "content": ["1,2"],
56
+ * "props": {
57
+ * "colspan": 2,
58
+ * "rowspan": 1
59
+ * }
60
+ * }
61
+ * ],
62
+ * },
63
+ * {
64
+ * "cells": [
65
+ * {
66
+ * "type": "tableCell",
67
+ * "content": ["2,1"],
68
+ * "props": {
69
+ * "colspan": 2,
70
+ * "rowspan": 2
71
+ * }
72
+ * },
73
+ * {
74
+ * "type": "tableCell",
75
+ * "content": ["2,2"],
76
+ * "props": {
77
+ * "colspan": 1,
78
+ * "rowspan": 1
79
+ * }
80
+ * ],
81
+ * },
82
+ * {
83
+ * "cells": [
84
+ * {
85
+ * "type": "tableCell",
86
+ * "content": ["3,1"],
87
+ * "props": {
88
+ * "colspan": 1,
89
+ * "rowspan": 1,
90
+ * }
91
+ * }
92
+ * ]
93
+ * }
94
+ * ]
95
+ *
96
+ * Which maps cleanly to the following HTML:
97
+ *
98
+ * <table>
99
+ * <tr>
100
+ * <td>1-1</td>
101
+ * <td colspan="2">1-2</td>
102
+ * </tr>
103
+ * <tr>
104
+ * <td rowspan="2" colspan="2">2-1</td>
105
+ * <td>2-2</td>
106
+ * </tr>
107
+ * <tr>
108
+ * <td>3-1</td>
109
+ * </tr>
110
+ * </table>
111
+ *
112
+ * We have a problem though, from the block json, there is no way to tell that the cell "2-1" is the second cell in the second row.
113
+ * To resolve this, we created the occupancy grid, which is a grid of all the cells in the table, as though they were only 1x1 cells.
114
+ * See {@link OccupancyGrid} for more information.
115
+ *
116
+ */
117
+
118
+ /**
119
+ * Relative cell indices are relative to the table block's content.
120
+ *
121
+ * This is a sparse representation of the table and is how HTML and BlockNote JSON represent tables.
122
+ *
123
+ * For example, if we have a table with a rowspan of 2, the second row may only have 1 element in a 2x2 table.
124
+ *
125
+ * ```
126
+ * // Visual representation of the table
127
+ * | 1-1 | 1-2 | // has 2 cells
128
+ * | 1-1 | 2-2 | // has only 1 cell
129
+ * // Relative cell indices
130
+ * [{ row: 1, col: 1, rowspan: 2 }, { row: 1, col: 2 }] // has 2 cells
131
+ * [{ row: 1, col: 2 }] // has only 1 cell
132
+ * ```
133
+ */
134
+ export type RelativeCellIndices = {
135
+ row: number;
136
+ col: number;
137
+ };
138
+
139
+ /**
140
+ * Absolute cell indices are relative to the table's layout (it's {@link OccupancyGrid}).
141
+ *
142
+ * It is as though the table is a grid of 1x1 cells, and any colspan or rowspan results in multiple 1x1 cells being occupied.
143
+ *
144
+ * For example, if we have a table with a colspan of 2, it will occupy 2 cells in the layout grid.
145
+ *
146
+ * ```
147
+ * // Visual representation of the table
148
+ * | 1-1 | 1-1 | // has 2 cells
149
+ * | 2-1 | 2-2 | // has 2 cell
150
+ * // Absolute cell indices
151
+ * [{ row: 1, col: 1, colspan: 2 }, { row: 1, col: 2, colspan: 2 }] // has 2 cells
152
+ * [{ row: 1, col: 1 }, { row: 1, col: 2 }] // has 2 cells
153
+ * ```
154
+ */
155
+ export type AbsoluteCellIndices = {
156
+ row: number;
157
+ col: number;
158
+ };
159
+
160
+ /**
161
+ * An occupancy grid is a grid of the occupied cells in the table.
162
+ * It is used to track the occupied cells in the table to know where to place the next cell.
163
+ *
164
+ * Since it allows us to resolve cell indices both {@link RelativeCellIndices} and {@link AbsoluteCellIndices}, it is the core data structure for table operations.
165
+ */
166
+ type OccupancyGrid = (RelativeCellIndices & {
167
+ /**
168
+ * The rowspan of the cell.
169
+ */
170
+ rowspan: number;
171
+ /**
172
+ * The colspan of the cell.
173
+ */
174
+ colspan: number;
175
+ /**
176
+ * The cell.
177
+ */
178
+ cell: TableCell<any, any>;
179
+ })[][];
180
+
181
+ /**
182
+ * This will return the {@link OccupancyGrid} of the table.
183
+ * By laying out the table as though it were a grid of 1x1 cells, we can easily track where the cells are located (both relatively and absolutely).
184
+ *
185
+ * @returns an {@link OccupancyGrid}
186
+ */
187
+ export function getTableCellOccupancyGrid(
188
+ block: BlockFromConfigNoChildren<DefaultBlockSchema["table"], any, any>
189
+ ): OccupancyGrid {
190
+ const { height, width } = getDimensionsOfTable(block);
191
+
192
+ /**
193
+ * Create a grid to track occupied cells
194
+ * This is used because rowspans and colspans take up multiple spaces
195
+ * So, we need to track the occupied cells in the grid to know where to place the next cell
196
+ */
197
+ const grid: OccupancyGrid = new Array(height)
198
+ .fill(false)
199
+ .map(() => new Array(width).fill(null));
200
+
201
+ // Find the next unoccupied cell in the table, row-major order
202
+ const findNextAvailable = (row: number, col: number) => {
203
+ for (let i = row; i < height; i++) {
204
+ for (let j = col; j < width; j++) {
205
+ if (!grid[i][j]) {
206
+ return { row: i, col: j };
207
+ }
208
+ }
209
+ }
210
+
211
+ throw new Error(
212
+ "Unable to create occupancy grid for table, no more available cells"
213
+ );
214
+ };
215
+
216
+ // Build up the grid, trying to fill in the cells with the correct relative row and column indices
217
+ for (let row = 0; row < block.content.rows.length; row++) {
218
+ for (let col = 0; col < block.content.rows[row].cells.length; col++) {
219
+ const cell = mapTableCell(block.content.rows[row].cells[col]);
220
+ const rowspan = getRowspan(cell);
221
+ const colspan = getColspan(cell);
222
+
223
+ // Rowspan and colspan complicate things, by taking up multiple cells in the grid
224
+ // We need to iterate over the cells that the rowspan and colspan take up
225
+ // and fill in the grid with the correct relative row and column indices
226
+ const { row: startRow, col: startCol } = findNextAvailable(row, col);
227
+
228
+ // Fill in the rowspan X colspan cells, starting from the next available cell, with the correct relative row and column indices
229
+ for (let i = startRow; i < startRow + rowspan; i++) {
230
+ for (let j = startCol; j < startCol + colspan; j++) {
231
+ if (grid[i][j]) {
232
+ // The cell is already occupied, the table is malformed
233
+ throw new Error(
234
+ `Unable to create occupancy grid for table, cell at ${i},${j} is already occupied`
235
+ );
236
+ }
237
+
238
+ grid[i][j] = {
239
+ row,
240
+ col,
241
+ rowspan,
242
+ colspan,
243
+ cell,
244
+ };
245
+ }
246
+ }
247
+ }
248
+ }
249
+
250
+ // console.log(grid);
251
+
252
+ return grid;
253
+ }
254
+
255
+ /**
256
+ * Given an {@link OccupancyGrid}, this will return the {@link TableContent} rows.
257
+ *
258
+ * @note This will remove duplicates from the occupancy grid. And does no bounds checking for validity of the occupancy grid.
259
+ */
260
+ export function getTableRowsFromOccupancyGrid(
261
+ occupancyGrid: OccupancyGrid
262
+ ): TableContent<any, any>["rows"] {
263
+ // Because a cell can have a rowspan or colspan, it can occupy multiple cells in the occupancy grid
264
+ // So, we need to remove duplicates from the occupancy grid before we can return the table rows
265
+ const seen = new Set<string>();
266
+
267
+ return occupancyGrid.map((row) => {
268
+ // Just read out the cells in the occupancy grid, removing duplicates
269
+ return {
270
+ cells: row
271
+ .map((cell) => {
272
+ if (seen.has(cell.row + ":" + cell.col)) {
273
+ return false;
274
+ }
275
+ seen.add(cell.row + ":" + cell.col);
276
+ return cell.cell;
277
+ })
278
+ .filter((cell): cell is TableCell<any, any> => cell !== false),
279
+ };
280
+ });
281
+ }
282
+
283
+ /**
284
+ * This will resolve the relative cell indices within the table block to the absolute cell indices within the table, accounting for colspan and rowspan.
285
+ *
286
+ * @note It will return only the first cell (i.e. top-left) that matches the relative cell indices. To find the other absolute cell indices this cell occupies, you can assume it is the rowspan and colspan number of cells away from the top-left cell.
287
+ *
288
+ * @returns The {@link AbsoluteCellIndices} and the {@link TableCell} at the absolute position.
289
+ */
290
+ export function getAbsoluteTableCells(
291
+ /**
292
+ * The relative position of the cell in the table.
293
+ */
294
+ relativeCellIndices: RelativeCellIndices,
295
+ /**
296
+ * The table block containing the cell.
297
+ */
298
+ block: BlockFromConfigNoChildren<DefaultBlockSchema["table"], any, any>,
299
+ /**
300
+ * The occupancy grid of the table.
301
+ */
302
+ occupancyGrid: OccupancyGrid = getTableCellOccupancyGrid(block)
303
+ ): AbsoluteCellIndices & {
304
+ cell: TableCell<any, any>;
305
+ } {
306
+ for (let r = 0; r < occupancyGrid.length; r++) {
307
+ for (let c = 0; c < occupancyGrid[r].length; c++) {
308
+ // console.log(r, c, occupancyGrid);
309
+ const cell = occupancyGrid[r][c];
310
+ if (
311
+ cell.row === relativeCellIndices.row &&
312
+ cell.col === relativeCellIndices.col
313
+ ) {
314
+ return { row: r, col: c, cell: cell.cell };
315
+ }
316
+ }
317
+ }
318
+
319
+ throw new Error(
320
+ `Unable to resolve relative table cell indices for table, cell at ${relativeCellIndices.row},${relativeCellIndices.col} is not occupied`
321
+ );
322
+ }
323
+
324
+ /**
325
+ * This will get the dimensions of the table block.
326
+ *
327
+ * @returns The height and width of the table.
328
+ */
329
+ export function getDimensionsOfTable(
330
+ block: BlockFromConfigNoChildren<DefaultBlockSchema["table"], any, any>
331
+ ): {
332
+ /**
333
+ * The number of rows in the table.
334
+ */
335
+ height: number;
336
+ /**
337
+ * The number of columns in the table.
338
+ */
339
+ width: number;
340
+ } {
341
+ // Due to the way we store the table, the height is always the number of rows
342
+ const height = block.content.rows.length;
343
+
344
+ // Calculating the width is a bit more complex, as it is the maximum width of any row
345
+ let width = 0;
346
+ block.content.rows.forEach((row) => {
347
+ // Find the width of the row by summing the colspan of each cell
348
+ let rowWidth = 0;
349
+ row.cells.forEach((cell) => {
350
+ rowWidth += getColspan(cell);
351
+ });
352
+
353
+ // Update the width if the row is wider than the current width
354
+ width = Math.max(width, rowWidth);
355
+ });
356
+
357
+ return { height, width };
358
+ }
359
+
360
+ /**
361
+ * This will resolve the absolute cell indices within the table block to the relative cell indices within the table, accounting for colspan and rowspan.
362
+ *
363
+ * @returns The {@link RelativeCellIndices} and the {@link TableCell} at the relative position.
364
+ */
365
+ export function getRelativeTableCells(
366
+ /**
367
+ * The {@link AbsoluteCellIndices} of the cell in the table.
368
+ */
369
+ absoluteCellIndices: AbsoluteCellIndices,
370
+ /**
371
+ * The table block containing the cell.
372
+ */
373
+ block: BlockFromConfigNoChildren<DefaultBlockSchema["table"], any, any>,
374
+ /**
375
+ * The occupancy grid of the table.
376
+ */
377
+ occupancyGrid: OccupancyGrid = getTableCellOccupancyGrid(block)
378
+ ):
379
+ | (RelativeCellIndices & {
380
+ cell: TableContent<any, any>["rows"][number]["cells"][number];
381
+ })
382
+ | undefined {
383
+ const occupancyCell =
384
+ occupancyGrid[absoluteCellIndices.row]?.[absoluteCellIndices.col];
385
+
386
+ // Double check that the cell can be accessed
387
+ if (!occupancyCell) {
388
+ // The cell is not occupied, so it is invalid
389
+ return undefined;
390
+ }
391
+
392
+ return {
393
+ row: occupancyCell.row,
394
+ col: occupancyCell.col,
395
+ cell: occupancyCell.cell,
396
+ };
397
+ }
398
+
399
+ /**
400
+ * This will get all the cells within a relative row of a table block.
401
+ *
402
+ * This method always starts the search for the row at the first column of the table.
403
+ *
404
+ * ```
405
+ * // Visual representation of a table
406
+ * | A | B | C |
407
+ * | | D | E |
408
+ * | F | G | H |
409
+ * // "A" has a rowspan of 2
410
+ *
411
+ * // getCellsAtRowHandle(0)
412
+ * // returns [
413
+ * { row: 0, col: 0, cell: "A" },
414
+ * { row: 0, col: 1, cell: "B" },
415
+ * { row: 0, col: 2, cell: "C" },
416
+ * ]
417
+ *
418
+ * // getCellsAtColumnHandle(1)
419
+ * // returns [
420
+ * { row: 1, col: 0, cell: "F" },
421
+ * { row: 1, col: 1, cell: "G" },
422
+ * { row: 1, col: 2, cell: "H" },
423
+ * ]
424
+ * ```
425
+ *
426
+ * As you can see, you may not be able to retrieve all nodes given a relative row index, as cells can span multiple rows.
427
+ *
428
+ * @returns All of the cells associated with the relative row of the table. (All cells that have the same relative row index)
429
+ */
430
+ export function getCellsAtRowHandle(
431
+ block: BlockFromConfigNoChildren<DefaultBlockSchema["table"], any, any>,
432
+ relativeRowIndex: RelativeCellIndices["row"]
433
+ ) {
434
+ const occupancyGrid = getTableCellOccupancyGrid(block);
435
+
436
+ if (relativeRowIndex < 0 || relativeRowIndex >= occupancyGrid.length) {
437
+ return [];
438
+ }
439
+
440
+ // First need to resolve the relative row index to an absolute row index
441
+ let absoluteRow = 0;
442
+
443
+ // Jump through the occupied cells ${relativeCellIndices.row} times to find the absolute row position
444
+ for (let i = 0; i < relativeRowIndex; i++) {
445
+ const cell = occupancyGrid[absoluteRow]?.[0];
446
+
447
+ if (!cell) {
448
+ return [];
449
+ }
450
+
451
+ // Skip the cells that the rowspan takes up
452
+ absoluteRow += cell.rowspan;
453
+ }
454
+
455
+ // Then for each column, get the cell at the absolute row index as a relative cell index
456
+ const cells = new Array(occupancyGrid[0].length)
457
+ .fill(false)
458
+ .map((_v, col) => {
459
+ return getRelativeTableCells(
460
+ { row: absoluteRow, col },
461
+ block,
462
+ occupancyGrid
463
+ );
464
+ })
465
+ .filter(
466
+ (a): a is RelativeCellIndices & { cell: TableCell<any, any> } =>
467
+ a !== undefined
468
+ );
469
+
470
+ // Filter out duplicates based on row and col properties
471
+ return cells.filter((cell, index) => {
472
+ return (
473
+ cells.findIndex((c) => c.row === cell.row && c.col === cell.col) === index
474
+ );
475
+ });
476
+ }
477
+
478
+ /**
479
+ * This will get all the cells within a relative column of a table block.
480
+ *
481
+ * This method always starts the search for the column at the first row of the table.
482
+ *
483
+ * ```
484
+ * // Visual representation of a table
485
+ * | A | B |
486
+ * | C | D | E |
487
+ * | F | G | H |
488
+ * // "A" has a colspan of 2
489
+ *
490
+ * // getCellsAtColumnHandle(0)
491
+ * // returns [
492
+ * { row: 0, col: 0, cell: "A" },
493
+ * { row: 1, col: 0, cell: "C" },
494
+ * { row: 2, col: 0, cell: "F" },
495
+ * ]
496
+ *
497
+ * // getCellsAtColumnHandle(1)
498
+ * // returns [
499
+ * { row: 0, col: 1, cell: "B" },
500
+ * { row: 1, col: 2, cell: "E" },
501
+ * { row: 2, col: 2, cell: "F" },
502
+ * ]
503
+ * ```
504
+ *
505
+ * As you can see, you may not be able to retrieve all nodes given a relative column index, as cells can span multiple columns.
506
+ *
507
+ * @returns All of the cells associated with the relative column of the table. (All cells that have the same relative column index)
508
+ */
509
+ export function getCellsAtColumnHandle(
510
+ block: BlockFromConfigNoChildren<DefaultBlockSchema["table"], any, any>,
511
+ relativeColumnIndex: RelativeCellIndices["col"]
512
+ ) {
513
+ const occupancyGrid = getTableCellOccupancyGrid(block);
514
+
515
+ if (
516
+ relativeColumnIndex < 0 ||
517
+ relativeColumnIndex >= occupancyGrid[0].length
518
+ ) {
519
+ return [];
520
+ }
521
+
522
+ // First need to resolve the relative column index to an absolute column index
523
+ let absoluteCol = 0;
524
+
525
+ // Now that we've already resolved the absolute row position, we can jump through the occupied cells ${relativeCellIndices.col} times to find the absolute column position
526
+ for (let i = 0; i < relativeColumnIndex; i++) {
527
+ const cell = occupancyGrid[0]?.[absoluteCol];
528
+
529
+ if (!cell) {
530
+ return [];
531
+ }
532
+
533
+ // Skip the cells that the colspan takes up
534
+ absoluteCol += cell.colspan;
535
+ }
536
+
537
+ // Then for each row, get the cell at the absolute column index as a relative cell index
538
+ const cells = new Array(occupancyGrid.length)
539
+ .fill(false)
540
+ .map((_v, row) => {
541
+ return getRelativeTableCells(
542
+ { row, col: absoluteCol },
543
+ block,
544
+ occupancyGrid
545
+ );
546
+ })
547
+ .filter(
548
+ (a): a is RelativeCellIndices & { cell: TableCell<any, any> } =>
549
+ a !== undefined
550
+ );
551
+
552
+ // Filter out duplicates based on row and col properties
553
+ return cells.filter((cell, index) => {
554
+ return (
555
+ cells.findIndex((c) => c.row === cell.row && c.col === cell.col) === index
556
+ );
557
+ });
558
+ }
559
+
560
+ /**
561
+ * This moves a column from one index to another.
562
+ *
563
+ * @note This is a destructive operation, it will modify the provided {@link OccupancyGrid} in place.
564
+ */
565
+ export function moveColumn(
566
+ block: BlockFromConfigNoChildren<DefaultBlockSchema["table"], any, any>,
567
+ fromColIndex: RelativeCellIndices["col"],
568
+ toColIndex: RelativeCellIndices["col"],
569
+ occupancyGrid: OccupancyGrid = getTableCellOccupancyGrid(block)
570
+ ): TableContent<any, any>["rows"] {
571
+ // To move cells in a column, we need to layout the whole table
572
+ // and then move the cells accordingly.
573
+ const { col: absoluteSourceCol } = getAbsoluteTableCells(
574
+ {
575
+ row: 0,
576
+ col: fromColIndex,
577
+ },
578
+ block,
579
+ occupancyGrid
580
+ );
581
+ const { col: absoluteTargetCol } = getAbsoluteTableCells(
582
+ {
583
+ row: 0,
584
+ col: toColIndex,
585
+ },
586
+ block,
587
+ occupancyGrid
588
+ );
589
+
590
+ /**
591
+ * Currently, this function assumes that the caller has already checked that the source and target columns are valid.
592
+ * Such as by using {@link canColumnBeDraggedInto}. In the future, we may want to have the move logic be smarter
593
+ * and handle invalid column indices in some way.
594
+ */
595
+ occupancyGrid.forEach((row) => {
596
+ // Move the cell to the target column
597
+ const [sourceCell] = row.splice(absoluteSourceCol, 1);
598
+ row.splice(absoluteTargetCol, 0, sourceCell);
599
+ });
600
+
601
+ return getTableRowsFromOccupancyGrid(occupancyGrid);
602
+ }
603
+
604
+ /**
605
+ * This moves a row from one index to another.
606
+ *
607
+ * @note This is a destructive operation, it will modify the {@link OccupancyGrid} in place.
608
+ */
609
+ export function moveRow(
610
+ block: BlockFromConfigNoChildren<DefaultBlockSchema["table"], any, any>,
611
+ fromRowIndex: RelativeCellIndices["row"],
612
+ toRowIndex: RelativeCellIndices["row"],
613
+ occupancyGrid: OccupancyGrid = getTableCellOccupancyGrid(block)
614
+ ): TableContent<any, any>["rows"] {
615
+ // To move cells in a column, we need to layout the whole table
616
+ // and then move the cells accordingly.
617
+ const { row: absoluteSourceRow } = getAbsoluteTableCells(
618
+ {
619
+ row: fromRowIndex,
620
+ col: 0,
621
+ },
622
+ block,
623
+ occupancyGrid
624
+ );
625
+ const { row: absoluteTargetRow } = getAbsoluteTableCells(
626
+ {
627
+ row: toRowIndex,
628
+ col: 0,
629
+ },
630
+ block,
631
+ occupancyGrid
632
+ );
633
+
634
+ /**
635
+ * Currently, this function assumes that the caller has already checked that the source and target rows are valid.
636
+ * Such as by using {@link canRowBeDraggedInto}. In the future, we may want to have the move logic be smarter
637
+ * and handle invalid row indices in some way.
638
+ */
639
+ const [sourceRow] = occupancyGrid.splice(absoluteSourceRow, 1);
640
+ occupancyGrid.splice(absoluteTargetRow, 0, sourceRow);
641
+
642
+ return getTableRowsFromOccupancyGrid(occupancyGrid);
643
+ }
644
+
645
+ /**
646
+ * This will check if a cell is empty.
647
+ *
648
+ * @returns True if the cell is empty, false otherwise.
649
+ */
650
+ function isCellEmpty(
651
+ cell:
652
+ | PartialTableContent<any, any>["rows"][number]["cells"][number]
653
+ | undefined
654
+ ): boolean {
655
+ if (!cell) {
656
+ return true;
657
+ }
658
+ if (isPartialTableCell(cell)) {
659
+ return isCellEmpty(cell.content);
660
+ } else if (typeof cell === "string") {
661
+ return cell.length === 0;
662
+ } else if (Array.isArray(cell)) {
663
+ return cell.every((c) =>
664
+ typeof c === "string"
665
+ ? c.length === 0
666
+ : isStyledTextInlineContent(c)
667
+ ? c.text.length === 0
668
+ : isPartialLinkInlineContent(c)
669
+ ? typeof c.content === "string"
670
+ ? c.content.length === 0
671
+ : c.content.every((s) => s.text.length === 0)
672
+ : false
673
+ );
674
+ } else {
675
+ return false;
676
+ }
677
+ }
678
+
679
+ /**
680
+ * This will remove empty rows or columns from the table.
681
+ *
682
+ * @note This is a destructive operation, it will modify the {@link OccupancyGrid} in place.
683
+ */
684
+ export function cropEmptyRowsOrColumns(
685
+ block: BlockFromConfigNoChildren<DefaultBlockSchema["table"], any, any>,
686
+ removeEmpty: "columns" | "rows",
687
+ occupancyGrid: OccupancyGrid = getTableCellOccupancyGrid(block)
688
+ ): TableContent<any, any>["rows"] {
689
+ if (removeEmpty === "columns") {
690
+ // strips empty columns on the right
691
+ let emptyColsOnRight = 0;
692
+ for (
693
+ let cellIndex = occupancyGrid[0].length - 1;
694
+ cellIndex >= 0;
695
+ cellIndex--
696
+ ) {
697
+ const isEmpty = occupancyGrid.every(
698
+ (row) =>
699
+ isCellEmpty(row[cellIndex].cell) && row[cellIndex].colspan === 1
700
+ );
701
+ if (!isEmpty) {
702
+ break;
703
+ }
704
+
705
+ emptyColsOnRight++;
706
+ }
707
+
708
+ for (let i = occupancyGrid.length - 1; i >= 0; i--) {
709
+ // We maintain at least one cell, even if all the cells are empty
710
+ const cellsToRemove = Math.max(
711
+ occupancyGrid[i].length - emptyColsOnRight,
712
+ 1
713
+ );
714
+ occupancyGrid[i] = occupancyGrid[i].slice(0, cellsToRemove);
715
+ }
716
+
717
+ return getTableRowsFromOccupancyGrid(occupancyGrid);
718
+ }
719
+
720
+ // strips empty rows at the bottom
721
+ let emptyRowsOnBottom = 0;
722
+ for (let rowIndex = occupancyGrid.length - 1; rowIndex >= 0; rowIndex--) {
723
+ const isEmpty = occupancyGrid[rowIndex].every(
724
+ (cell) => isCellEmpty(cell.cell) && cell.rowspan === 1
725
+ );
726
+ if (!isEmpty) {
727
+ break;
728
+ }
729
+
730
+ emptyRowsOnBottom++;
731
+ }
732
+
733
+ // We maintain at least one row, even if all the rows are empty
734
+ const rowsToRemove = Math.min(emptyRowsOnBottom, occupancyGrid.length - 1);
735
+
736
+ occupancyGrid.splice(occupancyGrid.length - rowsToRemove, rowsToRemove);
737
+
738
+ return getTableRowsFromOccupancyGrid(occupancyGrid);
739
+ }
740
+
741
+ /**
742
+ * This will add a specified number of rows or columns to the table (filling with empty cells).
743
+ *
744
+ * @note This is a destructive operation, it will modify the {@link OccupancyGrid} in place.
745
+ */
746
+ export function addRowsOrColumns(
747
+ block: BlockFromConfigNoChildren<DefaultBlockSchema["table"], any, any>,
748
+ addType: "columns" | "rows",
749
+ /**
750
+ * The number of rows or columns to add.
751
+ *
752
+ * @note if negative, it will remove rows or columns.
753
+ */
754
+ numToAdd: number,
755
+ occupancyGrid: OccupancyGrid = getTableCellOccupancyGrid(block)
756
+ ): TableContent<any, any>["rows"] {
757
+ const { width, height } = getDimensionsOfTable(block);
758
+
759
+ if (addType === "columns") {
760
+ // Add empty columns to the right
761
+ occupancyGrid.forEach((row, rowIndex) => {
762
+ if (numToAdd >= 0) {
763
+ for (let i = 0; i < numToAdd; i++) {
764
+ row.push({
765
+ row: rowIndex,
766
+ col: Math.max(...row.map((r) => r.col)) + 1,
767
+ rowspan: 1,
768
+ colspan: 1,
769
+ cell: mapTableCell(""),
770
+ });
771
+ }
772
+ } else {
773
+ // Remove columns on the right
774
+ row.splice(width + numToAdd, -1 * numToAdd);
775
+ }
776
+ });
777
+ } else {
778
+ if (numToAdd > 0) {
779
+ // Add empty rows to the bottom
780
+ for (let i = 0; i < numToAdd; i++) {
781
+ const newRow = new Array(width).fill(null).map((_, colIndex) => ({
782
+ row: height + i,
783
+ col: colIndex,
784
+ rowspan: 1,
785
+ colspan: 1,
786
+ cell: mapTableCell(""),
787
+ }));
788
+ occupancyGrid.push(newRow);
789
+ }
790
+ } else if (numToAdd < 0) {
791
+ // Remove rows at the bottom
792
+ occupancyGrid.splice(height + numToAdd, -1 * numToAdd);
793
+ }
794
+ }
795
+
796
+ return getTableRowsFromOccupancyGrid(occupancyGrid);
797
+ }
798
+
799
+ /**
800
+ * Checks if a row can be safely dropped at the target row index without splitting merged cells.
801
+ */
802
+ export function canRowBeDraggedInto(
803
+ block: BlockFromConfigNoChildren<DefaultBlockSchema["table"], any, any>,
804
+ draggingIndex: RelativeCellIndices["row"],
805
+ targetRowIndex: RelativeCellIndices["row"]
806
+ ) {
807
+ // Check cells at the target row
808
+ const targetCells = getCellsAtRowHandle(block, targetRowIndex);
809
+
810
+ // If no cells have rowspans > 1, dragging is always allowed
811
+ const hasMergedCells = targetCells.some((cell) => getRowspan(cell.cell) > 1);
812
+ if (!hasMergedCells) {
813
+ return true;
814
+ }
815
+
816
+ let endRowIndex = targetRowIndex;
817
+ let startRowIndex = targetRowIndex;
818
+ targetCells.forEach((cell) => {
819
+ const rowspan = getRowspan(cell.cell);
820
+ endRowIndex = Math.max(endRowIndex, cell.row + rowspan - 1);
821
+ startRowIndex = Math.min(startRowIndex, cell.row);
822
+ });
823
+
824
+ // Check the direction of the drag
825
+ const isDraggingDown = draggingIndex < targetRowIndex;
826
+
827
+ // Allow dragging only at the start/end of merged cells
828
+ // Otherwise, the target row was within a merged cell which we don't allow
829
+ return isDraggingDown
830
+ ? targetRowIndex === endRowIndex
831
+ : targetRowIndex === startRowIndex;
832
+ }
833
+
834
+ /**
835
+ * Checks if a column can be safely dropped at the target column index without splitting merged cells.
836
+ */
837
+ export function canColumnBeDraggedInto(
838
+ block: BlockFromConfigNoChildren<DefaultBlockSchema["table"], any, any>,
839
+ draggingIndex: RelativeCellIndices["col"],
840
+ targetColumnIndex: RelativeCellIndices["col"]
841
+ ) {
842
+ // Check cells at the target column
843
+ const targetCells = getCellsAtColumnHandle(block, targetColumnIndex);
844
+
845
+ // If no cells have colspans > 1, dragging is always allowed
846
+ const hasMergedCells = targetCells.some((cell) => getColspan(cell.cell) > 1);
847
+ if (!hasMergedCells) {
848
+ return true;
849
+ }
850
+
851
+ let endColumnIndex = targetColumnIndex;
852
+ let startColumnIndex = targetColumnIndex;
853
+ targetCells.forEach((cell) => {
854
+ const colspan = getColspan(cell.cell);
855
+ endColumnIndex = Math.max(endColumnIndex, cell.col + colspan - 1);
856
+ startColumnIndex = Math.min(startColumnIndex, cell.col);
857
+ });
858
+
859
+ // Check the direction of the drag
860
+ const isDraggingRight = draggingIndex < targetColumnIndex;
861
+
862
+ // Allow dragging only at the start/end of merged cells
863
+ // Otherwise, the target column was within a merged cell which we don't allow
864
+ return isDraggingRight
865
+ ? targetColumnIndex === endColumnIndex
866
+ : targetColumnIndex === startColumnIndex;
867
+ }
868
+
869
+ /**
870
+ * Checks if two cells are in the same column.
871
+ *
872
+ * @returns True if the cells are in the same column, false otherwise.
873
+ */
874
+ export function areInSameColumn(
875
+ from: RelativeCellIndices,
876
+ to: RelativeCellIndices,
877
+ block: BlockFromConfigNoChildren<DefaultBlockSchema["table"], any, any>
878
+ ) {
879
+ // Table indices are relative to the table, so we need to resolve the absolute cell indices
880
+ const anchorAbsoluteCellIndices = getAbsoluteTableCells(from, block);
881
+
882
+ // Table indices are relative to the table, so we need to resolve the absolute cell indices
883
+ const headAbsoluteCellIndices = getAbsoluteTableCells(to, block);
884
+
885
+ // Compare the column indices to determine the merge direction
886
+ return anchorAbsoluteCellIndices.col === headAbsoluteCellIndices.col;
887
+ }