@blocknote/xl-multi-column 0.47.0 → 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,508 +0,0 @@
1
- import type { BlockNoteEditor } from "@blocknote/core";
2
- import {
3
- UniqueID,
4
- getBlockInfo,
5
- getNearestBlockPos,
6
- nodeToBlock,
7
- } from "@blocknote/core";
8
- import { EditorState, Plugin } from "prosemirror-state";
9
- import { dropPoint } from "prosemirror-transform";
10
- import { EditorView } from "prosemirror-view";
11
-
12
- const PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP = 0.1;
13
-
14
- function eventCoords(event: MouseEvent) {
15
- return { left: event.clientX, top: event.clientY };
16
- }
17
-
18
- interface DropCursorOptions {
19
- /// The color of the cursor. Defaults to `black`. Use `false` to apply no color and rely only on class.
20
- color?: string | false;
21
-
22
- /// The precise width of the cursor in pixels. Defaults to 1.
23
- width?: number;
24
-
25
- /// A CSS class name to add to the cursor element.
26
- class?: string;
27
- }
28
-
29
- /// Create a plugin that, when added to a ProseMirror instance,
30
- /// causes a decoration to show up at the drop position when something
31
- /// is dragged over the editor.
32
- ///
33
- /// Nodes may add a `disableDropCursor` property to their spec to
34
- /// control the showing of a drop cursor inside them. This may be a
35
- /// boolean or a function, which will be called with a view and a
36
- /// position, and should return a boolean.
37
- export function multiColumnDropCursor(
38
- options: DropCursorOptions & {
39
- editor: BlockNoteEditor<any, any, any>;
40
- },
41
- ): Plugin {
42
- const editor = options.editor;
43
- return new Plugin({
44
- view(editorView) {
45
- return new DropCursorView(editorView, options);
46
- },
47
- props: {
48
- handleDrop(view, event, slice, _moved) {
49
- const eventPos = view.posAtCoords(eventCoords(event));
50
-
51
- if (!eventPos) {
52
- throw new Error("Could not get event position");
53
- }
54
-
55
- const posInfo = getTargetPosInfo(view.state, eventPos);
56
- const blockInfo = getBlockInfo(posInfo);
57
-
58
- const blockElement = view.nodeDOM(posInfo.posBeforeNode);
59
- const blockRect = (blockElement as HTMLElement).getBoundingClientRect();
60
- let position: "regular" | "left" | "right" = "regular";
61
- if (
62
- event.clientX <=
63
- blockRect.left +
64
- blockRect.width * PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP
65
- ) {
66
- position = "left";
67
- }
68
- if (
69
- event.clientX >=
70
- blockRect.right -
71
- blockRect.width * PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP
72
- ) {
73
- position = "right";
74
- }
75
-
76
- if (position === "regular") {
77
- // handled by default prosemirror drop behaviour
78
- return false;
79
- }
80
-
81
- const draggedBlock = nodeToBlock(
82
- slice.content.child(0),
83
- editor.pmSchema,
84
- // TODO: cache?
85
- );
86
-
87
- // const block = blockInfo.block(editor);
88
- if (blockInfo.blockNoteType === "column") {
89
- // insert new column in existing columnList
90
- const parentBlock = view.state.doc
91
- .resolve(blockInfo.bnBlock.beforePos)
92
- .node();
93
-
94
- const columnList = nodeToBlock<any, any, any>(
95
- parentBlock,
96
- editor.pmSchema,
97
- );
98
-
99
- // In a `columnList`, we expect that the average width of each column
100
- // is 1. However, there are cases in which this stops being true. For
101
- // example, having one wider column and then removing it will cause
102
- // the average width to go down. This isn't really an issue until the
103
- // user tries to add a new column, which will, in this case, be wider
104
- // than expected. Therefore, we normalize the column widths to an
105
- // average of 1 here to avoid this issue.
106
- let sumColumnWidthPercent = 0;
107
- columnList.children.forEach((column) => {
108
- sumColumnWidthPercent += column.props.width as number;
109
- });
110
- const avgColumnWidthPercent =
111
- sumColumnWidthPercent / columnList.children.length;
112
-
113
- // If the average column width is not 1, normalize it. We're dealing
114
- // with floats so we need a small margin to account for precision
115
- // errors.
116
- if (avgColumnWidthPercent < 0.99 || avgColumnWidthPercent > 1.01) {
117
- const scalingFactor = 1 / avgColumnWidthPercent;
118
-
119
- columnList.children.forEach((column) => {
120
- column.props.width =
121
- (column.props.width as number) * scalingFactor;
122
- });
123
- }
124
-
125
- const index = columnList.children.findIndex(
126
- (b) => b.id === blockInfo.bnBlock.node.attrs.id,
127
- );
128
-
129
- const newChildren = columnList.children
130
- // If the dragged block is in one of the columns, remove it.
131
- .map((column) => ({
132
- ...column,
133
- children: column.children.filter(
134
- (block) => block.id !== draggedBlock.id,
135
- ),
136
- }))
137
- // Remove empty columns (can happen when dragged block is removed).
138
- .filter((column) => column.children.length > 0)
139
- // Insert the dragged block in the correct position.
140
- .toSpliced(position === "left" ? index : index + 1, 0, {
141
- type: "column",
142
- children: [draggedBlock],
143
- props: {},
144
- content: undefined,
145
- id: UniqueID.options.generateID(),
146
- });
147
-
148
- if (editor.getBlock(draggedBlock.id)) {
149
- editor.removeBlocks([draggedBlock]);
150
- }
151
-
152
- editor.updateBlock(columnList, {
153
- children: newChildren,
154
- });
155
- } else {
156
- // create new columnList with blocks as columns
157
- const block = nodeToBlock(blockInfo.bnBlock.node, editor.pmSchema);
158
-
159
- // The user is dropping next to the original block being dragged - do
160
- // nothing.
161
- if (block.id === draggedBlock.id) {
162
- return;
163
- }
164
-
165
- const blocks =
166
- position === "left" ? [draggedBlock, block] : [block, draggedBlock];
167
-
168
- if (editor.getBlock(draggedBlock.id)) {
169
- editor.removeBlocks([draggedBlock]);
170
- }
171
-
172
- editor.replaceBlocks(
173
- [block],
174
- [
175
- {
176
- type: "columnList",
177
- children: blocks.map((b) => {
178
- return {
179
- type: "column",
180
- children: [b],
181
- };
182
- }),
183
- },
184
- ],
185
- );
186
- }
187
-
188
- return true;
189
- },
190
- },
191
- });
192
- }
193
-
194
- class DropCursorView {
195
- width: number;
196
- color: string | undefined;
197
- class: string | undefined;
198
- cursorPos:
199
- | { pos: number; position: "left" | "right" | "regular" }
200
- | undefined = undefined;
201
- element: HTMLElement | null = null;
202
- timeout: ReturnType<typeof setTimeout> | undefined = undefined;
203
- handlers: { name: string; handler: (event: Event) => void }[];
204
-
205
- constructor(
206
- readonly editorView: EditorView,
207
- options: DropCursorOptions,
208
- ) {
209
- this.width = options.width ?? 1;
210
- this.color = options.color === false ? undefined : options.color || "black";
211
- this.class = options.class;
212
-
213
- this.handlers = ["dragover", "dragend", "drop", "dragleave"].map((name) => {
214
- const handler = (e: Event) => {
215
- (this as any)[name](e);
216
- };
217
- editorView.dom.addEventListener(
218
- name,
219
- handler,
220
- // drop event captured in bubbling phase to make sure
221
- // "cursorPos" is set to undefined before the "handleDrop" handler is called
222
- // (otherwise an error could be thrown, see https://github.com/TypeCellOS/BlockNote/pull/1240)
223
- name === "drop" ? true : undefined,
224
- );
225
- return { name, handler };
226
- });
227
- }
228
-
229
- destroy() {
230
- this.handlers.forEach(({ name, handler }) =>
231
- this.editorView.dom.removeEventListener(
232
- name,
233
- handler,
234
- name === "drop" ? true : undefined,
235
- ),
236
- );
237
- }
238
-
239
- update(editorView: EditorView, prevState: EditorState) {
240
- if (this.cursorPos != null && prevState.doc !== editorView.state.doc) {
241
- if (this.cursorPos.pos > editorView.state.doc.content.size) {
242
- this.setCursor(undefined);
243
- } else {
244
- // update overlay because document has changed
245
- this.updateOverlay();
246
- }
247
- }
248
- }
249
-
250
- setCursor(
251
- cursorPos:
252
- | { pos: number; position: "left" | "right" | "regular" }
253
- | undefined,
254
- ) {
255
- if (
256
- cursorPos === this.cursorPos ||
257
- (cursorPos?.pos === this.cursorPos?.pos &&
258
- cursorPos?.position === this.cursorPos?.position)
259
- ) {
260
- // no change
261
- return;
262
- }
263
- this.cursorPos = cursorPos;
264
- if (!cursorPos) {
265
- this.element!.parentNode!.removeChild(this.element!);
266
- this.element = null;
267
- } else {
268
- // update overlay because cursor has changed
269
- this.updateOverlay();
270
- }
271
- }
272
-
273
- updateOverlay() {
274
- if (!this.cursorPos) {
275
- throw new Error("updateOverlay called with no cursor position");
276
- }
277
- const $pos = this.editorView.state.doc.resolve(this.cursorPos.pos);
278
- const isBlock = !$pos.parent.inlineContent;
279
- let rect;
280
- const editorDOM = this.editorView.dom;
281
- const editorRect = editorDOM.getBoundingClientRect();
282
- const scaleX = editorRect.width / editorDOM.offsetWidth;
283
- const scaleY = editorRect.height / editorDOM.offsetHeight;
284
- if (isBlock) {
285
- const before = $pos.nodeBefore;
286
- const after = $pos.nodeAfter;
287
- if (before || after) {
288
- if (
289
- this.cursorPos.position === "left" ||
290
- this.cursorPos.position === "right"
291
- ) {
292
- const block = this.editorView.nodeDOM(this.cursorPos.pos);
293
-
294
- if (!block) {
295
- throw new Error("nodeDOM returned null in updateOverlay");
296
- }
297
-
298
- const blockRect = (block as HTMLElement).getBoundingClientRect();
299
- const halfWidth = (this.width / 2) * scaleY;
300
- const left =
301
- this.cursorPos.position === "left"
302
- ? blockRect.left
303
- : blockRect.right;
304
- rect = {
305
- left: left - halfWidth,
306
- right: left + halfWidth,
307
- top: blockRect.top,
308
- bottom: blockRect.bottom,
309
- // left: blockRect.left,
310
- // right: blockRect.right,
311
- };
312
- } else {
313
- // regular logic
314
- const node = this.editorView.nodeDOM(
315
- this.cursorPos.pos - (before ? before.nodeSize : 0),
316
- );
317
- if (node) {
318
- const nodeRect = (node as HTMLElement).getBoundingClientRect();
319
-
320
- let top = before ? nodeRect.bottom : nodeRect.top;
321
- if (before && after) {
322
- // find the middle between the node above and below
323
- top =
324
- (top +
325
- (
326
- this.editorView.nodeDOM(this.cursorPos.pos) as HTMLElement
327
- ).getBoundingClientRect().top) /
328
- 2;
329
- }
330
- // console.log("node");
331
- const halfWidth = (this.width / 2) * scaleY;
332
-
333
- if (this.cursorPos.position === "regular") {
334
- rect = {
335
- left: nodeRect.left,
336
- right: nodeRect.right,
337
- top: top - halfWidth,
338
- bottom: top + halfWidth,
339
- };
340
- }
341
- }
342
- }
343
- }
344
- }
345
-
346
- if (!rect) {
347
- // Cursor is an inline vertical dropcursor
348
- const coords = this.editorView.coordsAtPos(this.cursorPos.pos);
349
- const halfWidth = (this.width / 2) * scaleX;
350
- rect = {
351
- left: coords.left - halfWidth,
352
- right: coords.left + halfWidth,
353
- top: coords.top,
354
- bottom: coords.bottom,
355
- };
356
- }
357
-
358
- // Code below positions the cursor overlay based on the rect
359
- const parent = this.editorView.dom.offsetParent as HTMLElement;
360
- if (!this.element) {
361
- this.element = parent.appendChild(document.createElement("div"));
362
- if (this.class) {
363
- this.element.className = this.class;
364
- }
365
- this.element.style.cssText =
366
- "position: absolute; z-index: 50; pointer-events: none;";
367
- if (this.color) {
368
- this.element.style.backgroundColor = this.color;
369
- }
370
- }
371
- this.element.classList.toggle("prosemirror-dropcursor-block", isBlock);
372
- this.element.classList.toggle(
373
- "prosemirror-dropcursor-vertical",
374
- this.cursorPos.position !== "regular",
375
- );
376
- this.element.classList.toggle("prosemirror-dropcursor-inline", !isBlock);
377
- let parentLeft, parentTop;
378
- if (
379
- !parent ||
380
- (parent === document.body &&
381
- getComputedStyle(parent).position === "static")
382
- ) {
383
- parentLeft = -window.scrollX;
384
- parentTop = -window.scrollY;
385
- } else {
386
- const rect = parent.getBoundingClientRect();
387
- const parentScaleX = rect.width / parent.offsetWidth;
388
- const parentScaleY = rect.height / parent.offsetHeight;
389
- parentLeft = rect.left - parent.scrollLeft * parentScaleX;
390
- parentTop = rect.top - parent.scrollTop * parentScaleY;
391
- }
392
- this.element.style.left = (rect.left - parentLeft) / scaleX + "px";
393
- this.element.style.top = (rect.top - parentTop) / scaleY + "px";
394
- this.element.style.width = (rect.right - rect.left) / scaleX + "px";
395
- this.element.style.height = (rect.bottom - rect.top) / scaleY + "px";
396
- }
397
-
398
- scheduleRemoval(timeout: number) {
399
- clearTimeout(this.timeout);
400
- this.timeout = setTimeout(() => this.setCursor(undefined), timeout);
401
- }
402
-
403
- // this gets executed on every mouse move when dragging (drag over)
404
- dragover(event: DragEvent) {
405
- if (!this.editorView.editable) {
406
- return;
407
- }
408
- const pos = this.editorView.posAtCoords({
409
- left: event.clientX,
410
- top: event.clientY,
411
- });
412
-
413
- // console.log("posatcoords", pos);
414
-
415
- const node =
416
- pos && pos.inside >= 0 && this.editorView.state.doc.nodeAt(pos.inside);
417
- const disableDropCursor = node && node.type.spec.disableDropCursor;
418
- const disabled =
419
- typeof disableDropCursor == "function"
420
- ? disableDropCursor(this.editorView, pos, event)
421
- : disableDropCursor;
422
-
423
- if (pos && !disabled) {
424
- let position: "regular" | "left" | "right" = "regular";
425
- let target: number | null = pos.pos;
426
-
427
- const posInfo = getTargetPosInfo(this.editorView.state, pos);
428
-
429
- const block = this.editorView.nodeDOM(posInfo.posBeforeNode);
430
- const blockRect = (block as HTMLElement).getBoundingClientRect();
431
-
432
- if (
433
- event.clientX <=
434
- blockRect.left +
435
- blockRect.width * PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP
436
- ) {
437
- position = "left";
438
- target = posInfo.posBeforeNode;
439
- }
440
- if (
441
- event.clientX >=
442
- blockRect.right -
443
- blockRect.width * PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP
444
- ) {
445
- position = "right";
446
- target = posInfo.posBeforeNode;
447
- }
448
-
449
- // "regular logic"
450
- if (
451
- position === "regular" &&
452
- this.editorView.dragging &&
453
- this.editorView.dragging.slice
454
- ) {
455
- const point = dropPoint(
456
- this.editorView.state.doc,
457
- target,
458
- this.editorView.dragging.slice,
459
- );
460
-
461
- if (point != null) {
462
- target = point;
463
- }
464
- }
465
-
466
- this.setCursor({ pos: target, position });
467
- this.scheduleRemoval(5000);
468
- }
469
- }
470
-
471
- dragend() {
472
- this.scheduleRemoval(20);
473
- }
474
-
475
- drop() {
476
- this.setCursor(undefined);
477
- }
478
-
479
- dragleave(event: DragEvent) {
480
- if (
481
- event.target === this.editorView.dom ||
482
- !this.editorView.dom.contains((event as any).relatedTarget)
483
- ) {
484
- this.setCursor(undefined);
485
- }
486
- }
487
- }
488
-
489
- /**
490
- * From a position inside the document get the block that should be the "drop target" block.
491
- */
492
- function getTargetPosInfo(
493
- state: EditorState,
494
- eventPos: { pos: number; inside: number },
495
- ) {
496
- const blockPos = getNearestBlockPos(state.doc, eventPos.pos);
497
-
498
- // 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
499
- // why? because we want to insert a new column in the columnList, instead of a new columnList inside of the column
500
- let resolved = state.doc.resolve(blockPos.posBeforeNode);
501
- if (resolved.parent.type.name === "column") {
502
- resolved = state.doc.resolve(resolved.before());
503
- }
504
- return {
505
- posBeforeNode: resolved.pos,
506
- node: resolved.nodeAfter!,
507
- };
508
- }
@@ -1,11 +0,0 @@
1
- import type { BlockNoteEditor } from "@blocknote/core";
2
- import { Plugin } from "prosemirror-state";
3
- interface DropCursorOptions {
4
- color?: string | false;
5
- width?: number;
6
- class?: string;
7
- }
8
- export declare function multiColumnDropCursor(options: DropCursorOptions & {
9
- editor: BlockNoteEditor<any, any, any>;
10
- }): Plugin;
11
- export {};