@blocknote/core 0.23.5 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/blocknote.js +750 -610
  2. package/dist/blocknote.js.map +1 -1
  3. package/dist/blocknote.umd.cjs +6 -6
  4. package/dist/blocknote.umd.cjs.map +1 -1
  5. package/dist/tsconfig.tsbuildinfo +1 -1
  6. package/dist/webpack-stats.json +1 -1
  7. package/package.json +3 -3
  8. package/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap +492 -0
  9. package/src/api/blockManipulation/commands/insertBlocks/insertBlocks.test.ts +6 -0
  10. package/src/api/clipboard/toClipboard/copyExtension.ts +37 -2
  11. package/src/api/nodeConversions/blockToNode.ts +5 -3
  12. package/src/api/nodeConversions/nodeToBlock.ts +11 -5
  13. package/src/blocks/TableBlockContent/TableBlockContent.ts +2 -1
  14. package/src/editor/BlockNoteEditor.ts +21 -6
  15. package/src/editor/BlockNoteExtensions.ts +9 -4
  16. package/src/editor/BlockNoteTipTapEditor.ts +37 -36
  17. package/src/extensions/Placeholder/PlaceholderPlugin.ts +69 -54
  18. package/src/extensions/SideMenu/dragging.ts +2 -5
  19. package/src/i18n/locales/ar.ts +3 -0
  20. package/src/i18n/locales/de.ts +3 -0
  21. package/src/i18n/locales/en.ts +4 -1
  22. package/src/i18n/locales/es.ts +3 -0
  23. package/src/i18n/locales/fr.ts +3 -0
  24. package/src/i18n/locales/hr.ts +3 -0
  25. package/src/i18n/locales/is.ts +3 -0
  26. package/src/i18n/locales/it.ts +316 -314
  27. package/src/i18n/locales/ja.ts +3 -0
  28. package/src/i18n/locales/ko.ts +3 -0
  29. package/src/i18n/locales/nl.ts +3 -0
  30. package/src/i18n/locales/pl.ts +3 -0
  31. package/src/i18n/locales/pt.ts +3 -0
  32. package/src/i18n/locales/ru.ts +3 -0
  33. package/src/i18n/locales/uk.ts +337 -278
  34. package/src/i18n/locales/vi.ts +3 -0
  35. package/src/i18n/locales/zh.ts +3 -0
  36. package/types/src/editor/BlockNoteEditor.d.ts +2 -2
  37. package/types/src/editor/BlockNoteExtensions.d.ts +2 -2
  38. package/types/src/editor/BlockNoteTipTapEditor.d.ts +1 -2
  39. package/types/src/extensions/Placeholder/PlaceholderPlugin.d.ts +1 -1
  40. package/types/src/i18n/locales/de.d.ts +3 -0
  41. package/types/src/i18n/locales/en.d.ts +4 -7
  42. package/types/src/i18n/locales/es.d.ts +3 -0
  43. package/types/src/i18n/locales/hr.d.ts +3 -0
  44. package/types/src/i18n/locales/it.d.ts +3 -0
@@ -129,8 +129,7 @@ export function selectedFragmentToHTML<
129
129
  }
130
130
 
131
131
  // Uses default ProseMirror clipboard serialization.
132
- const clipboardHTML: string = (pmView as any).__serializeForClipboard(
133
- view,
132
+ const clipboardHTML: string = view.serializeForClipboard(
134
133
  view.state.selection.content()
135
134
  ).dom.innerHTML;
136
135
 
@@ -147,6 +146,34 @@ export function selectedFragmentToHTML<
147
146
  return { clipboardHTML, externalHTML, markdown };
148
147
  }
149
148
 
149
+ const checkIfSelectionInNonEditableBlock = () => {
150
+ // Let browser handle event if selection is empty (nothing
151
+ // happens).
152
+ const selection = window.getSelection();
153
+ if (!selection || selection.isCollapsed) {
154
+ return true;
155
+ }
156
+
157
+ // Let browser handle event if it's within a non-editable
158
+ // "island". This means it's in selectable content within a
159
+ // non-editable block. We only need to check one node as it's
160
+ // not possible for the browser selection to start in an
161
+ // editable block and end in a non-editable one.
162
+ let node = selection.focusNode;
163
+ while (node) {
164
+ if (
165
+ node instanceof HTMLElement &&
166
+ node.getAttribute("contenteditable") === "false"
167
+ ) {
168
+ return true;
169
+ }
170
+
171
+ node = node.parentElement;
172
+ }
173
+
174
+ return false;
175
+ };
176
+
150
177
  const copyToClipboard = <
151
178
  BSchema extends BlockSchema,
152
179
  I extends InlineContentSchema,
@@ -187,11 +214,19 @@ export const createCopyToClipboardExtension = <
187
214
  props: {
188
215
  handleDOMEvents: {
189
216
  copy(view, event) {
217
+ if (checkIfSelectionInNonEditableBlock()) {
218
+ return true;
219
+ }
220
+
190
221
  copyToClipboard(editor, view, event);
191
222
  // Prevent default PM handler to be called
192
223
  return true;
193
224
  },
194
225
  cut(view, event) {
226
+ if (checkIfSelectionInNonEditableBlock()) {
227
+ return true;
228
+ }
229
+
195
230
  copyToClipboard(editor, view, event);
196
231
  if (view.editable) {
197
232
  view.dispatch(view.state.tr.deleteSelection());
@@ -279,9 +279,11 @@ export function blockToNode(
279
279
  }
280
280
  }
281
281
 
282
- const nodeTypeCorrespondingToBlock = schema.nodes[block.type];
282
+ const isBlockContent =
283
+ !block.type || // can happen if block.type is not defined (this should create the default node)
284
+ schema.nodes[block.type].isInGroup("blockContent");
283
285
 
284
- if (nodeTypeCorrespondingToBlock.isInGroup("blockContent")) {
286
+ if (isBlockContent) {
285
287
  // Blocks with a type that matches "blockContent" group always need to be wrapped in a blockContainer
286
288
 
287
289
  const contentNode = blockOrInlineContentToContentNode(
@@ -302,7 +304,7 @@ export function blockToNode(
302
304
  },
303
305
  groupNode ? [contentNode, groupNode] : contentNode
304
306
  );
305
- } else if (nodeTypeCorrespondingToBlock.isInGroup("bnBlock")) {
307
+ } else if (schema.nodes[block.type].isInGroup("bnBlock")) {
306
308
  // this is a bnBlock node like Column or ColumnList that directly translates to a prosemirror node
307
309
  return schema.nodes[block.type].createChecked(
308
310
  {
@@ -104,11 +104,12 @@ export function contentNodeToInlineContent<
104
104
  return;
105
105
  }
106
106
 
107
- if (
108
- node.type.name !== "link" &&
109
- node.type.name !== "text" &&
110
- inlineContentSchema[node.type.name]
111
- ) {
107
+ if (node.type.name !== "link" && node.type.name !== "text") {
108
+ if (!inlineContentSchema[node.type.name]) {
109
+ // eslint-disable-next-line no-console
110
+ console.warn("unrecognized inline content type", node.type.name);
111
+ return;
112
+ }
112
113
  if (currentContent) {
113
114
  content.push(currentContent);
114
115
  currentContent = undefined;
@@ -130,6 +131,11 @@ export function contentNodeToInlineContent<
130
131
  } else {
131
132
  const config = styleSchema[mark.type.name];
132
133
  if (!config) {
134
+ if (mark.type.spec.blocknoteIgnore) {
135
+ // at this point, we don't want to show certain marks (such as comments)
136
+ // in the BlockNote JSON output. These marks should be tagged with "blocknoteIgnore" in the spec
137
+ continue;
138
+ }
133
139
  throw new Error(`style ${mark.type.name} not found in styleSchema`);
134
140
  }
135
141
  if (config.propSchema === "boolean") {
@@ -5,6 +5,7 @@ import { TableRow } from "@tiptap/extension-table-row";
5
5
  import { Node as PMNode } from "prosemirror-model";
6
6
  import { TableView } from "prosemirror-tables";
7
7
 
8
+ import { NodeView } from "prosemirror-view";
8
9
  import {
9
10
  createBlockSpecFromStronglyTypedTiptapNode,
10
11
  createStronglyTypedTiptapNode,
@@ -101,7 +102,7 @@ export const TableBlockContent = createStronglyTypedTiptapNode({
101
102
  return new BlockNoteTableView(node, EMPTY_CELL_WIDTH, {
102
103
  ...(this.options.domAttributes?.blockContent || {}),
103
104
  ...HTMLAttributes,
104
- });
105
+ }) as NodeView;
105
106
  };
106
107
  },
107
108
  });
@@ -130,7 +130,10 @@ export type BlockNoteEditorOptions<
130
130
  /**
131
131
  * @deprecated, provide placeholders via dictionary instead
132
132
  */
133
- placeholders: Record<string | "default", string>;
133
+ placeholders: Record<
134
+ string | "default" | "emptyDocument",
135
+ string | undefined
136
+ >;
134
137
 
135
138
  /**
136
139
  * An object containing attributes that should be added to HTML elements of the editor.
@@ -486,7 +489,11 @@ export class BlockNoteEditor<
486
489
  this.resolveFileUrl = newOptions.resolveFileUrl;
487
490
  this.headless = newOptions._headless;
488
491
 
489
- if (newOptions.collaboration && newOptions.initialContent) {
492
+ const collaborationEnabled =
493
+ "collaboration" in this.extensions ||
494
+ "liveblocksExtension" in this.extensions;
495
+
496
+ if (collaborationEnabled && newOptions.initialContent) {
490
497
  // eslint-disable-next-line no-console
491
498
  console.warn(
492
499
  "When using Collaboration, initialContent might cause conflicts, because changes should come from the collaboration provider"
@@ -495,7 +502,7 @@ export class BlockNoteEditor<
495
502
 
496
503
  const initialContent =
497
504
  newOptions.initialContent ||
498
- (options.collaboration
505
+ (collaborationEnabled
499
506
  ? [
500
507
  {
501
508
  type: "paragraph",
@@ -589,8 +596,11 @@ export class BlockNoteEditor<
589
596
  *
590
597
  * @warning Not needed to call manually when using React, use BlockNoteView to take care of mounting
591
598
  */
592
- public mount = (parentElement?: HTMLElement | null) => {
593
- this._tiptapEditor.mount(parentElement);
599
+ public mount = (
600
+ parentElement?: HTMLElement | null,
601
+ contentComponent?: any
602
+ ) => {
603
+ this._tiptapEditor.mount(parentElement, contentComponent);
594
604
  };
595
605
 
596
606
  public get prosemirrorView() {
@@ -924,7 +934,12 @@ export class BlockNoteEditor<
924
934
  for (const mark of marks) {
925
935
  const config = this.schema.styleSchema[mark.type.name];
926
936
  if (!config) {
927
- if (mark.type.name !== "link") {
937
+ if (
938
+ // Links are not considered styles in blocknote
939
+ mark.type.name !== "link" &&
940
+ // "blocknoteIgnore" tagged marks (such as comments) are also not considered BlockNote "styles"
941
+ !mark.type.spec.blocknoteIgnore
942
+ ) {
928
943
  // eslint-disable-next-line no-console
929
944
  console.warn("mark not found in styleschema", mark.type.name);
930
945
  }
@@ -7,11 +7,11 @@ import { Text } from "@tiptap/extension-text";
7
7
  import { Plugin } from "prosemirror-state";
8
8
  import * as Y from "yjs";
9
9
 
10
- import type { BlockNoteEditor, BlockNoteExtension } from "./BlockNoteEditor.js";
11
10
  import { createDropFileExtension } from "../api/clipboard/fromClipboard/fileDropExtension.js";
12
11
  import { createPasteFromClipboardExtension } from "../api/clipboard/fromClipboard/pasteExtension.js";
13
12
  import { createCopyToClipboardExtension } from "../api/clipboard/toClipboard/copyExtension.js";
14
13
  import { BackgroundColorExtension } from "../extensions/BackgroundColor/BackgroundColorExtension.js";
14
+ import { createCollaborationExtensions } from "../extensions/Collaboration/createCollaborationExtensions.js";
15
15
  import { FilePanelProsemirrorPlugin } from "../extensions/FilePanel/FilePanelPlugin.js";
16
16
  import { FormattingToolbarProsemirrorPlugin } from "../extensions/FormattingToolbar/FormattingToolbarPlugin.js";
17
17
  import { KeyboardShortcutsExtension } from "../extensions/KeyboardShortcuts/KeyboardShortcutsExtension.js";
@@ -40,7 +40,7 @@ import {
40
40
  StyleSchema,
41
41
  StyleSpecs,
42
42
  } from "../schema/index.js";
43
- import { createCollaborationExtensions } from "../extensions/Collaboration/createCollaborationExtensions.js";
43
+ import type { BlockNoteEditor, BlockNoteExtension } from "./BlockNoteEditor.js";
44
44
 
45
45
  type ExtensionOptions<
46
46
  BSchema extends BlockSchema,
@@ -69,7 +69,10 @@ type ExtensionOptions<
69
69
  animations: boolean;
70
70
  tableHandles: boolean;
71
71
  dropCursor: (opts: any) => Plugin;
72
- placeholders: Record<string | "default", string>;
72
+ placeholders: Record<
73
+ string | "default" | "emptyDocument",
74
+ string | undefined
75
+ >;
73
76
  tabBehavior?: "prefer-navigate-ui" | "prefer-indent";
74
77
  sideMenuDetection: "viewport" | "editor";
75
78
  };
@@ -171,7 +174,9 @@ const getTipTapExtensions = <
171
174
  protocols: VALID_LINK_PROTOCOLS,
172
175
  }),
173
176
  ...Object.values(opts.styleSpecs).map((styleSpec) => {
174
- return styleSpec.implementation.mark;
177
+ return styleSpec.implementation.mark.configure({
178
+ editor: opts.editor as any,
179
+ });
175
180
  }),
176
181
 
177
182
  TextColorExtension,
@@ -21,10 +21,9 @@ export type BlockNoteTipTapEditorOptions = Partial<
21
21
  * Custom Editor class that extends TiptapEditor and separates
22
22
  * the creation of the view from the constructor.
23
23
  */
24
- // @ts-ignore
25
24
  export class BlockNoteTipTapEditor extends TiptapEditor {
26
25
  private _state: EditorState;
27
- private _creating = false;
26
+
28
27
  public static create = (
29
28
  options: BlockNoteTipTapEditorOptions,
30
29
  styleSchema: StyleSchema
@@ -150,40 +149,44 @@ export class BlockNoteTipTapEditor extends TiptapEditor {
150
149
  /**
151
150
  * Replace the default `createView` method with a custom one - which we call on mount
152
151
  */
153
- private createViewAlternative() {
154
- this._creating = true;
155
- // Without queueMicrotask, custom IC / styles will give a React FlushSync error
156
- queueMicrotask(() => {
157
- if (!this._creating) {
158
- return;
152
+ private createViewAlternative(contentComponent?: any) {
153
+ (this as any).contentComponent = contentComponent;
154
+
155
+ const markViews: any = {};
156
+ this.extensionManager.extensions.forEach((extension) => {
157
+ if (extension.type === "mark" && extension.config.addMarkView) {
158
+ // Note: migrate to using `addMarkView` from tiptap as soon as this lands
159
+ // (currently tiptap doesn't support markviews)
160
+ markViews[extension.name] = extension.config.addMarkView;
159
161
  }
160
- this.view = new EditorView(
161
- { mount: this.options.element as any }, // use mount option so that we reuse the existing element instead of creating a new one
162
- {
163
- ...this.options.editorProps,
164
- // @ts-ignore
165
- dispatchTransaction: this.dispatchTransaction.bind(this),
166
- state: this.state,
167
- }
168
- );
162
+ });
169
163
 
170
- // `editor.view` is not yet available at this time.
171
- // Therefore we will add all plugins and node views directly afterwards.
172
- const newState = this.state.reconfigure({
173
- plugins: this.extensionManager.plugins,
174
- });
164
+ this.view = new EditorView(
165
+ { mount: this.options.element as any }, // use mount option so that we reuse the existing element instead of creating a new one
166
+ {
167
+ ...this.options.editorProps,
168
+ // @ts-ignore
169
+ dispatchTransaction: this.dispatchTransaction.bind(this),
170
+ state: this.state,
171
+ markViews,
172
+ }
173
+ );
174
+
175
+ // `editor.view` is not yet available at this time.
176
+ // Therefore we will add all plugins and node views directly afterwards.
177
+ const newState = this.state.reconfigure({
178
+ plugins: this.extensionManager.plugins,
179
+ });
175
180
 
176
- this.view.updateState(newState);
181
+ this.view.updateState(newState);
177
182
 
178
- this.createNodeViews();
183
+ this.createNodeViews();
179
184
 
180
- // emit the created event, call here manually because we blocked the default call in the constructor
181
- // (https://github.com/ueberdosis/tiptap/blob/45bac803283446795ad1b03f43d3746fa54a68ff/packages/core/src/Editor.ts#L117)
182
- this.commands.focus(this.options.autofocus);
183
- this.emit("create", { editor: this });
184
- this.isInitialized = true;
185
- this._creating = false;
186
- });
185
+ // emit the created event, call here manually because we blocked the default call in the constructor
186
+ // (https://github.com/ueberdosis/tiptap/blob/45bac803283446795ad1b03f43d3746fa54a68ff/packages/core/src/Editor.ts#L117)
187
+ this.commands.focus(this.options.autofocus);
188
+ this.emit("create", { editor: this });
189
+ this.isInitialized = true;
187
190
  }
188
191
 
189
192
  /**
@@ -191,15 +194,12 @@ export class BlockNoteTipTapEditor extends TiptapEditor {
191
194
  *
192
195
  * @param element DOM element to mount to, ur null / undefined to destroy
193
196
  */
194
- public mount = (element?: HTMLElement | null) => {
197
+ public mount = (element?: HTMLElement | null, contentComponent?: any) => {
195
198
  if (!element) {
196
199
  this.destroy();
197
- // cancel pending microtask
198
- this._creating = false;
199
200
  } else {
200
201
  this.options.element = element;
201
- // @ts-ignore
202
- this.createViewAlternative();
202
+ this.createViewAlternative(contentComponent);
203
203
  }
204
204
  };
205
205
  }
@@ -210,5 +210,6 @@ export class BlockNoteTipTapEditor extends TiptapEditor {
210
210
  // We should call `createView` manually only when a DOM element is available
211
211
 
212
212
  // additional fix because onPaste and onDrop depend on installing plugins in constructor which we don't support
213
+ // (note: can probably be removed after tiptap upgrade fixed in 2.8.0)
213
214
  this.options.onPaste = this.options.onDrop = undefined;
214
215
  };
@@ -1,5 +1,6 @@
1
1
  import { Plugin, PluginKey } from "prosemirror-state";
2
2
  import { Decoration, DecorationSet } from "prosemirror-view";
3
+ import { v4 } from "uuid";
3
4
  import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
4
5
 
5
6
  const PLUGIN_KEY = new PluginKey(`blocknote-placeholder`);
@@ -8,16 +9,23 @@ export class PlaceholderPlugin {
8
9
  public readonly plugin: Plugin;
9
10
  constructor(
10
11
  editor: BlockNoteEditor<any, any, any>,
11
- placeholders: Record<string | "default", string>
12
+ placeholders: Record<
13
+ string | "default" | "emptyDocument",
14
+ string | undefined
15
+ >
12
16
  ) {
13
17
  this.plugin = new Plugin({
14
18
  key: PLUGIN_KEY,
15
- view: () => {
19
+ view: (view) => {
20
+ const uniqueEditorSelector = `placeholder-selector-${v4()}`;
21
+ view.dom.classList.add(uniqueEditorSelector);
16
22
  const styleEl = document.createElement("style");
23
+
17
24
  const nonce = editor._tiptapEditor.options.injectNonce;
18
25
  if (nonce) {
19
26
  styleEl.setAttribute("nonce", nonce);
20
27
  }
28
+
21
29
  if (editor.prosemirrorView?.root instanceof ShadowRoot) {
22
30
  editor.prosemirrorView.root.append(styleEl);
23
31
  } else {
@@ -26,54 +34,50 @@ export class PlaceholderPlugin {
26
34
 
27
35
  const styleSheet = styleEl.sheet!;
28
36
 
29
- const getBaseSelector = (additionalSelectors = "") =>
30
- `.bn-block-content${additionalSelectors} .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child):before`;
37
+ const getSelector = (additionalSelectors = "") =>
38
+ `.${uniqueEditorSelector} .bn-block-content${additionalSelectors} .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child):before`;
31
39
 
32
- const getSelector = (
33
- blockType: string | "default",
34
- mustBeFocused = true
35
- ) => {
36
- const mustBeFocusedSelector = mustBeFocused
37
- ? `[data-is-empty-and-focused]`
38
- : ``;
40
+ try {
41
+ // FIXME: the names "default" and "emptyDocument" are hardcoded
42
+ const {
43
+ default: defaultPlaceholder,
44
+ emptyDocument: emptyPlaceholder,
45
+ ...rest
46
+ } = placeholders;
39
47
 
40
- if (blockType === "default") {
41
- return getBaseSelector(mustBeFocusedSelector);
42
- }
43
-
44
- const blockTypeSelector = `[data-content-type="${blockType}"]`;
45
- return getBaseSelector(mustBeFocusedSelector + blockTypeSelector);
46
- };
48
+ // add block specific placeholders
49
+ for (const [blockType, placeholder] of Object.entries(rest)) {
50
+ const blockTypeSelector = `[data-content-type="${blockType}"]`;
47
51
 
48
- for (const [blockType, placeholder] of Object.entries(placeholders)) {
49
- const mustBeFocused = blockType === "default";
50
-
51
- try {
52
52
  styleSheet.insertRule(
53
- `${getSelector(
54
- blockType,
55
- mustBeFocused
56
- )} { content: ${JSON.stringify(placeholder)}; }`
57
- );
58
-
59
- // For some reason, the placeholders which show when the block is focused
60
- // take priority over ones which show depending on block type, so we need
61
- // to make sure the block specific ones are also used when the block is
62
- // focused.
63
- if (!mustBeFocused) {
64
- styleSheet.insertRule(
65
- `${getSelector(blockType, true)} { content: ${JSON.stringify(
66
- placeholder
67
- )}; }`
68
- );
69
- }
70
- } catch (e) {
71
- // eslint-disable-next-line no-console
72
- console.warn(
73
- `Failed to insert placeholder CSS rule - this is likely due to the browser not supporting certain CSS pseudo-element selectors (:has, :only-child:, or :before)`,
74
- e
53
+ `${getSelector(blockTypeSelector)} { content: ${JSON.stringify(
54
+ placeholder
55
+ )}; }`
75
56
  );
76
57
  }
58
+
59
+ const onlyBlockSelector = `[data-is-only-empty-block]`;
60
+ const mustBeFocusedSelector = `[data-is-empty-and-focused]`;
61
+
62
+ // placeholder for when there's only one empty block
63
+ styleSheet.insertRule(
64
+ `${getSelector(onlyBlockSelector)} { content: ${JSON.stringify(
65
+ emptyPlaceholder
66
+ )}; }`
67
+ );
68
+
69
+ // placeholder for default blocks, only when the cursor is in the block (mustBeFocused)
70
+ styleSheet.insertRule(
71
+ `${getSelector(mustBeFocusedSelector)} { content: ${JSON.stringify(
72
+ defaultPlaceholder
73
+ )}; }`
74
+ );
75
+ } catch (e) {
76
+ // eslint-disable-next-line no-console
77
+ console.warn(
78
+ `Failed to insert placeholder CSS rule - this is likely due to the browser not supporting certain CSS pseudo-element selectors (:has, :only-child:, or :before)`,
79
+ e
80
+ );
77
81
  }
78
82
 
79
83
  return {
@@ -87,7 +91,6 @@ export class PlaceholderPlugin {
87
91
  };
88
92
  },
89
93
  props: {
90
- // TODO: maybe also add placeholder for empty document ("e.g.: start writing..")
91
94
  decorations: (state) => {
92
95
  const { doc, selection } = state;
93
96
 
@@ -104,20 +107,32 @@ export class PlaceholderPlugin {
104
107
  return;
105
108
  }
106
109
 
107
- const $pos = selection.$anchor;
108
- const node = $pos.parent;
110
+ const decs = [];
109
111
 
110
- if (node.content.size > 0) {
111
- return null;
112
+ // decoration for when there's only one empty block
113
+ // positions are hardcoded for now
114
+ if (state.doc.content.size === 6) {
115
+ decs.push(
116
+ Decoration.node(2, 4, {
117
+ "data-is-only-empty-block": "true",
118
+ })
119
+ );
112
120
  }
113
121
 
114
- const before = $pos.before();
122
+ const $pos = selection.$anchor;
123
+ const node = $pos.parent;
124
+
125
+ if (node.content.size === 0) {
126
+ const before = $pos.before();
115
127
 
116
- const dec = Decoration.node(before, before + node.nodeSize, {
117
- "data-is-empty-and-focused": "true",
118
- });
128
+ decs.push(
129
+ Decoration.node(before, before + node.nodeSize, {
130
+ "data-is-empty-and-focused": "true",
131
+ })
132
+ );
133
+ }
119
134
 
120
- return DecorationSet.create(doc, [dec]);
135
+ return DecorationSet.create(doc, decs);
121
136
  },
122
137
  },
123
138
  });
@@ -1,6 +1,5 @@
1
1
  import { Node } from "prosemirror-model";
2
2
  import { NodeSelection, Selection } from "prosemirror-state";
3
- import * as pmView from "prosemirror-view";
4
3
  import { EditorView } from "prosemirror-view";
5
4
 
6
5
  import { createExternalHTMLExporter } from "../../api/exporters/html/externalHTMLExporter.js";
@@ -184,10 +183,8 @@ export function dragStart<
184
183
  const selectedSlice = view.state.selection.content();
185
184
  const schema = editor.pmSchema;
186
185
 
187
- const clipboardHTML = (pmView as any).__serializeForClipboard(
188
- view,
189
- selectedSlice
190
- ).dom.innerHTML;
186
+ const clipboardHTML =
187
+ view.serializeForClipboard(selectedSlice).dom.innerHTML;
191
188
 
192
189
  const externalHTMLExporter = createExternalHTMLExporter(schema, editor);
193
190
 
@@ -261,6 +261,9 @@ export const ar: Dictionary = {
261
261
  align_justify: {
262
262
  tooltip: "ضبط النص",
263
263
  },
264
+ comment: {
265
+ tooltip: "إضافة ملاحظة",
266
+ },
264
267
  },
265
268
  file_panel: {
266
269
  upload: {
@@ -273,6 +273,9 @@ export const de = {
273
273
  align_justify: {
274
274
  tooltip: "Text Blocksatz",
275
275
  },
276
+ comment: {
277
+ tooltip: "Kommentar hinzufügen",
278
+ },
276
279
  },
277
280
  file_panel: {
278
281
  upload: {
@@ -129,7 +129,7 @@ export const en = {
129
129
  bulletListItem: "List",
130
130
  numberedListItem: "List",
131
131
  checkListItem: "List",
132
- },
132
+ } as Record<"default" | "emptyDocument" | string, string | undefined>,
133
133
  file_blocks: {
134
134
  image: {
135
135
  add_button_text: "Add image",
@@ -275,6 +275,9 @@ export const en = {
275
275
  align_justify: {
276
276
  tooltip: "Justify text",
277
277
  },
278
+ comment: {
279
+ tooltip: "Add comment",
280
+ },
278
281
  },
279
282
  file_panel: {
280
283
  upload: {
@@ -272,6 +272,9 @@ export const es = {
272
272
  align_justify: {
273
273
  tooltip: "Justificar texto",
274
274
  },
275
+ comment: {
276
+ tooltip: "Agregar comentario",
277
+ },
275
278
  },
276
279
  file_panel: {
277
280
  upload: {
@@ -300,6 +300,9 @@ export const fr: Dictionary = {
300
300
  align_justify: {
301
301
  tooltip: "Justifier le texte",
302
302
  },
303
+ comment: {
304
+ tooltip: "Ajouter un commentaire",
305
+ },
303
306
  },
304
307
  file_panel: {
305
308
  upload: {
@@ -281,6 +281,9 @@ export const hr = {
281
281
  align_justify: {
282
282
  tooltip: "Poravnaj tekst obostrano",
283
283
  },
284
+ comment: {
285
+ tooltip: "Dodaj komentar",
286
+ },
284
287
  },
285
288
  file_panel: {
286
289
  upload: {
@@ -268,6 +268,9 @@ export const is: Dictionary = {
268
268
  align_justify: {
269
269
  tooltip: "Jafna texta",
270
270
  },
271
+ comment: {
272
+ tooltip: "Bæta við athugun",
273
+ },
271
274
  },
272
275
  file_panel: {
273
276
  upload: {