@blocknote/xl-multi-column 0.47.1 → 0.47.2

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.
@@ -1 +1 @@
1
- {"builtAt":1772452740129,"assets":[{"name":"blocknote-xl-multi-column.cjs","size":20696},{"name":"blocknote-xl-multi-column.cjs.map","size":3792252}],"chunks":[{"id":"a1ee98a","entry":true,"initial":true,"files":["blocknote-xl-multi-column.cjs"],"names":["blocknote-xl-multi-column"]}],"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":8414,"chunks":["a1ee98a"]},{"name":"./src/pm-nodes/Column.ts","size":2591,"chunks":["a1ee98a"]},{"name":"./src/pm-nodes/ColumnList.ts","size":1144,"chunks":["a1ee98a"]},{"name":"./src/blocks/Columns/index.ts","size":316,"chunks":["a1ee98a"]},{"name":"./src/blocks/schema.ts","size":295,"chunks":["a1ee98a"]},{"name":"./src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts","size":11843,"chunks":["a1ee98a"]},{"name":"../../node_modules/.pnpm/react-icons@5.5.0_react@19.2.3/node_modules/react-icons/lib/iconContext.mjs","size":251,"chunks":["a1ee98a"]},{"name":"../../node_modules/.pnpm/react-icons@5.5.0_react@19.2.3/node_modules/react-icons/lib/iconBase.mjs","size":4003,"chunks":["a1ee98a"]},{"name":"../../node_modules/.pnpm/react-icons@5.5.0_react@19.2.3/node_modules/react-icons/tb/index.mjs","size":708,"chunks":["a1ee98a"]},{"name":"./src/extensions/SuggestionMenu/getMultiColumnSlashMenuItems.tsx","size":2105,"chunks":["a1ee98a"]},{"name":"./src/index.ts","size":0,"chunks":["a1ee98a"]}]}
1
+ {"builtAt":1774004019713,"assets":[{"name":"blocknote-xl-multi-column.cjs","size":17011},{"name":"blocknote-xl-multi-column.cjs.map","size":3780581}],"chunks":[{"id":"a1ee98a","entry":true,"initial":true,"files":["blocknote-xl-multi-column.cjs"],"names":["blocknote-xl-multi-column"]}],"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/DropCursor/multiColumnDropCursor.ts","size":1778,"chunks":["a1ee98a"]},{"name":"./src/extensions/DropCursor/multiColumnHandleDropPlugin.ts","size":3253,"chunks":["a1ee98a"]},{"name":"./src/extensions/ColumnResize/ColumnResizeExtension.ts","size":8412,"chunks":["a1ee98a"]},{"name":"./src/pm-nodes/Column.ts","size":2593,"chunks":["a1ee98a"]},{"name":"./src/pm-nodes/ColumnList.ts","size":1146,"chunks":["a1ee98a"]},{"name":"./src/blocks/Columns/index.ts","size":351,"chunks":["a1ee98a"]},{"name":"./src/blocks/schema.ts","size":293,"chunks":["a1ee98a"]},{"name":"../../node_modules/.pnpm/react-icons@5.5.0_react@19.2.3/node_modules/react-icons/lib/iconContext.mjs","size":251,"chunks":["a1ee98a"]},{"name":"../../node_modules/.pnpm/react-icons@5.5.0_react@19.2.3/node_modules/react-icons/lib/iconBase.mjs","size":4003,"chunks":["a1ee98a"]},{"name":"../../node_modules/.pnpm/react-icons@5.5.0_react@19.2.3/node_modules/react-icons/tb/index.mjs","size":708,"chunks":["a1ee98a"]},{"name":"./src/extensions/SuggestionMenu/getMultiColumnSlashMenuItems.tsx","size":2105,"chunks":["a1ee98a"]},{"name":"./src/index.ts","size":0,"chunks":["a1ee98a"]}]}
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "directory": "packages/xl-multi-column"
10
10
  },
11
11
  "license": "GPL-3.0 OR PROPRIETARY",
12
- "version": "0.47.1",
12
+ "version": "0.47.2",
13
13
  "files": [
14
14
  "dist",
15
15
  "types",
@@ -42,8 +42,8 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "@blocknote/core": "0.47.1",
46
- "@blocknote/react": "0.47.1",
45
+ "@blocknote/core": "0.47.2",
46
+ "@blocknote/react": "0.47.2",
47
47
  "@tiptap/core": "^3.13.0",
48
48
  "prosemirror-model": "^1.25.4",
49
49
  "prosemirror-state": "^1.4.4",
@@ -1,3 +1,4 @@
1
+ import { MultiColumnDropHandlerExtension } from "../../extensions/DropCursor/multiColumnHandleDropPlugin.js";
1
2
  import { Column } from "../../pm-nodes/Column.js";
2
3
  import { ColumnList } from "../../pm-nodes/ColumnList.js";
3
4
 
@@ -14,6 +15,7 @@ export const ColumnBlock = createBlockSpecFromTiptapNode(
14
15
  default: 1,
15
16
  },
16
17
  },
18
+ [MultiColumnDropHandlerExtension()],
17
19
  );
18
20
 
19
21
  export const ColumnListBlock = createBlockSpecFromTiptapNode(
@@ -0,0 +1,112 @@
1
+ import { type DropCursorHooks, getNearestBlockPos } from "@blocknote/core";
2
+ import type { EditorState } from "prosemirror-state";
3
+ import type { EditorView } from "prosemirror-view";
4
+
5
+ const PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP = 0.1;
6
+
7
+ export interface EdgeDropPosition {
8
+ position: "left" | "right" | "regular";
9
+ posBeforeNode: number;
10
+ node: any;
11
+ }
12
+
13
+ /**
14
+ * Detects if the drop event is near the left or right edge of a block.
15
+ * Shared utility used by both the drop cursor visualization and the drop handler.
16
+ * Returns null when the event position cannot be resolved (e.g. drop outside editor bounds).
17
+ */
18
+ export function detectEdgePosition(
19
+ event: DragEvent,
20
+ view: EditorView,
21
+ state: EditorState,
22
+ ): EdgeDropPosition | null {
23
+ const eventPos = view.posAtCoords({
24
+ left: event.clientX,
25
+ top: event.clientY,
26
+ });
27
+
28
+ if (!eventPos) {
29
+ return null;
30
+ }
31
+
32
+ const blockPos = getNearestBlockPos(state.doc, eventPos.pos);
33
+
34
+ // If we're at a block that's in a column, we want to compare the mouse position to the column, not the block inside it
35
+ // Why? Because we want to insert a new column in the columnList, instead of a new columnList inside of the column
36
+ let resolved = state.doc.resolve(blockPos.posBeforeNode);
37
+ if (resolved.parent.type.name === "column") {
38
+ resolved = state.doc.resolve(resolved.before());
39
+ }
40
+
41
+ const posInfo = {
42
+ posBeforeNode: resolved.pos,
43
+ node: resolved.nodeAfter!,
44
+ };
45
+
46
+ const blockElement = view.nodeDOM(posInfo.posBeforeNode);
47
+ if (blockElement === null) {
48
+ return {
49
+ position: "regular",
50
+ posBeforeNode: posInfo.posBeforeNode,
51
+ node: posInfo.node,
52
+ };
53
+ }
54
+ const blockRect = (blockElement as HTMLElement).getBoundingClientRect();
55
+
56
+ let position: "regular" | "left" | "right" = "regular";
57
+
58
+ if (
59
+ event.clientX <=
60
+ blockRect.left +
61
+ blockRect.width * PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP
62
+ ) {
63
+ position = "left";
64
+ } else if (
65
+ event.clientX >=
66
+ blockRect.right -
67
+ blockRect.width * PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP
68
+ ) {
69
+ position = "right";
70
+ }
71
+
72
+ return {
73
+ position,
74
+ posBeforeNode: posInfo.posBeforeNode,
75
+ node: posInfo.node,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Creates the computeDropPosition hook for multi-column support.
81
+ * This hook detects edge drops and returns vertical cursor orientations.
82
+ */
83
+ export const multiColumnDropCursor: { hooks: DropCursorHooks } = {
84
+ hooks: {
85
+ computeDropPosition: (context) => {
86
+ const edgePos = detectEdgePosition(
87
+ context.event,
88
+ context.view,
89
+ context.view.state,
90
+ );
91
+
92
+ // Fall back to default when position cannot be resolved
93
+ if (edgePos === null) {
94
+ return context.defaultPosition;
95
+ }
96
+
97
+ // If it's a regular (non-edge) drop, use the default position
98
+ if (edgePos.position === "regular") {
99
+ return context.defaultPosition;
100
+ }
101
+
102
+ // Edge drop - show vertical cursor
103
+ return {
104
+ pos: edgePos.posBeforeNode,
105
+ orientation:
106
+ edgePos.position === "left"
107
+ ? "block-vertical-left"
108
+ : "block-vertical-right",
109
+ };
110
+ },
111
+ },
112
+ };
@@ -0,0 +1,162 @@
1
+ import type { BlockNoteEditor } from "@blocknote/core";
2
+ import {
3
+ UniqueID,
4
+ createExtension,
5
+ getBlockInfo,
6
+ nodeToBlock,
7
+ } from "@blocknote/core";
8
+ import { Plugin } from "prosemirror-state";
9
+ import type { EditorView } from "prosemirror-view";
10
+ import { detectEdgePosition } from "./multiColumnDropCursor.js";
11
+
12
+ /**
13
+ * Creates a ProseMirror plugin that handles drop events for multi-column layouts.
14
+ * When a block is dropped near the left or right edge of another block, it creates
15
+ * or modifies column layouts.
16
+ */
17
+ export function createMultiColumnHandleDropPlugin(
18
+ editor: BlockNoteEditor<any, any, any>,
19
+ ): Plugin {
20
+ return new Plugin({
21
+ props: {
22
+ handleDrop(view: EditorView, event: DragEvent, slice, _moved) {
23
+ const edgePos = detectEdgePosition(event, view, view.state);
24
+ if (edgePos === null) {
25
+ return false; // Let ProseMirror handle the drop (e.g. outside editor bounds)
26
+ }
27
+
28
+ const blockInfo = getBlockInfo(edgePos);
29
+
30
+ // Only handle edge drops (left/right)
31
+ if (edgePos.position === "regular") {
32
+ return false; // Let ProseMirror handle regular drops
33
+ }
34
+
35
+ if (slice.content.childCount === 0) {
36
+ return false; // Let ProseMirror handle empty slice drops
37
+ }
38
+
39
+ const draggedBlock = nodeToBlock(
40
+ slice.content.child(0),
41
+ editor.pmSchema,
42
+ );
43
+
44
+ if (blockInfo.blockNoteType === "column") {
45
+ // Insert new column in existing columnList
46
+ const parentBlock = view.state.doc
47
+ .resolve(blockInfo.bnBlock.beforePos)
48
+ .node();
49
+
50
+ const columnList = nodeToBlock<any, any, any>(
51
+ parentBlock,
52
+ editor.pmSchema,
53
+ );
54
+
55
+ // Normalize column widths to average of 1
56
+ // In a `columnList`, we expect that the average width of each column
57
+ // is 1. However, there are cases in which this stops being true. For
58
+ // example, having one wider column and then removing it will cause
59
+ // the average width to go down. This isn't really an issue until the
60
+ // user tries to add a new column, which will, in this case, be wider
61
+ // than expected. Therefore, we normalize the column widths to an
62
+ // average of 1 here to avoid this issue.
63
+ let sumColumnWidthPercent = 0;
64
+ columnList.children.forEach((column) => {
65
+ sumColumnWidthPercent += column.props.width as number;
66
+ });
67
+ const avgColumnWidthPercent =
68
+ sumColumnWidthPercent / columnList.children.length;
69
+
70
+ // If the average column width is not 1, normalize it. We're dealing
71
+ // with floats so we need a small margin to account for precision
72
+ // errors.
73
+ if (avgColumnWidthPercent < 0.99 || avgColumnWidthPercent > 1.01) {
74
+ const scalingFactor = 1 / avgColumnWidthPercent;
75
+
76
+ columnList.children.forEach((column) => {
77
+ column.props.width =
78
+ (column.props.width as number) * scalingFactor;
79
+ });
80
+ }
81
+
82
+ const index = columnList.children.findIndex(
83
+ (b) => b.id === blockInfo.bnBlock.node.attrs.id,
84
+ );
85
+
86
+ const newChildren = columnList.children
87
+ // If the dragged block is in one of the columns, remove it.
88
+ .map((column) => ({
89
+ ...column,
90
+ children: column.children.filter(
91
+ (block) => block.id !== draggedBlock.id,
92
+ ),
93
+ }))
94
+ // Remove empty columns (can happen when dragged block is removed).
95
+ .filter((column) => column.children.length > 0)
96
+ // Insert the dragged block in the correct position.
97
+ .toSpliced(edgePos.position === "left" ? index : index + 1, 0, {
98
+ type: "column",
99
+ children: [draggedBlock],
100
+ props: {},
101
+ content: undefined,
102
+ id: UniqueID.options.generateID(),
103
+ });
104
+
105
+ if (editor.getBlock(draggedBlock.id)) {
106
+ editor.removeBlocks([draggedBlock]);
107
+ }
108
+
109
+ editor.updateBlock(columnList, {
110
+ children: newChildren,
111
+ });
112
+ } else {
113
+ // Create new columnList with blocks as columns
114
+ const block = nodeToBlock(blockInfo.bnBlock.node, editor.pmSchema);
115
+
116
+ // The user is dropping next to the original block being dragged - do
117
+ // nothing.
118
+ if (block.id === draggedBlock.id) {
119
+ return true;
120
+ }
121
+
122
+ const blocks =
123
+ edgePos.position === "left"
124
+ ? [draggedBlock, block]
125
+ : [block, draggedBlock];
126
+
127
+ if (editor.getBlock(draggedBlock.id)) {
128
+ editor.removeBlocks([draggedBlock]);
129
+ }
130
+
131
+ editor.replaceBlocks(
132
+ [block],
133
+ [
134
+ {
135
+ type: "columnList",
136
+ children: blocks.map((b) => {
137
+ return {
138
+ type: "column",
139
+ children: [b],
140
+ };
141
+ }),
142
+ },
143
+ ],
144
+ );
145
+ }
146
+
147
+ return true; // Prevent default ProseMirror drop behavior
148
+ },
149
+ },
150
+ });
151
+ }
152
+
153
+ /**
154
+ * BlockNote extension that adds the multi-column drop handler plugin.
155
+ * This should be added to the editor's extensions to enable column creation via drag-and-drop.
156
+ */
157
+ export const MultiColumnDropHandlerExtension = createExtension(
158
+ ({ editor }) => ({
159
+ key: "multiColumnDropHandler",
160
+ prosemirrorPlugins: [createMultiColumnHandleDropPlugin(editor)],
161
+ }),
162
+ );
package/src/index.ts CHANGED
@@ -3,5 +3,5 @@ export { locales };
3
3
  export * from "./i18n/dictionary.js";
4
4
  export * from "./blocks/Columns/index.js";
5
5
  export * from "./blocks/schema.js";
6
- export * from "./extensions/DropCursor/MultiColumnDropCursorPlugin.js";
6
+ export * from "./extensions/DropCursor/multiColumnDropCursor.js";
7
7
  export * from "./extensions/SuggestionMenu/getMultiColumnSlashMenuItems.js";
@@ -0,0 +1,21 @@
1
+ import { type DropCursorHooks } from "@blocknote/core";
2
+ import type { EditorState } from "prosemirror-state";
3
+ import type { EditorView } from "prosemirror-view";
4
+ export interface EdgeDropPosition {
5
+ position: "left" | "right" | "regular";
6
+ posBeforeNode: number;
7
+ node: any;
8
+ }
9
+ /**
10
+ * Detects if the drop event is near the left or right edge of a block.
11
+ * Shared utility used by both the drop cursor visualization and the drop handler.
12
+ * Returns null when the event position cannot be resolved (e.g. drop outside editor bounds).
13
+ */
14
+ export declare function detectEdgePosition(event: DragEvent, view: EditorView, state: EditorState): EdgeDropPosition | null;
15
+ /**
16
+ * Creates the computeDropPosition hook for multi-column support.
17
+ * This hook detects edge drops and returns vertical cursor orientations.
18
+ */
19
+ export declare const multiColumnDropCursor: {
20
+ hooks: DropCursorHooks;
21
+ };
@@ -0,0 +1,16 @@
1
+ import type { BlockNoteEditor } from "@blocknote/core";
2
+ import { Plugin } from "prosemirror-state";
3
+ /**
4
+ * Creates a ProseMirror plugin that handles drop events for multi-column layouts.
5
+ * When a block is dropped near the left or right edge of another block, it creates
6
+ * or modifies column layouts.
7
+ */
8
+ export declare function createMultiColumnHandleDropPlugin(editor: BlockNoteEditor<any, any, any>): Plugin;
9
+ /**
10
+ * BlockNote extension that adds the multi-column drop handler plugin.
11
+ * This should be added to the editor's extensions to enable column creation via drag-and-drop.
12
+ */
13
+ export declare const MultiColumnDropHandlerExtension: (options?: any) => import("@blocknote/core").ExtensionFactoryInstance<{
14
+ key: string;
15
+ prosemirrorPlugins: Plugin<any>[];
16
+ }>;
@@ -3,5 +3,5 @@ export { locales };
3
3
  export * from "./i18n/dictionary.js";
4
4
  export * from "./blocks/Columns/index.js";
5
5
  export * from "./blocks/schema.js";
6
- export * from "./extensions/DropCursor/MultiColumnDropCursorPlugin.js";
6
+ export * from "./extensions/DropCursor/multiColumnDropCursor.js";
7
7
  export * from "./extensions/SuggestionMenu/getMultiColumnSlashMenuItems.js";