@blocknote/core 0.8.0 → 0.8.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.
@@ -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 styledTextToNode(styledText: StyledText, schema: Schema): Node {
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 schema.text(styledText.text, marks);
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
- return node.mark([...node.marks, linkMark]);
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(schema.text(content));
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(styledTextToNode(styledText, schema));
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
- const styles: Styles = {};
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
- if (linkMark && currentLink && linkMark.attrs.href === currentLink.href) {
186
- // if the node is a link that matches the current link, add it to the current link
187
- currentLink.content.push({
188
- type: "text",
189
- text: node.textContent,
190
- styles,
191
- });
192
- } else if (linkMark) {
193
- // if the node is a link that doesn't match the current link, create a new link
194
- currentLink = {
195
- type: "link",
196
- href: linkMark.attrs.href,
197
- content: [
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
- content.push(currentLink);
206
- } else {
207
- // if the node is not a link, add it to the content
208
- content.push({
209
- type: "text",
210
- text: node.textContent,
211
- styles,
212
- });
213
- currentLink = undefined;
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
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  .bnEditor {
4
4
  outline: none;
5
- padding-inline: 50px;
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: #ffffff;
60
- color: #444444;
59
+ background-color: #FFFFFF;
60
+ color: #3F3F3F;
61
61
  }
62
62
 
63
63
  [data-theme="dark"] {
64
- background-color: #444444;
65
- color: #dddddd;
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 #cccccc;
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 #999999;
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: #cccccc;
253
+ color: #CFCFCF;
254
254
  }
255
255
 
256
256
  [data-theme="dark"] .isEmpty .inlineContent:before,
257
257
  .isFilter .inlineContent:before {
258
- color: #999999;
258
+ color: #7F7F7F;
259
259
  }
260
260
 
261
261
  /* TODO: would be nicer if defined from code */
@@ -320,7 +320,7 @@ export class BlockMenuView<BSchema extends BlockSchema> {
320
320
  };
321
321
 
322
322
  /**
323
- * If the event is outside of the editor contents,
323
+ * If the event is outside the editor contents,
324
324
  * we dispatch a fake event, so that we can still drop the content
325
325
  * when dragging / dropping to the side of the editor
326
326
  */
@@ -375,11 +375,45 @@ export class BlockMenuView<BSchema extends BlockSchema> {
375
375
  return;
376
376
  }
377
377
 
378
- // Editor itself may have padding or other styling which affects size/position, so we get the boundingRect of
379
- // the first child (i.e. the blockGroup that wraps all blocks in the editor) for a more accurate bounding box.
378
+ // Editor itself may have padding or other styling which affects
379
+ // size/position, so we get the boundingRect of the first child (i.e. the
380
+ // blockGroup that wraps all blocks in the editor) for more accurate side
381
+ // menu placement.
380
382
  const editorBoundingBox = (
381
383
  this.ttEditor.view.dom.firstChild! as HTMLElement
382
384
  ).getBoundingClientRect();
385
+ // We want the full area of the editor to check if the cursor is hovering
386
+ // above it though.
387
+ const editorOuterBoundingBox =
388
+ this.ttEditor.view.dom.getBoundingClientRect();
389
+ const cursorWithinEditor =
390
+ event.clientX >= editorOuterBoundingBox.left &&
391
+ event.clientX <= editorOuterBoundingBox.right &&
392
+ event.clientY >= editorOuterBoundingBox.top &&
393
+ event.clientY <= editorOuterBoundingBox.bottom;
394
+
395
+ // Doesn't update if the mouse hovers an element that's over the editor but
396
+ // isn't a part of it or the side menu.
397
+ if (
398
+ // Cursor is within the editor area
399
+ cursorWithinEditor &&
400
+ // An element is hovered
401
+ event &&
402
+ event.target &&
403
+ // Element is outside the editor
404
+ this.ttEditor.view.dom !== event.target &&
405
+ !this.ttEditor.view.dom.contains(event.target as HTMLElement) &&
406
+ // Element is outside the side menu
407
+ this.blockMenu.element !== event.target &&
408
+ !this.blockMenu.element?.contains(event.target as HTMLElement)
409
+ ) {
410
+ if (this.menuOpen) {
411
+ this.menuOpen = false;
412
+ this.blockMenu.hide();
413
+ }
414
+
415
+ return;
416
+ }
383
417
 
384
418
  this.horizontalPosAnchor = editorBoundingBox.x;
385
419
 
@@ -430,6 +464,14 @@ export class BlockMenuView<BSchema extends BlockSchema> {
430
464
  };
431
465
 
432
466
  onScroll = () => {
467
+ // Editor itself may have padding or other styling which affects size/position, so we get the boundingRect of
468
+ // the first child (i.e. the blockGroup that wraps all blocks in the editor) for a more accurate bounding box.
469
+ const editorBoundingBox = (
470
+ this.ttEditor.view.dom.firstChild! as HTMLElement
471
+ ).getBoundingClientRect();
472
+
473
+ this.horizontalPosAnchor = editorBoundingBox.x;
474
+
433
475
  if (this.menuOpen) {
434
476
  this.blockMenu.render(this.getDynamicParams(), false);
435
477
  }
@@ -110,11 +110,15 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
110
110
  return;
111
111
  }
112
112
 
113
+ // Checks if the focus is moving to an element outside the editor. If it is,
114
+ // the toolbar is hidden.
113
115
  if (
114
- event?.relatedTarget &&
115
- this.formattingToolbar.element?.parentNode?.contains(
116
- event.relatedTarget as Node
117
- )
116
+ // An element is clicked.
117
+ event &&
118
+ event.relatedTarget &&
119
+ // Element is outside the toolbar.
120
+ (this.formattingToolbar.element === (event.relatedTarget as Node) ||
121
+ this.formattingToolbar.element?.contains(event.relatedTarget as Node))
118
122
  ) {
119
123
  return;
120
124
  }
@@ -169,12 +173,6 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
169
173
  this.formattingToolbar.render(this.getDynamicParams(), true);
170
174
  this.toolbarIsOpen = true;
171
175
 
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
176
  return;
179
177
  }
180
178
 
@@ -197,12 +195,6 @@ export class FormattingToolbarView<BSchema extends BlockSchema> {
197
195
  this.formattingToolbar.hide();
198
196
  this.toolbarIsOpen = false;
199
197
 
200
- // Listener stops focus moving to the menu on click.
201
- this.formattingToolbar.element!.removeEventListener(
202
- "mousedown",
203
- (event) => event.preventDefault()
204
- );
205
-
206
198
  return;
207
199
  }
208
200
  }
@@ -57,6 +57,7 @@ class HyperlinkToolbarView {
57
57
  };
58
58
 
59
59
  this.editor.view.dom.addEventListener("mouseover", this.mouseOverHandler);
60
+ document.addEventListener("click", this.clickHandler, true);
60
61
  document.addEventListener("scroll", this.scrollHandler);
61
62
  }
62
63
 
@@ -101,6 +102,24 @@ class HyperlinkToolbarView {
101
102
  return false;
102
103
  };
103
104
 
105
+ clickHandler = (event: MouseEvent) => {
106
+ if (
107
+ // Toolbar is open.
108
+ this.hyperlinkMark &&
109
+ // An element is clicked.
110
+ event &&
111
+ event.target &&
112
+ // Element is outside the editor.
113
+ this.editor.view.dom !== (event.target as Node) &&
114
+ !this.editor.view.dom.contains(event.target as Node) &&
115
+ // Element is outside the toolbar.
116
+ this.hyperlinkToolbar.element !== (event.target as Node) &&
117
+ !this.hyperlinkToolbar.element?.contains(event.target as Node)
118
+ ) {
119
+ this.hyperlinkToolbar.hide();
120
+ }
121
+ };
122
+
104
123
  scrollHandler = () => {
105
124
  if (this.hyperlinkMark !== undefined) {
106
125
  this.hyperlinkToolbar.render(this.getDynamicParams(), false);
@@ -105,6 +105,7 @@ export declare class BlockNoteEditor<BSchema extends BlockSchema = DefaultBlockS
105
105
  blockCache: WeakMap<Node, Block<BSchema>>;
106
106
  readonly schema: BSchema;
107
107
  get domElement(): HTMLDivElement;
108
+ isFocused(): boolean;
108
109
  focus(): void;
109
110
  constructor(options?: Partial<BlockNoteEditorOptions<BSchema>>);
110
111
  /**
@@ -32,7 +32,7 @@ export declare class BlockMenuView<BSchema extends BlockSchema> {
32
32
  */
33
33
  onDrop: (event: DragEvent) => void;
34
34
  /**
35
- * If the event is outside of the editor contents,
35
+ * If the event is outside the editor contents,
36
36
  * we dispatch a fake event, so that we can still drop the content
37
37
  * when dragging / dropping to the side of the editor
38
38
  */
@@ -0,0 +1,2 @@
1
+ import i18next from "i18next";
2
+ export default i18next;
@@ -0,0 +1,51 @@
1
+ export declare const translation: {
2
+ en: {
3
+ translation: {
4
+ placeholder: string;
5
+ heading: string;
6
+ list: string;
7
+ };
8
+ };
9
+ de: {
10
+ translation: {
11
+ placeholder: string;
12
+ heading: string;
13
+ list: string;
14
+ };
15
+ };
16
+ it: {
17
+ translation: {
18
+ placeholder: string;
19
+ heading: string;
20
+ list: string;
21
+ };
22
+ };
23
+ es: {
24
+ translation: {
25
+ placeholder: string;
26
+ heading: string;
27
+ list: string;
28
+ };
29
+ };
30
+ fr: {
31
+ translation: {
32
+ placeholder: string;
33
+ heading: string;
34
+ list: string;
35
+ };
36
+ };
37
+ fi: {
38
+ translation: {
39
+ placeholder: string;
40
+ heading: string;
41
+ list: string;
42
+ };
43
+ };
44
+ sv: {
45
+ translation: {
46
+ placeholder: string;
47
+ heading: string;
48
+ list: string;
49
+ };
50
+ };
51
+ };
@@ -1,13 +0,0 @@
1
- export declare const Alert: import("./blockTypes").BlockSpec<"alert", {
2
- readonly textAlignment: {
3
- default: "left";
4
- values: readonly ["left", "center", "right", "justify"];
5
- };
6
- readonly textColor: {
7
- default: "black";
8
- };
9
- readonly type: {
10
- readonly default: "warning";
11
- readonly values: readonly ["warning", "error", "info", "success"];
12
- };
13
- }>;
@@ -1,13 +0,0 @@
1
- export declare const Alert2: import("./blockTypes").BlockSpec<"alert2", {
2
- readonly textAlignment: {
3
- default: "left";
4
- values: readonly ["left", "center", "right", "justify"];
5
- };
6
- readonly textColor: {
7
- default: "black";
8
- };
9
- readonly type: {
10
- readonly default: "warning";
11
- readonly values: readonly ["warning", "error", "info", "success"];
12
- };
13
- }>;