@blocknote/xl-multi-column 0.19.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 (78) hide show
  1. package/LICENSE +661 -0
  2. package/dist/blocknote-xl-multi-column.js +3038 -0
  3. package/dist/blocknote-xl-multi-column.js.map +1 -0
  4. package/dist/blocknote-xl-multi-column.umd.cjs +69 -0
  5. package/dist/blocknote-xl-multi-column.umd.cjs.map +1 -0
  6. package/dist/webpack-stats.json +1 -0
  7. package/package.json +80 -0
  8. package/src/blocks/Columns/index.ts +15 -0
  9. package/src/blocks/schema.ts +43 -0
  10. package/src/extensions/ColumnResize/ColumnResizeExtension.ts +357 -0
  11. package/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts +480 -0
  12. package/src/extensions/SuggestionMenu/getMultiColumnSlashMenuItems.tsx +105 -0
  13. package/src/i18n/dictionary.ts +27 -0
  14. package/src/i18n/locales/ar.ts +18 -0
  15. package/src/i18n/locales/de.ts +18 -0
  16. package/src/i18n/locales/en.ts +16 -0
  17. package/src/i18n/locales/es.ts +18 -0
  18. package/src/i18n/locales/fr.ts +18 -0
  19. package/src/i18n/locales/hr.ts +18 -0
  20. package/src/i18n/locales/index.ts +15 -0
  21. package/src/i18n/locales/is.ts +18 -0
  22. package/src/i18n/locales/ja.ts +18 -0
  23. package/src/i18n/locales/ko.ts +18 -0
  24. package/src/i18n/locales/nl.ts +18 -0
  25. package/src/i18n/locales/pl.ts +18 -0
  26. package/src/i18n/locales/pt.ts +18 -0
  27. package/src/i18n/locales/ru.ts +18 -0
  28. package/src/i18n/locales/vi.ts +18 -0
  29. package/src/i18n/locales/zh.ts +18 -0
  30. package/src/index.ts +7 -0
  31. package/src/pm-nodes/Column.ts +87 -0
  32. package/src/pm-nodes/ColumnList.ts +44 -0
  33. package/src/test/commands/__snapshots__/insertBlocks.test.ts.snap +757 -0
  34. package/src/test/commands/__snapshots__/textCursorPosition.test.ts.snap +169 -0
  35. package/src/test/commands/__snapshots__/updateBlock.test.ts.snap +964 -0
  36. package/src/test/commands/insertBlocks.test.ts +206 -0
  37. package/src/test/commands/textCursorPosition.test.ts +19 -0
  38. package/src/test/commands/updateBlock.test.ts +212 -0
  39. package/src/test/conversions/__snapshots__/multi-column/undefined/external.html +1 -0
  40. package/src/test/conversions/__snapshots__/multi-column/undefined/internal.html +1 -0
  41. package/src/test/conversions/__snapshots__/nodeConversion.test.ts.snap +118 -0
  42. package/src/test/conversions/htmlConversion.test.ts +100 -0
  43. package/src/test/conversions/nodeConversion.test.ts +84 -0
  44. package/src/test/conversions/testCases.ts +54 -0
  45. package/src/test/setupTestEnv.ts +99 -0
  46. package/src/vite-env.d.ts +1 -0
  47. package/types/src/blocks/Columns/index.d.ts +32 -0
  48. package/types/src/blocks/schema.d.ts +102 -0
  49. package/types/src/extensions/ColumnResize/ColumnResizeExtension.d.ts +3 -0
  50. package/types/src/extensions/DropCursor/MultiColumnDropCursorPlugin.d.ts +11 -0
  51. package/types/src/extensions/SuggestionMenu/getMultiColumnSlashMenuItems.d.ts +5 -0
  52. package/types/src/i18n/dictionary.d.ts +19 -0
  53. package/types/src/i18n/locales/ar.d.ts +2 -0
  54. package/types/src/i18n/locales/de.d.ts +2 -0
  55. package/types/src/i18n/locales/en.d.ts +16 -0
  56. package/types/src/i18n/locales/es.d.ts +2 -0
  57. package/types/src/i18n/locales/fr.d.ts +2 -0
  58. package/types/src/i18n/locales/hr.d.ts +2 -0
  59. package/types/src/i18n/locales/index.d.ts +15 -0
  60. package/types/src/i18n/locales/is.d.ts +2 -0
  61. package/types/src/i18n/locales/ja.d.ts +2 -0
  62. package/types/src/i18n/locales/ko.d.ts +2 -0
  63. package/types/src/i18n/locales/nl.d.ts +2 -0
  64. package/types/src/i18n/locales/pl.d.ts +2 -0
  65. package/types/src/i18n/locales/pt.d.ts +2 -0
  66. package/types/src/i18n/locales/ru.d.ts +2 -0
  67. package/types/src/i18n/locales/vi.d.ts +2 -0
  68. package/types/src/i18n/locales/zh.d.ts +2 -0
  69. package/types/src/index.d.ts +7 -0
  70. package/types/src/pm-nodes/Column.d.ts +6 -0
  71. package/types/src/pm-nodes/ColumnList.d.ts +6 -0
  72. package/types/src/test/commands/insertBlocks.test.d.ts +1 -0
  73. package/types/src/test/commands/textCursorPosition.test.d.ts +1 -0
  74. package/types/src/test/commands/updateBlock.test.d.ts +1 -0
  75. package/types/src/test/conversions/htmlConversion.test.d.ts +1 -0
  76. package/types/src/test/conversions/nodeConversion.test.d.ts +1 -0
  77. package/types/src/test/conversions/testCases.d.ts +3 -0
  78. package/types/src/test/setupTestEnv.d.ts +1041 -0
@@ -0,0 +1 @@
1
+ {"builtAt":1730893824879,"assets":[{"name":"blocknote-xl-multi-column.umd.cjs","size":68564},{"name":"blocknote-xl-multi-column.umd.cjs.map","size":3630227}],"chunks":[{"id":"a1ee98a","entry":true,"initial":true,"files":["blocknote-xl-multi-column.umd.cjs"],"names":["index"]}],"modules":[{"name":"./src/i18n/locales/ar.ts","size":364,"chunks":["a1ee98a"]},{"name":"./src/i18n/locales/de.ts","size":400,"chunks":["a1ee98a"]},{"name":"./src/i18n/locales/en.ts","size":378,"chunks":["a1ee98a"]},{"name":"./src/i18n/locales/es.ts","size":392,"chunks":["a1ee98a"]},{"name":"./src/i18n/locales/fr.ts","size":398,"chunks":["a1ee98a"]},{"name":"./src/i18n/locales/hr.ts","size":396,"chunks":["a1ee98a"]},{"name":"./src/i18n/locales/is.ts","size":376,"chunks":["a1ee98a"]},{"name":"./src/i18n/locales/ja.ts","size":284,"chunks":["a1ee98a"]},{"name":"./src/i18n/locales/ko.ts","size":288,"chunks":["a1ee98a"]},{"name":"./src/i18n/locales/nl.ts","size":390,"chunks":["a1ee98a"]},{"name":"./src/i18n/locales/pl.ts","size":394,"chunks":["a1ee98a"]},{"name":"./src/i18n/locales/pt.ts","size":388,"chunks":["a1ee98a"]},{"name":"./src/i18n/locales/ru.ts","size":370,"chunks":["a1ee98a"]},{"name":"./src/i18n/locales/vi.ts","size":340,"chunks":["a1ee98a"]},{"name":"./src/i18n/locales/zh.ts","size":274,"chunks":["a1ee98a"]},{"name":"./src/i18n/locales/index.ts","size":0,"chunks":["a1ee98a"]},{"name":"./src/i18n/dictionary.ts","size":189,"chunks":["a1ee98a"]},{"name":"./src/extensions/ColumnResize/ColumnResizeExtension.ts","size":8106,"chunks":["a1ee98a"]},{"name":"./src/pm-nodes/Column.ts","size":2567,"chunks":["a1ee98a"]},{"name":"./src/pm-nodes/ColumnList.ts","size":1120,"chunks":["a1ee98a"]},{"name":"./src/blocks/Columns/index.ts","size":210,"chunks":["a1ee98a"]},{"name":"./src/blocks/schema.ts","size":425,"chunks":["a1ee98a"]},{"name":"./src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts","size":11324,"chunks":["a1ee98a"]},{"name":"./commonjsHelpers.js","size":140,"chunks":["a1ee98a"]},{"name":"../../node_modules/react/jsx-runtime.js?commonjs-module","size":31,"chunks":["a1ee98a"]},{"name":"../../node_modules/react/cjs/react-jsx-runtime.production.min.js?commonjs-exports","size":40,"chunks":["a1ee98a"]},{"name":"../../node_modules/react/index.js?commonjs-module","size":26,"chunks":["a1ee98a"]},{"name":"../../node_modules/react/cjs/react.production.min.js?commonjs-exports","size":30,"chunks":["a1ee98a"]},{"name":"../../node_modules/react/cjs/react.production.min.js","size":7647,"chunks":["a1ee98a"]},{"name":"../../node_modules/react/cjs/react.development.js?commonjs-module","size":38,"chunks":["a1ee98a"]},{"name":"../../node_modules/react/cjs/react.development.js","size":92443,"chunks":["a1ee98a"]},{"name":"../../node_modules/react/index.js","size":299,"chunks":["a1ee98a"]},{"name":"../../node_modules/react/cjs/react-jsx-runtime.production.min.js","size":1192,"chunks":["a1ee98a"]},{"name":"../../node_modules/react/cjs/react-jsx-runtime.development.js?commonjs-exports","size":37,"chunks":["a1ee98a"]},{"name":"../../node_modules/react/cjs/react-jsx-runtime.development.js","size":43835,"chunks":["a1ee98a"]},{"name":"../../node_modules/react/jsx-runtime.js","size":225,"chunks":["a1ee98a"]},{"name":"../../node_modules/react/index.js?commonjs-es-import","size":100,"chunks":["a1ee98a"]},{"name":"../../node_modules/react-icons/lib/iconContext.mjs","size":217,"chunks":["a1ee98a"]},{"name":"../../node_modules/react-icons/lib/iconBase.mjs","size":3929,"chunks":["a1ee98a"]},{"name":"../../node_modules/react-icons/tb/index.mjs","size":732,"chunks":["a1ee98a"]},{"name":"./src/extensions/SuggestionMenu/getMultiColumnSlashMenuItems.tsx","size":2087,"chunks":["a1ee98a"]},{"name":"./src/index.ts","size":0,"chunks":["a1ee98a"]}]}
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@blocknote/xl-multi-column",
3
+ "homepage": "https://github.com/TypeCellOS/BlockNote",
4
+ "private": false,
5
+ "license": "AGPL-3.0 OR PROPRIETARY",
6
+ "version": "0.19.0",
7
+ "files": [
8
+ "dist",
9
+ "types",
10
+ "src"
11
+ ],
12
+ "keywords": [
13
+ "react",
14
+ "javascript",
15
+ "editor",
16
+ "typescript",
17
+ "prosemirror",
18
+ "wysiwyg",
19
+ "rich-text-editor",
20
+ "notion",
21
+ "yjs",
22
+ "block-based",
23
+ "tiptap"
24
+ ],
25
+ "description": "A \"Notion-style\" block-based extensible text editor built on top of Prosemirror and Tiptap.",
26
+ "type": "module",
27
+ "source": "src/index.ts",
28
+ "types": "./types/src/index.d.ts",
29
+ "main": "./dist/blocknote-xl-multi-column.umd.cjs",
30
+ "module": "./dist/blocknote-xl-multi-column.js",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./types/src/index.d.ts",
34
+ "import": "./dist/blocknote-xl-multi-column.js",
35
+ "require": "./dist/blocknote-xl-multi-column.umd.cjs"
36
+ }
37
+ },
38
+ "scripts": {
39
+ "dev": "vite",
40
+ "build": "tsc && vite build",
41
+ "preview": "vite preview",
42
+ "lint": "eslint src --max-warnings 0",
43
+ "test": "vitest",
44
+ "test-watch": "vitest watch",
45
+ "clean": "rimraf dist && rimraf types"
46
+ },
47
+ "dependencies": {
48
+ "@blocknote/core": "^0.19.0",
49
+ "@blocknote/react": "^0.19.0",
50
+ "@tiptap/core": "^2.7.1",
51
+ "prosemirror-model": "^1.23.0",
52
+ "prosemirror-state": "^1.4.3",
53
+ "prosemirror-tables": "^1.3.7",
54
+ "prosemirror-transform": "^1.9.0",
55
+ "prosemirror-view": "^1.33.7",
56
+ "react-icons": "^5.2.1"
57
+ },
58
+ "devDependencies": {
59
+ "@vitest/ui": "^2.1.4",
60
+ "eslint": "^8.10.0",
61
+ "jsdom": "^21.1.0",
62
+ "prettier": "^2.7.1",
63
+ "rimraf": "^5.0.5",
64
+ "rollup-plugin-webpack-stats": "^0.2.2",
65
+ "typescript": "^5.3.3",
66
+ "vite": "^5.3.4",
67
+ "vite-plugin-eslint": "^1.8.1",
68
+ "vitest": "^2.0.3"
69
+ },
70
+ "eslintConfig": {
71
+ "extends": [
72
+ "../../.eslintrc.js"
73
+ ]
74
+ },
75
+ "publishConfig": {
76
+ "access": "public",
77
+ "registry": "https://registry.npmjs.org/"
78
+ },
79
+ "gitHead": "c48248efc5ceab7962ed6ba39cf8db0cbf3f0d5c"
80
+ }
@@ -0,0 +1,15 @@
1
+ import { Column } from "../../pm-nodes/Column.js";
2
+ import { ColumnList } from "../../pm-nodes/ColumnList.js";
3
+
4
+ import { createBlockSpecFromStronglyTypedTiptapNode } from "@blocknote/core";
5
+
6
+ export const ColumnBlock = createBlockSpecFromStronglyTypedTiptapNode(Column, {
7
+ width: {
8
+ default: 1,
9
+ },
10
+ });
11
+
12
+ export const ColumnListBlock = createBlockSpecFromStronglyTypedTiptapNode(
13
+ ColumnList,
14
+ {}
15
+ );
@@ -0,0 +1,43 @@
1
+ import {
2
+ BlockNoteSchema,
3
+ BlockSchema,
4
+ InlineContentSchema,
5
+ StyleSchema,
6
+ } from "@blocknote/core";
7
+ import { ColumnBlock, ColumnListBlock } from "./Columns/index.js";
8
+
9
+ export const multiColumnSchema = BlockNoteSchema.create({
10
+ blockSpecs: {
11
+ column: ColumnBlock,
12
+ columnList: ColumnListBlock,
13
+ },
14
+ });
15
+
16
+ /**
17
+ * Adds multi-column support to the given schema.
18
+ */
19
+ export const withMultiColumn = <
20
+ B extends BlockSchema,
21
+ I extends InlineContentSchema,
22
+ S extends StyleSchema
23
+ >(
24
+ schema: BlockNoteSchema<B, I, S>
25
+ ) => {
26
+ return BlockNoteSchema.create({
27
+ blockSpecs: {
28
+ ...schema.blockSpecs,
29
+ column: ColumnBlock,
30
+ columnList: ColumnListBlock,
31
+ },
32
+ inlineContentSpecs: schema.inlineContentSpecs,
33
+ styleSpecs: schema.styleSpecs,
34
+ }) as any as BlockNoteSchema<
35
+ // typescript needs some help here
36
+ B & {
37
+ column: typeof ColumnBlock.config;
38
+ columnList: typeof ColumnListBlock.config;
39
+ },
40
+ I,
41
+ S
42
+ >;
43
+ };
@@ -0,0 +1,357 @@
1
+ import { BlockNoteEditor, getNodeById } from "@blocknote/core";
2
+ import { Extension } from "@tiptap/core";
3
+ import { Node } from "prosemirror-model";
4
+ import { Plugin, PluginKey, PluginView } from "prosemirror-state";
5
+ import { Decoration, DecorationSet, EditorView } from "prosemirror-view";
6
+
7
+ type ColumnData = {
8
+ element: HTMLElement;
9
+ id: string;
10
+ node: Node;
11
+ posBeforeNode: number;
12
+ };
13
+
14
+ type ColumnDataWithWidths = ColumnData & {
15
+ widthPx: number;
16
+ widthPercent: number;
17
+ };
18
+
19
+ type ColumnDefaultState = {
20
+ type: "default";
21
+ };
22
+
23
+ type ColumnHoverState = {
24
+ type: "hover";
25
+ leftColumn: ColumnData;
26
+ rightColumn: ColumnData;
27
+ };
28
+
29
+ type ColumnResizeState = {
30
+ type: "resize";
31
+ startPos: number;
32
+ leftColumn: ColumnDataWithWidths;
33
+ rightColumn: ColumnDataWithWidths;
34
+ };
35
+
36
+ type ColumnState = ColumnDefaultState | ColumnHoverState | ColumnResizeState;
37
+
38
+ const columnResizePluginKey = new PluginKey<ColumnState>("ColumnResizePlugin");
39
+
40
+ class ColumnResizePluginView implements PluginView {
41
+ editor: BlockNoteEditor<any, any, any>;
42
+ view: EditorView;
43
+
44
+ readonly RESIZE_MARGIN_WIDTH_PX = 20;
45
+ readonly COLUMN_MIN_WIDTH_PERCENT = 0.5;
46
+
47
+ constructor(editor: BlockNoteEditor<any, any, any>, view: EditorView) {
48
+ this.editor = editor;
49
+ this.view = view;
50
+
51
+ this.view.dom.addEventListener("mousedown", this.mouseDownHandler);
52
+ document.body.addEventListener("mousemove", this.mouseMoveHandler);
53
+ document.body.addEventListener("mouseup", this.mouseUpHandler);
54
+ }
55
+
56
+ getColumnHoverOrDefaultState = (
57
+ event: MouseEvent
58
+ ): ColumnDefaultState | ColumnHoverState => {
59
+ const target = event.target as HTMLElement;
60
+
61
+ // Do nothing if the event target is outside the editor.
62
+ if (!this.view.dom.contains(target)) {
63
+ return { type: "default" };
64
+ }
65
+
66
+ const columnElement = target.closest(
67
+ ".bn-block-column"
68
+ ) as HTMLElement | null;
69
+
70
+ // Do nothing if a column element does not exist in the event target's
71
+ // ancestors.
72
+ if (!columnElement) {
73
+ return { type: "default" };
74
+ }
75
+
76
+ const startPos = event.clientX;
77
+ const columnElementDOMRect = columnElement.getBoundingClientRect();
78
+
79
+ // Whether the cursor is within the width margin to trigger a resize.
80
+ const cursorElementSide =
81
+ startPos < columnElementDOMRect.left + this.RESIZE_MARGIN_WIDTH_PX
82
+ ? "left"
83
+ : startPos > columnElementDOMRect.right - this.RESIZE_MARGIN_WIDTH_PX
84
+ ? "right"
85
+ : "none";
86
+
87
+ // The column element before or after the one hovered by the cursor,
88
+ // depending on which side the cursor is on.
89
+ const adjacentColumnElement =
90
+ cursorElementSide === "left"
91
+ ? columnElement.previousElementSibling
92
+ : cursorElementSide === "right"
93
+ ? columnElement.nextElementSibling
94
+ : undefined;
95
+
96
+ // Do nothing if the cursor is not within the resize margin or if there
97
+ // is no column before or after the one hovered by the cursor, depending
98
+ // on which side the cursor is on.
99
+ if (!adjacentColumnElement) {
100
+ return { type: "default" };
101
+ }
102
+
103
+ const leftColumnElement =
104
+ cursorElementSide === "left"
105
+ ? (adjacentColumnElement as HTMLElement)
106
+ : columnElement;
107
+
108
+ const rightColumnElement =
109
+ cursorElementSide === "left"
110
+ ? columnElement
111
+ : (adjacentColumnElement as HTMLElement);
112
+
113
+ const leftColumnId = leftColumnElement.getAttribute("data-id")!;
114
+ const rightColumnId = rightColumnElement.getAttribute("data-id")!;
115
+
116
+ const leftColumnNodeAndPos = getNodeById(leftColumnId, this.view.state.doc);
117
+
118
+ const rightColumnNodeAndPos = getNodeById(
119
+ rightColumnId,
120
+ this.view.state.doc
121
+ );
122
+
123
+ if (
124
+ !leftColumnNodeAndPos ||
125
+ !rightColumnNodeAndPos ||
126
+ !leftColumnNodeAndPos.posBeforeNode
127
+ ) {
128
+ throw new Error("Column not found");
129
+ }
130
+
131
+ return {
132
+ type: "hover",
133
+ leftColumn: {
134
+ element: leftColumnElement,
135
+ id: leftColumnId,
136
+ ...leftColumnNodeAndPos,
137
+ },
138
+ rightColumn: {
139
+ element: rightColumnElement,
140
+ id: rightColumnId,
141
+ ...rightColumnNodeAndPos,
142
+ },
143
+ };
144
+ };
145
+
146
+ // When the user mouses down near the boundary between two columns, we
147
+ // want to set the plugin state to resize, so the columns can be resized
148
+ // by moving the mouse.
149
+ mouseDownHandler = (event: MouseEvent) => {
150
+ let newState: ColumnState = this.getColumnHoverOrDefaultState(event);
151
+ if (newState.type === "default") {
152
+ return;
153
+ }
154
+
155
+ event.preventDefault();
156
+
157
+ const startPos = event.clientX;
158
+
159
+ const leftColumnWidthPx =
160
+ newState.leftColumn.element.getBoundingClientRect().width;
161
+ const rightColumnWidthPx =
162
+ newState.rightColumn.element.getBoundingClientRect().width;
163
+
164
+ const leftColumnWidthPercent = newState.leftColumn.node.attrs
165
+ .width as number;
166
+ const rightColumnWidthPercent = newState.rightColumn.node.attrs
167
+ .width as number;
168
+
169
+ newState = {
170
+ type: "resize",
171
+ startPos,
172
+ leftColumn: {
173
+ ...newState.leftColumn,
174
+ widthPx: leftColumnWidthPx,
175
+ widthPercent: leftColumnWidthPercent,
176
+ },
177
+ rightColumn: {
178
+ ...newState.rightColumn,
179
+ widthPx: rightColumnWidthPx,
180
+ widthPercent: rightColumnWidthPercent,
181
+ },
182
+ };
183
+
184
+ this.view.dispatch(
185
+ this.view.state.tr.setMeta(columnResizePluginKey, newState)
186
+ );
187
+
188
+ this.editor.sideMenu.freezeMenu();
189
+ };
190
+
191
+ // If the plugin isn't in a resize state, we want to update it to either a
192
+ // hover state if the mouse is near the boundary between two columns, or
193
+ // default otherwise. If the plugin is in a resize state, we want to
194
+ // update the column widths based on the horizontal mouse movement.
195
+ mouseMoveHandler = (event: MouseEvent) => {
196
+ const pluginState = columnResizePluginKey.getState(this.view.state);
197
+ if (!pluginState) {
198
+ return;
199
+ }
200
+
201
+ // If the user isn't currently resizing columns, we want to update the
202
+ // plugin state to maybe show or hide the resize border between columns.
203
+ if (pluginState.type !== "resize") {
204
+ const newState = this.getColumnHoverOrDefaultState(event);
205
+
206
+ // Prevent unnecessary state updates (when the state before and after
207
+ // is the same).
208
+ const bothDefaultStates =
209
+ pluginState.type === "default" && newState.type === "default";
210
+ const sameColumnIds =
211
+ pluginState.type !== "default" &&
212
+ newState.type !== "default" &&
213
+ pluginState.leftColumn.id === newState.leftColumn.id &&
214
+ pluginState.rightColumn.id === newState.rightColumn.id;
215
+ if (bothDefaultStates || sameColumnIds) {
216
+ return;
217
+ }
218
+
219
+ // Since the resize bar overlaps the side menu, we don't want to show it
220
+ // if the side menu is already open.
221
+ if (newState.type === "hover" && this.editor.sideMenu.view?.state?.show) {
222
+ return;
223
+ }
224
+
225
+ // Update the plugin state.
226
+ this.view.dispatch(
227
+ this.view.state.tr.setMeta(columnResizePluginKey, newState)
228
+ );
229
+
230
+ return;
231
+ }
232
+
233
+ const widthChangePx = event.clientX - pluginState.startPos;
234
+ // We need to scale the width change by the left column's width in
235
+ // percent, otherwise the rate at which the resizing happens will change
236
+ // based on the width of the left column.
237
+ const scaledWidthChangePx =
238
+ widthChangePx * pluginState.leftColumn.widthPercent;
239
+ const widthChangePercent =
240
+ (pluginState.leftColumn.widthPx + scaledWidthChangePx) /
241
+ pluginState.leftColumn.widthPx -
242
+ 1;
243
+
244
+ let newLeftColumnWidth =
245
+ pluginState.leftColumn.widthPercent + widthChangePercent;
246
+ let newRightColumnWidth =
247
+ pluginState.rightColumn.widthPercent - widthChangePercent;
248
+
249
+ // Ensures that the column widths do not go below the minimum width.
250
+ // There is no maximum width, the user can resize the columns as much as
251
+ // they want provided the others don't go below the minimum width.
252
+ if (newLeftColumnWidth < this.COLUMN_MIN_WIDTH_PERCENT) {
253
+ newRightColumnWidth -= this.COLUMN_MIN_WIDTH_PERCENT - newLeftColumnWidth;
254
+ newLeftColumnWidth = this.COLUMN_MIN_WIDTH_PERCENT;
255
+ } else if (newRightColumnWidth < this.COLUMN_MIN_WIDTH_PERCENT) {
256
+ newLeftColumnWidth -= this.COLUMN_MIN_WIDTH_PERCENT - newRightColumnWidth;
257
+ newRightColumnWidth = this.COLUMN_MIN_WIDTH_PERCENT;
258
+ }
259
+
260
+ // possible improvement: only dispatch on mouse up, and use a different way
261
+ // to update the column widths while dragging.
262
+ // this prevents a lot of document updates
263
+ this.view.dispatch(
264
+ this.view.state.tr
265
+ .setNodeAttribute(
266
+ pluginState.leftColumn.posBeforeNode,
267
+ "width",
268
+ newLeftColumnWidth
269
+ )
270
+ .setNodeAttribute(
271
+ pluginState.rightColumn.posBeforeNode,
272
+ "width",
273
+ newRightColumnWidth
274
+ )
275
+ .setMeta("addToHistory", false)
276
+ );
277
+ };
278
+
279
+ // If the plugin is in a resize state, we want to revert it to a default
280
+ // or hover, depending on where the mouse cursor is, when the user
281
+ // releases the mouse button.
282
+ mouseUpHandler = (event: MouseEvent) => {
283
+ const pluginState = columnResizePluginKey.getState(this.view.state);
284
+ if (!pluginState || pluginState.type !== "resize") {
285
+ return;
286
+ }
287
+
288
+ const newState = this.getColumnHoverOrDefaultState(event);
289
+
290
+ // Revert plugin state to default or hover, depending on where the mouse
291
+ // cursor is.
292
+ this.view.dispatch(
293
+ this.view.state.tr.setMeta(columnResizePluginKey, newState)
294
+ );
295
+
296
+ this.editor.sideMenu.unfreezeMenu();
297
+ };
298
+
299
+ // This is a required method for PluginView, so we get a type error if we
300
+ // don't implement it.
301
+ update: undefined;
302
+ }
303
+
304
+ const createColumnResizePlugin = (editor: BlockNoteEditor<any, any, any>) =>
305
+ new Plugin({
306
+ key: columnResizePluginKey,
307
+ props: {
308
+ // This adds a border between the columns when the user is
309
+ // resizing them or when the cursor is near their boundary.
310
+ decorations: (state) => {
311
+ const pluginState = columnResizePluginKey.getState(state);
312
+ if (!pluginState || pluginState.type === "default") {
313
+ return DecorationSet.empty;
314
+ }
315
+
316
+ return DecorationSet.create(state.doc, [
317
+ Decoration.node(
318
+ pluginState.leftColumn.posBeforeNode,
319
+ pluginState.leftColumn.posBeforeNode +
320
+ pluginState.leftColumn.node.nodeSize,
321
+ {
322
+ style: "box-shadow: 4px 0 0 #ccc; cursor: col-resize",
323
+ }
324
+ ),
325
+ Decoration.node(
326
+ pluginState.rightColumn.posBeforeNode,
327
+ pluginState.rightColumn.posBeforeNode +
328
+ pluginState.rightColumn.node.nodeSize,
329
+ {
330
+ style: "cursor: col-resize",
331
+ }
332
+ ),
333
+ ]);
334
+ },
335
+ },
336
+ state: {
337
+ init: () => ({ type: "default" } as ColumnState),
338
+ apply: (tr, oldPluginState) => {
339
+ const newPluginState = tr.getMeta(columnResizePluginKey) as
340
+ | ColumnState
341
+ | undefined;
342
+
343
+ return newPluginState === undefined ? oldPluginState : newPluginState;
344
+ },
345
+ },
346
+ view: (view) => new ColumnResizePluginView(editor, view),
347
+ });
348
+
349
+ export const createColumnResizeExtension = (
350
+ editor: BlockNoteEditor<any, any, any>
351
+ ) =>
352
+ Extension.create({
353
+ name: "columnResize",
354
+ addProseMirrorPlugins() {
355
+ return [createColumnResizePlugin(editor)];
356
+ },
357
+ });