@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.
- package/README.md +99 -0
- package/dist/blocknote.js +4485 -0
- package/dist/blocknote.js.map +1 -0
- package/dist/blocknote.umd.cjs +90 -0
- package/dist/blocknote.umd.cjs.map +1 -0
- package/dist/style.css +1 -0
- package/package.json +109 -0
- package/src/BlockNoteExtensions.ts +90 -0
- package/src/EditorContent.tsx +1 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-100.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-100.woff2 +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-200.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-200.woff2 +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-300.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-300.woff2 +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-500.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-500.woff2 +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-600.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-600.woff2 +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-700.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-700.woff2 +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-800.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-800.woff2 +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-900.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-900.woff2 +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-regular.woff +0 -0
- package/src/assets/inter-v12-latin/inter-v12-latin-regular.woff2 +0 -0
- package/src/editor.module.css +3 -0
- package/src/extensions/Blocks/OrderedListPlugin.ts +46 -0
- package/src/extensions/Blocks/PreviousBlockTypePlugin.ts +146 -0
- package/src/extensions/Blocks/commands/joinBackward.ts +274 -0
- package/src/extensions/Blocks/helpers/findBlock.ts +3 -0
- package/src/extensions/Blocks/helpers/setBlockHeading.ts +30 -0
- package/src/extensions/Blocks/index.ts +15 -0
- package/src/extensions/Blocks/nodes/Block.module.css +226 -0
- package/src/extensions/Blocks/nodes/Block.ts +390 -0
- package/src/extensions/Blocks/nodes/BlockGroup.ts +28 -0
- package/src/extensions/Blocks/nodes/Content.ts +50 -0
- package/src/extensions/Blocks/nodes/README.md +26 -0
- package/src/extensions/Blocks/rule.ts +48 -0
- package/src/extensions/BubbleMenu/BubbleMenuExtension.tsx +28 -0
- package/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +245 -0
- package/src/extensions/BubbleMenu/component/BubbleMenu.tsx +216 -0
- package/src/extensions/BubbleMenu/component/DropdownBlockItem.module.css +13 -0
- package/src/extensions/BubbleMenu/component/DropdownBlockItem.tsx +25 -0
- package/src/extensions/BubbleMenu/component/LinkToolbarButton.tsx +67 -0
- package/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts +15 -0
- package/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx +266 -0
- package/src/extensions/DraggableBlocks/components/DragHandle.module.css +33 -0
- package/src/extensions/DraggableBlocks/components/DragHandle.tsx +108 -0
- package/src/extensions/DraggableBlocks/components/DragHandleMenu.module.css +10 -0
- package/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx +18 -0
- package/src/extensions/Hyperlinks/HyperlinkMark.tsx +16 -0
- package/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +200 -0
- package/src/extensions/Hyperlinks/menus/HyperlinkBasicMenu.tsx +59 -0
- package/src/extensions/Hyperlinks/menus/HyperlinkEditMenu.tsx +72 -0
- package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInput.tsx +173 -0
- package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInputStyles.ts +36 -0
- package/src/extensions/Hyperlinks/menus/atlaskit/README.md +1 -0
- package/src/extensions/Hyperlinks/menus/atlaskit/ToolbarComponent.tsx +61 -0
- package/src/extensions/Paragraph/FixedParagraph.ts +12 -0
- package/src/extensions/Placeholder/PlaceholderExtension.ts +127 -0
- package/src/extensions/SlashMenu/SlashMenuExtension.ts +43 -0
- package/src/extensions/SlashMenu/SlashMenuItem.ts +56 -0
- package/src/extensions/SlashMenu/defaultCommands.tsx +229 -0
- package/src/extensions/SlashMenu/index.ts +11 -0
- package/src/extensions/TrailingNode/TrailingNodeExtension.ts +70 -0
- package/src/extensions/UniqueID/UniqueID.ts +281 -0
- package/src/extensions/helpers/formatKeyboardShortcut.ts +9 -0
- package/src/fonts-inter.css +94 -0
- package/src/globals.css +28 -0
- package/src/index.ts +5 -0
- package/src/lib/atlaskit/browser.ts +47 -0
- package/src/root.module.css +19 -0
- package/src/shared/components/toolbar/SimpleToolbarButton.module.css +13 -0
- package/src/shared/components/toolbar/SimpleToolbarButton.tsx +56 -0
- package/src/shared/components/toolbar/Toolbar.module.css +10 -0
- package/src/shared/components/toolbar/Toolbar.tsx +5 -0
- package/src/shared/components/toolbar/ToolbarSeparator.module.css +13 -0
- package/src/shared/components/toolbar/ToolbarSeparator.tsx +7 -0
- package/src/shared/components/tooltip/TooltipContent.module.css +15 -0
- package/src/shared/components/tooltip/TooltipContent.tsx +23 -0
- package/src/shared/hooks/useEditorForceUpdate.tsx +30 -0
- package/src/shared/plugins/suggestion/SuggestionItem.ts +31 -0
- package/src/shared/plugins/suggestion/SuggestionListReactRenderer.ts +227 -0
- package/src/shared/plugins/suggestion/SuggestionPlugin.ts +365 -0
- package/src/shared/plugins/suggestion/components/SuggestionGroup.module.css +45 -0
- package/src/shared/plugins/suggestion/components/SuggestionGroup.tsx +134 -0
- package/src/shared/plugins/suggestion/components/SuggestionList.module.css +10 -0
- package/src/shared/plugins/suggestion/components/SuggestionList.tsx +91 -0
- package/src/style.css +7 -0
- package/src/useEditor.ts +47 -0
- package/src/vite-env.d.ts +1 -0
- package/types/src/BlockNoteExtensions.d.ts +4 -0
- package/types/src/EditorContent.d.ts +1 -0
- package/types/src/extensions/Blocks/OrderedListPlugin.d.ts +2 -0
- package/types/src/extensions/Blocks/PreviousBlockTypePlugin.d.ts +13 -0
- package/types/src/extensions/Blocks/commands/joinBackward.d.ts +14 -0
- package/types/src/extensions/Blocks/helpers/findBlock.d.ts +6 -0
- package/types/src/extensions/Blocks/helpers/setBlockHeading.d.ts +5 -0
- package/types/src/extensions/Blocks/index.d.ts +1 -0
- package/types/src/extensions/Blocks/nodes/Block.d.ts +32 -0
- package/types/src/extensions/Blocks/nodes/BlockGroup.d.ts +2 -0
- package/types/src/extensions/Blocks/nodes/Content.d.ts +5 -0
- package/types/src/extensions/Blocks/rule.d.ts +16 -0
- package/types/src/extensions/BubbleMenu/BubbleMenuExtension.d.ts +5 -0
- package/types/src/extensions/BubbleMenu/BubbleMenuPlugin.d.ts +46 -0
- package/types/src/extensions/BubbleMenu/component/BubbleMenu.d.ts +5 -0
- package/types/src/extensions/BubbleMenu/component/DropdownBlockItem.d.ts +10 -0
- package/types/src/extensions/BubbleMenu/component/LinkToolbarButton.d.ts +11 -0
- package/types/src/extensions/DraggableBlocks/DraggableBlocksExtension.d.ts +7 -0
- package/types/src/extensions/DraggableBlocks/DraggableBlocksPlugin.d.ts +18 -0
- package/types/src/extensions/DraggableBlocks/components/DragHandle.d.ts +12 -0
- package/types/src/extensions/DraggableBlocks/components/DragHandleMenu.d.ts +6 -0
- package/types/src/extensions/Hyperlinks/HyperlinkMark.d.ts +7 -0
- package/types/src/extensions/Hyperlinks/HyperlinkMenuPlugin.d.ts +2 -0
- package/types/src/extensions/Hyperlinks/menus/HyperlinkBasicMenu.d.ts +12 -0
- package/types/src/extensions/Hyperlinks/menus/HyperlinkEditMenu.d.ts +10 -0
- package/types/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInput.d.ts +39 -0
- package/types/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInputStyles.d.ts +1 -0
- package/types/src/extensions/Hyperlinks/menus/atlaskit/ToolbarComponent.d.ts +11 -0
- package/types/src/extensions/Paragraph/FixedParagraph.d.ts +1 -0
- package/types/src/extensions/Placeholder/PlaceholderExtension.d.ts +25 -0
- package/types/src/extensions/SlashMenu/SlashMenuExtension.d.ts +10 -0
- package/types/src/extensions/SlashMenu/SlashMenuItem.d.ts +43 -0
- package/types/src/extensions/SlashMenu/defaultCommands.d.ts +8 -0
- package/types/src/extensions/SlashMenu/index.d.ts +5 -0
- package/types/src/extensions/TrailingNode/TrailingNodeExtension.d.ts +10 -0
- package/types/src/extensions/UniqueID/UniqueID.d.ts +3 -0
- package/types/src/extensions/helpers/formatKeyboardShortcut.d.ts +1 -0
- package/types/src/index.d.ts +4 -0
- package/types/src/lib/atlaskit/browser.d.ts +12 -0
- package/types/src/shared/components/toolbar/SimpleToolbarButton.d.ts +16 -0
- package/types/src/shared/components/toolbar/Toolbar.d.ts +4 -0
- package/types/src/shared/components/toolbar/ToolbarSeparator.d.ts +2 -0
- package/types/src/shared/components/tooltip/TooltipContent.d.ts +15 -0
- package/types/src/shared/hooks/useEditorForceUpdate.d.ts +2 -0
- package/types/src/shared/plugins/suggestion/SuggestionItem.d.ts +29 -0
- package/types/src/shared/plugins/suggestion/SuggestionListReactRenderer.d.ts +71 -0
- package/types/src/shared/plugins/suggestion/SuggestionPlugin.d.ts +74 -0
- package/types/src/shared/plugins/suggestion/components/SuggestionGroup.d.ts +23 -0
- package/types/src/shared/plugins/suggestion/components/SuggestionList.d.ts +26 -0
- 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,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
|
+
};
|