@blocknote/core 0.11.2 → 0.12.1
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.
- package/README.md +13 -17
- package/dist/blocknote.js +1662 -1447
- package/dist/blocknote.js.map +1 -1
- package/dist/blocknote.umd.cjs +6 -6
- package/dist/blocknote.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/webpack-stats.json +1 -1
- package/package.json +7 -3
- package/src/api/blockManipulation/blockManipulation.test.ts +19 -15
- package/src/api/blockManipulation/blockManipulation.ts +107 -17
- package/src/api/exporters/html/externalHTMLExporter.ts +3 -7
- package/src/api/exporters/html/htmlConversion.test.ts +6 -3
- package/src/api/exporters/html/internalHTMLSerializer.ts +3 -7
- package/src/api/exporters/html/util/sharedHTMLConversion.ts +3 -3
- package/src/api/exporters/markdown/markdownExporter.test.ts +7 -3
- package/src/api/exporters/markdown/markdownExporter.ts +2 -6
- package/src/api/getCurrentBlockContentType.ts +14 -0
- package/src/api/nodeConversions/nodeConversions.test.ts +14 -7
- package/src/api/nodeConversions/nodeConversions.ts +1 -2
- package/src/api/parsers/html/parseHTML.test.ts +5 -1
- package/src/api/parsers/html/parseHTML.ts +2 -6
- package/src/api/parsers/html/util/nestedLists.ts +11 -1
- package/src/api/parsers/markdown/parseMarkdown.test.ts +3 -0
- package/src/api/parsers/markdown/parseMarkdown.ts +2 -6
- package/src/api/testUtil/cases/customBlocks.ts +18 -16
- package/src/api/testUtil/cases/customInlineContent.ts +12 -13
- package/src/api/testUtil/cases/customStyles.ts +12 -10
- package/src/api/testUtil/index.ts +4 -2
- package/src/api/testUtil/partialBlockTestUtil.ts +2 -6
- package/src/blocks/HeadingBlockContent/HeadingBlockContent.ts +50 -21
- package/src/blocks/ImageBlockContent/ImageBlockContent.ts +1 -2
- package/src/blocks/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.ts +8 -1
- package/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +18 -5
- package/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts +7 -1
- package/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +18 -5
- package/src/blocks/ParagraphBlockContent/ParagraphBlockContent.ts +14 -5
- package/src/blocks/defaultBlockHelpers.ts +3 -3
- package/src/blocks/defaultBlockTypeGuards.ts +84 -0
- package/src/blocks/defaultBlocks.ts +29 -3
- package/src/editor/Block.css +2 -31
- package/src/editor/BlockNoteEditor.ts +223 -267
- package/src/editor/BlockNoteExtensions.ts +5 -2
- package/src/editor/BlockNoteSchema.ts +98 -0
- package/src/editor/BlockNoteTipTapEditor.ts +162 -0
- package/src/editor/cursorPositionTypes.ts +2 -6
- package/src/editor/editor.css +0 -1
- package/src/editor/selectionTypes.ts +2 -6
- package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +22 -29
- package/src/extensions/{ImageToolbar → ImagePanel}/ImageToolbarPlugin.ts +54 -60
- package/src/extensions/LinkToolbar/LinkToolbarPlugin.ts +330 -0
- package/src/extensions/Placeholder/PlaceholderExtension.ts +81 -88
- package/src/extensions/SideMenu/SideMenuPlugin.ts +55 -56
- package/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts +8 -0
- package/src/extensions/SuggestionMenu/SuggestionPlugin.ts +353 -0
- package/src/extensions/{SlashMenu/defaultSlashMenuItems.ts → SuggestionMenu/getDefaultSlashMenuItems.ts} +119 -89
- package/src/extensions/TableHandles/TableHandlesPlugin.ts +62 -45
- package/src/extensions-shared/UiElementPosition.ts +4 -0
- package/src/index.ts +8 -8
- package/src/pm-nodes/BlockContainer.ts +5 -5
- package/src/schema/blocks/types.ts +15 -15
- package/src/schema/inlineContent/createSpec.ts +2 -2
- package/src/schema/inlineContent/types.ts +1 -1
- package/src/util/browser.ts +6 -4
- package/src/util/typescript.ts +7 -4
- package/types/src/api/blockManipulation/blockManipulation.d.ts +6 -1
- package/types/src/api/exporters/html/externalHTMLExporter.d.ts +2 -1
- package/types/src/api/exporters/html/internalHTMLSerializer.d.ts +2 -1
- package/types/src/api/exporters/markdown/markdownExporter.d.ts +2 -1
- package/types/src/api/getCurrentBlockContentType.d.ts +2 -0
- package/types/src/api/nodeConversions/nodeConversions.d.ts +2 -1
- package/types/src/api/parsers/html/parseHTML.d.ts +2 -1
- package/types/src/api/parsers/markdown/parseMarkdown.d.ts +2 -1
- package/types/src/api/testUtil/cases/customBlocks.d.ts +72 -13
- package/types/src/api/testUtil/cases/customInlineContent.d.ts +281 -6
- package/types/src/api/testUtil/cases/customStyles.d.ts +247 -13
- package/types/src/api/testUtil/index.d.ts +4 -2
- package/types/src/api/testUtil/partialBlockTestUtil.d.ts +2 -1
- package/types/src/blocks/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.d.ts +6 -1
- package/types/src/blocks/defaultBlockHelpers.d.ts +2 -2
- package/types/src/blocks/defaultBlockTypeGuards.d.ts +24 -0
- package/types/src/blocks/defaultBlocks.d.ts +21 -15
- package/types/src/editor/BlockNoteEditor.d.ts +51 -56
- package/types/src/editor/BlockNoteExtensions.d.ts +1 -0
- package/types/src/editor/BlockNoteSchema.d.ts +34 -0
- package/types/src/editor/BlockNoteTipTapEditor.d.ts +28 -0
- package/types/src/editor/cursorPositionTypes.d.ts +2 -1
- package/types/src/editor/selectionTypes.d.ts +2 -1
- package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +5 -6
- package/types/src/extensions/ImagePanel/ImageToolbarPlugin.d.ts +32 -0
- package/types/src/extensions/LinkToolbar/LinkToolbarPlugin.d.ts +40 -0
- package/types/src/extensions/Placeholder/PlaceholderExtension.d.ts +2 -15
- package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +8 -7
- package/types/src/extensions/SuggestionMenu/DefaultSuggestionItem.d.ts +8 -0
- package/types/src/extensions/SuggestionMenu/SuggestionPlugin.d.ts +31 -0
- package/types/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.d.ts +10 -0
- package/types/src/extensions/TableHandles/TableHandlesPlugin.d.ts +7 -7
- package/types/src/extensions-shared/UiElementPosition.d.ts +4 -0
- package/types/src/index.d.ts +8 -8
- package/types/src/pm-nodes/BlockContainer.d.ts +3 -2
- package/types/src/pm-nodes/BlockGroup.d.ts +1 -1
- package/types/src/schema/blocks/types.d.ts +15 -15
- package/types/src/schema/inlineContent/types.d.ts +1 -1
- package/types/src/util/browser.d.ts +1 -0
- package/types/src/util/typescript.d.ts +1 -0
- package/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +0 -335
- package/src/extensions/SlashMenu/BaseSlashMenuItem.ts +0 -12
- package/src/extensions/SlashMenu/SlashMenuPlugin.ts +0 -53
- package/src/extensions-shared/BaseUiElementTypes.ts +0 -8
- package/src/extensions-shared/README.md +0 -3
- package/src/extensions-shared/suggestion/SuggestionItem.ts +0 -3
- package/src/extensions-shared/suggestion/SuggestionPlugin.ts +0 -448
- package/types/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.d.ts +0 -38
- package/types/src/extensions/ImageToolbar/ImageToolbarPlugin.d.ts +0 -31
- package/types/src/extensions/SlashMenu/BaseSlashMenuItem.d.ts +0 -7
- package/types/src/extensions/SlashMenu/SlashMenuPlugin.d.ts +0 -13
- package/types/src/extensions/SlashMenu/defaultSlashMenuItems.d.ts +0 -3
- package/types/src/extensions-shared/BaseUiElementTypes.d.ts +0 -7
- package/types/src/extensions-shared/suggestion/SuggestionItem.d.ts +0 -3
- package/types/src/extensions-shared/suggestion/SuggestionPlugin.d.ts +0 -36
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-100.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-100.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-200.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-200.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-300.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-300.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-500.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-500.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-600.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-600.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-700.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-700.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-800.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-800.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-900.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-900.woff2 +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-regular.woff +0 -0
- /package/src/{assets → fonts}/inter-v12-latin/inter-v12-latin-regular.woff2 +0 -0
- /package/src/{assets/fonts-inter.css → fonts/inter.css} +0 -0
|
@@ -2,20 +2,17 @@ import { PluginView } from "@tiptap/pm/state";
|
|
|
2
2
|
import { Node } from "prosemirror-model";
|
|
3
3
|
import { NodeSelection, Plugin, PluginKey, Selection } from "prosemirror-state";
|
|
4
4
|
import { EditorView } from "prosemirror-view";
|
|
5
|
+
|
|
5
6
|
import { createExternalHTMLExporter } from "../../api/exporters/html/externalHTMLExporter";
|
|
6
7
|
import { createInternalHTMLSerializer } from "../../api/exporters/html/internalHTMLSerializer";
|
|
7
8
|
import { cleanHTMLToMarkdown } from "../../api/exporters/markdown/markdownExporter";
|
|
8
9
|
import { getBlockInfoFromPos } from "../../api/getBlockInfoFromPos";
|
|
10
|
+
import { Block } from "../../blocks/defaultBlocks";
|
|
9
11
|
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
Block,
|
|
13
|
-
BlockSchema,
|
|
14
|
-
InlineContentSchema,
|
|
15
|
-
StyleSchema,
|
|
16
|
-
} from "../../schema";
|
|
12
|
+
import { UiElementPosition } from "../../extensions-shared/UiElementPosition";
|
|
13
|
+
import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
|
|
17
14
|
import { EventEmitter } from "../../util/EventEmitter";
|
|
18
|
-
import {
|
|
15
|
+
import { suggestionMenuPluginKey } from "../SuggestionMenu/SuggestionPlugin";
|
|
19
16
|
import { MultipleNodeSelection } from "./MultipleNodeSelection";
|
|
20
17
|
|
|
21
18
|
let dragImageElement: Element | undefined;
|
|
@@ -24,7 +21,7 @@ export type SideMenuState<
|
|
|
24
21
|
BSchema extends BlockSchema,
|
|
25
22
|
I extends InlineContentSchema,
|
|
26
23
|
S extends StyleSchema
|
|
27
|
-
> =
|
|
24
|
+
> = UiElementPosition & {
|
|
28
25
|
// The block that the side menu is attached to.
|
|
29
26
|
block: Block<BSchema, I, S>;
|
|
30
27
|
};
|
|
@@ -255,7 +252,8 @@ export class SideMenuView<
|
|
|
255
252
|
S extends StyleSchema
|
|
256
253
|
> implements PluginView
|
|
257
254
|
{
|
|
258
|
-
private
|
|
255
|
+
private state?: SideMenuState<BSchema, I, S>;
|
|
256
|
+
private readonly emitUpdate: (state: SideMenuState<BSchema, I, S>) => void;
|
|
259
257
|
|
|
260
258
|
// When true, the drag handle with be anchored at the same level as root elements
|
|
261
259
|
// When false, the drag handle with be just to the left of the element
|
|
@@ -273,10 +271,16 @@ export class SideMenuView<
|
|
|
273
271
|
constructor(
|
|
274
272
|
private readonly editor: BlockNoteEditor<BSchema, I, S>,
|
|
275
273
|
private readonly pmView: EditorView,
|
|
276
|
-
|
|
277
|
-
sideMenuState: SideMenuState<BSchema, I, S>
|
|
278
|
-
) => void
|
|
274
|
+
emitUpdate: (state: SideMenuState<BSchema, I, S>) => void
|
|
279
275
|
) {
|
|
276
|
+
this.emitUpdate = () => {
|
|
277
|
+
if (!this.state) {
|
|
278
|
+
throw new Error("Attempting to update uninitialized side menu");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
emitUpdate(this.state);
|
|
282
|
+
};
|
|
283
|
+
|
|
280
284
|
this.horizontalPosAnchoredAtRoot = true;
|
|
281
285
|
this.horizontalPosAnchor = (
|
|
282
286
|
this.pmView.dom.firstChild! as HTMLElement
|
|
@@ -369,17 +373,17 @@ export class SideMenuView<
|
|
|
369
373
|
};
|
|
370
374
|
|
|
371
375
|
onKeyDown = (_event: KeyboardEvent) => {
|
|
372
|
-
if (this.
|
|
373
|
-
this.
|
|
374
|
-
this.
|
|
376
|
+
if (this.state?.show) {
|
|
377
|
+
this.state.show = false;
|
|
378
|
+
this.emitUpdate(this.state);
|
|
375
379
|
}
|
|
376
380
|
this.menuFrozen = false;
|
|
377
381
|
};
|
|
378
382
|
|
|
379
383
|
onMouseDown = (_event: MouseEvent) => {
|
|
380
|
-
if (this.
|
|
381
|
-
this.
|
|
382
|
-
this.
|
|
384
|
+
if (this.state && !this.state.show) {
|
|
385
|
+
this.state.show = true;
|
|
386
|
+
this.emitUpdate(this.state);
|
|
383
387
|
}
|
|
384
388
|
this.menuFrozen = false;
|
|
385
389
|
};
|
|
@@ -421,9 +425,9 @@ export class SideMenuView<
|
|
|
421
425
|
editorWrapper.contains(event.target as HTMLElement)
|
|
422
426
|
)
|
|
423
427
|
) {
|
|
424
|
-
if (this.
|
|
425
|
-
this.
|
|
426
|
-
this.
|
|
428
|
+
if (this.state?.show) {
|
|
429
|
+
this.state.show = false;
|
|
430
|
+
this.emitUpdate(this.state);
|
|
427
431
|
}
|
|
428
432
|
|
|
429
433
|
return;
|
|
@@ -440,9 +444,9 @@ export class SideMenuView<
|
|
|
440
444
|
|
|
441
445
|
// Closes the menu if the mouse cursor is beyond the editor vertically.
|
|
442
446
|
if (!block || !this.editor.isEditable) {
|
|
443
|
-
if (this.
|
|
444
|
-
this.
|
|
445
|
-
this.
|
|
447
|
+
if (this.state?.show) {
|
|
448
|
+
this.state.show = false;
|
|
449
|
+
this.emitUpdate(this.state);
|
|
446
450
|
}
|
|
447
451
|
|
|
448
452
|
return;
|
|
@@ -450,7 +454,7 @@ export class SideMenuView<
|
|
|
450
454
|
|
|
451
455
|
// Doesn't update if the menu is already open and the mouse cursor is still hovering the same block.
|
|
452
456
|
if (
|
|
453
|
-
this.
|
|
457
|
+
this.state?.show &&
|
|
454
458
|
this.hoveredBlock?.hasAttribute("data-id") &&
|
|
455
459
|
this.hoveredBlock?.getAttribute("data-id") === block.id
|
|
456
460
|
) {
|
|
@@ -470,7 +474,7 @@ export class SideMenuView<
|
|
|
470
474
|
if (this.editor.isEditable) {
|
|
471
475
|
const blockContentBoundingBox = blockContent.getBoundingClientRect();
|
|
472
476
|
|
|
473
|
-
this.
|
|
477
|
+
this.state = {
|
|
474
478
|
show: true,
|
|
475
479
|
referencePos: new DOMRect(
|
|
476
480
|
this.horizontalPosAnchoredAtRoot
|
|
@@ -485,16 +489,16 @@ export class SideMenuView<
|
|
|
485
489
|
)!,
|
|
486
490
|
};
|
|
487
491
|
|
|
488
|
-
this.
|
|
492
|
+
this.emitUpdate(this.state);
|
|
489
493
|
}
|
|
490
494
|
};
|
|
491
495
|
|
|
492
496
|
onScroll = () => {
|
|
493
|
-
if (this.
|
|
497
|
+
if (this.state?.show) {
|
|
494
498
|
const blockContent = this.hoveredBlock!.firstChild as HTMLElement;
|
|
495
499
|
const blockContentBoundingBox = blockContent.getBoundingClientRect();
|
|
496
500
|
|
|
497
|
-
this.
|
|
501
|
+
this.state.referencePos = new DOMRect(
|
|
498
502
|
this.horizontalPosAnchoredAtRoot
|
|
499
503
|
? this.horizontalPosAnchor
|
|
500
504
|
: blockContentBoundingBox.x,
|
|
@@ -502,16 +506,16 @@ export class SideMenuView<
|
|
|
502
506
|
blockContentBoundingBox.width,
|
|
503
507
|
blockContentBoundingBox.height
|
|
504
508
|
);
|
|
505
|
-
this.
|
|
509
|
+
this.emitUpdate(this.state);
|
|
506
510
|
}
|
|
507
511
|
};
|
|
508
512
|
|
|
509
513
|
destroy() {
|
|
510
|
-
if (this.
|
|
511
|
-
this.
|
|
512
|
-
this.
|
|
514
|
+
if (this.state?.show) {
|
|
515
|
+
this.state.show = false;
|
|
516
|
+
this.emitUpdate(this.state);
|
|
513
517
|
}
|
|
514
|
-
document.body.removeEventListener("mousemove", this.onMouseMove);
|
|
518
|
+
document.body.removeEventListener("mousemove", this.onMouseMove, true);
|
|
515
519
|
document.body.removeEventListener("dragover", this.onDragOver);
|
|
516
520
|
this.pmView.dom.removeEventListener("dragstart", this.onDragStart);
|
|
517
521
|
document.body.removeEventListener("drop", this.onDrop, true);
|
|
@@ -521,9 +525,9 @@ export class SideMenuView<
|
|
|
521
525
|
}
|
|
522
526
|
|
|
523
527
|
addBlock() {
|
|
524
|
-
if (this.
|
|
525
|
-
this.
|
|
526
|
-
this.
|
|
528
|
+
if (this.state?.show) {
|
|
529
|
+
this.state.show = false;
|
|
530
|
+
this.emitUpdate(this.state);
|
|
527
531
|
}
|
|
528
532
|
|
|
529
533
|
this.menuFrozen = true;
|
|
@@ -560,7 +564,7 @@ export class SideMenuView<
|
|
|
560
564
|
this.editor._tiptapEditor
|
|
561
565
|
.chain()
|
|
562
566
|
.BNCreateBlock(newBlockInsertionPos)
|
|
563
|
-
.BNUpdateBlock(newBlockContentPos, { type: "paragraph", props: {} })
|
|
567
|
+
// .BNUpdateBlock(newBlockContentPos, { type: "paragraph", props: {} })
|
|
564
568
|
.setTextSelection(newBlockContentPos)
|
|
565
569
|
.run();
|
|
566
570
|
} else {
|
|
@@ -570,10 +574,9 @@ export class SideMenuView<
|
|
|
570
574
|
// Focuses and activates the suggestion menu.
|
|
571
575
|
this.pmView.focus();
|
|
572
576
|
this.pmView.dispatch(
|
|
573
|
-
this.pmView.state.tr.scrollIntoView().setMeta(
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
type: "drag",
|
|
577
|
+
this.pmView.state.tr.scrollIntoView().setMeta(suggestionMenuPluginKey, {
|
|
578
|
+
triggerCharacter: "/",
|
|
579
|
+
fromUserInput: false,
|
|
577
580
|
})
|
|
578
581
|
);
|
|
579
582
|
}
|
|
@@ -586,7 +589,7 @@ export class SideMenuProsemirrorPlugin<
|
|
|
586
589
|
I extends InlineContentSchema,
|
|
587
590
|
S extends StyleSchema
|
|
588
591
|
> extends EventEmitter<any> {
|
|
589
|
-
|
|
592
|
+
public view: SideMenuView<BSchema, I, S> | undefined;
|
|
590
593
|
public readonly plugin: Plugin;
|
|
591
594
|
|
|
592
595
|
constructor(private readonly editor: BlockNoteEditor<BSchema, I, S>) {
|
|
@@ -594,14 +597,10 @@ export class SideMenuProsemirrorPlugin<
|
|
|
594
597
|
this.plugin = new Plugin({
|
|
595
598
|
key: sideMenuPluginKey,
|
|
596
599
|
view: (editorView) => {
|
|
597
|
-
this.
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
this.emit("update", sideMenuState);
|
|
602
|
-
}
|
|
603
|
-
);
|
|
604
|
-
return this.sideMenuView;
|
|
600
|
+
this.view = new SideMenuView(editor, editorView, (state) => {
|
|
601
|
+
this.emit("update", state);
|
|
602
|
+
});
|
|
603
|
+
return this.view;
|
|
605
604
|
},
|
|
606
605
|
});
|
|
607
606
|
}
|
|
@@ -614,7 +613,7 @@ export class SideMenuProsemirrorPlugin<
|
|
|
614
613
|
* If the block is empty, opens the slash menu. If the block has content,
|
|
615
614
|
* creates a new block below and opens the slash menu in it.
|
|
616
615
|
*/
|
|
617
|
-
addBlock = () => this.
|
|
616
|
+
addBlock = () => this.view!.addBlock();
|
|
618
617
|
|
|
619
618
|
/**
|
|
620
619
|
* Handles drag & drop events for blocks.
|
|
@@ -623,7 +622,7 @@ export class SideMenuProsemirrorPlugin<
|
|
|
623
622
|
dataTransfer: DataTransfer | null;
|
|
624
623
|
clientY: number;
|
|
625
624
|
}) => {
|
|
626
|
-
this.
|
|
625
|
+
this.view!.isDragging = true;
|
|
627
626
|
dragStart(event, this.editor);
|
|
628
627
|
};
|
|
629
628
|
|
|
@@ -636,11 +635,11 @@ export class SideMenuProsemirrorPlugin<
|
|
|
636
635
|
* attached to the same block regardless of which block is hovered by the
|
|
637
636
|
* mouse cursor.
|
|
638
637
|
*/
|
|
639
|
-
freezeMenu = () => (this.
|
|
638
|
+
freezeMenu = () => (this.view!.menuFrozen = true);
|
|
640
639
|
/**
|
|
641
640
|
* Unfreezes the side menu. When frozen, the side menu will stay
|
|
642
641
|
* attached to the same block regardless of which block is hovered by the
|
|
643
642
|
* mouse cursor.
|
|
644
643
|
*/
|
|
645
|
-
unfreezeMenu = () => (this.
|
|
644
|
+
unfreezeMenu = () => (this.view!.menuFrozen = false);
|
|
646
645
|
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { findParentNode } from "@tiptap/core";
|
|
2
|
+
import { EditorState, Plugin, PluginKey } from "prosemirror-state";
|
|
3
|
+
import { Decoration, DecorationSet, EditorView } from "prosemirror-view";
|
|
4
|
+
|
|
5
|
+
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
|
|
6
|
+
import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
|
|
7
|
+
import { UiElementPosition } from "../../extensions-shared/UiElementPosition";
|
|
8
|
+
import { EventEmitter } from "../../util/EventEmitter";
|
|
9
|
+
|
|
10
|
+
const findBlock = findParentNode((node) => node.type.name === "blockContainer");
|
|
11
|
+
|
|
12
|
+
export type SuggestionMenuState = UiElementPosition & {
|
|
13
|
+
query: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
class SuggestionMenuView<
|
|
17
|
+
BSchema extends BlockSchema,
|
|
18
|
+
I extends InlineContentSchema,
|
|
19
|
+
S extends StyleSchema
|
|
20
|
+
> {
|
|
21
|
+
private state?: SuggestionMenuState;
|
|
22
|
+
public emitUpdate: (triggerCharacter: string) => void;
|
|
23
|
+
|
|
24
|
+
pluginState: SuggestionPluginState;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
private readonly editor: BlockNoteEditor<BSchema, I, S>,
|
|
28
|
+
emitUpdate: (menuName: string, state: SuggestionMenuState) => void
|
|
29
|
+
) {
|
|
30
|
+
this.pluginState = undefined;
|
|
31
|
+
|
|
32
|
+
this.emitUpdate = (menuName: string) => {
|
|
33
|
+
if (!this.state) {
|
|
34
|
+
throw new Error("Attempting to update uninitialized suggestions menu");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
emitUpdate(menuName, this.state);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
document.addEventListener("scroll", this.handleScroll);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
handleScroll = () => {
|
|
44
|
+
if (this.state?.show) {
|
|
45
|
+
const decorationNode = document.querySelector(
|
|
46
|
+
`[data-decoration-id="${this.pluginState!.decorationId}"]`
|
|
47
|
+
);
|
|
48
|
+
this.state.referencePos = decorationNode!.getBoundingClientRect();
|
|
49
|
+
this.emitUpdate(this.pluginState!.triggerCharacter!);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
update(view: EditorView, prevState: EditorState) {
|
|
54
|
+
const prev: SuggestionPluginState =
|
|
55
|
+
suggestionMenuPluginKey.getState(prevState);
|
|
56
|
+
const next: SuggestionPluginState = suggestionMenuPluginKey.getState(
|
|
57
|
+
view.state
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// See how the state changed
|
|
61
|
+
const started = prev === undefined && next !== undefined;
|
|
62
|
+
const stopped = prev !== undefined && next === undefined;
|
|
63
|
+
const changed = prev !== undefined && next !== undefined;
|
|
64
|
+
|
|
65
|
+
// Cancel when suggestion isn't active
|
|
66
|
+
if (!started && !changed && !stopped) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.pluginState = stopped ? prev : next;
|
|
71
|
+
|
|
72
|
+
if (stopped || !this.editor.isEditable) {
|
|
73
|
+
this.state!.show = false;
|
|
74
|
+
this.emitUpdate(this.pluginState!.triggerCharacter);
|
|
75
|
+
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const decorationNode = document.querySelector(
|
|
80
|
+
`[data-decoration-id="${this.pluginState!.decorationId}"]`
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (this.editor.isEditable) {
|
|
84
|
+
this.state = {
|
|
85
|
+
show: true,
|
|
86
|
+
referencePos: decorationNode!.getBoundingClientRect(),
|
|
87
|
+
query: this.pluginState!.query,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
this.emitUpdate(this.pluginState!.triggerCharacter!);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
destroy() {
|
|
95
|
+
document.removeEventListener("scroll", this.handleScroll);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
closeMenu = () => {
|
|
99
|
+
this.editor._tiptapEditor.view.dispatch(
|
|
100
|
+
this.editor._tiptapEditor.view.state.tr.setMeta(
|
|
101
|
+
suggestionMenuPluginKey,
|
|
102
|
+
null
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
clearQuery = () => {
|
|
108
|
+
if (this.pluginState === undefined) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.editor._tiptapEditor
|
|
113
|
+
.chain()
|
|
114
|
+
.focus()
|
|
115
|
+
.deleteRange({
|
|
116
|
+
from:
|
|
117
|
+
this.pluginState.queryStartPos! -
|
|
118
|
+
(this.pluginState.fromUserInput
|
|
119
|
+
? this.pluginState.triggerCharacter!.length
|
|
120
|
+
: 0),
|
|
121
|
+
to: this.editor._tiptapEditor.state.selection.from,
|
|
122
|
+
})
|
|
123
|
+
.run();
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
type SuggestionPluginState =
|
|
128
|
+
| {
|
|
129
|
+
triggerCharacter: string;
|
|
130
|
+
fromUserInput: boolean;
|
|
131
|
+
queryStartPos: number;
|
|
132
|
+
query: string;
|
|
133
|
+
decorationId: string;
|
|
134
|
+
}
|
|
135
|
+
| undefined;
|
|
136
|
+
|
|
137
|
+
export const suggestionMenuPluginKey = new PluginKey("SuggestionMenuPlugin");
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A ProseMirror plugin for suggestions, designed to make '/'-commands possible as well as mentions.
|
|
141
|
+
*
|
|
142
|
+
* This is basically a simplified version of TipTap's [Suggestions](https://github.com/ueberdosis/tiptap/tree/db92a9b313c5993b723c85cd30256f1d4a0b65e1/packages/suggestion) plugin.
|
|
143
|
+
*
|
|
144
|
+
* This version is adapted from the aforementioned version in the following ways:
|
|
145
|
+
* - This version supports generic items instead of only strings (to allow for more advanced filtering for example)
|
|
146
|
+
* - This version hides some unnecessary complexity from the user of the plugin.
|
|
147
|
+
* - This version handles key events differently
|
|
148
|
+
*/
|
|
149
|
+
export class SuggestionMenuProseMirrorPlugin<
|
|
150
|
+
BSchema extends BlockSchema,
|
|
151
|
+
I extends InlineContentSchema,
|
|
152
|
+
S extends StyleSchema
|
|
153
|
+
> extends EventEmitter<any> {
|
|
154
|
+
private view: SuggestionMenuView<BSchema, I, S> | undefined;
|
|
155
|
+
public readonly plugin: Plugin;
|
|
156
|
+
|
|
157
|
+
private triggerCharacters: string[] = [];
|
|
158
|
+
|
|
159
|
+
constructor(editor: BlockNoteEditor<BSchema, I, S>) {
|
|
160
|
+
super();
|
|
161
|
+
const triggerCharacters = this.triggerCharacters;
|
|
162
|
+
this.plugin = new Plugin({
|
|
163
|
+
key: suggestionMenuPluginKey,
|
|
164
|
+
|
|
165
|
+
view: () => {
|
|
166
|
+
this.view = new SuggestionMenuView<BSchema, I, S>(
|
|
167
|
+
editor,
|
|
168
|
+
(triggerCharacter, state) => {
|
|
169
|
+
this.emit(`update ${triggerCharacter}`, state);
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
return this.view;
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
state: {
|
|
176
|
+
// Initialize the plugin's internal state.
|
|
177
|
+
init(): SuggestionPluginState {
|
|
178
|
+
return undefined;
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
// Apply changes to the plugin state from an editor transaction.
|
|
182
|
+
apply(transaction, prev, _oldState, newState): SuggestionPluginState {
|
|
183
|
+
// TODO: More clearly define which transactions should be ignored.
|
|
184
|
+
if (transaction.getMeta("orderedListIndexing") !== undefined) {
|
|
185
|
+
return prev;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Either contains the trigger character if the menu should be shown,
|
|
189
|
+
// or null if it should be hidden.
|
|
190
|
+
const suggestionPluginTransactionMeta: {
|
|
191
|
+
triggerCharacter: string;
|
|
192
|
+
fromUserInput?: boolean;
|
|
193
|
+
} | null = transaction.getMeta(suggestionMenuPluginKey);
|
|
194
|
+
|
|
195
|
+
// Only opens a menu of no menu is already open
|
|
196
|
+
if (
|
|
197
|
+
typeof suggestionPluginTransactionMeta === "object" &&
|
|
198
|
+
suggestionPluginTransactionMeta !== null &&
|
|
199
|
+
prev === undefined
|
|
200
|
+
) {
|
|
201
|
+
return {
|
|
202
|
+
triggerCharacter:
|
|
203
|
+
suggestionPluginTransactionMeta.triggerCharacter,
|
|
204
|
+
fromUserInput:
|
|
205
|
+
suggestionPluginTransactionMeta.fromUserInput !== false,
|
|
206
|
+
queryStartPos: newState.selection.from,
|
|
207
|
+
query: "",
|
|
208
|
+
decorationId: `id_${Math.floor(Math.random() * 0xffffffff)}`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Checks if the menu is hidden, in which case it doesn't need to be hidden or updated.
|
|
213
|
+
if (prev === undefined) {
|
|
214
|
+
return prev;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Checks if the menu should be hidden.
|
|
218
|
+
if (
|
|
219
|
+
// Highlighting text should hide the menu.
|
|
220
|
+
newState.selection.from !== newState.selection.to ||
|
|
221
|
+
// Transactions with plugin metadata should hide the menu.
|
|
222
|
+
suggestionPluginTransactionMeta === null ||
|
|
223
|
+
// Certain mouse events should hide the menu.
|
|
224
|
+
// TODO: Change to global mousedown listener.
|
|
225
|
+
transaction.getMeta("focus") ||
|
|
226
|
+
transaction.getMeta("blur") ||
|
|
227
|
+
transaction.getMeta("pointer") ||
|
|
228
|
+
// Moving the caret before the character which triggered the menu should hide it.
|
|
229
|
+
(prev.triggerCharacter !== undefined &&
|
|
230
|
+
newState.selection.from < prev.queryStartPos!)
|
|
231
|
+
) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const next = { ...prev };
|
|
236
|
+
|
|
237
|
+
// Updates the current query.
|
|
238
|
+
next.query = newState.doc.textBetween(
|
|
239
|
+
prev.queryStartPos!,
|
|
240
|
+
newState.selection.from
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
return next;
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
props: {
|
|
248
|
+
handleKeyDown(view, event) {
|
|
249
|
+
const suggestionPluginState: SuggestionPluginState = (
|
|
250
|
+
this as Plugin
|
|
251
|
+
).getState(view.state);
|
|
252
|
+
|
|
253
|
+
if (
|
|
254
|
+
triggerCharacters.includes(event.key) &&
|
|
255
|
+
suggestionPluginState === undefined
|
|
256
|
+
) {
|
|
257
|
+
event.preventDefault();
|
|
258
|
+
|
|
259
|
+
view.dispatch(
|
|
260
|
+
view.state.tr
|
|
261
|
+
.insertText(event.key)
|
|
262
|
+
.scrollIntoView()
|
|
263
|
+
.setMeta(suggestionMenuPluginKey, {
|
|
264
|
+
triggerCharacter: event.key,
|
|
265
|
+
})
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return false;
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
// Setup decorator on the currently active suggestion.
|
|
275
|
+
decorations(state) {
|
|
276
|
+
const suggestionPluginState: SuggestionPluginState = (
|
|
277
|
+
this as Plugin
|
|
278
|
+
).getState(state);
|
|
279
|
+
|
|
280
|
+
if (suggestionPluginState === undefined) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// If the menu was opened programmatically by another extension, it may not use a trigger character. In this
|
|
285
|
+
// case, the decoration is set on the whole block instead, as the decoration range would otherwise be empty.
|
|
286
|
+
if (!suggestionPluginState.fromUserInput) {
|
|
287
|
+
const blockNode = findBlock(state.selection);
|
|
288
|
+
if (blockNode) {
|
|
289
|
+
return DecorationSet.create(state.doc, [
|
|
290
|
+
Decoration.node(
|
|
291
|
+
blockNode.pos,
|
|
292
|
+
blockNode.pos + blockNode.node.nodeSize,
|
|
293
|
+
{
|
|
294
|
+
nodeName: "span",
|
|
295
|
+
class: "bn-suggestion-decorator",
|
|
296
|
+
"data-decoration-id": suggestionPluginState.decorationId,
|
|
297
|
+
}
|
|
298
|
+
),
|
|
299
|
+
]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Creates an inline decoration around the trigger character.
|
|
303
|
+
return DecorationSet.create(state.doc, [
|
|
304
|
+
Decoration.inline(
|
|
305
|
+
suggestionPluginState.queryStartPos! -
|
|
306
|
+
suggestionPluginState.triggerCharacter!.length,
|
|
307
|
+
suggestionPluginState.queryStartPos!,
|
|
308
|
+
{
|
|
309
|
+
nodeName: "span",
|
|
310
|
+
class: "bn-suggestion-decorator",
|
|
311
|
+
"data-decoration-id": suggestionPluginState.decorationId,
|
|
312
|
+
}
|
|
313
|
+
),
|
|
314
|
+
]);
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
public onUpdate(
|
|
321
|
+
triggerCharacter: string,
|
|
322
|
+
callback: (state: SuggestionMenuState) => void
|
|
323
|
+
) {
|
|
324
|
+
if (!this.triggerCharacters.includes(triggerCharacter)) {
|
|
325
|
+
this.addTriggerCharacter(triggerCharacter);
|
|
326
|
+
}
|
|
327
|
+
// TODO: be able to remove the triggerCharacter
|
|
328
|
+
return this.on(`update ${triggerCharacter}`, callback);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
addTriggerCharacter = (triggerCharacter: string) => {
|
|
332
|
+
this.triggerCharacters.push(triggerCharacter);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// TODO: Should this be called automatically when listeners are removed?
|
|
336
|
+
removeTriggerCharacter = (triggerCharacter: string) => {
|
|
337
|
+
this.triggerCharacters = this.triggerCharacters.filter(
|
|
338
|
+
(c) => c !== triggerCharacter
|
|
339
|
+
);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
closeMenu = () => this.view!.closeMenu();
|
|
343
|
+
|
|
344
|
+
clearQuery = () => this.view!.clearQuery();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function createSuggestionMenu<
|
|
348
|
+
BSchema extends BlockSchema,
|
|
349
|
+
I extends InlineContentSchema,
|
|
350
|
+
S extends StyleSchema
|
|
351
|
+
>(editor: BlockNoteEditor<BSchema, I, S>, triggerCharacter: string) {
|
|
352
|
+
editor.suggestionMenus.addTriggerCharacter(triggerCharacter);
|
|
353
|
+
}
|