@blocknote/core 0.47.1 → 0.47.3

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 (74) hide show
  1. package/dist/BlockNoteExtension-BWw0r8Gy.cjs.map +1 -1
  2. package/dist/BlockNoteExtension-C2X7LW-V.js.map +1 -1
  3. package/dist/{BlockNoteSchema-CwhtPpVC.cjs → BlockNoteSchema-CCs_V3lo.cjs} +2 -2
  4. package/dist/{BlockNoteSchema-CwhtPpVC.cjs.map → BlockNoteSchema-CCs_V3lo.cjs.map} +1 -1
  5. package/dist/{BlockNoteSchema-dmbNkHA-.js → BlockNoteSchema-ooiKsd5B.js} +2 -2
  6. package/dist/{BlockNoteSchema-dmbNkHA-.js.map → BlockNoteSchema-ooiKsd5B.js.map} +1 -1
  7. package/dist/{TrailingNode-F9hX_UlQ.js → TrailingNode-GzE59m_7.js} +585 -415
  8. package/dist/TrailingNode-GzE59m_7.js.map +1 -0
  9. package/dist/TrailingNode-n0WdMPUl.cjs +2 -0
  10. package/dist/TrailingNode-n0WdMPUl.cjs.map +1 -0
  11. package/dist/blocknote.cjs +4 -4
  12. package/dist/blocknote.cjs.map +1 -1
  13. package/dist/blocknote.js +938 -862
  14. package/dist/blocknote.js.map +1 -1
  15. package/dist/blocks.cjs +1 -1
  16. package/dist/blocks.js +2 -2
  17. package/dist/comments.cjs.map +1 -1
  18. package/dist/comments.js.map +1 -1
  19. package/dist/defaultBlocks-Dg9kQWXm.cjs +6 -0
  20. package/dist/defaultBlocks-Dg9kQWXm.cjs.map +1 -0
  21. package/dist/{defaultBlocks-Caw1U1oV.js → defaultBlocks-ZzGbYgQn.js} +611 -530
  22. package/dist/defaultBlocks-ZzGbYgQn.js.map +1 -0
  23. package/dist/extensions.cjs +1 -1
  24. package/dist/extensions.cjs.map +1 -1
  25. package/dist/extensions.js +33 -54
  26. package/dist/extensions.js.map +1 -1
  27. package/dist/locales.cjs +1 -1
  28. package/dist/locales.cjs.map +1 -1
  29. package/dist/locales.js +25 -24
  30. package/dist/locales.js.map +1 -1
  31. package/dist/style.css +1 -1
  32. package/dist/tsconfig.tsbuildinfo +1 -1
  33. package/dist/webpack-stats.json +1 -1
  34. package/package.json +1 -10
  35. package/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts +4 -1
  36. package/src/api/exporters/html/util/serializeBlocksExternalHTML.ts +7 -1
  37. package/src/api/parsers/html/parseHTML.ts +2 -0
  38. package/src/api/parsers/html/util/normalizeWhitespace.ts +87 -0
  39. package/src/blocks/Heading/block.ts +48 -1
  40. package/src/blocks/ListItem/ToggleListItem/block.ts +51 -1
  41. package/src/blocks/Paragraph/block.ts +1 -1
  42. package/src/blocks/getDetailsContent.ts +77 -0
  43. package/src/comments/threadstore/TipTapThreadStore.ts +5 -5
  44. package/src/comments/threadstore/tiptap/types.ts +131 -0
  45. package/src/editor/Block.css +6 -0
  46. package/src/editor/BlockNoteEditor.ts +9 -14
  47. package/src/editor/managers/ExtensionManager/symbol.ts +0 -1
  48. package/src/editor/managers/SelectionManager.ts +3 -1
  49. package/src/extensions/DropCursor/DropCursor.ts +262 -25
  50. package/src/extensions/DropCursor/utils.ts +195 -0
  51. package/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts +15 -10
  52. package/src/i18n/locales/fa.ts +4 -21
  53. package/src/i18n/locales/index.ts +1 -1
  54. package/src/i18n/locales/ru.ts +1 -1
  55. package/src/i18n/locales/uz.ts +22 -4
  56. package/src/index.ts +1 -0
  57. package/src/schema/blocks/createSpec.ts +33 -45
  58. package/src/schema/blocks/types.ts +101 -1
  59. package/types/src/api/parsers/html/util/normalizeWhitespace.d.ts +6 -0
  60. package/types/src/blocks/getDetailsContent.d.ts +19 -0
  61. package/types/src/comments/threadstore/TipTapThreadStore.d.ts +1 -1
  62. package/types/src/comments/threadstore/tiptap/types.d.ts +73 -0
  63. package/types/src/editor/BlockNoteEditor.d.ts +6 -9
  64. package/types/src/extensions/DropCursor/DropCursor.d.ts +42 -5
  65. package/types/src/extensions/DropCursor/utils.d.ts +48 -0
  66. package/types/src/index.d.ts +1 -0
  67. package/types/src/schema/blocks/createSpec.d.ts +3 -3
  68. package/types/src/schema/blocks/types.d.ts +31 -1
  69. package/dist/TrailingNode-DHOdUVUO.cjs +0 -2
  70. package/dist/TrailingNode-DHOdUVUO.cjs.map +0 -1
  71. package/dist/TrailingNode-F9hX_UlQ.js.map +0 -1
  72. package/dist/defaultBlocks-CSB5GiAu.cjs +0 -6
  73. package/dist/defaultBlocks-CSB5GiAu.cjs.map +0 -1
  74. package/dist/defaultBlocks-Caw1U1oV.js.map +0 -1
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Tiptap comment types have moved to a private tiptap package and we don't want to create a dependency on it.
3
+ * We've extracted the types from https://github.com/ueberdosis/hocuspocus/blob/v2.15.3/packages/provider/src/types.ts
4
+ * and added them here.
5
+ */
6
+
7
+ export type TCollabThread<Data = any, CommentData = any> = {
8
+ id: string;
9
+ createdAt: number;
10
+ updatedAt: number;
11
+ deletedAt: number | null;
12
+ resolvedAt?: string; // (new Date()).toISOString()
13
+ comments: TCollabComment<CommentData>[];
14
+ deletedComments: TCollabComment<CommentData>[];
15
+ data: Data;
16
+ };
17
+
18
+ export type TCollabComment<Data = any> = {
19
+ id: string;
20
+ createdAt: string;
21
+ updatedAt: string;
22
+ deletedAt?: string;
23
+ data: Data;
24
+ content: any;
25
+ };
26
+
27
+ export type ThreadType = "archived" | "unarchived";
28
+
29
+ export type GetThreadsOptions = {
30
+ /**
31
+ * The types of threads to get
32
+ * @default ['unarchived']
33
+ */
34
+ types?: Array<ThreadType>;
35
+ };
36
+
37
+ export type DeleteCommentOptions = {
38
+ /**
39
+ * If `true`, the thread will also be deleted if the deleted comment was the first comment in the thread.
40
+ */
41
+ deleteThread?: boolean;
42
+
43
+ /**
44
+ * If `true`, will remove the content of the deleted comment
45
+ */
46
+ deleteContent?: boolean;
47
+ };
48
+
49
+ export type DeleteThreadOptions = {
50
+ /**
51
+ * If `true`, will remove the comments on the thread,
52
+ * otherwise will only mark the thread as deleted
53
+ * and keep the comments
54
+ * @default false
55
+ */
56
+ deleteComments?: boolean;
57
+
58
+ /**
59
+ * If `true`, will forcefully remove the thread and all comments,
60
+ * otherwise will only mark the thread as deleted
61
+ * and keep the comments
62
+ * @default false
63
+ */
64
+ force?: boolean;
65
+ };
66
+
67
+ export type TiptapCollabProvider = {
68
+ getThread<Data, CommentData>(
69
+ id: string,
70
+ ): TCollabThread<Data, CommentData> | null;
71
+
72
+ getThreads<Data, CommentData>(
73
+ options?: GetThreadsOptions,
74
+ ): TCollabThread<Data, CommentData>[];
75
+ createThread(
76
+ data: Omit<
77
+ TCollabThread,
78
+ | "id"
79
+ | "createdAt"
80
+ | "updatedAt"
81
+ | "deletedAt"
82
+ | "comments"
83
+ | "deletedComments"
84
+ >,
85
+ ): TCollabThread;
86
+
87
+ addComment(
88
+ threadId: TCollabThread["id"],
89
+ data: Omit<TCollabComment, "id" | "updatedAt" | "createdAt">,
90
+ ): TCollabThread;
91
+
92
+ updateComment(
93
+ threadId: TCollabThread["id"],
94
+ commentId: TCollabComment["id"],
95
+ data: Partial<Pick<TCollabComment, "data" | "content">>,
96
+ ): TCollabThread;
97
+
98
+ deleteComment(
99
+ threadId: TCollabThread["id"],
100
+ commentId: TCollabComment["id"],
101
+ options?: DeleteCommentOptions,
102
+ ): TCollabThread;
103
+
104
+ getThreadComments(
105
+ threadId: TCollabThread["id"],
106
+ includeDeleted?: boolean,
107
+ ): TCollabComment[] | null;
108
+
109
+ getThreadComment(
110
+ threadId: TCollabThread["id"],
111
+ commentId: TCollabComment["id"],
112
+ includeDeleted?: boolean,
113
+ ): TCollabComment | null;
114
+
115
+ deleteThread(
116
+ id: TCollabThread["id"],
117
+ options?: DeleteThreadOptions,
118
+ ): TCollabThread;
119
+
120
+ updateThread(
121
+ id: TCollabThread["id"],
122
+ data: Partial<
123
+ Pick<TCollabThread, "data"> & {
124
+ resolvedAt: TCollabThread["resolvedAt"] | null;
125
+ }
126
+ >,
127
+ ): TCollabThread;
128
+
129
+ watchThreads(callback: () => void): void;
130
+ unwatchThreads(callback: () => void): void;
131
+ };
@@ -42,6 +42,12 @@ BASIC STYLES
42
42
 
43
43
  .bn-inline-content {
44
44
  width: 100%;
45
+ /* Ensure pre-wrap even when a parent node view wrapper (e.g. tiptap's
46
+ NodeViewWrapper) resets white-space to "normal". Without this, browsers
47
+ normalize trailing spaces to NBSP on input, which causes ProseMirror to
48
+ compute a replacement instead of a pure insertion and breaks the
49
+ suggestion-menu trigger detection (issue #2531). */
50
+ white-space: pre-wrap;
45
51
  }
46
52
 
47
53
  /*
@@ -5,7 +5,7 @@ import {
5
5
  getSchema,
6
6
  Editor as TiptapEditor,
7
7
  } from "@tiptap/core";
8
- import { type Command, type Plugin, type Transaction } from "@tiptap/pm/state";
8
+ import { type Command, type Transaction } from "@tiptap/pm/state";
9
9
  import { Node, Schema } from "prosemirror-model";
10
10
  import type { BlocksChanged } from "../api/getBlocksChangedByTransaction.js";
11
11
  import { blockToNode } from "../api/nodeConversions/blockToNode.js";
@@ -18,7 +18,10 @@ import {
18
18
  PartialBlock,
19
19
  } from "../blocks/index.js";
20
20
  import type { CollaborationOptions } from "../extensions/Collaboration/Collaboration.js";
21
- import { BlockChangeExtension } from "../extensions/index.js";
21
+ import {
22
+ BlockChangeExtension,
23
+ DropCursorOptions,
24
+ } from "../extensions/index.js";
22
25
  import { UniqueID } from "../extensions/tiptap-extensions/UniqueID/UniqueID.js";
23
26
  import type { Dictionary } from "../i18n/dictionary.js";
24
27
  import { en } from "../i18n/locales/index.js";
@@ -118,19 +121,11 @@ export interface BlockNoteEditorOptions<
118
121
  domAttributes?: Partial<BlockNoteDOMAttributes>;
119
122
 
120
123
  /**
121
- * A replacement indicator to use when dragging and dropping blocks. Uses the [ProseMirror drop cursor](https://github.com/ProseMirror/prosemirror-dropcursor), or a modified version when [Column Blocks](https://www.blocknotejs.org/docs/document-structure#column-blocks) are enabled.
122
- * @remarks `() => Plugin`
124
+ * Options for configuring the drop cursor behavior when dragging and dropping blocks.
125
+ * Allows customization of cursor appearance and drop position computation through hooks.
126
+ * @remarks `DropCursorOptions`
123
127
  */
124
- dropCursor?: (opts: {
125
- editor: BlockNoteEditor<
126
- NoInfer<BSchema>,
127
- NoInfer<ISchema>,
128
- NoInfer<SSchema>
129
- >;
130
- color?: string | false;
131
- width?: number;
132
- class?: string;
133
- }) => Plugin;
128
+ dropCursor?: DropCursorOptions;
134
129
 
135
130
  /**
136
131
  * The content that should be in the editor when it's created, represented as an array of {@link PartialBlock} objects.
@@ -3,4 +3,3 @@
3
3
  * This allows us to retrieve the original factory for comparison and other operations.
4
4
  */
5
5
  export const originalFactorySymbol = Symbol("originalFactory");
6
-
@@ -48,7 +48,9 @@ export class SelectionManager<
48
48
  * only the part of the block that is included in the selection.
49
49
  */
50
50
  public getSelectionCutBlocks(expandToWords = false) {
51
- return this.editor.transact((tr) => getSelectionCutBlocks(tr, expandToWords));
51
+ return this.editor.transact((tr) =>
52
+ getSelectionCutBlocks(tr, expandToWords),
53
+ );
52
54
  }
53
55
 
54
56
  /**
@@ -1,26 +1,263 @@
1
- import { dropCursor } from "prosemirror-dropcursor";
1
+ import { dropPoint } from "prosemirror-transform";
2
+ import type { EditorView } from "prosemirror-view";
2
3
  import {
3
- createExtension,
4
- ExtensionOptions,
5
- } from "../../editor/BlockNoteExtension.js";
6
- import { BlockNoteEditorOptions } from "../../editor/BlockNoteEditor.js";
7
-
8
- export const DropCursorExtension = createExtension(
9
- ({
10
- editor,
11
- options,
12
- }: ExtensionOptions<
13
- Pick<BlockNoteEditorOptions<any, any, any>, "dropCursor">
14
- >) => {
15
- return {
16
- key: "dropCursor",
17
- prosemirrorPlugins: [
18
- (options.dropCursor ?? dropCursor)({
19
- width: 5,
20
- color: "#ddeeff",
21
- editor: editor,
22
- }),
23
- ],
24
- } as const;
25
- },
26
- );
4
+ applyOrientationClasses,
5
+ getBlockDropRect,
6
+ getInlineDropRect,
7
+ getParentOffsets,
8
+ hasExclusionClassname,
9
+ type DropCursorPosition,
10
+ } from "./utils.js";
11
+ import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
12
+ import { createExtension } from "../../editor/BlockNoteExtension.js";
13
+
14
+ export const DRAG_EXCLUSION_CLASSNAME = "bn-drag-exclude";
15
+
16
+ /**
17
+ * Context passed to the computeDropPosition hook.
18
+ */
19
+ export interface ComputeDropPositionContext {
20
+ editor: BlockNoteEditor<any, any, any>;
21
+ event: DragEvent;
22
+ view: EditorView;
23
+ defaultPosition: DropCursorPosition | null;
24
+ }
25
+
26
+ /**
27
+ * Hooks for customizing drop cursor behavior.
28
+ */
29
+ export interface DropCursorHooks {
30
+ /**
31
+ * Compute cursor position and orientation.
32
+ * Return null to prevent dropping (no cursor shown).
33
+ */
34
+ computeDropPosition?: (
35
+ context: ComputeDropPositionContext,
36
+ ) => DropCursorPosition | null;
37
+ }
38
+
39
+ /**
40
+ * Options for the DropCursor extension.
41
+ */
42
+ export interface DropCursorOptions {
43
+ width?: number; // Cursor width in pixels (default: 5)
44
+ color?: string | false; // Cursor color (default: "#ddeeff")
45
+ exclude?: string; // CSS class for exclusion (default: "bn-drag-exclude")
46
+ hooks?: DropCursorHooks; // Optional behavior hooks
47
+ }
48
+
49
+ /**
50
+ * Drop cursor visualization based on prosemirror-dropcursor:
51
+ * https://github.com/ProseMirror/prosemirror-dropcursor/blob/master/src/dropcursor.ts
52
+ *
53
+ * Refactored to use BlockNote extension pattern with mount callback and AbortSignal
54
+ * for lifecycle management instead of ProseMirror PluginView.
55
+ */
56
+ export const DropCursorExtension = createExtension<
57
+ any,
58
+ {
59
+ dropCursor?: DropCursorOptions;
60
+ }
61
+ >(({ editor, options }) => {
62
+ // State
63
+ let cursorPos: DropCursorPosition | null = null;
64
+ let element: HTMLElement | null = null;
65
+ let timeout = -1;
66
+ let dragSourceElement: Element | null = null;
67
+
68
+ const config = {
69
+ width: options.dropCursor?.width ?? 5,
70
+ color: options.dropCursor?.color ?? "#ddeeff",
71
+ exclude: options.dropCursor?.exclude ?? DRAG_EXCLUSION_CLASSNAME,
72
+ hooks: options.dropCursor?.hooks,
73
+ } as const;
74
+
75
+ // Helper functions
76
+ const setCursor = (pos: DropCursorPosition | null) => {
77
+ if (
78
+ pos?.pos === cursorPos?.pos &&
79
+ pos?.orientation === cursorPos?.orientation
80
+ ) {
81
+ return;
82
+ }
83
+ cursorPos = pos;
84
+
85
+ if (pos == null) {
86
+ if (element && element.parentNode) {
87
+ element.parentNode.removeChild(element);
88
+ }
89
+ element = null;
90
+ } else {
91
+ updateOverlay();
92
+ }
93
+ };
94
+
95
+ const updateOverlay = () => {
96
+ if (!cursorPos) {
97
+ return;
98
+ }
99
+
100
+ const view = editor.prosemirrorView;
101
+ const editorDOM = view.dom;
102
+ const editorRect = editorDOM.getBoundingClientRect();
103
+ const scaleX = editorRect.width / editorDOM.offsetWidth;
104
+ const scaleY = editorRect.height / editorDOM.offsetHeight;
105
+
106
+ const blockRect = getBlockDropRect(
107
+ view,
108
+ cursorPos,
109
+ config.width,
110
+ scaleX,
111
+ scaleY,
112
+ );
113
+ const rect =
114
+ blockRect ?? getInlineDropRect(view, cursorPos, config.width, scaleX);
115
+
116
+ const parent = view.dom.offsetParent as HTMLElement;
117
+ if (!element) {
118
+ element = parent.appendChild(document.createElement("div"));
119
+ element.style.cssText =
120
+ "position: absolute; z-index: 50; pointer-events: none;";
121
+ if (config.color) {
122
+ element.style.backgroundColor = config.color;
123
+ }
124
+ }
125
+
126
+ applyOrientationClasses(element, cursorPos.orientation);
127
+
128
+ const { parentLeft, parentTop } = getParentOffsets(parent);
129
+
130
+ element.style.left = (rect.left - parentLeft) / scaleX + "px";
131
+ element.style.top = (rect.top - parentTop) / scaleY + "px";
132
+ element.style.width = (rect.right - rect.left) / scaleX + "px";
133
+ element.style.height = (rect.bottom - rect.top) / scaleY + "px";
134
+ };
135
+
136
+ const scheduleRemoval = (ms: number) => {
137
+ clearTimeout(timeout);
138
+ timeout = window.setTimeout(() => setCursor(null), ms);
139
+ };
140
+
141
+ // Event handlers
142
+ const onDragStart = (event: Event) => {
143
+ const e = event as DragEvent;
144
+ dragSourceElement = e.target instanceof Element ? e.target : null;
145
+ };
146
+
147
+ const onDragOver = (event: Event) => {
148
+ const e = event as DragEvent;
149
+
150
+ // Check if drag source has exclusion classname
151
+ if (
152
+ dragSourceElement &&
153
+ hasExclusionClassname(dragSourceElement, config.exclude)
154
+ ) {
155
+ return;
156
+ }
157
+
158
+ // Check if drop target has exclusion classname
159
+ if (
160
+ e.target instanceof Element &&
161
+ hasExclusionClassname(e.target, config.exclude)
162
+ ) {
163
+ return;
164
+ }
165
+
166
+ const view = editor.prosemirrorView;
167
+ if (!view.editable) {
168
+ return;
169
+ }
170
+
171
+ const pos = view.posAtCoords({
172
+ left: e.clientX,
173
+ top: e.clientY,
174
+ });
175
+
176
+ const node = pos && pos.inside >= 0 && view.state.doc.nodeAt(pos.inside);
177
+ const disableDropCursor = node && (node.type.spec as any).disableDropCursor;
178
+ const disabled =
179
+ typeof disableDropCursor === "function"
180
+ ? disableDropCursor(view, pos, e)
181
+ : disableDropCursor;
182
+
183
+ if (pos && !disabled) {
184
+ let target = pos.pos;
185
+ if (view.dragging && view.dragging.slice) {
186
+ const point = dropPoint(view.state.doc, target, view.dragging.slice);
187
+ if (point != null) {
188
+ target = point;
189
+ }
190
+ }
191
+
192
+ // Compute default position
193
+ const $pos = view.state.doc.resolve(target);
194
+ const isBlock = !$pos.parent.inlineContent;
195
+ const defaultPosition: DropCursorPosition = {
196
+ pos: target,
197
+ orientation: isBlock ? "block-horizontal" : "inline",
198
+ };
199
+
200
+ // Allow hook to override position
201
+ let finalPosition = defaultPosition;
202
+ if (config.hooks?.computeDropPosition) {
203
+ const hookResult = config.hooks.computeDropPosition({
204
+ editor,
205
+ event: e,
206
+ view,
207
+ defaultPosition,
208
+ });
209
+ if (hookResult === null) {
210
+ // Hook returned null - don't show cursor
211
+ setCursor(null);
212
+ return;
213
+ }
214
+ finalPosition = hookResult;
215
+ }
216
+
217
+ setCursor(finalPosition);
218
+ scheduleRemoval(5000);
219
+ }
220
+ };
221
+
222
+ const onDragLeave = (event: Event) => {
223
+ const e = event as DragEvent;
224
+ if (
225
+ !(e.relatedTarget instanceof Node) ||
226
+ !editor.prosemirrorView.dom.contains(e.relatedTarget)
227
+ ) {
228
+ setCursor(null);
229
+ }
230
+ };
231
+
232
+ const onDrop = () => {
233
+ scheduleRemoval(20);
234
+ };
235
+
236
+ const onDragEnd = () => {
237
+ scheduleRemoval(20);
238
+ dragSourceElement = null;
239
+ };
240
+
241
+ return {
242
+ key: "dropCursor",
243
+ mount({ signal, dom, root }) {
244
+ // Track drag source at document level
245
+ root.addEventListener("dragstart", onDragStart, {
246
+ capture: true,
247
+ signal,
248
+ });
249
+
250
+ // Handle drag events on the editor
251
+ dom.addEventListener("dragover", onDragOver, { signal });
252
+ dom.addEventListener("dragleave", onDragLeave, { signal });
253
+ dom.addEventListener("drop", onDrop, { signal });
254
+ dom.addEventListener("dragend", onDragEnd, { signal });
255
+
256
+ // Clean up on unmount
257
+ signal.addEventListener("abort", () => {
258
+ clearTimeout(timeout);
259
+ setCursor(null);
260
+ });
261
+ },
262
+ } as const;
263
+ });
@@ -0,0 +1,195 @@
1
+ import type { EditorView } from "prosemirror-view";
2
+
3
+ /**
4
+ * The orientation of the drop cursor.
5
+ */
6
+ export type DropCursorOrientation =
7
+ | "inline" // Vertical line within text
8
+ | "block-horizontal" // Horizontal line between blocks
9
+ | "block-vertical-left" // Vertical line on left edge of block
10
+ | "block-vertical-right"; // Vertical line on right edge of block
11
+
12
+ /**
13
+ * The position and orientation of the drop cursor.
14
+ */
15
+ export type DropCursorPosition = {
16
+ pos: number; // Document position
17
+ orientation: DropCursorOrientation;
18
+ };
19
+ /**
20
+ * Bounding rectangle in viewport coordinates (e.g. from getBoundingClientRect).
21
+ */
22
+ export type Rect = {
23
+ left: number;
24
+ right: number;
25
+ top: number;
26
+ bottom: number;
27
+ };
28
+
29
+ /**
30
+ * Returns true if the element or any ancestor has the given CSS class.
31
+ * Used to skip drop cursor for elements marked with the exclusion class (e.g. drag handles).
32
+ */
33
+ export function hasExclusionClassname(
34
+ element: Element | null,
35
+ exclude: string,
36
+ ): boolean {
37
+ if (!element || !exclude) {
38
+ return false;
39
+ }
40
+ return !!element.closest(`.${exclude}`);
41
+ }
42
+
43
+ /**
44
+ * Computes the viewport rect for a block-level drop cursor (horizontal line between blocks
45
+ * or vertical line on left/right edge). Returns null for inline positions or when no DOM node exists.
46
+ */
47
+ export function getBlockDropRect(
48
+ view: EditorView,
49
+ cursorPos: DropCursorPosition,
50
+ width: number,
51
+ scaleX: number,
52
+ scaleY: number,
53
+ ): Rect | null {
54
+ const $pos = view.state.doc.resolve(cursorPos.pos);
55
+ const isBlock = !$pos.parent.inlineContent;
56
+
57
+ if (!isBlock || cursorPos.orientation === "inline") {
58
+ return null;
59
+ }
60
+
61
+ const before = $pos.nodeBefore;
62
+
63
+ const after = $pos.nodeAfter;
64
+
65
+ if (!before && !after) {
66
+ return null;
67
+ }
68
+
69
+ const isVertical =
70
+ cursorPos.orientation === "block-vertical-left" ||
71
+ cursorPos.orientation === "block-vertical-right";
72
+ // For vertical cursors, position is at the node position, for horizontal cursors, position is at the node before position
73
+ const nodePos = isVertical
74
+ ? cursorPos.pos
75
+ : cursorPos.pos - (before ? before.nodeSize : 0);
76
+
77
+ const node = view.nodeDOM(nodePos) as HTMLElement | null;
78
+ if (!node) {
79
+ return null;
80
+ }
81
+
82
+ const nodeRect = node.getBoundingClientRect();
83
+
84
+ if (isVertical) {
85
+ const halfWidth = (width / 2) * scaleX;
86
+ const left =
87
+ cursorPos.orientation === "block-vertical-left"
88
+ ? nodeRect.left
89
+ : nodeRect.right;
90
+
91
+ return {
92
+ left: left - halfWidth,
93
+ right: left + halfWidth,
94
+ top: nodeRect.top,
95
+ bottom: nodeRect.bottom,
96
+ };
97
+ }
98
+
99
+ let top = before ? nodeRect.bottom : nodeRect.top;
100
+ if (before && after) {
101
+ top =
102
+ (top +
103
+ (view.nodeDOM(cursorPos.pos) as HTMLElement).getBoundingClientRect()
104
+ .top) /
105
+ 2;
106
+ }
107
+ const halfHeight = (width / 2) * scaleY;
108
+
109
+ return {
110
+ left: nodeRect.left,
111
+ right: nodeRect.right,
112
+ top: top - halfHeight,
113
+ bottom: top + halfHeight,
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Computes the viewport rect for an inline drop cursor (vertical line within text).
119
+ */
120
+ export function getInlineDropRect(
121
+ view: EditorView,
122
+ cursorPos: DropCursorPosition,
123
+ width: number,
124
+ scaleX: number,
125
+ ): Rect {
126
+ const coords = view.coordsAtPos(cursorPos.pos);
127
+ const halfWidth = (width / 2) * scaleX;
128
+
129
+ return {
130
+ left: coords.left - halfWidth,
131
+ right: coords.left + halfWidth,
132
+ top: coords.top,
133
+ bottom: coords.bottom,
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Applies orientation-specific CSS classes to the drop cursor element so it can be
139
+ * styled correctly (e.g. horizontal vs vertical line, inline vs block).
140
+ */
141
+ export function applyOrientationClasses(
142
+ el: HTMLElement,
143
+ orientation: DropCursorOrientation,
144
+ ) {
145
+ el.classList.toggle(
146
+ "prosemirror-dropcursor-inline",
147
+ orientation === "inline",
148
+ );
149
+ el.classList.toggle(
150
+ "prosemirror-dropcursor-block-horizontal",
151
+ orientation === "block-horizontal",
152
+ );
153
+ el.classList.toggle(
154
+ "prosemirror-dropcursor-block-vertical-left",
155
+ orientation === "block-vertical-left",
156
+ );
157
+ el.classList.toggle(
158
+ "prosemirror-dropcursor-block-vertical-right",
159
+ orientation === "block-vertical-right",
160
+ );
161
+ el.classList.toggle(
162
+ "prosemirror-dropcursor-block",
163
+ orientation === "block-horizontal",
164
+ );
165
+ el.classList.toggle(
166
+ "prosemirror-dropcursor-vertical",
167
+ orientation === "block-vertical-left" ||
168
+ orientation === "block-vertical-right",
169
+ );
170
+ }
171
+
172
+ /**
173
+ * Returns the offset of the parent element for converting viewport coordinates to
174
+ * parent-relative coordinates. Handles document.body and static positioning.
175
+ */
176
+ export function getParentOffsets(parent: HTMLElement | null) {
177
+ if (
178
+ !parent ||
179
+ (parent === document.body && getComputedStyle(parent).position === "static")
180
+ ) {
181
+ return {
182
+ parentLeft: -window.pageXOffset,
183
+ parentTop: -window.pageYOffset,
184
+ };
185
+ }
186
+
187
+ const parentRect = parent.getBoundingClientRect();
188
+ const parentScaleX = parentRect.width / parent.offsetWidth;
189
+ const parentScaleY = parentRect.height / parent.offsetHeight;
190
+
191
+ return {
192
+ parentLeft: parentRect.left - parent.scrollLeft * parentScaleX,
193
+ parentTop: parentRect.top - parent.scrollTop * parentScaleY,
194
+ };
195
+ }