@blocknote/core 0.1.0-alpha.3 → 0.1.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.
Files changed (109) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +4 -2
  3. package/dist/blocknote.js +3461 -2429
  4. package/dist/blocknote.js.map +1 -1
  5. package/dist/blocknote.umd.cjs +35 -71
  6. package/dist/blocknote.umd.cjs.map +1 -1
  7. package/dist/style.css +1 -1
  8. package/package.json +9 -7
  9. package/src/BlockNoteExtensions.ts +10 -17
  10. package/src/BlockNoteTheme.ts +150 -0
  11. package/src/EditorContent.tsx +2 -1
  12. package/src/extensions/Blocks/BlockAttributes.ts +12 -0
  13. package/src/extensions/Blocks/MultipleNodeSelection.ts +87 -0
  14. package/src/extensions/Blocks/OrderedListPlugin.ts +2 -2
  15. package/src/extensions/Blocks/PreviousBlockTypePlugin.ts +8 -2
  16. package/src/extensions/Blocks/helpers/findBlock.ts +1 -1
  17. package/src/extensions/Blocks/nodes/Block.module.css +37 -37
  18. package/src/extensions/Blocks/nodes/Block.ts +89 -45
  19. package/src/extensions/Blocks/nodes/BlockGroup.ts +19 -2
  20. package/src/extensions/Blocks/nodes/Content.ts +15 -2
  21. package/src/extensions/BubbleMenu/BubbleMenuExtension.tsx +10 -2
  22. package/src/extensions/BubbleMenu/component/BubbleMenu.tsx +122 -98
  23. package/src/extensions/BubbleMenu/component/LinkToolbarButton.tsx +8 -8
  24. package/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx +143 -33
  25. package/src/extensions/DraggableBlocks/components/DragHandle.tsx +15 -21
  26. package/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx +8 -7
  27. package/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +31 -66
  28. package/src/extensions/Hyperlinks/menus/EditHyperlinkMenu.tsx +44 -0
  29. package/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItem.tsx +34 -0
  30. package/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemIcon.tsx +31 -0
  31. package/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemInput.tsx +40 -0
  32. package/src/extensions/Hyperlinks/menus/HoverHyperlinkMenu.tsx +37 -0
  33. package/src/extensions/Hyperlinks/menus/HyperlinkMenu.tsx +63 -0
  34. package/src/extensions/SlashMenu/SlashMenuItem.ts +3 -1
  35. package/src/extensions/SlashMenu/defaultCommands.tsx +4 -4
  36. package/src/extensions/TrailingNode/TrailingNodeExtension.ts +8 -5
  37. package/src/shared/components/toolbar/Toolbar.tsx +8 -3
  38. package/src/shared/components/toolbar/ToolbarButton.tsx +57 -0
  39. package/src/shared/components/toolbar/ToolbarDropdown.tsx +35 -0
  40. package/src/shared/components/toolbar/ToolbarDropdownItem.tsx +35 -0
  41. package/src/shared/components/toolbar/ToolbarDropdownTarget.tsx +31 -0
  42. package/src/shared/plugins/suggestion/SuggestionItem.ts +3 -1
  43. package/src/shared/plugins/suggestion/{SuggestionListReactRenderer.ts → SuggestionListReactRenderer.tsx} +13 -4
  44. package/src/shared/plugins/suggestion/components/SuggestionGroup.tsx +6 -93
  45. package/src/shared/plugins/suggestion/components/SuggestionGroupItem.tsx +82 -0
  46. package/src/shared/plugins/suggestion/components/SuggestionList.tsx +24 -23
  47. package/src/useEditor.ts +4 -0
  48. package/src/utils.ts +12 -0
  49. package/types/src/BlockNoteExtensions.d.ts +3 -0
  50. package/types/src/BlockNoteTheme.d.ts +2 -0
  51. package/types/src/commands/indentation.d.ts +2 -0
  52. package/types/src/extensions/Blocks/BlockAttributes.d.ts +2 -0
  53. package/types/src/extensions/Blocks/MultipleNodeSelection.d.ts +24 -0
  54. package/types/src/extensions/Blocks/nodes/Block.d.ts +1 -1
  55. package/types/src/extensions/BubbleMenu/component/LinkToolbarButton.d.ts +2 -2
  56. package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenu.d.ts +11 -0
  57. package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItem.d.ts +13 -0
  58. package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemIcon.d.ts +8 -0
  59. package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemInput.d.ts +9 -0
  60. package/types/src/extensions/Hyperlinks/menus/HoverHyperlinkMenu.d.ts +12 -0
  61. package/types/src/extensions/Hyperlinks/menus/HyperlinkMenu.d.ts +21 -0
  62. package/types/src/extensions/Hyperlinks/menus/helpers/PanelTextInput.d.ts +39 -0
  63. package/types/src/extensions/Hyperlinks/menus/helpers/PanelTextInputStyles.d.ts +3 -0
  64. package/types/src/extensions/Hyperlinks/menus/helpers/ToolbarComponent.d.ts +13 -0
  65. package/types/src/extensions/SlashMenu/SlashMenuItem.d.ts +4 -7
  66. package/types/src/extensions/TrailingNode/TrailingNodeExtension.d.ts +3 -0
  67. package/types/src/nodes/ChildgroupNode.d.ts +28 -0
  68. package/types/src/nodes/patchNodes.d.ts +1 -0
  69. package/types/src/plugins/TreeViewPlugin/index.d.ts +2 -0
  70. package/types/src/plugins/animation.d.ts +2 -0
  71. package/types/src/react/BlockNoteComposer.d.ts +17 -0
  72. package/types/src/react/BlockNotePlugin.d.ts +1 -0
  73. package/types/src/react/index.d.ts +3 -0
  74. package/types/src/react/useBlockNoteSetup.d.ts +2 -0
  75. package/types/src/registerBlockNote.d.ts +2 -0
  76. package/types/src/shared/components/toolbar/SimpleToolbarButton.d.ts +2 -3
  77. package/types/src/shared/components/toolbar/SimpleToolbarDropdown.d.ts +11 -0
  78. package/types/src/shared/components/toolbar/SimpleToolbarDropdownItem.d.ts +11 -0
  79. package/types/src/shared/components/toolbar/Toolbar.d.ts +2 -2
  80. package/types/src/shared/components/toolbar/ToolbarButton.d.ts +15 -0
  81. package/types/src/shared/components/toolbar/ToolbarDropdown.d.ts +17 -0
  82. package/types/src/shared/components/toolbar/ToolbarDropdownItem.d.ts +11 -0
  83. package/types/src/shared/components/toolbar/ToolbarDropdownTarget.d.ts +8 -0
  84. package/types/src/shared/plugins/suggestion/SuggestionItem.d.ts +2 -4
  85. package/types/src/shared/plugins/suggestion/components/SuggestionGroupItem.d.ts +9 -0
  86. package/types/src/shared/plugins/suggestion/components/SuggestionList.d.ts +0 -15
  87. package/types/src/themes/BlockNoteEditorTheme.d.ts +11 -0
  88. package/types/src/useEditor.d.ts +3 -0
  89. package/types/src/utils.d.ts +2 -0
  90. package/src/extensions/Blocks/nodes/README.md +0 -26
  91. package/src/extensions/BubbleMenu/component/DropdownBlockItem.module.css +0 -13
  92. package/src/extensions/BubbleMenu/component/DropdownBlockItem.tsx +0 -25
  93. package/src/extensions/DraggableBlocks/components/DragHandle.module.css +0 -33
  94. package/src/extensions/DraggableBlocks/components/DragHandleMenu.module.css +0 -10
  95. package/src/extensions/Hyperlinks/menus/HyperlinkBasicMenu.tsx +0 -59
  96. package/src/extensions/Hyperlinks/menus/HyperlinkEditMenu.tsx +0 -72
  97. package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInput.tsx +0 -173
  98. package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInputStyles.ts +0 -36
  99. package/src/extensions/Hyperlinks/menus/atlaskit/README.md +0 -1
  100. package/src/extensions/Hyperlinks/menus/atlaskit/ToolbarComponent.tsx +0 -61
  101. package/src/extensions/helpers/formatKeyboardShortcut.ts +0 -9
  102. package/src/lib/atlaskit/browser.ts +0 -47
  103. package/src/shared/components/toolbar/SimpleToolbarButton.module.css +0 -13
  104. package/src/shared/components/toolbar/SimpleToolbarButton.tsx +0 -56
  105. package/src/shared/components/toolbar/Toolbar.module.css +0 -10
  106. package/src/shared/components/toolbar/ToolbarSeparator.module.css +0 -13
  107. package/src/shared/components/toolbar/ToolbarSeparator.tsx +0 -7
  108. package/src/shared/plugins/suggestion/components/SuggestionGroup.module.css +0 -45
  109. package/src/shared/plugins/suggestion/components/SuggestionList.module.css +0 -10
@@ -2,12 +2,12 @@ import Tippy from "@tippyjs/react";
2
2
  import { Editor } from "@tiptap/core";
3
3
  import { useCallback, useState } from "react";
4
4
  import {
5
- SimpleToolbarButton,
6
- SimpleToolbarButtonProps,
7
- } from "../../../shared/components/toolbar/SimpleToolbarButton";
8
- import { HyperlinkEditMenu } from "../../Hyperlinks/menus/HyperlinkEditMenu";
5
+ ToolbarButton,
6
+ ToolbarButtonProps,
7
+ } from "../../../shared/components/toolbar/ToolbarButton";
8
+ import { EditHyperlinkMenu } from "../../Hyperlinks/menus/EditHyperlinkMenu";
9
9
 
10
- type Props = SimpleToolbarButtonProps & {
10
+ type Props = ToolbarButtonProps & {
11
11
  editor: Editor;
12
12
  };
13
13
 
@@ -41,11 +41,11 @@ export const LinkToolbarButton = (props: Props) => {
41
41
  : "";
42
42
 
43
43
  setCreationMenu(
44
- <HyperlinkEditMenu
44
+ <EditHyperlinkMenu
45
45
  key={Math.random() + ""} // Math.random to prevent old element from being re-used
46
46
  url={activeUrl}
47
47
  text={selectedText}
48
- onSubmit={onSubmit}
48
+ update={onSubmit}
49
49
  />
50
50
  );
51
51
  }, [props.editor]);
@@ -59,7 +59,7 @@ export const LinkToolbarButton = (props: Props) => {
59
59
  }}
60
60
  interactive={true}
61
61
  maxWidth={500}>
62
- <SimpleToolbarButton {...props} />
62
+ <ToolbarButton {...props} />
63
63
  </Tippy>
64
64
  );
65
65
  };
@@ -1,13 +1,19 @@
1
- import { NodeSelection, Plugin, PluginKey } from "prosemirror-state";
1
+ import { NodeSelection, Plugin, PluginKey, Selection } from "prosemirror-state";
2
+ import { Node } from "prosemirror-model";
2
3
  import * as pv from "prosemirror-view";
3
4
  import { EditorView } from "prosemirror-view";
4
5
  import ReactDOM from "react-dom";
5
6
  import { DragHandle } from "./components/DragHandle";
7
+ import { MantineProvider } from "@mantine/core";
8
+ import { BlockNoteTheme } from "../../BlockNoteTheme";
9
+ import { MultipleNodeSelection } from "../Blocks/MultipleNodeSelection";
6
10
 
7
11
  const serializeForClipboard = (pv as any).__serializeForClipboard;
8
12
  // code based on https://github.com/ueberdosis/tiptap/issues/323#issuecomment-506637799
9
13
 
10
14
  let horizontalAnchor: number;
15
+ let dragImageElement: Element | undefined;
16
+
11
17
  function getHorizontalAnchor() {
12
18
  if (!horizontalAnchor) {
13
19
  const firstBlockGroup = document.querySelector(
@@ -38,24 +44,6 @@ export function absoluteRect(element: HTMLElement) {
38
44
  return createRect(element.getBoundingClientRect());
39
45
  }
40
46
 
41
- function blockPosAtCoords(
42
- coords: { left: number; top: number },
43
- view: EditorView
44
- ) {
45
- let block = getDraggableBlockFromCoords(coords, view);
46
-
47
- if (block && block.node.nodeType === 1) {
48
- // TODO: this uses undocumented PM APIs? do we need this / let's add docs?
49
- const docView = (view as any).docView;
50
- let desc = docView.nearestDesc(block.node, true);
51
- if (!desc || desc === docView) {
52
- return null;
53
- }
54
- return desc.posBefore;
55
- }
56
- return null;
57
- }
58
-
59
47
  function getDraggableBlockFromCoords(
60
48
  coords: { left: number; top: number },
61
49
  view: EditorView
@@ -85,6 +73,107 @@ function getDraggableBlockFromCoords(
85
73
  return { node, id: node.getAttribute("data-id")! };
86
74
  }
87
75
 
76
+ function blockPositionFromCoords(
77
+ coords: { left: number; top: number },
78
+ view: EditorView
79
+ ) {
80
+ let block = getDraggableBlockFromCoords(coords, view);
81
+
82
+ if (block && block.node.nodeType === 1) {
83
+ // TODO: this uses undocumented PM APIs? do we need this / let's add docs?
84
+ const docView = (view as any).docView;
85
+ let desc = docView.nearestDesc(block.node, true);
86
+ if (!desc || desc === docView) {
87
+ return null;
88
+ }
89
+ return desc.posBefore;
90
+ }
91
+ return null;
92
+ }
93
+
94
+ function blockPositionsFromSelection(
95
+ selection: Selection,
96
+ doc: Node
97
+ ) {
98
+ // Absolute positions just before the first block spanned by the selection, and just after the last block. Having the
99
+ // selection start and end just before and just after the target blocks ensures no whitespace/line breaks are left
100
+ // behind after dragging & dropping them.
101
+ let beforeFirstBlockPos: number;
102
+ let afterLastBlockPos: number;
103
+
104
+ // Even the user starts dragging blocks but drops them in the same place, the selection will still be moved just
105
+ // before & just after the blocks spanned by the selection, and therefore doesn't need to change if they try to drag
106
+ // the same blocks again. If this happens, the anchor & head move out of the block content node they were originally
107
+ // in. If the anchor should update but the head shouldn't and vice versa, it means the user selection is outside a
108
+ // block content node, which should never happen.
109
+ const selectionStartInBlockContent =
110
+ doc.resolve(selection.from).node().type.name === "content";
111
+ const selectionEndInBlockContent =
112
+ doc.resolve(selection.to).node().type.name === "content";
113
+
114
+ // Ensures that entire outermost nodes are selected if the selection spans multiple nesting levels.
115
+ const minDepth = Math.min(selection.$anchor.depth, selection.$head.depth);
116
+
117
+ if (selectionStartInBlockContent && selectionEndInBlockContent) {
118
+ // Absolute positions at the start of the first block in the selection and at the end of the last block. User
119
+ // selections will always start and end in block content nodes, but we want the start and end positions of their
120
+ // parent block nodes, which is why minDepth - 1 is used.
121
+ const startFirstBlockPos = selection.$from.start(minDepth - 1);
122
+ const endLastBlockPos = selection.$to.end(minDepth - 1);
123
+
124
+ // Shifting start and end positions by one moves them just outside the first and last selected blocks.
125
+ beforeFirstBlockPos = doc.resolve(startFirstBlockPos - 1).pos;
126
+ afterLastBlockPos = doc.resolve(endLastBlockPos + 1).pos;
127
+ } else {
128
+ beforeFirstBlockPos = selection.from;
129
+ afterLastBlockPos = selection.to;
130
+ }
131
+
132
+ return { from: beforeFirstBlockPos, to: afterLastBlockPos };
133
+ }
134
+
135
+ function setDragImage(view: EditorView, from: number, to = from) {
136
+ if (from === to) {
137
+ // Moves to position to be just after the first (and only) selected block.
138
+ to += view.state.doc.resolve(from + 1).node().nodeSize;
139
+ }
140
+
141
+ // Parent element is cloned to remove all unselected children without affecting the editor content.
142
+ const parentClone = view.domAtPos(from).node.cloneNode(true) as Element;
143
+ const parent = view.domAtPos(from).node as Element;
144
+
145
+ const getElementIndex = (parentElement: Element, targetElement: Element) =>
146
+ Array.prototype.indexOf.call(parentElement.children, targetElement);
147
+
148
+ const firstSelectedBlockIndex = getElementIndex(
149
+ parent,
150
+ // Expects from position to be just before the first selected block.
151
+ view.domAtPos(from + 1).node.parentElement!
152
+ );
153
+ const lastSelectedBlockIndex = getElementIndex(
154
+ parent,
155
+ // Expects to position to be just after the last selected block.
156
+ view.domAtPos(to - 1).node.parentElement!
157
+ );
158
+
159
+ for (let i = parent.childElementCount - 1; i >= 0; i--) {
160
+ if (i > lastSelectedBlockIndex || i < firstSelectedBlockIndex) {
161
+ parentClone.removeChild(parentClone.children[i]);
162
+ }
163
+ }
164
+
165
+ // dataTransfer.setDragImage(element) only works if element is attached to the DOM.
166
+ dragImageElement = parentClone;
167
+ document.body.appendChild(dragImageElement);
168
+ }
169
+
170
+ function unsetDragImage() {
171
+ if (dragImageElement !== undefined) {
172
+ document.body.removeChild(dragImageElement);
173
+ dragImageElement = undefined;
174
+ }
175
+ }
176
+
88
177
  function dragStart(e: DragEvent, view: EditorView) {
89
178
  if (!e.dataTransfer) {
90
179
  return;
@@ -94,11 +183,30 @@ function dragStart(e: DragEvent, view: EditorView) {
94
183
  left: view.dom.clientWidth / 2, // take middle of editor
95
184
  top: e.clientY,
96
185
  };
97
- let pos = blockPosAtCoords(coords, view);
186
+
187
+ let pos = blockPositionFromCoords(coords, view);
98
188
  if (pos != null) {
99
- view.dispatch(
100
- view.state.tr.setSelection(NodeSelection.create(view.state.doc, pos))
101
- );
189
+ const selection = view.state.selection;
190
+ const doc = view.state.doc;
191
+
192
+ const { from, to } = blockPositionsFromSelection(selection, doc);
193
+
194
+ const draggedBlockInSelection = from <= pos && pos < to;
195
+ const multipleBlocksSelected = !selection.$anchor
196
+ .node()
197
+ .eq(selection.$head.node());
198
+
199
+ if (draggedBlockInSelection && multipleBlocksSelected) {
200
+ view.dispatch(
201
+ view.state.tr.setSelection(MultipleNodeSelection.create(doc, from, to))
202
+ );
203
+ setDragImage(view, from, to);
204
+ } else {
205
+ view.dispatch(
206
+ view.state.tr.setSelection(NodeSelection.create(view.state.doc, pos))
207
+ );
208
+ setDragImage(view, pos);
209
+ }
102
210
 
103
211
  let slice = view.state.selection.content();
104
212
  let { dom, text } = serializeForClipboard(view, slice);
@@ -107,8 +215,7 @@ function dragStart(e: DragEvent, view: EditorView) {
107
215
  e.dataTransfer.setData("text/html", dom.innerHTML);
108
216
  e.dataTransfer.setData("text/plain", text);
109
217
  e.dataTransfer.effectAllowed = "move";
110
- const block = getDraggableBlockFromCoords(coords, view);
111
- e.dataTransfer.setDragImage(block?.node as any, 0, 0);
218
+ e.dataTransfer.setDragImage(dragImageElement!, 0, 0);
112
219
  view.dragging = { slice, move: true };
113
220
  }
114
221
  }
@@ -147,6 +254,7 @@ export const createDraggableBlocksPlugin = () => {
147
254
  dropElement.addEventListener("dragstart", (e) =>
148
255
  dragStart(e, editorView)
149
256
  );
257
+ dropElement.addEventListener("dragend", () => unsetDragImage());
150
258
 
151
259
  return {
152
260
  // update(view, prevState) {},
@@ -248,14 +356,16 @@ export const createDraggableBlocksPlugin = () => {
248
356
  dropElement.style.top = rect.top + "px";
249
357
 
250
358
  ReactDOM.render(
251
- <DragHandle
252
- onShow={onShow}
253
- onHide={onHide}
254
- onAddClicked={onAddClicked}
255
- key={block.id + ""}
256
- view={view}
257
- coords={coords}
258
- />,
359
+ <MantineProvider theme={BlockNoteTheme}>
360
+ <DragHandle
361
+ onShow={onShow}
362
+ onHide={onHide}
363
+ onAddClicked={onAddClicked}
364
+ key={block.id + ""}
365
+ view={view}
366
+ coords={coords}
367
+ />
368
+ </MantineProvider>,
259
369
  dropElement
260
370
  );
261
371
  return true;
@@ -1,11 +1,12 @@
1
- import Tippy from "@tippyjs/react";
2
1
  import { TextSelection } from "prosemirror-state";
3
2
  import { EditorView } from "prosemirror-view";
4
3
  import { useState } from "react";
5
4
  import { AiOutlinePlus } from "react-icons/ai";
6
5
  import { findBlock } from "../../Blocks/helpers/findBlock";
7
6
  import { SlashMenuPluginKey } from "../../SlashMenu/SlashMenuExtension";
8
- import styles from "./DragHandle.module.css";
7
+ import { Menu } from "@mantine/core";
8
+ import { MdDragIndicator } from "react-icons/all";
9
+ import { ActionIcon } from "@mantine/core";
9
10
  import DragHandleMenu from "./DragHandleMenu";
10
11
 
11
12
  export const DragHandle = (props: {
@@ -59,8 +60,7 @@ export const DragHandle = (props: {
59
60
  if (currentBlock.node.firstChild?.textContent.length !== 0) {
60
61
  // Create new block after current block
61
62
  const endOfBlock = currentBlock.pos + currentBlock.node.nodeSize;
62
- let newBlock =
63
- props.view.state.schema.nodes["tccontent"].createAndFill()!;
63
+ let newBlock = props.view.state.schema.nodes["content"].createAndFill()!;
64
64
  props.view.state.tr.insert(endOfBlock, newBlock);
65
65
  props.view.dispatch(props.view.state.tr.insert(endOfBlock, newBlock));
66
66
  props.view.dispatch(
@@ -86,23 +86,17 @@ export const DragHandle = (props: {
86
86
 
87
87
  return (
88
88
  <div style={{ display: "flex", flexDirection: "row" }}>
89
- <AiOutlinePlus
90
- size={24}
91
- fillOpacity={"0.25"}
92
- className={styles.dragHandleAdd}
93
- onClick={onAddClick}
94
- />
95
- <Tippy
96
- content={<DragHandleMenu onDelete={onDelete} />}
97
- placement={"left"}
98
- trigger={"click"}
99
- duration={0}
100
- interactiveBorder={100}
101
- interactive={true}
102
- onShow={props.onShow}
103
- onHide={props.onHide}>
104
- <div className={styles.dragHandle} />
105
- </Tippy>
89
+ <ActionIcon size={24} color={"brandFinal.3"} data-test={"dragHandleAdd"}>
90
+ {<AiOutlinePlus size={24} onClick={onAddClick} />}
91
+ </ActionIcon>
92
+ <Menu onOpen={props.onShow} onClose={props.onHide} position={"left"}>
93
+ <Menu.Target>
94
+ <ActionIcon size={24} color={"brandFinal.3"} data-test={"dragHandle"}>
95
+ {<MdDragIndicator size={24} />}
96
+ </ActionIcon>
97
+ </Menu.Target>
98
+ <DragHandleMenu onDelete={onDelete} />
99
+ </Menu>
106
100
  </div>
107
101
  );
108
102
  };
@@ -1,17 +1,18 @@
1
- import styles from "./DragHandleMenu.module.css";
2
- import { MenuGroup, ButtonItem } from "@atlaskit/menu";
1
+ import { createStyles, Menu } from "@mantine/core";
3
2
 
4
3
  type Props = {
5
4
  onDelete: () => void;
6
5
  };
7
6
 
8
7
  const DragHandleMenu = (props: Props) => {
8
+ const { classes } = createStyles({ root: {} })(undefined, {
9
+ name: "DragHandleMenu",
10
+ });
11
+
9
12
  return (
10
- <div className={styles.menuList}>
11
- <MenuGroup>
12
- <ButtonItem onClick={props.onDelete}>Delete</ButtonItem>
13
- </MenuGroup>
14
- </div>
13
+ <Menu.Dropdown className={classes.root}>
14
+ <Menu.Item onClick={props.onDelete}>Delete</Menu.Item>
15
+ </Menu.Dropdown>
15
16
  );
16
17
  };
17
18
 
@@ -1,43 +1,13 @@
1
+ import { MantineProvider } from "@mantine/core";
1
2
  import Tippy from "@tippyjs/react";
2
3
  import { getMarkRange } from "@tiptap/core";
3
4
  import { Mark, ResolvedPos } from "prosemirror-model";
4
5
  import { Plugin, PluginKey } from "prosemirror-state";
5
6
  import ReactDOM from "react-dom";
6
- import rootStyles from "../../root.module.css";
7
- import { HyperlinkBasicMenu } from "./menus/HyperlinkBasicMenu";
8
- import {
9
- HyperlinkEditMenu,
10
- HyperlinkEditorMenuProps,
11
- } from "./menus/HyperlinkEditMenu";
7
+ import { BlockNoteTheme } from "../../BlockNoteTheme";
8
+ import { HyperlinkMenu } from "./menus/HyperlinkMenu";
12
9
  const PLUGIN_KEY = new PluginKey("HyperlinkMenuPlugin");
13
10
 
14
- /**
15
- * a helper function that wraps a Tippy around a HyperlinkEditMenu
16
- * @param props has {text, url, onSubmit and anchorPos}
17
- * @returns a Tippy instance whose content is a editMenu
18
- */
19
- const tippyWrapperHyperlinkEditMenu = (
20
- props: HyperlinkEditorMenuProps & {
21
- anchorPos: { left: number; top: number; width: number; height: number };
22
- }
23
- ) => {
24
- const { anchorPos, ...editMenuProps } = props;
25
- return (
26
- <Tippy
27
- getReferenceClientRect={() => anchorPos as any}
28
- content={<HyperlinkEditMenu {...editMenuProps}></HyperlinkEditMenu>}
29
- interactive={true}
30
- interactiveBorder={30}
31
- showOnCreate={true}
32
- trigger={"click"} // so that we don't hide on mouse out
33
- hideOnClick
34
- className={rootStyles.bnRoot}
35
- appendTo={document.body}>
36
- <div></div>
37
- </Tippy>
38
- );
39
- };
40
-
41
11
  export const createHyperlinkMenuPlugin = () => {
42
12
  // as we always use Tippy appendTo(document.body), we can just create an element
43
13
  // that we use for ReactDOM, but it isn't used anywhere (except by React internally)
@@ -135,40 +105,35 @@ export const createHyperlinkMenuPlugin = () => {
135
105
  );
136
106
  };
137
107
 
138
- // the hyperlinkEditMenu will be positioned at the same place as hyperlinkBasicMenu
139
- // this is achieved by making this editMenu a property of the basicMenu below
140
- // and returning this editMenu directly by introducing another isEditing state
141
- const hyperlinkEditMenu = tippyWrapperHyperlinkEditMenu({
142
- anchorPos,
143
- text,
144
- url,
145
- onSubmit: editHandler,
146
- });
147
-
148
- const hyperlinkBasicMenu = (
149
- <Tippy
150
- key={nextTippyKey + ""} // it could be tippy has "hidden" itself after mouseout. We use a key to get a new instance with a clean state.
151
- getReferenceClientRect={() => anchorPos as any}
152
- content={
153
- <HyperlinkBasicMenu
154
- editMenu={hyperlinkEditMenu}
155
- removeHandler={removeHandler}
156
- href={url}></HyperlinkBasicMenu>
157
- }
158
- onHide={() => {
159
- nextTippyKey++;
160
- menuState = "hidden";
161
- }}
162
- aria={{ expanded: false }}
163
- interactive={true}
164
- interactiveBorder={30}
165
- triggerTarget={hoveredLink}
166
- showOnCreate={basedOnCursorPos}
167
- appendTo={document.body}>
168
- <div></div>
169
- </Tippy>
108
+ const hyperlinkMenu = (
109
+ <MantineProvider theme={BlockNoteTheme}>
110
+ <Tippy
111
+ key={nextTippyKey + ""} // it could be tippy has "hidden" itself after mouseout. We use a key to get a new instance with a clean state.
112
+ getReferenceClientRect={() => anchorPos as any}
113
+ content={
114
+ <HyperlinkMenu
115
+ update={editHandler}
116
+ pos={anchorPos}
117
+ remove={removeHandler}
118
+ text={text}
119
+ url={url}
120
+ />
121
+ }
122
+ onHide={() => {
123
+ nextTippyKey++;
124
+ menuState = "hidden";
125
+ }}
126
+ aria={{ expanded: false }}
127
+ interactive={true}
128
+ interactiveBorder={30}
129
+ triggerTarget={hoveredLink}
130
+ showOnCreate={basedOnCursorPos}
131
+ appendTo={document.body}>
132
+ <div></div>
133
+ </Tippy>
134
+ </MantineProvider>
170
135
  );
171
- ReactDOM.render(hyperlinkBasicMenu, fakeRenderTarget);
136
+ ReactDOM.render(hyperlinkMenu, fakeRenderTarget);
172
137
  menuState = basedOnCursorPos ? "cursor-based" : "mouse-based";
173
138
  },
174
139
  };
@@ -0,0 +1,44 @@
1
+ import { createStyles, Stack } from "@mantine/core";
2
+ import { useState } from "react";
3
+ import { RiLink, RiText } from "react-icons/ri";
4
+ import { EditHyperlinkMenuItem } from "./EditHyperlinkMenuItem";
5
+
6
+ export type EditHyperlinkMenuProps = {
7
+ url: string;
8
+ text: string;
9
+ update: (url: string, text: string) => void;
10
+ };
11
+
12
+ /**
13
+ * Menu which opens when editing an existing hyperlink or creating a new one.
14
+ * Provides input fields for setting the hyperlink URL and title.
15
+ */
16
+ export const EditHyperlinkMenu = (props: EditHyperlinkMenuProps) => {
17
+ const [url, setUrl] = useState(props.url);
18
+ const [title, setTitle] = useState(props.text);
19
+ const { classes } = createStyles({ root: {} })(undefined, {
20
+ name: "EditHyperlinkMenu",
21
+ });
22
+
23
+ return (
24
+ <Stack className={classes.root}>
25
+ <EditHyperlinkMenuItem
26
+ icon={RiLink}
27
+ mainIconTooltip={"Edit URL"}
28
+ autofocus={true}
29
+ placeholder={"Edit URL"}
30
+ value={url}
31
+ onChange={(value) => setUrl(value)}
32
+ onSubmit={() => props.update(url, title)}
33
+ />
34
+ <EditHyperlinkMenuItem
35
+ icon={RiText}
36
+ mainIconTooltip={"Edit Title"}
37
+ placeholder={"Edit Title"}
38
+ value={title}
39
+ onChange={(value) => setTitle(value)}
40
+ onSubmit={() => props.update(url, title)}
41
+ />
42
+ </Stack>
43
+ );
44
+ };
@@ -0,0 +1,34 @@
1
+ import { IconType } from "react-icons";
2
+ import { EditHyperlinkMenuItemIcon } from "./EditHyperlinkMenuItemIcon";
3
+ import { EditHyperlinkMenuItemInput } from "./EditHyperlinkMenuItemInput";
4
+ import { Group } from "@mantine/core";
5
+
6
+ export type EditHyperlinkMenuItemProps = {
7
+ icon: IconType;
8
+ mainIconTooltip: string;
9
+ secondaryIconTooltip?: string;
10
+ autofocus?: boolean;
11
+ placeholder?: string;
12
+ value?: string;
13
+ onChange: (value: string) => void;
14
+ onSubmit: () => void;
15
+ };
16
+
17
+ export function EditHyperlinkMenuItem(props: EditHyperlinkMenuItemProps) {
18
+ return (
19
+ <Group>
20
+ <EditHyperlinkMenuItemIcon
21
+ icon={props.icon}
22
+ mainTooltip={props.mainIconTooltip}
23
+ secondaryTooltip={props.secondaryIconTooltip}
24
+ />
25
+ <EditHyperlinkMenuItemInput
26
+ autofocus={props.autofocus}
27
+ placeholder={props.placeholder}
28
+ value={props.value}
29
+ onChange={props.onChange}
30
+ onSubmit={props.onSubmit}
31
+ />
32
+ </Group>
33
+ );
34
+ }
@@ -0,0 +1,31 @@
1
+ import { IconType } from "react-icons";
2
+ import Tippy from "@tippyjs/react";
3
+ import { TooltipContent } from "../../../shared/components/tooltip/TooltipContent";
4
+ import { Container } from "@mantine/core";
5
+
6
+ export type EditHyperlinkMenuItemIconProps = {
7
+ icon: IconType;
8
+ mainTooltip: string;
9
+ secondaryTooltip?: string;
10
+ };
11
+
12
+ export function EditHyperlinkMenuItemIcon(
13
+ props: EditHyperlinkMenuItemIconProps
14
+ ) {
15
+ const Icon = props.icon;
16
+
17
+ return (
18
+ <Tippy
19
+ content={
20
+ <TooltipContent
21
+ mainTooltip={props.mainTooltip}
22
+ secondaryTooltip={props.secondaryTooltip}
23
+ />
24
+ }
25
+ placement="left">
26
+ <Container>
27
+ <Icon size={16}></Icon>
28
+ </Container>
29
+ </Tippy>
30
+ );
31
+ }
@@ -0,0 +1,40 @@
1
+ import { KeyboardEvent, useEffect, useRef } from "react";
2
+ import { TextInput } from "@mantine/core";
3
+
4
+ export type EditHyperlinkMenuItemInputProps = {
5
+ autofocus?: boolean;
6
+ placeholder?: string;
7
+ value?: string;
8
+ onChange: (value: string) => void;
9
+ onSubmit: () => void;
10
+ };
11
+
12
+ export function EditHyperlinkMenuItemInput(
13
+ props: EditHyperlinkMenuItemInputProps
14
+ ) {
15
+ const inputRef = useRef<HTMLInputElement>(null);
16
+
17
+ useEffect(() => {
18
+ setTimeout(() => {
19
+ props.autofocus && inputRef.current?.focus();
20
+ });
21
+ }, [props.autofocus]);
22
+
23
+ function handleEnter(event: KeyboardEvent) {
24
+ if (event.key === "Enter") {
25
+ event.preventDefault();
26
+ props.onSubmit();
27
+ }
28
+ }
29
+
30
+ return (
31
+ <TextInput
32
+ size={"xs"}
33
+ value={props.value}
34
+ onChange={(event) => props.onChange(event.currentTarget.value)}
35
+ onKeyDown={handleEnter}
36
+ placeholder={props.placeholder}
37
+ ref={inputRef}
38
+ />
39
+ );
40
+ }
@@ -0,0 +1,37 @@
1
+ import { RiExternalLinkFill, RiLinkUnlink } from "react-icons/ri";
2
+ import { Toolbar } from "../../../shared/components/toolbar/Toolbar";
3
+ import { ToolbarButton } from "../../../shared/components/toolbar/ToolbarButton";
4
+
5
+ type HoverHyperlinkMenuProps = {
6
+ url: string;
7
+ edit: () => void;
8
+ remove: () => void;
9
+ };
10
+
11
+ /**
12
+ * Menu which opens when hovering an existing hyperlink.
13
+ * Provides buttons for editing, opening, and removing the hyperlink.
14
+ */
15
+ export const HoverHyperlinkMenu = (props: HoverHyperlinkMenuProps) => {
16
+ return (
17
+ <Toolbar>
18
+ <ToolbarButton mainTooltip="Edit" isSelected={false} onClick={props.edit}>
19
+ Edit Link
20
+ </ToolbarButton>
21
+ <ToolbarButton
22
+ mainTooltip="Open in new tab"
23
+ isSelected={false}
24
+ onClick={() => {
25
+ window.open(props.url, "_blank");
26
+ }}
27
+ icon={RiExternalLinkFill}
28
+ />
29
+ <ToolbarButton
30
+ mainTooltip="Remove link"
31
+ isSelected={false}
32
+ onClick={props.remove}
33
+ icon={RiLinkUnlink}
34
+ />
35
+ </Toolbar>
36
+ );
37
+ };