@blocknote/core 0.1.0-alpha.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 (143) hide show
  1. package/README.md +99 -0
  2. package/dist/blocknote.js +4485 -0
  3. package/dist/blocknote.js.map +1 -0
  4. package/dist/blocknote.umd.cjs +90 -0
  5. package/dist/blocknote.umd.cjs.map +1 -0
  6. package/dist/style.css +1 -0
  7. package/package.json +109 -0
  8. package/src/BlockNoteExtensions.ts +90 -0
  9. package/src/EditorContent.tsx +1 -0
  10. package/src/assets/inter-v12-latin/inter-v12-latin-100.woff +0 -0
  11. package/src/assets/inter-v12-latin/inter-v12-latin-100.woff2 +0 -0
  12. package/src/assets/inter-v12-latin/inter-v12-latin-200.woff +0 -0
  13. package/src/assets/inter-v12-latin/inter-v12-latin-200.woff2 +0 -0
  14. package/src/assets/inter-v12-latin/inter-v12-latin-300.woff +0 -0
  15. package/src/assets/inter-v12-latin/inter-v12-latin-300.woff2 +0 -0
  16. package/src/assets/inter-v12-latin/inter-v12-latin-500.woff +0 -0
  17. package/src/assets/inter-v12-latin/inter-v12-latin-500.woff2 +0 -0
  18. package/src/assets/inter-v12-latin/inter-v12-latin-600.woff +0 -0
  19. package/src/assets/inter-v12-latin/inter-v12-latin-600.woff2 +0 -0
  20. package/src/assets/inter-v12-latin/inter-v12-latin-700.woff +0 -0
  21. package/src/assets/inter-v12-latin/inter-v12-latin-700.woff2 +0 -0
  22. package/src/assets/inter-v12-latin/inter-v12-latin-800.woff +0 -0
  23. package/src/assets/inter-v12-latin/inter-v12-latin-800.woff2 +0 -0
  24. package/src/assets/inter-v12-latin/inter-v12-latin-900.woff +0 -0
  25. package/src/assets/inter-v12-latin/inter-v12-latin-900.woff2 +0 -0
  26. package/src/assets/inter-v12-latin/inter-v12-latin-regular.woff +0 -0
  27. package/src/assets/inter-v12-latin/inter-v12-latin-regular.woff2 +0 -0
  28. package/src/editor.module.css +3 -0
  29. package/src/extensions/Blocks/OrderedListPlugin.ts +46 -0
  30. package/src/extensions/Blocks/PreviousBlockTypePlugin.ts +146 -0
  31. package/src/extensions/Blocks/commands/joinBackward.ts +274 -0
  32. package/src/extensions/Blocks/helpers/findBlock.ts +3 -0
  33. package/src/extensions/Blocks/helpers/setBlockHeading.ts +30 -0
  34. package/src/extensions/Blocks/index.ts +15 -0
  35. package/src/extensions/Blocks/nodes/Block.module.css +226 -0
  36. package/src/extensions/Blocks/nodes/Block.ts +390 -0
  37. package/src/extensions/Blocks/nodes/BlockGroup.ts +28 -0
  38. package/src/extensions/Blocks/nodes/Content.ts +50 -0
  39. package/src/extensions/Blocks/nodes/README.md +26 -0
  40. package/src/extensions/Blocks/rule.ts +48 -0
  41. package/src/extensions/BubbleMenu/BubbleMenuExtension.tsx +28 -0
  42. package/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +245 -0
  43. package/src/extensions/BubbleMenu/component/BubbleMenu.tsx +216 -0
  44. package/src/extensions/BubbleMenu/component/DropdownBlockItem.module.css +13 -0
  45. package/src/extensions/BubbleMenu/component/DropdownBlockItem.tsx +25 -0
  46. package/src/extensions/BubbleMenu/component/LinkToolbarButton.tsx +67 -0
  47. package/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts +15 -0
  48. package/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx +266 -0
  49. package/src/extensions/DraggableBlocks/components/DragHandle.module.css +33 -0
  50. package/src/extensions/DraggableBlocks/components/DragHandle.tsx +108 -0
  51. package/src/extensions/DraggableBlocks/components/DragHandleMenu.module.css +10 -0
  52. package/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx +18 -0
  53. package/src/extensions/Hyperlinks/HyperlinkMark.tsx +16 -0
  54. package/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +200 -0
  55. package/src/extensions/Hyperlinks/menus/HyperlinkBasicMenu.tsx +59 -0
  56. package/src/extensions/Hyperlinks/menus/HyperlinkEditMenu.tsx +72 -0
  57. package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInput.tsx +173 -0
  58. package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInputStyles.ts +36 -0
  59. package/src/extensions/Hyperlinks/menus/atlaskit/README.md +1 -0
  60. package/src/extensions/Hyperlinks/menus/atlaskit/ToolbarComponent.tsx +61 -0
  61. package/src/extensions/Paragraph/FixedParagraph.ts +12 -0
  62. package/src/extensions/Placeholder/PlaceholderExtension.ts +127 -0
  63. package/src/extensions/SlashMenu/SlashMenuExtension.ts +43 -0
  64. package/src/extensions/SlashMenu/SlashMenuItem.ts +56 -0
  65. package/src/extensions/SlashMenu/defaultCommands.tsx +229 -0
  66. package/src/extensions/SlashMenu/index.ts +11 -0
  67. package/src/extensions/TrailingNode/TrailingNodeExtension.ts +70 -0
  68. package/src/extensions/UniqueID/UniqueID.ts +281 -0
  69. package/src/extensions/helpers/formatKeyboardShortcut.ts +9 -0
  70. package/src/fonts-inter.css +94 -0
  71. package/src/globals.css +28 -0
  72. package/src/index.ts +5 -0
  73. package/src/lib/atlaskit/browser.ts +47 -0
  74. package/src/root.module.css +19 -0
  75. package/src/shared/components/toolbar/SimpleToolbarButton.module.css +13 -0
  76. package/src/shared/components/toolbar/SimpleToolbarButton.tsx +56 -0
  77. package/src/shared/components/toolbar/Toolbar.module.css +10 -0
  78. package/src/shared/components/toolbar/Toolbar.tsx +5 -0
  79. package/src/shared/components/toolbar/ToolbarSeparator.module.css +13 -0
  80. package/src/shared/components/toolbar/ToolbarSeparator.tsx +7 -0
  81. package/src/shared/components/tooltip/TooltipContent.module.css +15 -0
  82. package/src/shared/components/tooltip/TooltipContent.tsx +23 -0
  83. package/src/shared/hooks/useEditorForceUpdate.tsx +30 -0
  84. package/src/shared/plugins/suggestion/SuggestionItem.ts +31 -0
  85. package/src/shared/plugins/suggestion/SuggestionListReactRenderer.ts +227 -0
  86. package/src/shared/plugins/suggestion/SuggestionPlugin.ts +365 -0
  87. package/src/shared/plugins/suggestion/components/SuggestionGroup.module.css +45 -0
  88. package/src/shared/plugins/suggestion/components/SuggestionGroup.tsx +134 -0
  89. package/src/shared/plugins/suggestion/components/SuggestionList.module.css +10 -0
  90. package/src/shared/plugins/suggestion/components/SuggestionList.tsx +91 -0
  91. package/src/style.css +7 -0
  92. package/src/useEditor.ts +47 -0
  93. package/src/vite-env.d.ts +1 -0
  94. package/types/src/BlockNoteExtensions.d.ts +4 -0
  95. package/types/src/EditorContent.d.ts +1 -0
  96. package/types/src/extensions/Blocks/OrderedListPlugin.d.ts +2 -0
  97. package/types/src/extensions/Blocks/PreviousBlockTypePlugin.d.ts +13 -0
  98. package/types/src/extensions/Blocks/commands/joinBackward.d.ts +14 -0
  99. package/types/src/extensions/Blocks/helpers/findBlock.d.ts +6 -0
  100. package/types/src/extensions/Blocks/helpers/setBlockHeading.d.ts +5 -0
  101. package/types/src/extensions/Blocks/index.d.ts +1 -0
  102. package/types/src/extensions/Blocks/nodes/Block.d.ts +32 -0
  103. package/types/src/extensions/Blocks/nodes/BlockGroup.d.ts +2 -0
  104. package/types/src/extensions/Blocks/nodes/Content.d.ts +5 -0
  105. package/types/src/extensions/Blocks/rule.d.ts +16 -0
  106. package/types/src/extensions/BubbleMenu/BubbleMenuExtension.d.ts +5 -0
  107. package/types/src/extensions/BubbleMenu/BubbleMenuPlugin.d.ts +46 -0
  108. package/types/src/extensions/BubbleMenu/component/BubbleMenu.d.ts +5 -0
  109. package/types/src/extensions/BubbleMenu/component/DropdownBlockItem.d.ts +10 -0
  110. package/types/src/extensions/BubbleMenu/component/LinkToolbarButton.d.ts +11 -0
  111. package/types/src/extensions/DraggableBlocks/DraggableBlocksExtension.d.ts +7 -0
  112. package/types/src/extensions/DraggableBlocks/DraggableBlocksPlugin.d.ts +18 -0
  113. package/types/src/extensions/DraggableBlocks/components/DragHandle.d.ts +12 -0
  114. package/types/src/extensions/DraggableBlocks/components/DragHandleMenu.d.ts +6 -0
  115. package/types/src/extensions/Hyperlinks/HyperlinkMark.d.ts +7 -0
  116. package/types/src/extensions/Hyperlinks/HyperlinkMenuPlugin.d.ts +2 -0
  117. package/types/src/extensions/Hyperlinks/menus/HyperlinkBasicMenu.d.ts +12 -0
  118. package/types/src/extensions/Hyperlinks/menus/HyperlinkEditMenu.d.ts +10 -0
  119. package/types/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInput.d.ts +39 -0
  120. package/types/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInputStyles.d.ts +1 -0
  121. package/types/src/extensions/Hyperlinks/menus/atlaskit/ToolbarComponent.d.ts +11 -0
  122. package/types/src/extensions/Paragraph/FixedParagraph.d.ts +1 -0
  123. package/types/src/extensions/Placeholder/PlaceholderExtension.d.ts +25 -0
  124. package/types/src/extensions/SlashMenu/SlashMenuExtension.d.ts +10 -0
  125. package/types/src/extensions/SlashMenu/SlashMenuItem.d.ts +43 -0
  126. package/types/src/extensions/SlashMenu/defaultCommands.d.ts +8 -0
  127. package/types/src/extensions/SlashMenu/index.d.ts +5 -0
  128. package/types/src/extensions/TrailingNode/TrailingNodeExtension.d.ts +10 -0
  129. package/types/src/extensions/UniqueID/UniqueID.d.ts +3 -0
  130. package/types/src/extensions/helpers/formatKeyboardShortcut.d.ts +1 -0
  131. package/types/src/index.d.ts +4 -0
  132. package/types/src/lib/atlaskit/browser.d.ts +12 -0
  133. package/types/src/shared/components/toolbar/SimpleToolbarButton.d.ts +16 -0
  134. package/types/src/shared/components/toolbar/Toolbar.d.ts +4 -0
  135. package/types/src/shared/components/toolbar/ToolbarSeparator.d.ts +2 -0
  136. package/types/src/shared/components/tooltip/TooltipContent.d.ts +15 -0
  137. package/types/src/shared/hooks/useEditorForceUpdate.d.ts +2 -0
  138. package/types/src/shared/plugins/suggestion/SuggestionItem.d.ts +29 -0
  139. package/types/src/shared/plugins/suggestion/SuggestionListReactRenderer.d.ts +71 -0
  140. package/types/src/shared/plugins/suggestion/SuggestionPlugin.d.ts +74 -0
  141. package/types/src/shared/plugins/suggestion/components/SuggestionGroup.d.ts +23 -0
  142. package/types/src/shared/plugins/suggestion/components/SuggestionList.d.ts +26 -0
  143. package/types/src/useEditor.d.ts +8 -0
@@ -0,0 +1,266 @@
1
+ import { NodeSelection, Plugin, PluginKey } from "prosemirror-state";
2
+ import * as pv from "prosemirror-view";
3
+ import { EditorView } from "prosemirror-view";
4
+ import ReactDOM from "react-dom";
5
+ import { DragHandle } from "./components/DragHandle";
6
+
7
+ const serializeForClipboard = (pv as any).__serializeForClipboard;
8
+ // code based on https://github.com/ueberdosis/tiptap/issues/323#issuecomment-506637799
9
+
10
+ let horizontalAnchor: number;
11
+ function getHorizontalAnchor() {
12
+ if (!horizontalAnchor) {
13
+ const firstBlockGroup = document.querySelector(
14
+ ".ProseMirror > [class*='blockGroup']"
15
+ ) as HTMLElement | undefined; // first block group node
16
+ if (firstBlockGroup) {
17
+ horizontalAnchor = absoluteRect(firstBlockGroup).left;
18
+ } // Anchor to the left of the first block group
19
+ }
20
+ return horizontalAnchor;
21
+ }
22
+
23
+ export function createRect(rect: DOMRect) {
24
+ let newRect = {
25
+ left: rect.left + document.body.scrollLeft,
26
+ top: rect.top + document.body.scrollTop,
27
+ width: rect.width,
28
+ height: rect.height,
29
+ bottom: 0,
30
+ right: 0,
31
+ };
32
+ newRect.bottom = newRect.top + newRect.height;
33
+ newRect.right = newRect.left + newRect.width;
34
+ return newRect;
35
+ }
36
+
37
+ export function absoluteRect(element: HTMLElement) {
38
+ return createRect(element.getBoundingClientRect());
39
+ }
40
+
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
+ function getDraggableBlockFromCoords(
60
+ coords: { left: number; top: number },
61
+ view: EditorView
62
+ ) {
63
+ let pos = view.posAtCoords(coords);
64
+ if (!pos) {
65
+ return undefined;
66
+ }
67
+ let node = view.domAtPos(pos.pos).node as HTMLElement;
68
+
69
+ if (node === view.dom) {
70
+ // mouse over root
71
+ return undefined;
72
+ }
73
+
74
+ while (
75
+ node &&
76
+ node.parentNode &&
77
+ node.parentNode !== view.dom &&
78
+ !node.hasAttribute?.("data-id")
79
+ ) {
80
+ node = node.parentNode as HTMLElement;
81
+ }
82
+ if (!node) {
83
+ return undefined;
84
+ }
85
+ return { node, id: node.getAttribute("data-id")! };
86
+ }
87
+
88
+ function dragStart(e: DragEvent, view: EditorView) {
89
+ if (!e.dataTransfer) {
90
+ return;
91
+ }
92
+
93
+ let coords = {
94
+ left: view.dom.clientWidth / 2, // take middle of editor
95
+ top: e.clientY,
96
+ };
97
+ let pos = blockPosAtCoords(coords, view);
98
+ if (pos != null) {
99
+ view.dispatch(
100
+ view.state.tr.setSelection(NodeSelection.create(view.state.doc, pos))
101
+ );
102
+
103
+ let slice = view.state.selection.content();
104
+ let { dom, text } = serializeForClipboard(view, slice);
105
+
106
+ e.dataTransfer.clearData();
107
+ e.dataTransfer.setData("text/html", dom.innerHTML);
108
+ e.dataTransfer.setData("text/plain", text);
109
+ e.dataTransfer.effectAllowed = "move";
110
+ const block = getDraggableBlockFromCoords(coords, view);
111
+ e.dataTransfer.setDragImage(block?.node as any, 0, 0);
112
+ view.dragging = { slice, move: true };
113
+ }
114
+ }
115
+
116
+ export const createDraggableBlocksPlugin = () => {
117
+ let dropElement: HTMLElement | undefined;
118
+
119
+ const WIDTH = 48;
120
+
121
+ // When true, the drag handle with be anchored at the same level as root elements
122
+ // When false, the drag handle with be just to the left of the element
123
+ const horizontalPosAnchoredAtRoot = true;
124
+
125
+ let menuShown = false;
126
+ let addClicked = false;
127
+
128
+ const onShow = () => {
129
+ menuShown = true;
130
+ };
131
+ const onHide = () => {
132
+ menuShown = false;
133
+ };
134
+ const onAddClicked = () => {
135
+ addClicked = true;
136
+ };
137
+
138
+ return new Plugin({
139
+ key: new PluginKey("DraggableBlocksPlugin"),
140
+ view(editorView) {
141
+ dropElement = document.createElement("div");
142
+ dropElement.setAttribute("draggable", "true");
143
+ dropElement.style.position = "absolute";
144
+ dropElement.style.height = "24px"; // default height
145
+ document.body.append(dropElement);
146
+
147
+ dropElement.addEventListener("dragstart", (e) =>
148
+ dragStart(e, editorView)
149
+ );
150
+
151
+ return {
152
+ // update(view, prevState) {},
153
+ destroy() {
154
+ if (!dropElement) {
155
+ throw new Error("unexpected");
156
+ }
157
+ dropElement.parentNode!.removeChild(dropElement);
158
+ dropElement = undefined;
159
+ },
160
+ };
161
+ },
162
+ props: {
163
+ // handleDOMEvents: {
164
+
165
+ // },
166
+ // handleDOMEvents: {
167
+ // dragend(view, event) {
168
+ // // setTimeout(() => {
169
+ // // let node = document.querySelector(".ProseMirror-hideselection");
170
+ // // if (node) {
171
+ // // node.classList.remove("ProseMirror-hideselection");
172
+ // // }
173
+ // // }, 50);
174
+ // return true;
175
+ // },
176
+ handleKeyDown(_view, _event) {
177
+ if (!dropElement) {
178
+ throw new Error("unexpected");
179
+ }
180
+ menuShown = false;
181
+ addClicked = false;
182
+ ReactDOM.render(<></>, dropElement);
183
+ return false;
184
+ },
185
+ handleDOMEvents: {
186
+ // drag(view, event) {
187
+ // // event.dataTransfer!.;
188
+ // return false;
189
+ // },
190
+ mouseleave(_view, _event: any) {
191
+ if (!dropElement) {
192
+ throw new Error("unexpected");
193
+ }
194
+ // TODO
195
+ // dropElement.style.display = "none";
196
+ return true;
197
+ },
198
+ mousedown(_view, _event: any) {
199
+ if (!dropElement) {
200
+ throw new Error("unexpected");
201
+ }
202
+ menuShown = false;
203
+ addClicked = false;
204
+ ReactDOM.render(<></>, dropElement);
205
+ return false;
206
+ },
207
+ mousemove(view, event: any) {
208
+ if (!dropElement) {
209
+ throw new Error("unexpected");
210
+ }
211
+
212
+ if (menuShown || addClicked) {
213
+ // The submenu is open, don't move draghandle
214
+ // Or if the user clicked the add button
215
+ return true;
216
+ }
217
+ const coords = {
218
+ left: view.dom.clientWidth / 2, // take middle of editor
219
+ top: event.clientY,
220
+ };
221
+ const block = getDraggableBlockFromCoords(coords, view);
222
+
223
+ if (!block) {
224
+ console.warn("Perhaps we should hide element?");
225
+ return true;
226
+ }
227
+
228
+ // I want the dim of the blocks content node
229
+ // because if the block contains other blocks
230
+ // Its dims change, moving the position of the drag handle
231
+ const blockContent = block.node.firstChild as HTMLElement;
232
+
233
+ if (!blockContent) {
234
+ return true;
235
+ }
236
+
237
+ const rect = absoluteRect(blockContent);
238
+ const win = block.node.ownerDocument.defaultView!;
239
+ const dropElementRect = dropElement.getBoundingClientRect();
240
+ const left =
241
+ (horizontalPosAnchoredAtRoot ? getHorizontalAnchor() : rect.left) -
242
+ WIDTH +
243
+ win.pageXOffset;
244
+ rect.top +=
245
+ rect.height / 2 - dropElementRect.height / 2 + win.pageYOffset;
246
+
247
+ dropElement.style.left = left + "px";
248
+ dropElement.style.top = rect.top + "px";
249
+
250
+ ReactDOM.render(
251
+ <DragHandle
252
+ onShow={onShow}
253
+ onHide={onHide}
254
+ onAddClicked={onAddClicked}
255
+ key={block.id + ""}
256
+ view={view}
257
+ coords={coords}
258
+ />,
259
+ dropElement
260
+ );
261
+ return true;
262
+ },
263
+ },
264
+ },
265
+ });
266
+ };
@@ -0,0 +1,33 @@
1
+ .dragHandle {
2
+ /* position: absolute; */
3
+ transition: opacity 300ms;
4
+ width: 20px;
5
+ height: 24px;
6
+
7
+ background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -4 10 24"><path fill-opacity="0.2" d="M4 14c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zM2 6C.9 6 0 6.9 0 8s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6C.9 0 0 .9 0 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" /></svg>');
8
+ background-repeat: no-repeat;
9
+ background-size: contain;
10
+ background-position: center;
11
+ cursor: grab;
12
+ border-radius: 3px;
13
+ }
14
+
15
+ /* .dragHandle:active {
16
+ cursor: !important;
17
+ } */
18
+
19
+ .dragHandleAdd:hover,
20
+ .dragHandle:hover {
21
+ background-color: #eee;
22
+ }
23
+
24
+ .dragHandleAdd {
25
+ /* position: absolute; */
26
+ transition: opacity 300ms;
27
+
28
+ background-repeat: no-repeat;
29
+ background-size: contain;
30
+ background-position: center;
31
+ cursor: pointer;
32
+ border-radius: 3px;
33
+ }
@@ -0,0 +1,108 @@
1
+ import Tippy from "@tippyjs/react";
2
+ import { TextSelection } from "prosemirror-state";
3
+ import { EditorView } from "prosemirror-view";
4
+ import { useState } from "react";
5
+ import { AiOutlinePlus } from "react-icons/ai";
6
+ import { findBlock } from "../../Blocks/helpers/findBlock";
7
+ import { SlashMenuPluginKey } from "../../SlashMenu/SlashMenuExtension";
8
+ import styles from "./DragHandle.module.css";
9
+ import DragHandleMenu from "./DragHandleMenu";
10
+
11
+ export const DragHandle = (props: {
12
+ view: EditorView;
13
+ coords: { left: number; top: number };
14
+ onShow?: () => void;
15
+ onHide?: () => void;
16
+ onAddClicked?: () => void;
17
+ }) => {
18
+ const [clicked, setClicked] = useState<boolean>(false);
19
+ const [deleted, setDeleted] = useState<boolean>(false);
20
+
21
+ const onDelete = () => {
22
+ const pos = props.view.posAtCoords(props.coords);
23
+ if (!pos) {
24
+ return;
25
+ }
26
+ const currentBlock = findBlock(
27
+ TextSelection.create(props.view.state.doc, pos.pos)
28
+ );
29
+
30
+ if (currentBlock) {
31
+ if (props.view.dispatch) {
32
+ props.view.dispatch(
33
+ props.view.state.tr.deleteRange(
34
+ currentBlock.pos,
35
+ currentBlock.pos + currentBlock.node.nodeSize
36
+ )
37
+ );
38
+ }
39
+ setDeleted(true);
40
+ }
41
+ };
42
+
43
+ const onAddClick = () => {
44
+ setClicked(true);
45
+ if (props.onAddClicked) {
46
+ props.onAddClicked();
47
+ }
48
+ const pos = props.view.posAtCoords(props.coords);
49
+ if (!pos) {
50
+ return;
51
+ }
52
+ const currentBlock = findBlock(
53
+ TextSelection.create(props.view.state.doc, pos.pos)
54
+ );
55
+ if (!currentBlock) {
56
+ return;
57
+ }
58
+ // If current blocks content is empty dont create a new block
59
+ if (currentBlock.node.firstChild?.textContent.length !== 0) {
60
+ // Create new block after current block
61
+ const endOfBlock = currentBlock.pos + currentBlock.node.nodeSize;
62
+ let newBlock =
63
+ props.view.state.schema.nodes["tccontent"].createAndFill()!;
64
+ props.view.state.tr.insert(endOfBlock, newBlock);
65
+ props.view.dispatch(props.view.state.tr.insert(endOfBlock, newBlock));
66
+ props.view.dispatch(
67
+ props.view.state.tr.setSelection(
68
+ new TextSelection(props.view.state.tr.doc.resolve(endOfBlock + 1))
69
+ )
70
+ );
71
+ }
72
+ // Focus and activate slash menu
73
+ props.view.focus();
74
+ props.view.dispatch(
75
+ props.view.state.tr.scrollIntoView().setMeta(SlashMenuPluginKey, {
76
+ // TODO import suggestion plugin key
77
+ activate: true,
78
+ type: "drag",
79
+ })
80
+ );
81
+ };
82
+
83
+ if (deleted || clicked) {
84
+ return null;
85
+ }
86
+
87
+ return (
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>
106
+ </div>
107
+ );
108
+ };
@@ -0,0 +1,10 @@
1
+ .menuList {
2
+ color: var(--N800);
3
+ background-color: white;
4
+ border: 1px solid var(--N40);
5
+ box-shadow: 0px 4px 8px rgba(9, 30, 66, 0.25),
6
+ 0px 0px 1px rgba(9, 30, 66, 0.31);
7
+ border-radius: 4px;
8
+ max-width: 320;
9
+ margin: 16px auto;
10
+ }
@@ -0,0 +1,18 @@
1
+ import styles from "./DragHandleMenu.module.css";
2
+ import { MenuGroup, ButtonItem } from "@atlaskit/menu";
3
+
4
+ type Props = {
5
+ onDelete: () => void;
6
+ };
7
+
8
+ const DragHandleMenu = (props: Props) => {
9
+ return (
10
+ <div className={styles.menuList}>
11
+ <MenuGroup>
12
+ <ButtonItem onClick={props.onDelete}>Delete</ButtonItem>
13
+ </MenuGroup>
14
+ </div>
15
+ );
16
+ };
17
+
18
+ export default DragHandleMenu;
@@ -0,0 +1,16 @@
1
+ import { Link } from "@tiptap/extension-link";
2
+ import { createHyperlinkMenuPlugin } from "./HyperlinkMenuPlugin";
3
+
4
+ /**
5
+ * This custom link includes a special menu for editing/deleting/opening the link.
6
+ * The menu will be triggered by hovering over the link with the mouse,
7
+ * or by moving the cursor inside the link text
8
+ */
9
+ const Hyperlink = Link.extend({
10
+ priority: 500,
11
+ addProseMirrorPlugins() {
12
+ return [...(this.parent?.() || []), createHyperlinkMenuPlugin()];
13
+ },
14
+ });
15
+
16
+ export default Hyperlink;
@@ -0,0 +1,200 @@
1
+ import Tippy from "@tippyjs/react";
2
+ import { getMarkRange } from "@tiptap/core";
3
+ import { Mark, ResolvedPos } from "prosemirror-model";
4
+ import { Plugin, PluginKey } from "prosemirror-state";
5
+ 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";
12
+ const PLUGIN_KEY = new PluginKey("HyperlinkMenuPlugin");
13
+
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
+ export const createHyperlinkMenuPlugin = () => {
42
+ // as we always use Tippy appendTo(document.body), we can just create an element
43
+ // that we use for ReactDOM, but it isn't used anywhere (except by React internally)
44
+ const fakeRenderTarget = document.createElement("div");
45
+
46
+ let hoveredLink: HTMLAnchorElement | undefined;
47
+ let menuState: "cursor-based" | "mouse-based" | "hidden" = "hidden";
48
+ let nextTippyKey = 0;
49
+
50
+ return new Plugin({
51
+ key: PLUGIN_KEY,
52
+ view() {
53
+ return {
54
+ update: async (view, _prevState) => {
55
+ const selection = view.state.selection;
56
+ if (selection.from !== selection.to) {
57
+ // don't show menu when we have an active selection
58
+ if (menuState !== "hidden") {
59
+ menuState = "hidden";
60
+ ReactDOM.render(<></>, fakeRenderTarget);
61
+ }
62
+ return;
63
+ }
64
+
65
+ let pos: number | undefined;
66
+ let resPos: ResolvedPos | undefined;
67
+ let linkMark: Mark | undefined;
68
+ let basedOnCursorPos = false;
69
+ if (hoveredLink) {
70
+ pos = view.posAtDOM(hoveredLink.firstChild!, 0);
71
+ resPos = view.state.doc.resolve(pos);
72
+ // based on https://github.com/ueberdosis/tiptap/blob/main/packages/core/src/helpers/getMarkRange.ts
73
+ const start = resPos.parent.childAfter(resPos.parentOffset).node;
74
+ linkMark = start?.marks.find((m) => m.type.name.startsWith("link"));
75
+ }
76
+
77
+ if (
78
+ !linkMark &&
79
+ (view.hasFocus() || menuState === "cursor-based") // prevents re-opening menu after submission. Only open cursor-based menu if editor has focus
80
+ ) {
81
+ // no hovered link mark, try get from cursor position
82
+ pos = selection.from;
83
+ resPos = view.state.doc.resolve(pos);
84
+ const start = resPos.parent.childAfter(resPos.parentOffset).node;
85
+ linkMark = start?.marks.find((m) => m.type.name.startsWith("link"));
86
+ basedOnCursorPos = true;
87
+ }
88
+
89
+ if (!linkMark || !pos || !resPos) {
90
+ // The mouse-based popup takes care of hiding itself (tippy)
91
+ // Because the cursor-based popup is has "showOnCreate", we want to hide it manually
92
+ // if the cursor moves way
93
+ if (menuState === "cursor-based") {
94
+ menuState = "hidden";
95
+ ReactDOM.render(<></>, fakeRenderTarget);
96
+ }
97
+ return;
98
+ }
99
+
100
+ const range = getMarkRange(resPos, linkMark.type, linkMark.attrs);
101
+ if (!range) {
102
+ return;
103
+ }
104
+ const text = view.state.doc.textBetween(range.from, range.to);
105
+ const url = linkMark.attrs.href;
106
+
107
+ const anchorPos = {
108
+ // use the 'median' position of the range
109
+ ...view.coordsAtPos(Math.round((range.from + range.to) / 2)),
110
+ height: 0, // needed to satisfy types
111
+ width: 0,
112
+ };
113
+
114
+ const foundLinkMark = linkMark; // typescript workaround for event handlers
115
+
116
+ // A URL has to begin with http(s):// to be interpreted as an absolute path
117
+ const editHandler = (href: string, text: string) => {
118
+ menuState = "hidden";
119
+ ReactDOM.render(<></>, fakeRenderTarget);
120
+
121
+ // update the mark with new href
122
+ (foundLinkMark as any).attrs = { ...foundLinkMark.attrs, href }; // TODO: invalid assign to attrs
123
+ // insertText actually replaces the range with text
124
+ const tr = view.state.tr.insertText(text, range.from, range.to);
125
+ // the former range.to is no longer in use
126
+ tr.addMark(range.from, range.from + text.length, foundLinkMark);
127
+ view.dispatch(tr);
128
+ };
129
+
130
+ const removeHandler = () => {
131
+ view.dispatch(
132
+ view.state.tr
133
+ .removeMark(range.from, range.to, foundLinkMark.type)
134
+ .setMeta("preventAutolink", true)
135
+ );
136
+ };
137
+
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>
170
+ );
171
+ ReactDOM.render(hyperlinkBasicMenu, fakeRenderTarget);
172
+ menuState = basedOnCursorPos ? "cursor-based" : "mouse-based";
173
+ },
174
+ };
175
+ },
176
+
177
+ props: {
178
+ handleDOMEvents: {
179
+ // update view when an <a> is hovered over
180
+ mouseover(view, event: any) {
181
+ const newHoveredLink =
182
+ event.target instanceof HTMLAnchorElement &&
183
+ event.target.nodeName === "A"
184
+ ? event.target
185
+ : undefined;
186
+
187
+ if (newHoveredLink !== hoveredLink) {
188
+ // dispatch a meta transaction to make sure the view gets updated
189
+ hoveredLink = newHoveredLink;
190
+
191
+ view.dispatch(
192
+ view.state.tr.setMeta(PLUGIN_KEY, { hoveredLinkChanged: true })
193
+ );
194
+ }
195
+ return false;
196
+ },
197
+ },
198
+ },
199
+ });
200
+ };