@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.
- package/LICENSE +373 -0
- package/README.md +4 -2
- package/dist/blocknote.js +3461 -2429
- package/dist/blocknote.js.map +1 -1
- package/dist/blocknote.umd.cjs +35 -71
- package/dist/blocknote.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +9 -7
- package/src/BlockNoteExtensions.ts +10 -17
- package/src/BlockNoteTheme.ts +150 -0
- package/src/EditorContent.tsx +2 -1
- package/src/extensions/Blocks/BlockAttributes.ts +12 -0
- package/src/extensions/Blocks/MultipleNodeSelection.ts +87 -0
- package/src/extensions/Blocks/OrderedListPlugin.ts +2 -2
- package/src/extensions/Blocks/PreviousBlockTypePlugin.ts +8 -2
- package/src/extensions/Blocks/helpers/findBlock.ts +1 -1
- package/src/extensions/Blocks/nodes/Block.module.css +37 -37
- package/src/extensions/Blocks/nodes/Block.ts +89 -45
- package/src/extensions/Blocks/nodes/BlockGroup.ts +19 -2
- package/src/extensions/Blocks/nodes/Content.ts +15 -2
- package/src/extensions/BubbleMenu/BubbleMenuExtension.tsx +10 -2
- package/src/extensions/BubbleMenu/component/BubbleMenu.tsx +122 -98
- package/src/extensions/BubbleMenu/component/LinkToolbarButton.tsx +8 -8
- package/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx +143 -33
- package/src/extensions/DraggableBlocks/components/DragHandle.tsx +15 -21
- package/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx +8 -7
- package/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +31 -66
- package/src/extensions/Hyperlinks/menus/EditHyperlinkMenu.tsx +44 -0
- package/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItem.tsx +34 -0
- package/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemIcon.tsx +31 -0
- package/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemInput.tsx +40 -0
- package/src/extensions/Hyperlinks/menus/HoverHyperlinkMenu.tsx +37 -0
- package/src/extensions/Hyperlinks/menus/HyperlinkMenu.tsx +63 -0
- package/src/extensions/SlashMenu/SlashMenuItem.ts +3 -1
- package/src/extensions/SlashMenu/defaultCommands.tsx +4 -4
- package/src/extensions/TrailingNode/TrailingNodeExtension.ts +8 -5
- package/src/shared/components/toolbar/Toolbar.tsx +8 -3
- package/src/shared/components/toolbar/ToolbarButton.tsx +57 -0
- package/src/shared/components/toolbar/ToolbarDropdown.tsx +35 -0
- package/src/shared/components/toolbar/ToolbarDropdownItem.tsx +35 -0
- package/src/shared/components/toolbar/ToolbarDropdownTarget.tsx +31 -0
- package/src/shared/plugins/suggestion/SuggestionItem.ts +3 -1
- package/src/shared/plugins/suggestion/{SuggestionListReactRenderer.ts → SuggestionListReactRenderer.tsx} +13 -4
- package/src/shared/plugins/suggestion/components/SuggestionGroup.tsx +6 -93
- package/src/shared/plugins/suggestion/components/SuggestionGroupItem.tsx +82 -0
- package/src/shared/plugins/suggestion/components/SuggestionList.tsx +24 -23
- package/src/useEditor.ts +4 -0
- package/src/utils.ts +12 -0
- package/types/src/BlockNoteExtensions.d.ts +3 -0
- package/types/src/BlockNoteTheme.d.ts +2 -0
- package/types/src/commands/indentation.d.ts +2 -0
- package/types/src/extensions/Blocks/BlockAttributes.d.ts +2 -0
- package/types/src/extensions/Blocks/MultipleNodeSelection.d.ts +24 -0
- package/types/src/extensions/Blocks/nodes/Block.d.ts +1 -1
- package/types/src/extensions/BubbleMenu/component/LinkToolbarButton.d.ts +2 -2
- package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenu.d.ts +11 -0
- package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItem.d.ts +13 -0
- package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemIcon.d.ts +8 -0
- package/types/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemInput.d.ts +9 -0
- package/types/src/extensions/Hyperlinks/menus/HoverHyperlinkMenu.d.ts +12 -0
- package/types/src/extensions/Hyperlinks/menus/HyperlinkMenu.d.ts +21 -0
- package/types/src/extensions/Hyperlinks/menus/helpers/PanelTextInput.d.ts +39 -0
- package/types/src/extensions/Hyperlinks/menus/helpers/PanelTextInputStyles.d.ts +3 -0
- package/types/src/extensions/Hyperlinks/menus/helpers/ToolbarComponent.d.ts +13 -0
- package/types/src/extensions/SlashMenu/SlashMenuItem.d.ts +4 -7
- package/types/src/extensions/TrailingNode/TrailingNodeExtension.d.ts +3 -0
- package/types/src/nodes/ChildgroupNode.d.ts +28 -0
- package/types/src/nodes/patchNodes.d.ts +1 -0
- package/types/src/plugins/TreeViewPlugin/index.d.ts +2 -0
- package/types/src/plugins/animation.d.ts +2 -0
- package/types/src/react/BlockNoteComposer.d.ts +17 -0
- package/types/src/react/BlockNotePlugin.d.ts +1 -0
- package/types/src/react/index.d.ts +3 -0
- package/types/src/react/useBlockNoteSetup.d.ts +2 -0
- package/types/src/registerBlockNote.d.ts +2 -0
- package/types/src/shared/components/toolbar/SimpleToolbarButton.d.ts +2 -3
- package/types/src/shared/components/toolbar/SimpleToolbarDropdown.d.ts +11 -0
- package/types/src/shared/components/toolbar/SimpleToolbarDropdownItem.d.ts +11 -0
- package/types/src/shared/components/toolbar/Toolbar.d.ts +2 -2
- package/types/src/shared/components/toolbar/ToolbarButton.d.ts +15 -0
- package/types/src/shared/components/toolbar/ToolbarDropdown.d.ts +17 -0
- package/types/src/shared/components/toolbar/ToolbarDropdownItem.d.ts +11 -0
- package/types/src/shared/components/toolbar/ToolbarDropdownTarget.d.ts +8 -0
- package/types/src/shared/plugins/suggestion/SuggestionItem.d.ts +2 -4
- package/types/src/shared/plugins/suggestion/components/SuggestionGroupItem.d.ts +9 -0
- package/types/src/shared/plugins/suggestion/components/SuggestionList.d.ts +0 -15
- package/types/src/themes/BlockNoteEditorTheme.d.ts +11 -0
- package/types/src/useEditor.d.ts +3 -0
- package/types/src/utils.d.ts +2 -0
- package/src/extensions/Blocks/nodes/README.md +0 -26
- package/src/extensions/BubbleMenu/component/DropdownBlockItem.module.css +0 -13
- package/src/extensions/BubbleMenu/component/DropdownBlockItem.tsx +0 -25
- package/src/extensions/DraggableBlocks/components/DragHandle.module.css +0 -33
- package/src/extensions/DraggableBlocks/components/DragHandleMenu.module.css +0 -10
- package/src/extensions/Hyperlinks/menus/HyperlinkBasicMenu.tsx +0 -59
- package/src/extensions/Hyperlinks/menus/HyperlinkEditMenu.tsx +0 -72
- package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInput.tsx +0 -173
- package/src/extensions/Hyperlinks/menus/atlaskit/PanelTextInputStyles.ts +0 -36
- package/src/extensions/Hyperlinks/menus/atlaskit/README.md +0 -1
- package/src/extensions/Hyperlinks/menus/atlaskit/ToolbarComponent.tsx +0 -61
- package/src/extensions/helpers/formatKeyboardShortcut.ts +0 -9
- package/src/lib/atlaskit/browser.ts +0 -47
- package/src/shared/components/toolbar/SimpleToolbarButton.module.css +0 -13
- package/src/shared/components/toolbar/SimpleToolbarButton.tsx +0 -56
- package/src/shared/components/toolbar/Toolbar.module.css +0 -10
- package/src/shared/components/toolbar/ToolbarSeparator.module.css +0 -13
- package/src/shared/components/toolbar/ToolbarSeparator.tsx +0 -7
- package/src/shared/plugins/suggestion/components/SuggestionGroup.module.css +0 -45
- 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
|
-
|
|
6
|
-
|
|
7
|
-
} from "../../../shared/components/toolbar/
|
|
8
|
-
import {
|
|
5
|
+
ToolbarButton,
|
|
6
|
+
ToolbarButtonProps,
|
|
7
|
+
} from "../../../shared/components/toolbar/ToolbarButton";
|
|
8
|
+
import { EditHyperlinkMenu } from "../../Hyperlinks/menus/EditHyperlinkMenu";
|
|
9
9
|
|
|
10
|
-
type Props =
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
186
|
+
|
|
187
|
+
let pos = blockPositionFromCoords(coords, view);
|
|
98
188
|
if (pos != null) {
|
|
99
|
-
view.
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
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
|
-
<
|
|
90
|
-
size={24}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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
|
-
<
|
|
11
|
-
<
|
|
12
|
-
|
|
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
|
|
7
|
-
import {
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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(
|
|
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
|
+
};
|