@blocknote/core 0.8.0 → 0.8.2
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/dist/blocknote.js +594 -503
- package/dist/blocknote.js.map +1 -1
- package/dist/blocknote.umd.cjs +6 -2
- package/dist/blocknote.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +3 -3
- package/src/BlockNoteEditor.ts +18 -0
- package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +292 -0
- package/src/api/nodeConversions/nodeConversions.test.ts +236 -0
- package/src/api/nodeConversions/nodeConversions.ts +166 -35
- package/src/editor.module.css +5 -5
- package/src/extensions/Blocks/nodes/Block.module.css +4 -4
- package/src/extensions/Blocks/nodes/BlockContainer.ts +12 -3
- package/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts +2 -2
- package/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts +59 -24
- package/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts +4 -6
- package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +28 -35
- package/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.ts +2 -2
- package/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +38 -10
- package/src/shared/EditorElement.ts +3 -3
- package/src/shared/plugins/suggestion/SuggestionPlugin.ts +23 -17
- package/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts +2 -2
- package/types/src/BlockNoteEditor.d.ts +2 -0
- package/types/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.d.ts +1 -1
- package/types/src/extensions/DraggableBlocks/DraggableBlocksPlugin.d.ts +2 -2
- package/types/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.d.ts +2 -3
- package/types/src/extensions/FormattingToolbar/FormattingToolbarPlugin.d.ts +2 -3
- package/types/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.d.ts +1 -1
- package/types/src/extensions/Placeholder/localisation/index.d.ts +2 -0
- package/types/src/extensions/Placeholder/localisation/translation.d.ts +51 -0
- package/types/src/shared/EditorElement.d.ts +3 -3
- package/types/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.d.ts +1 -1
- package/types/src/extensions/Blocks/api/alertBlock.d.ts +0 -13
- package/types/src/extensions/Blocks/api/alertBlock2.d.ts +0 -13
|
@@ -10,7 +10,6 @@ import { defaultProps } from "../../extensions/Blocks/api/defaultBlocks";
|
|
|
10
10
|
import {
|
|
11
11
|
ColorStyle,
|
|
12
12
|
InlineContent,
|
|
13
|
-
Link,
|
|
14
13
|
PartialInlineContent,
|
|
15
14
|
PartialLink,
|
|
16
15
|
StyledText,
|
|
@@ -34,7 +33,7 @@ const colorStyles = new Set<ColorStyle>(["textColor", "backgroundColor"]);
|
|
|
34
33
|
* Convert a StyledText inline element to a
|
|
35
34
|
* prosemirror text node with the appropriate marks
|
|
36
35
|
*/
|
|
37
|
-
function
|
|
36
|
+
function styledTextToNodes(styledText: StyledText, schema: Schema): Node[] {
|
|
38
37
|
const marks: Mark[] = [];
|
|
39
38
|
|
|
40
39
|
for (const [style, value] of Object.entries(styledText.styles)) {
|
|
@@ -45,7 +44,22 @@ function styledTextToNode(styledText: StyledText, schema: Schema): Node {
|
|
|
45
44
|
}
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
return
|
|
47
|
+
return (
|
|
48
|
+
styledText.text
|
|
49
|
+
// Splits text & line breaks.
|
|
50
|
+
.split(/(\n)/g)
|
|
51
|
+
// If the content ends with a line break, an empty string is added to the
|
|
52
|
+
// end, which this removes.
|
|
53
|
+
.filter((text) => text.length > 0)
|
|
54
|
+
// Converts text & line breaks to nodes.
|
|
55
|
+
.map((text) => {
|
|
56
|
+
if (text === "\n") {
|
|
57
|
+
return schema.nodes["hardBreak"].create();
|
|
58
|
+
} else {
|
|
59
|
+
return schema.text(text, marks);
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
);
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
/**
|
|
@@ -58,7 +72,14 @@ function linkToNodes(link: PartialLink, schema: Schema): Node[] {
|
|
|
58
72
|
});
|
|
59
73
|
|
|
60
74
|
return styledTextArrayToNodes(link.content, schema).map((node) => {
|
|
61
|
-
|
|
75
|
+
if (node.type.name === "text") {
|
|
76
|
+
return node.mark([...node.marks, linkMark]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (node.type.name === "hardBreak") {
|
|
80
|
+
return node;
|
|
81
|
+
}
|
|
82
|
+
throw new Error("unexpected node type");
|
|
62
83
|
});
|
|
63
84
|
}
|
|
64
85
|
|
|
@@ -73,12 +94,14 @@ function styledTextArrayToNodes(
|
|
|
73
94
|
let nodes: Node[] = [];
|
|
74
95
|
|
|
75
96
|
if (typeof content === "string") {
|
|
76
|
-
nodes.push(
|
|
97
|
+
nodes.push(
|
|
98
|
+
...styledTextToNodes({ type: "text", text: content, styles: {} }, schema)
|
|
99
|
+
);
|
|
77
100
|
return nodes;
|
|
78
101
|
}
|
|
79
102
|
|
|
80
103
|
for (const styledText of content) {
|
|
81
|
-
nodes.push(
|
|
104
|
+
nodes.push(...styledTextToNodes(styledText, schema));
|
|
82
105
|
}
|
|
83
106
|
return nodes;
|
|
84
107
|
}
|
|
@@ -161,15 +184,39 @@ export function blockToNode<BSchema extends BlockSchema>(
|
|
|
161
184
|
*/
|
|
162
185
|
function contentNodeToInlineContent(contentNode: Node) {
|
|
163
186
|
const content: InlineContent[] = [];
|
|
164
|
-
|
|
165
|
-
let currentLink: Link | undefined = undefined;
|
|
187
|
+
let currentContent: InlineContent | undefined = undefined;
|
|
166
188
|
|
|
167
189
|
// Most of the logic below is for handling links because in ProseMirror links are marks
|
|
168
190
|
// while in BlockNote links are a type of inline content
|
|
169
191
|
contentNode.content.forEach((node) => {
|
|
170
|
-
|
|
192
|
+
// hardBreak nodes do not have an InlineContent equivalent, instead we
|
|
193
|
+
// add a newline to the previous node.
|
|
194
|
+
if (node.type.name === "hardBreak") {
|
|
195
|
+
if (currentContent) {
|
|
196
|
+
// Current content exists.
|
|
197
|
+
if (currentContent.type === "text") {
|
|
198
|
+
// Current content is text.
|
|
199
|
+
currentContent.text += "\n";
|
|
200
|
+
} else if (currentContent.type === "link") {
|
|
201
|
+
// Current content is a link.
|
|
202
|
+
currentContent.content[currentContent.content.length - 1].text +=
|
|
203
|
+
"\n";
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
// Current content does not exist.
|
|
207
|
+
currentContent = {
|
|
208
|
+
type: "text",
|
|
209
|
+
text: "\n",
|
|
210
|
+
styles: {},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
171
216
|
|
|
217
|
+
const styles: Styles = {};
|
|
172
218
|
let linkMark: Mark | undefined;
|
|
219
|
+
|
|
173
220
|
for (const mark of node.marks) {
|
|
174
221
|
if (mark.type.name === "link") {
|
|
175
222
|
linkMark = mark;
|
|
@@ -182,37 +229,121 @@ function contentNodeToInlineContent(contentNode: Node) {
|
|
|
182
229
|
}
|
|
183
230
|
}
|
|
184
231
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
232
|
+
// Parsing links and text.
|
|
233
|
+
// Current content exists.
|
|
234
|
+
if (currentContent) {
|
|
235
|
+
// Current content is text.
|
|
236
|
+
if (currentContent.type === "text") {
|
|
237
|
+
if (!linkMark) {
|
|
238
|
+
// Node is text (same type as current content).
|
|
239
|
+
if (
|
|
240
|
+
JSON.stringify(currentContent.styles) === JSON.stringify(styles)
|
|
241
|
+
) {
|
|
242
|
+
// Styles are the same.
|
|
243
|
+
currentContent.text += node.textContent;
|
|
244
|
+
} else {
|
|
245
|
+
// Styles are different.
|
|
246
|
+
content.push(currentContent);
|
|
247
|
+
currentContent = {
|
|
248
|
+
type: "text",
|
|
249
|
+
text: node.textContent,
|
|
250
|
+
styles,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
// Node is a link (different type to current content).
|
|
255
|
+
content.push(currentContent);
|
|
256
|
+
currentContent = {
|
|
257
|
+
type: "link",
|
|
258
|
+
href: linkMark.attrs.href,
|
|
259
|
+
content: [
|
|
260
|
+
{
|
|
261
|
+
type: "text",
|
|
262
|
+
text: node.textContent,
|
|
263
|
+
styles,
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
} else if (currentContent.type === "link") {
|
|
269
|
+
// Current content is a link.
|
|
270
|
+
if (linkMark) {
|
|
271
|
+
// Node is a link (same type as current content).
|
|
272
|
+
// Link URLs are the same.
|
|
273
|
+
if (currentContent.href === linkMark.attrs.href) {
|
|
274
|
+
// Styles are the same.
|
|
275
|
+
if (
|
|
276
|
+
JSON.stringify(
|
|
277
|
+
currentContent.content[currentContent.content.length - 1].styles
|
|
278
|
+
) === JSON.stringify(styles)
|
|
279
|
+
) {
|
|
280
|
+
currentContent.content[currentContent.content.length - 1].text +=
|
|
281
|
+
node.textContent;
|
|
282
|
+
} else {
|
|
283
|
+
// Styles are different.
|
|
284
|
+
currentContent.content.push({
|
|
285
|
+
type: "text",
|
|
286
|
+
text: node.textContent,
|
|
287
|
+
styles,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
// Link URLs are different.
|
|
292
|
+
content.push(currentContent);
|
|
293
|
+
currentContent = {
|
|
294
|
+
type: "link",
|
|
295
|
+
href: linkMark.attrs.href,
|
|
296
|
+
content: [
|
|
297
|
+
{
|
|
298
|
+
type: "text",
|
|
299
|
+
text: node.textContent,
|
|
300
|
+
styles,
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
// Node is text (different type to current content).
|
|
307
|
+
content.push(currentContent);
|
|
308
|
+
currentContent = {
|
|
199
309
|
type: "text",
|
|
200
310
|
text: node.textContent,
|
|
201
311
|
styles,
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Current content does not exist.
|
|
317
|
+
else {
|
|
318
|
+
// Node is text.
|
|
319
|
+
if (!linkMark) {
|
|
320
|
+
currentContent = {
|
|
321
|
+
type: "text",
|
|
322
|
+
text: node.textContent,
|
|
323
|
+
styles,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
// Node is a link.
|
|
327
|
+
else {
|
|
328
|
+
currentContent = {
|
|
329
|
+
type: "link",
|
|
330
|
+
href: linkMark.attrs.href,
|
|
331
|
+
content: [
|
|
332
|
+
{
|
|
333
|
+
type: "text",
|
|
334
|
+
text: node.textContent,
|
|
335
|
+
styles,
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
};
|
|
339
|
+
}
|
|
214
340
|
}
|
|
215
341
|
});
|
|
342
|
+
|
|
343
|
+
if (currentContent) {
|
|
344
|
+
content.push(currentContent);
|
|
345
|
+
}
|
|
346
|
+
|
|
216
347
|
return content;
|
|
217
348
|
}
|
|
218
349
|
|
package/src/editor.module.css
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
.bnEditor {
|
|
4
4
|
outline: none;
|
|
5
|
-
padding-inline:
|
|
5
|
+
padding-inline: 54px;
|
|
6
6
|
border-radius: 8px;
|
|
7
7
|
|
|
8
8
|
/* Define a set of colors to be used throughout the app for consistency
|
|
@@ -56,13 +56,13 @@ Tippy popups that are appended to document.body directly
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
[data-theme="light"] {
|
|
59
|
-
background-color: #
|
|
60
|
-
color: #
|
|
59
|
+
background-color: #FFFFFF;
|
|
60
|
+
color: #3F3F3F;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
[data-theme="dark"] {
|
|
64
|
-
background-color: #
|
|
65
|
-
color: #
|
|
64
|
+
background-color: #1F1F1F;
|
|
65
|
+
color: #CFCFCF;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
.dragPreview {
|
|
@@ -60,14 +60,14 @@ NESTED BLOCKS
|
|
|
60
60
|
.blockGroup
|
|
61
61
|
.blockGroup
|
|
62
62
|
> .blockOuter:not([data-prev-depth-changed])::before {
|
|
63
|
-
border-left: 1px solid #
|
|
63
|
+
border-left: 1px solid #AFAFAF;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
[data-theme="dark"]
|
|
67
67
|
.blockGroup
|
|
68
68
|
.blockGroup
|
|
69
69
|
> .blockOuter:not([data-prev-depth-changed])::before {
|
|
70
|
-
border-left: 1px solid #
|
|
70
|
+
border-left: 1px solid #7F7F7F;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
.blockGroup .blockGroup > .blockOuter[data-prev-depth-change="-2"]::before {
|
|
@@ -250,12 +250,12 @@ NESTED BLOCKS
|
|
|
250
250
|
|
|
251
251
|
[data-theme="light"] .isEmpty .inlineContent:before,
|
|
252
252
|
.isFilter .inlineContent:before {
|
|
253
|
-
color: #
|
|
253
|
+
color: #CFCFCF;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
[data-theme="dark"] .isEmpty .inlineContent:before,
|
|
257
257
|
.isFilter .inlineContent:before {
|
|
258
|
-
color: #
|
|
258
|
+
color: #7F7F7F;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
/* TODO: would be nicer if defined from code */
|
|
@@ -281,10 +281,19 @@ export const BlockContainer = Node.create<IBlock>({
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
// Deletes next block and adds its text content to the nearest previous block.
|
|
284
|
-
|
|
284
|
+
|
|
285
285
|
if (dispatch) {
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
dispatch(
|
|
287
|
+
state.tr
|
|
288
|
+
.deleteRange(startPos, startPos + contentNode.nodeSize)
|
|
289
|
+
.replace(
|
|
290
|
+
prevBlockEndPos - 1,
|
|
291
|
+
startPos,
|
|
292
|
+
new Slice(contentNode.content, 0, 0)
|
|
293
|
+
)
|
|
294
|
+
.scrollIntoView()
|
|
295
|
+
);
|
|
296
|
+
|
|
288
297
|
state.tr.setSelection(
|
|
289
298
|
new TextSelection(state.doc.resolve(prevBlockEndPos - 1))
|
|
290
299
|
);
|
|
@@ -12,12 +12,12 @@ export type BlockSideMenuStaticParams<BSchema extends BlockSchema> = {
|
|
|
12
12
|
|
|
13
13
|
freezeMenu: () => void;
|
|
14
14
|
unfreezeMenu: () => void;
|
|
15
|
+
|
|
16
|
+
getReferenceRect: () => DOMRect;
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
export type BlockSideMenuDynamicParams<BSchema extends BlockSchema> = {
|
|
18
20
|
block: Block<BSchema>;
|
|
19
|
-
|
|
20
|
-
referenceRect: DOMRect;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export type BlockSideMenu<BSchema extends BlockSchema> = EditorElement<
|
|
@@ -250,6 +250,8 @@ export class BlockMenuView<BSchema extends BlockSchema> {
|
|
|
250
250
|
menuOpen = false;
|
|
251
251
|
menuFrozen = false;
|
|
252
252
|
|
|
253
|
+
private lastPosition: DOMRect | undefined;
|
|
254
|
+
|
|
253
255
|
constructor({
|
|
254
256
|
tiptapEditor,
|
|
255
257
|
editor,
|
|
@@ -272,9 +274,6 @@ export class BlockMenuView<BSchema extends BlockSchema> {
|
|
|
272
274
|
// Shows or updates menu position whenever the cursor moves, if the menu isn't frozen.
|
|
273
275
|
document.body.addEventListener("mousemove", this.onMouseMove, true);
|
|
274
276
|
|
|
275
|
-
// Makes menu scroll with the page.
|
|
276
|
-
document.addEventListener("scroll", this.onScroll);
|
|
277
|
-
|
|
278
277
|
// Hides and unfreezes the menu whenever the user selects the editor with the mouse or presses a key.
|
|
279
278
|
// TODO: Better integration with suggestions menu and only editor scope?
|
|
280
279
|
document.body.addEventListener("mousedown", this.onMouseDown, true);
|
|
@@ -320,7 +319,7 @@ export class BlockMenuView<BSchema extends BlockSchema> {
|
|
|
320
319
|
};
|
|
321
320
|
|
|
322
321
|
/**
|
|
323
|
-
* If the event is outside
|
|
322
|
+
* If the event is outside the editor contents,
|
|
324
323
|
* we dispatch a fake event, so that we can still drop the content
|
|
325
324
|
* when dragging / dropping to the side of the editor
|
|
326
325
|
*/
|
|
@@ -375,11 +374,45 @@ export class BlockMenuView<BSchema extends BlockSchema> {
|
|
|
375
374
|
return;
|
|
376
375
|
}
|
|
377
376
|
|
|
378
|
-
// Editor itself may have padding or other styling which affects
|
|
379
|
-
// the first child (i.e. the
|
|
377
|
+
// Editor itself may have padding or other styling which affects
|
|
378
|
+
// size/position, so we get the boundingRect of the first child (i.e. the
|
|
379
|
+
// blockGroup that wraps all blocks in the editor) for more accurate side
|
|
380
|
+
// menu placement.
|
|
380
381
|
const editorBoundingBox = (
|
|
381
382
|
this.ttEditor.view.dom.firstChild! as HTMLElement
|
|
382
383
|
).getBoundingClientRect();
|
|
384
|
+
// We want the full area of the editor to check if the cursor is hovering
|
|
385
|
+
// above it though.
|
|
386
|
+
const editorOuterBoundingBox =
|
|
387
|
+
this.ttEditor.view.dom.getBoundingClientRect();
|
|
388
|
+
const cursorWithinEditor =
|
|
389
|
+
event.clientX >= editorOuterBoundingBox.left &&
|
|
390
|
+
event.clientX <= editorOuterBoundingBox.right &&
|
|
391
|
+
event.clientY >= editorOuterBoundingBox.top &&
|
|
392
|
+
event.clientY <= editorOuterBoundingBox.bottom;
|
|
393
|
+
|
|
394
|
+
// Doesn't update if the mouse hovers an element that's over the editor but
|
|
395
|
+
// isn't a part of it or the side menu.
|
|
396
|
+
if (
|
|
397
|
+
// Cursor is within the editor area
|
|
398
|
+
cursorWithinEditor &&
|
|
399
|
+
// An element is hovered
|
|
400
|
+
event &&
|
|
401
|
+
event.target &&
|
|
402
|
+
// Element is outside the editor
|
|
403
|
+
this.ttEditor.view.dom !== event.target &&
|
|
404
|
+
!this.ttEditor.view.dom.contains(event.target as HTMLElement) &&
|
|
405
|
+
// Element is outside the side menu
|
|
406
|
+
this.blockMenu.element !== event.target &&
|
|
407
|
+
!this.blockMenu.element?.contains(event.target as HTMLElement)
|
|
408
|
+
) {
|
|
409
|
+
if (this.menuOpen) {
|
|
410
|
+
this.menuOpen = false;
|
|
411
|
+
this.blockMenu.hide();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
383
416
|
|
|
384
417
|
this.horizontalPosAnchor = editorBoundingBox.x;
|
|
385
418
|
|
|
@@ -429,12 +462,6 @@ export class BlockMenuView<BSchema extends BlockSchema> {
|
|
|
429
462
|
}
|
|
430
463
|
};
|
|
431
464
|
|
|
432
|
-
onScroll = () => {
|
|
433
|
-
if (this.menuOpen) {
|
|
434
|
-
this.blockMenu.render(this.getDynamicParams(), false);
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
|
|
438
465
|
destroy() {
|
|
439
466
|
if (this.menuOpen) {
|
|
440
467
|
this.menuOpen = false;
|
|
@@ -445,7 +472,6 @@ export class BlockMenuView<BSchema extends BlockSchema> {
|
|
|
445
472
|
this.ttEditor.view.dom.removeEventListener("dragstart", this.onDragStart);
|
|
446
473
|
document.body.removeEventListener("drop", this.onDrop);
|
|
447
474
|
document.body.removeEventListener("mousedown", this.onMouseDown);
|
|
448
|
-
document.removeEventListener("scroll", this.onScroll);
|
|
449
475
|
document.body.removeEventListener("keydown", this.onKeyDown);
|
|
450
476
|
}
|
|
451
477
|
|
|
@@ -514,23 +540,32 @@ export class BlockMenuView<BSchema extends BlockSchema> {
|
|
|
514
540
|
unfreezeMenu: () => {
|
|
515
541
|
this.menuFrozen = false;
|
|
516
542
|
},
|
|
543
|
+
getReferenceRect: () => {
|
|
544
|
+
if (!this.menuOpen) {
|
|
545
|
+
if (this.lastPosition === undefined) {
|
|
546
|
+
throw new Error(
|
|
547
|
+
"Attempted to access block reference rect before rendering block side menu."
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return this.lastPosition;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const blockContent = this.hoveredBlock!.firstChild! as HTMLElement;
|
|
555
|
+
const blockContentBoundingBox = blockContent.getBoundingClientRect();
|
|
556
|
+
if (this.horizontalPosAnchoredAtRoot) {
|
|
557
|
+
blockContentBoundingBox.x = this.horizontalPosAnchor;
|
|
558
|
+
}
|
|
559
|
+
this.lastPosition = blockContentBoundingBox;
|
|
560
|
+
|
|
561
|
+
return blockContentBoundingBox;
|
|
562
|
+
},
|
|
517
563
|
};
|
|
518
564
|
}
|
|
519
565
|
|
|
520
566
|
getDynamicParams(): BlockSideMenuDynamicParams<BSchema> {
|
|
521
|
-
const blockContent = this.hoveredBlock!.firstChild! as HTMLElement;
|
|
522
|
-
const blockContentBoundingBox = blockContent.getBoundingClientRect();
|
|
523
|
-
|
|
524
567
|
return {
|
|
525
568
|
block: this.editor.getBlock(this.hoveredBlock!.getAttribute("data-id")!)!,
|
|
526
|
-
referenceRect: new DOMRect(
|
|
527
|
-
this.horizontalPosAnchoredAtRoot
|
|
528
|
-
? this.horizontalPosAnchor
|
|
529
|
-
: blockContentBoundingBox.x,
|
|
530
|
-
blockContentBoundingBox.y,
|
|
531
|
-
blockContentBoundingBox.width,
|
|
532
|
-
blockContentBoundingBox.height
|
|
533
|
-
),
|
|
534
569
|
};
|
|
535
570
|
}
|
|
536
571
|
}
|
|
@@ -4,15 +4,13 @@ import { BlockSchema } from "../Blocks/api/blockTypes";
|
|
|
4
4
|
|
|
5
5
|
export type FormattingToolbarStaticParams<BSchema extends BlockSchema> = {
|
|
6
6
|
editor: BlockNoteEditor<BSchema>;
|
|
7
|
-
};
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
referenceRect: DOMRect;
|
|
8
|
+
getReferenceRect: () => DOMRect;
|
|
11
9
|
};
|
|
12
10
|
|
|
13
|
-
export type
|
|
14
|
-
|
|
15
|
-
>;
|
|
11
|
+
export type FormattingToolbarDynamicParams = {};
|
|
12
|
+
|
|
13
|
+
export type FormattingToolbar = EditorElement<FormattingToolbarDynamicParams>;
|
|
16
14
|
export type FormattingToolbarFactory<BSchema extends BlockSchema> =
|
|
17
15
|
ElementFactory<
|
|
18
16
|
FormattingToolbarStaticParams<BSchema>,
|
|
@@ -9,7 +9,6 @@ import { EditorView } from "prosemirror-view";
|
|
|
9
9
|
import { BlockNoteEditor, BlockSchema } from "../..";
|
|
10
10
|
import {
|
|
11
11
|
FormattingToolbar,
|
|
12
|
-
FormattingToolbarDynamicParams,
|
|
13
12
|
FormattingToolbarFactory,
|
|
14
13
|
FormattingToolbarStaticParams,
|
|
15
14
|
} from "./FormattingToolbarFactoryTypes";
|
|
@@ -44,6 +43,8 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
|
|
|
44
43
|
|
|
45
44
|
public prevWasEditable: boolean | null = null;
|
|
46
45
|
|
|
46
|
+
private lastPosition: DOMRect | undefined;
|
|
47
|
+
|
|
47
48
|
public shouldShow: (props: {
|
|
48
49
|
view: EditorView;
|
|
49
50
|
state: EditorState;
|
|
@@ -80,8 +81,6 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
|
|
|
80
81
|
|
|
81
82
|
this.ttEditor.on("focus", this.focusHandler);
|
|
82
83
|
this.ttEditor.on("blur", this.blurHandler);
|
|
83
|
-
|
|
84
|
-
document.addEventListener("scroll", this.scrollHandler);
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
viewMousedownHandler = () => {
|
|
@@ -110,11 +109,15 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
|
|
|
110
109
|
return;
|
|
111
110
|
}
|
|
112
111
|
|
|
112
|
+
// Checks if the focus is moving to an element outside the editor. If it is,
|
|
113
|
+
// the toolbar is hidden.
|
|
113
114
|
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
// An element is clicked.
|
|
116
|
+
event &&
|
|
117
|
+
event.relatedTarget &&
|
|
118
|
+
// Element is outside the toolbar.
|
|
119
|
+
(this.formattingToolbar.element === (event.relatedTarget as Node) ||
|
|
120
|
+
this.formattingToolbar.element?.contains(event.relatedTarget as Node))
|
|
118
121
|
) {
|
|
119
122
|
return;
|
|
120
123
|
}
|
|
@@ -125,12 +128,6 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
|
|
|
125
128
|
}
|
|
126
129
|
};
|
|
127
130
|
|
|
128
|
-
scrollHandler = () => {
|
|
129
|
-
if (this.toolbarIsOpen) {
|
|
130
|
-
this.formattingToolbar.render(this.getDynamicParams(), false);
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
|
|
134
131
|
update(view: EditorView, oldState?: EditorState) {
|
|
135
132
|
const { state, composing } = view;
|
|
136
133
|
const { doc, selection } = state;
|
|
@@ -166,15 +163,9 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
|
|
|
166
163
|
!this.preventShow &&
|
|
167
164
|
(shouldShow || this.preventHide)
|
|
168
165
|
) {
|
|
169
|
-
this.formattingToolbar.render(
|
|
166
|
+
this.formattingToolbar.render({}, true);
|
|
170
167
|
this.toolbarIsOpen = true;
|
|
171
168
|
|
|
172
|
-
// TODO: Is this necessary? Also for other menu plugins.
|
|
173
|
-
// Listener stops focus moving to the menu on click.
|
|
174
|
-
this.formattingToolbar.element!.addEventListener("mousedown", (event) =>
|
|
175
|
-
event.preventDefault()
|
|
176
|
-
);
|
|
177
|
-
|
|
178
169
|
return;
|
|
179
170
|
}
|
|
180
171
|
|
|
@@ -184,7 +175,7 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
|
|
|
184
175
|
!this.preventShow &&
|
|
185
176
|
(shouldShow || this.preventHide)
|
|
186
177
|
) {
|
|
187
|
-
this.formattingToolbar.render(
|
|
178
|
+
this.formattingToolbar.render({}, false);
|
|
188
179
|
return;
|
|
189
180
|
}
|
|
190
181
|
|
|
@@ -197,12 +188,6 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
|
|
|
197
188
|
this.formattingToolbar.hide();
|
|
198
189
|
this.toolbarIsOpen = false;
|
|
199
190
|
|
|
200
|
-
// Listener stops focus moving to the menu on click.
|
|
201
|
-
this.formattingToolbar.element!.removeEventListener(
|
|
202
|
-
"mousedown",
|
|
203
|
-
(event) => event.preventDefault()
|
|
204
|
-
);
|
|
205
|
-
|
|
206
191
|
return;
|
|
207
192
|
}
|
|
208
193
|
}
|
|
@@ -214,8 +199,6 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
|
|
|
214
199
|
|
|
215
200
|
this.ttEditor.off("focus", this.focusHandler);
|
|
216
201
|
this.ttEditor.off("blur", this.blurHandler);
|
|
217
|
-
|
|
218
|
-
document.removeEventListener("scroll", this.scrollHandler);
|
|
219
202
|
}
|
|
220
203
|
|
|
221
204
|
getSelectionBoundingBox() {
|
|
@@ -241,12 +224,22 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
|
|
|
241
224
|
getStaticParams(): FormattingToolbarStaticParams<BSchema> {
|
|
242
225
|
return {
|
|
243
226
|
editor: this.editor,
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
227
|
+
getReferenceRect: () => {
|
|
228
|
+
if (!this.toolbarIsOpen) {
|
|
229
|
+
if (this.lastPosition === undefined) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
"Attempted to access selection reference rect before rendering formatting toolbar."
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return this.lastPosition;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const selectionBoundingBox = this.getSelectionBoundingBox();
|
|
239
|
+
this.lastPosition = selectionBoundingBox;
|
|
240
|
+
|
|
241
|
+
return selectionBoundingBox;
|
|
242
|
+
},
|
|
250
243
|
};
|
|
251
244
|
}
|
|
252
245
|
}
|
|
@@ -3,13 +3,13 @@ import { EditorElement, ElementFactory } from "../../shared/EditorElement";
|
|
|
3
3
|
export type HyperlinkToolbarStaticParams = {
|
|
4
4
|
editHyperlink: (url: string, text: string) => void;
|
|
5
5
|
deleteHyperlink: () => void;
|
|
6
|
+
|
|
7
|
+
getReferenceRect: () => DOMRect;
|
|
6
8
|
};
|
|
7
9
|
|
|
8
10
|
export type HyperlinkToolbarDynamicParams = {
|
|
9
11
|
url: string;
|
|
10
12
|
text: string;
|
|
11
|
-
|
|
12
|
-
referenceRect: DOMRect;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export type HyperlinkToolbar = EditorElement<HyperlinkToolbarDynamicParams>;
|