diff --git a/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts b/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts
index a76373127c..d37fb05005 100644
--- a/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts
+++ b/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts
@@ -177,6 +177,7 @@ const mergeBlocks = (
}
// Save suggestion node content before reconstruction
+ // drops prev suggestionBefore and next suggestionAfter on merge
const savedPrevSuggAfter = currentPrevInfo.suggestionAfter
? currentPrevInfo.suggestionAfter.node.copy(
currentPrevInfo.suggestionAfter.node.content,
@@ -236,6 +237,7 @@ const mergeBlocks = (
}
// Create the new blockContainer with the prev block's ID and attributes
+ // create() skips validation; bad child order ships silently
const newBlockContainer = currentPrevInfo.bnBlock.node.type.create(
currentPrevInfo.bnBlock.node.attrs,
newChildren,
diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts
index 24248cfc3c..f4b49be4b8 100644
--- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts
+++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts
@@ -106,6 +106,7 @@ function updateBlockSelectionFromData(
anchorBlockPos + data.headCellOffset,
);
} else if (data.type === "node") {
+ // +1 assumes blockContent is first child; may be a leading suggestion node
selection = NodeSelection.create(tr.doc, anchorBlockPos + 1);
} else {
const headBlockPos = getNodeById(data.headBlockId, tr.doc)?.posBeforeNode;
diff --git a/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts b/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts
index c995faeda1..aaaef246d5 100644
--- a/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts
+++ b/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts
@@ -39,6 +39,7 @@ function sinkItem(
if (nodeBefore.type !== itemType) {
return false;
}
+ // lastChild may be a trailing suggestion node, not blockGroup
const nestedBefore =
nodeBefore.lastChild && nodeBefore.lastChild.type === groupType; // change 2
const inner = Fragment.from(nestedBefore ? itemType.create() : null);
@@ -102,6 +103,7 @@ function liftToOuterList(
// There are siblings after the lifted items, which must become
// children of the last item
const blockBeingLifted = range.parent.child(range.endIndex - 1);
+ // lastChild may be a trailing suggestion node, not blockGroup
const nestedAfter =
blockBeingLifted.lastChild &&
blockBeingLifted.lastChild.type === groupType; // change 2
diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/util/fixColumnList.ts b/packages/core/src/api/blockManipulation/commands/replaceBlocks/util/fixColumnList.ts
index 3097851f47..32ed4782e4 100644
--- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/util/fixColumnList.ts
+++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/util/fixColumnList.ts
@@ -18,6 +18,7 @@ export function isEmptyColumn(column: Node) {
throw new Error("Invalid column: does not have child node.");
}
+ // firstChild may be a suggestion node; childCount===1 below assumes no suggestions
const blockContent = blockContainer.firstChild;
if (!blockContent) {
throw new Error("Invalid blockContainer: does not have child node.");
diff --git a/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.ts b/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.ts
index 785714fd13..a8c4a59f6b 100644
--- a/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.ts
+++ b/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.ts
@@ -47,6 +47,7 @@ export const splitBlockTr = (
// with the first block and the split happens at the blockContent boundary.
let effectivePos = posInBlock;
const $pos = tr.doc.resolve(posInBlock);
+ // dead guard — group is compound "suggestionBlockContent blockContent", never ===
if ($pos.parent.type.spec.group === "suggestionBlockContent") {
effectivePos = info.blockContent.beforePos + 1;
}
diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts
index a3e2b3b0db..7dbafe2c80 100644
--- a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts
+++ b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts
@@ -302,6 +302,7 @@ function updateChildren<
throw new Error("impossible");
}
// Inserts a new blockGroup containing the child nodes created earlier.
+ // inserts blockGroup before trailing suggestion, invalid content order
tr.insert(
blockInfo.blockContent.afterPos,
pmSchema.nodes["blockGroup"].createChecked({}, childNodes),
diff --git a/packages/core/src/api/blockManipulation/getBlock/getBlock.ts b/packages/core/src/api/blockManipulation/getBlock/getBlock.ts
index c018c907a5..4db6384044 100644
--- a/packages/core/src/api/blockManipulation/getBlock/getBlock.ts
+++ b/packages/core/src/api/blockManipulation/getBlock/getBlock.ts
@@ -22,6 +22,7 @@ export function getBlock<
typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id;
const pmSchema = getPmSchema(doc);
+ // suggested-deleted blocks resolve as live, no deletion flag
const posInfo = getNodeById(id, doc);
if (!posInfo) {
return undefined;
diff --git a/packages/core/src/api/blockManipulation/insertContentAt.ts b/packages/core/src/api/blockManipulation/insertContentAt.ts
index 3409583a37..2787cf50bf 100644
--- a/packages/core/src/api/blockManipulation/insertContentAt.ts
+++ b/packages/core/src/api/blockManipulation/insertContentAt.ts
@@ -47,6 +47,7 @@ export function insertContentAt(
// replace an empty paragraph by an inserted image
// instead of inserting the image below the paragraph
if (from === to && isOnlyBlockContent) {
+ // parent may be an empty suggestion shadow node, not blockContent
const { parent } = tr.doc.resolve(from);
const isEmptyTextBlock =
parent.isTextblock && !parent.type.spec.code && !parent.childCount;
diff --git a/packages/core/src/api/blockManipulation/selections/selection.ts b/packages/core/src/api/blockManipulation/selections/selection.ts
index e5bd761918..fc23c438d7 100644
--- a/packages/core/src/api/blockManipulation/selections/selection.ts
+++ b/packages/core/src/api/blockManipulation/selections/selection.ts
@@ -236,6 +236,7 @@ export function getSelectionCutBlocks(tr: Transaction, expandToWords = false) {
// the correct information about whether content is cut at the start or end of a block
// if the end is at the end of a node (|
) move it forward so we include all closing tags (|)
+ // end.parent may be a suggestion shadow node when crossing a block boundary
while (end.parentOffset >= end.parent.nodeSize - 2 && end.depth > 0) {
end = tr.doc.resolve(end.pos + 1);
}
@@ -251,6 +252,7 @@ export function getSelectionCutBlocks(tr: Transaction, expandToWords = false) {
}
// if the start is at the end of a node (||) move it forwards so we drop all closing tags (|)
+ // start.parent may be a suggestion shadow node when crossing a block boundary
while (start.parentOffset >= start.parent.nodeSize - 2 && start.depth > 0) {
start = tr.doc.resolve(start.pos + 1);
}
diff --git a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts
index e150af1309..7269dcd033 100644
--- a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts
+++ b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts
@@ -50,6 +50,7 @@ function fragmentToExternalHTML<
(child) =>
child.type.isInGroup("bnBlock") ||
child.type.name === "blockGroup" ||
+ // === "blockContent" misclassifies suggestion nodes (compound group)
child.type.spec.group === "blockContent",
) === undefined;
if (isWithinBlockContent) {
@@ -118,9 +119,11 @@ export function selectedFragmentToHTML<
// selected, e.g. an image block.
if (
"node" in view.state.selection &&
+ // === "blockContent" misclassifies suggestion nodes (compound group)
(view.state.selection.node as Node).type.spec.group === "blockContent"
) {
editor.transact((tr) =>
+ // from-1 block expansion assumes blockContent adjacency; off with leading suggestion node
tr.setSelection(
new NodeSelection(tr.doc.resolve(view.state.selection.from - 1)),
),
@@ -128,6 +131,7 @@ export function selectedFragmentToHTML<
}
// Uses default ProseMirror clipboard serialization.
+ // serializeForClipboard emits shadow nodes; paste re-creates them
const clipboardHTML: string = view.serializeForClipboard(
view.state.selection.content(),
).dom.innerHTML;
@@ -268,6 +272,7 @@ export const createCopyToClipboardExtension = <
// Expands the selection to the parent `blockContainer` node.
editor.transact((tr) =>
+ // from-1 block expansion assumes blockContent adjacency; off with leading suggestion node
tr.setSelection(
new NodeSelection(
tr.doc.resolve(view.state.selection.from - 1),
diff --git a/packages/core/src/api/exporters/markdown/htmlToMarkdown.ts b/packages/core/src/api/exporters/markdown/htmlToMarkdown.ts
index 7faa154dc6..716ad72650 100644
--- a/packages/core/src/api/exporters/markdown/htmlToMarkdown.ts
+++ b/packages/core/src/api/exporters/markdown/htmlToMarkdown.ts
@@ -45,6 +45,7 @@ function serializeChildren(node: Node, ctx: SerializeContext): string {
}
function serializeNode(node: Node, ctx: SerializeContext): string {
+ // no data-suggestion handling; shadow text duplicated to markdown
if (node.nodeType === 3 /* Node.TEXT_NODE */) {
return node.textContent || "";
}
diff --git a/packages/core/src/api/getBlockInfoFromPos.ts b/packages/core/src/api/getBlockInfoFromPos.ts
index 4f340d98a4..c898ba4e91 100644
--- a/packages/core/src/api/getBlockInfoFromPos.ts
+++ b/packages/core/src/api/getBlockInfoFromPos.ts
@@ -41,6 +41,7 @@ export type BlockInfo = {
* Whether bnBlock is a blockContainer node
*/
isBlockContainer: true;
+ // (Affects ~58 callsites) consumers use blockContent/bnBlock but never branch on suggestionBefore/After
/**
* A suggestion node that appears before the blockContent, if present.
* Suggestion nodes have group "suggestionBlockContent" and are used for
@@ -191,6 +192,7 @@ export function getBlockInfoWithManualOffset(
afterPos: suggestionAfterPos,
};
+ // singular suggestionBefore/After, schema allows suggestionBlockContent*
if (!foundBlockContent) {
suggestionBefore = info;
} else {
diff --git a/packages/core/src/api/nodeConversions/blockToNode.ts b/packages/core/src/api/nodeConversions/blockToNode.ts
index d970227a49..258ea93e19 100644
--- a/packages/core/src/api/nodeConversions/blockToNode.ts
+++ b/packages/core/src/api/nodeConversions/blockToNode.ts
@@ -321,6 +321,7 @@ function blockOrInlineContentToContentNode(
/**
* Converts a BlockNote block to a Prosemirror node.
*/
+// (Affects ~7 callsites) emits no suggestion nodes, round-trips drop suggestions
export function blockToNode(
block: PartialBlock,
schema: Schema,
diff --git a/packages/core/src/api/nodeConversions/fragmentToBlocks.ts b/packages/core/src/api/nodeConversions/fragmentToBlocks.ts
index 724b552bda..4fbae84809 100644
--- a/packages/core/src/api/nodeConversions/fragmentToBlocks.ts
+++ b/packages/core/src/api/nodeConversions/fragmentToBlocks.ts
@@ -22,6 +22,7 @@ export function fragmentToBlocks<
fragment.descendants((node) => {
const pmSchema = getPmSchema(node);
if (node.type.name === "blockContainer") {
+ // firstChild may be a suggestion node, not blockContent or blockGroup
if (node.firstChild?.type.name === "blockGroup") {
// selection started within a block group
// in this case the fragment starts with:
diff --git a/packages/core/src/api/nodeConversions/nodeToBlock.ts b/packages/core/src/api/nodeConversions/nodeToBlock.ts
index 1a777ad892..f921b51c69 100644
--- a/packages/core/src/api/nodeConversions/nodeToBlock.ts
+++ b/packages/core/src/api/nodeConversions/nodeToBlock.ts
@@ -482,6 +482,7 @@ export function nodeToBlock<
throw new UnreachableCaseError(blockConfig.content);
}
+ // (Affects ~23 callsites) strips suggestion state, Block API is suggestion-blind
const block = {
id,
type: blockConfig.type,
@@ -604,6 +605,7 @@ export function prosemirrorSliceToSlicedBlocks<
let firstNonSuggestionChild: Node | undefined;
let blockGroupChild: Node | undefined;
blockContainer.forEach((child) => {
+ // dead guard — group is compound "suggestionBlockContent blockContent", never ===
if (child.type.spec.group === "suggestionBlockContent") {
return; // skip suggestion nodes
}
diff --git a/packages/core/src/api/nodeUtil.ts b/packages/core/src/api/nodeUtil.ts
index 3388c95413..6530b60b3e 100644
--- a/packages/core/src/api/nodeUtil.ts
+++ b/packages/core/src/api/nodeUtil.ts
@@ -17,6 +17,7 @@ export function getNodeById(
}
// Keeps traversing nodes if block with target ID has not been found.
+ // suggested-deleted blocks resolve as live, no deletion flag
if (!isNodeBlock(node) || node.attrs.id !== id) {
return true;
}
diff --git a/packages/core/src/api/parsers/html/parseHTML.ts b/packages/core/src/api/parsers/html/parseHTML.ts
index 16e03f883a..a8fac6f305 100644
--- a/packages/core/src/api/parsers/html/parseHTML.ts
+++ b/packages/core/src/api/parsers/html/parseHTML.ts
@@ -23,6 +23,7 @@ export function HTMLToBlocks<
// const doc = pmSchema.nodes["doc"].createAndFill()!;
// and context: doc.resolve(3),
+ // parse may auto-create *--attributed shadow nodes from BlockNote HTML
const parentNode = parser.parse(htmlNode, {
topNode: pmSchema.nodes["blockGroup"].create(),
});
diff --git a/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.ts b/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.ts
index eb6b06dc7d..9099d743f8 100644
--- a/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.ts
+++ b/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.ts
@@ -28,6 +28,7 @@ function calculateListItemIndex(
tr: Transaction,
map: Map,
): { index: number; isFirst: boolean; hasStart: boolean } {
+ // firstChild may be a suggestion node, not blockContent
const hasStart = !!node.firstChild!.attrs["start"];
// Fast path: previous sibling already in cache
@@ -93,6 +94,7 @@ function calculateListItemIndex(
isFirst = false;
} else {
// Start of a new list
+ // firstChild may be a suggestion node, not blockContent
index = (lastInChain.node.firstChild!.attrs["start"] || 1) - 1;
isFirst = true;
}
@@ -156,6 +158,7 @@ function getDecorations(
if (
node.type.name === "blockContainer" &&
+ // firstChild may be a suggestion node, not blockContent
node.firstChild!.type.name === "numberedListItem"
) {
const { index, isFirst, hasStart } = calculateListItemIndex(
@@ -168,6 +171,7 @@ function getDecorations(
// Search only the numberedListItem node range, not the full
// blockContainer (which includes nested blockGroups whose
// decorations could falsely match).
+ // pos + 1 assumes blockContent is first child, not a suggestion node
const blockNode = tr.doc.nodeAt(pos + 1)!;
const existingDecorations = nextDecorationSet.find(
pos + 1,
diff --git a/packages/core/src/blocks/defaultBlockHelpers.ts b/packages/core/src/blocks/defaultBlockHelpers.ts
index ccadf93e11..800359565c 100644
--- a/packages/core/src/blocks/defaultBlockHelpers.ts
+++ b/packages/core/src/blocks/defaultBlockHelpers.ts
@@ -71,6 +71,7 @@ export const defaultBlockToHTML = <
if (node.type.name === "blockContainer") {
// for regular blocks, get the toDOM spec from the blockContent node
+ // firstChild safe here; node built by suggestion-blind blockToNode
node = node.firstChild!;
}
diff --git a/packages/core/src/comments/extension.ts b/packages/core/src/comments/extension.ts
index c037e80ddf..5141a13611 100644
--- a/packages/core/src/comments/extension.ts
+++ b/packages/core/src/comments/extension.ts
@@ -30,6 +30,7 @@ function getUpdatedThreadPositions(doc: Node, markType: string) {
const threadPositions = new Map();
// find all thread marks and store their position + create decoration for selected thread
+ // comment marks inside shadow nodes anchor threads to suggestion content
doc.descendants((node, pos) => {
node.marks.forEach((mark) => {
if (mark.type.name === markType) {
@@ -228,6 +229,7 @@ export const CommentsExtension = createExtension(
return false;
}
+ // click may land in a suggestion shadow node, resolving its comment marks
const node = view.state.doc.nodeAt(pos);
if (!node) {
diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css
index afb9222e64..6614b4caa1 100644
--- a/packages/core/src/editor/Block.css
+++ b/packages/core/src/editor/Block.css
@@ -734,6 +734,7 @@ div[data-type="modification"] {
text-decoration: none;
}
+/* no block-level suggestion-state styling (deletion rule disabled) */
.bn-root del,
[DISABLED-data-node-deletion] {
color: rgba(100, 90, 75, 0.3);
diff --git a/packages/core/src/editor/managers/ExportManager.ts b/packages/core/src/editor/managers/ExportManager.ts
index 3fe1ee2f0e..bf6fee875f 100644
--- a/packages/core/src/editor/managers/ExportManager.ts
+++ b/packages/core/src/editor/managers/ExportManager.ts
@@ -34,6 +34,7 @@ export class ExportManager<
* @param blocks An array of blocks that should be serialized into HTML.
* @returns The blocks, serialized as an HTML string.
*/
+ // (Affects ~3) exports suggestion-blind Block JSON, drops shadow content
public blocksToHTMLLossy(
blocks: PartialBlock[] = this.editor.document,
): string {
diff --git a/packages/core/src/editor/transformPasted.ts b/packages/core/src/editor/transformPasted.ts
index 4f0515df95..e94a150f3f 100644
--- a/packages/core/src/editor/transformPasted.ts
+++ b/packages/core/src/editor/transformPasted.ts
@@ -145,6 +145,7 @@ export function transformPasted(slice: Slice, view: EditorView) {
}
for (let i = 0; i < f.childCount; i++) {
+ // === "blockContent" misclassifies suggestion nodes (compound group)
if (f.child(i).type.spec.group === "blockContent") {
const content = [f.child(i)];
@@ -154,6 +155,7 @@ export function transformPasted(slice: Slice, view: EditorView) {
i + 1 < f.childCount &&
f.child(i + 1).type.name === "blockGroup" // TODO
) {
+ // .child(0).child(0) assumes blockContent is first child of blockContainer
const nestedChild = f
.child(i + 1)
.child(0)
@@ -227,6 +229,7 @@ function retypeLeadingParagraphForEmptyTarget(
const blockGroup = fragment.firstChild;
const blockContainer = blockGroup?.firstChild;
+ // firstChild may be a suggestion node, not blockContent
const leading = blockContainer?.firstChild;
if (
blockGroup?.type.name !== "blockGroup" ||
@@ -238,6 +241,7 @@ function retypeLeadingParagraphForEmptyTarget(
const retyped = target.type.create(target.attrs, leading.content);
const newBlockContainer = blockContainer.copy(
+ // replaceChild(0,...) assumes index 0 is blockContent, not a suggestion node
blockContainer.content.replaceChild(0, retyped),
);
const newBlockGroup = blockGroup.copy(
diff --git a/packages/core/src/extensions/DropCursor/utils.ts b/packages/core/src/extensions/DropCursor/utils.ts
index 25e3985238..8e7d00e170 100644
--- a/packages/core/src/extensions/DropCursor/utils.ts
+++ b/packages/core/src/extensions/DropCursor/utils.ts
@@ -58,8 +58,10 @@ export function getBlockDropRect(
return null;
}
+ // nodeBefore may be a suggestion node, not blockContent
const before = $pos.nodeBefore;
+ // nodeAfter may be a suggestion node, not blockContent
const after = $pos.nodeAfter;
if (!before && !after) {
diff --git a/packages/core/src/extensions/NodeSelectionKeyboard/NodeSelectionKeyboard.ts b/packages/core/src/extensions/NodeSelectionKeyboard/NodeSelectionKeyboard.ts
index 357780f44a..2fe139a5a2 100644
--- a/packages/core/src/extensions/NodeSelectionKeyboard/NodeSelectionKeyboard.ts
+++ b/packages/core/src/extensions/NodeSelectionKeyboard/NodeSelectionKeyboard.ts
@@ -49,6 +49,7 @@ export const NodeSelectionKeyboardExtension = createExtension(
const tr = view.state.tr;
view.dispatch(
tr
+ // $to.after() may land before a trailing suggestion node
.insert(
view.state.tr.selection.$to.after(),
view.state.schema.nodes["paragraph"].createChecked(),
@@ -56,6 +57,7 @@ export const NodeSelectionKeyboardExtension = createExtension(
.setSelection(
new TextSelection(
tr.doc.resolve(
+ // +1 may resolve inside a trailing suggestion node
view.state.tr.selection.$to.after() + 1,
),
),
diff --git a/packages/core/src/extensions/Placeholder/Placeholder.ts b/packages/core/src/extensions/Placeholder/Placeholder.ts
index b8ff2e14ed..90e39d341b 100644
--- a/packages/core/src/extensions/Placeholder/Placeholder.ts
+++ b/packages/core/src/extensions/Placeholder/Placeholder.ts
@@ -117,6 +117,7 @@ export const PlaceholderExtension = createExtension(
// decoration for when there's only one empty block
// positions are hardcoded for now
+ // hardcoded size===6 and positions 2,4 assume no suggestion nodes in empty doc
if (state.doc.content.size === 6) {
decs.push(
Decoration.node(2, 4, {
diff --git a/packages/core/src/extensions/PreviousBlockType/PreviousBlockType.ts b/packages/core/src/extensions/PreviousBlockType/PreviousBlockType.ts
index 714783ad28..290783ad6e 100644
--- a/packages/core/src/extensions/PreviousBlockType/PreviousBlockType.ts
+++ b/packages/core/src/extensions/PreviousBlockType/PreviousBlockType.ts
@@ -104,6 +104,7 @@ export const PreviousBlockTypeExtension = createExtension(() => {
for (const node of newNodes) {
const oldNode = oldNodesById.get(node.node.attrs.id);
+ // firstChild may be a suggestion node, not blockContent
const oldContentNode = oldNode?.node.firstChild;
const newContentNode = node.node.firstChild;
diff --git a/packages/core/src/extensions/SideMenu/SideMenu.ts b/packages/core/src/extensions/SideMenu/SideMenu.ts
index e98059b585..d36acac8ad 100644
--- a/packages/core/src/extensions/SideMenu/SideMenu.ts
+++ b/packages/core/src/extensions/SideMenu/SideMenu.ts
@@ -258,6 +258,7 @@ export class SideMenuView<
blockContentBoundingBox.width,
blockContentBoundingBox.height,
),
+ // resolves suggested-deleted/modified blocks as if live (no suggestion state)
block: this.editor.getBlock(
this.hoveredBlock!.getAttribute("data-id")!,
)!,
diff --git a/packages/core/src/extensions/SideMenu/dragging.ts b/packages/core/src/extensions/SideMenu/dragging.ts
index f8ba326538..63bb784ddd 100644
--- a/packages/core/src/extensions/SideMenu/dragging.ts
+++ b/packages/core/src/extensions/SideMenu/dragging.ts
@@ -39,6 +39,7 @@ function blockPositionsFromSelection(selection: Selection, doc: Node) {
// the same blocks again. If this happens, the anchor & head move out of the block content node they were originally
// in. If the anchor should update but the head shouldn't and vice versa, it means the user selection is outside a
// block content node, which should never happen.
+ // === "blockContent" misclassifies suggestion nodes (compound group)
const selectionStartInBlockContent =
doc.resolve(selection.from).node().type.spec.group === "blockContent";
const selectionEndInBlockContent =
diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts
index 4809607e6e..a52d39f4e8 100644
--- a/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts
+++ b/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts
@@ -309,6 +309,7 @@ export const SuggestionMenu = createExtension(({ editor }) => {
transaction.getMeta("blur") ||
transaction.getMeta("pointer") ||
// Moving the caret before the character which triggered the menu should hide it.
+ // no cursor-in-suggestion-node guard (same-parent check is suggestion-blind)
(prev.triggerCharacter !== undefined &&
newState.selection.from < prev.queryStartPos()) ||
// Moving the caret to a new block should hide the menu.
@@ -381,6 +382,7 @@ export const SuggestionMenu = createExtension(({ editor }) => {
const blockNode = findBlock(state.selection);
if (blockNode) {
return DecorationSet.create(state.doc, [
+ // decoration spans whole container incl suggestion shadow nodes
Decoration.node(
blockNode.pos,
blockNode.pos + blockNode.node.nodeSize,
diff --git a/packages/core/src/extensions/TableHandles/TableHandles.ts b/packages/core/src/extensions/TableHandles/TableHandles.ts
index d957056f4e..3253a690b0 100644
--- a/packages/core/src/extensions/TableHandles/TableHandles.ts
+++ b/packages/core/src/extensions/TableHandles/TableHandles.ts
@@ -273,6 +273,7 @@ export class TableHandlesView implements PluginView {
);
if (editorHasBlockWithType(this.editor, "table")) {
+ // posBeforeNode+1 assumes blockContent is first child; off with leading suggestion node
this.tablePos = pmNodeInfo.posBeforeNode + 1;
tableBlock = block;
}
diff --git a/packages/core/src/extensions/TrailingNode/TrailingNode.ts b/packages/core/src/extensions/TrailingNode/TrailingNode.ts
index 59f95d2bed..921fcd07cf 100644
--- a/packages/core/src/extensions/TrailingNode/TrailingNode.ts
+++ b/packages/core/src/extensions/TrailingNode/TrailingNode.ts
@@ -17,6 +17,7 @@ function shouldShowTrailingWidget(doc: PMNode, isEditable: boolean): boolean {
const rootGroup = doc.lastChild;
const lastBlock = rootGroup?.lastChild;
+ // firstChild may be a suggestion node, not blockContent
const lastContent = lastBlock?.firstChild;
return !(
diff --git a/packages/core/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts b/packages/core/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts
index ee0123bcc7..a36a827986 100644
--- a/packages/core/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts
+++ b/packages/core/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts
@@ -271,6 +271,7 @@ export const KeyboardShortcutsExtension = Extension.create<{
bottomNestedPrevBlockInfo.blockContent.node.type.spec
.content === "tableRow+"
) {
+ // cascading -1 arithmetic; off if a leading suggestion node precedes blockContent
const tableBlockEndPos = blockInfo.bnBlock.beforePos - 1;
const tableBlockContentEndPos = tableBlockEndPos - 1;
const lastRowEndPos = tableBlockContentEndPos - 1;
@@ -674,6 +675,7 @@ export const KeyboardShortcutsExtension = Extension.create<{
nextBlockInfo.blockContent.node.type.spec.content ===
"tableRow+"
) {
+ // hardcoded +1 arithmetic; off if next block has a leading suggestion node
const tableBlockStartPos = blockInfo.bnBlock.afterPos + 1;
const tableBlockContentStartPos = tableBlockStartPos + 1;
const firstRowStartPos = tableBlockContentStartPos + 1;
diff --git a/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts b/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts
index 54cb8b7340..a795c6f344 100644
--- a/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts
+++ b/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts
@@ -205,6 +205,7 @@ const UniqueID = Extension.create({
});
return;
}
+ // dedupe may regenerate the wrong id when a suggestion creates a duplicate id
// check if the node doesn’t exist in the old state
const { deleted } = mapping.invert().mapResult(pos);
const newNode = deleted && duplicatedNewIds.includes(id);
diff --git a/packages/core/src/pm-nodes/SpecialNode.test.ts b/packages/core/src/pm-nodes/SpecialNode.test.ts
index c4f3442f63..f4163b4a5e 100644
--- a/packages/core/src/pm-nodes/SpecialNode.test.ts
+++ b/packages/core/src/pm-nodes/SpecialNode.test.ts
@@ -78,6 +78,7 @@ describe("SuggestionNode - structural", () => {
it("should have suggestion-paragraph type registered in the PM schema", () => {
const editor = BlockNoteEditor.create();
const nodeTypes = Object.keys(editor.pmSchema.nodes);
+ // asserts suggestion-* names, real nodes are *--attributed
expect(nodeTypes).toContain("suggestion-paragraph");
expect(nodeTypes).toContain("blockContainer");
expect(nodeTypes).toContain("blockGroup");
diff --git a/packages/core/src/schema/blocks/createSpec.ts b/packages/core/src/schema/blocks/createSpec.ts
index 99bd7aad85..324ab4a612 100644
--- a/packages/core/src/schema/blocks/createSpec.ts
+++ b/packages/core/src/schema/blocks/createSpec.ts
@@ -258,6 +258,7 @@ export function addNodeAndExtensionsToSpec<
// 1. isRequired: true prevents ProseMirror's DOMParser from auto-creating
// suggestion nodes to satisfy optional content expressions
// 2. Rendered as data-suggestion="true" on the wrapper div for HTML parsing
+ // isRequired sentinel hack blocks DOMParser auto-creating shadow nodes
attrs["y-attributed"] = {
isRequired: true,
parseHTML: (element: HTMLElement) => {
@@ -285,6 +286,7 @@ export function addNodeAndExtensionsToSpec<
];
},
renderHTML({ HTMLAttributes }) {
+ // shadow node gets editable contentDOM, no nodeView selectable fix
const div = document.createElement("div");
return wrapInBlockStructure(
{
diff --git a/packages/core/src/schema/blocks/internal.ts b/packages/core/src/schema/blocks/internal.ts
index 6a76d1ca05..15d1078573 100644
--- a/packages/core/src/schema/blocks/internal.ts
+++ b/packages/core/src/schema/blocks/internal.ts
@@ -101,6 +101,7 @@ export function getBlockFromPos<
}
// Gets parent blockContainer node
const blockContainer = tipTapEditor.state.doc.resolve(pos!).node();
+ // dead guard — real nodes are named "*--attributed", never "suggestion-"
if (blockContainer.type.name.startsWith("suggestion-")) {
// The blockContent is inside a suggestion node, which is inside a blockContainer.
// Return a stub block since suggestion nodes are transparent to the Block API.
diff --git a/packages/core/src/y/extensions/YSync.ts b/packages/core/src/y/extensions/YSync.ts
index 0acb7c8d10..c05fcdc032 100644
--- a/packages/core/src/y/extensions/YSync.ts
+++ b/packages/core/src/y/extensions/YSync.ts
@@ -122,6 +122,7 @@ export const YSyncExtension = createExtension(
nodeName: string,
kinds: { delete: boolean; insert: boolean; format: boolean },
) => {
+ // attributedNodes gates shadows on blockSpecs and delete only
const result = Boolean(
editor.schema.blockSpecs[nodeName] && kinds.delete,
);
diff --git a/packages/core/src/yjs/extensions/schemaMigration/migrationRules/moveColorAttributes.ts b/packages/core/src/yjs/extensions/schemaMigration/migrationRules/moveColorAttributes.ts
index 0866c3523c..16a7daac5a 100644
--- a/packages/core/src/yjs/extensions/schemaMigration/migrationRules/moveColorAttributes.ts
+++ b/packages/core/src/yjs/extensions/schemaMigration/migrationRules/moveColorAttributes.ts
@@ -73,11 +73,13 @@ export const moveColorAttributes: MigrationRule = (fragment, tr) => {
node.type.name === "blockContainer" &&
targetBlockContainers.has(node.attrs.id)
) {
+ // nodeAt(pos+1) assumes blockContent is first child (legacy input has no suggestions)
const el = tr.doc.nodeAt(pos + 1);
if (!el) {
throw new Error("No element found");
}
+ // setNodeMarkup(pos+1) assumes blockContent is first child (legacy input has no suggestions)
tr.setNodeMarkup(pos + 1, undefined, {
// preserve existing attributes
...el.attrs,
diff --git a/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx b/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx
index fd9ab688bf..d9e8f1d6b6 100644
--- a/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx
+++ b/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx
@@ -41,6 +41,7 @@ export const EmbedTab = <
(event: KeyboardEvent) => {
if (event.key === "Enter" && !event.nativeEvent.isComposing) {
event.preventDefault();
+ // untracked prop edit on a possibly suggested file block
editor.updateBlock(block.id, {
props: {
name: filenameFromURL(currentURL),
@@ -53,6 +54,7 @@ export const EmbedTab = <
);
const handleURLClick = useCallback(() => {
+ // untracked prop edit on a possibly suggested file block
editor.updateBlock(block.id, {
props: {
name: filenameFromURL(currentURL),
diff --git a/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx b/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx
index 64d5a1c74f..77b0a97bd3 100644
--- a/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx
+++ b/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx
@@ -62,6 +62,7 @@ export const UploadTab = <
},
};
}
+ // untracked prop edit on a possibly suggested file block
editor.updateBlock(props.blockId, updateData);
} catch (e) {
setUploadFailed(true);
diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx
index 470a50dcba..83a7cea12e 100644
--- a/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx
+++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx
@@ -19,6 +19,7 @@ export const AddCommentButtonInner = () => {
const { store } = useExtension(FormattingToolbarExtension);
const onClick = useCallback(() => {
+ // comment can anchor to a suggested-deleted range
comments.startPendingComment();
store.setState(false);
}, [comments, store]);
diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx
index 309cc10e13..0655af2451 100644
--- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx
+++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx
@@ -68,6 +68,7 @@ export const FileCaptionButton = () => {
caption: "string",
})
) {
+ // untracked prop edit on a possibly suggested file block
editor.updateBlock(block.id, {
props: {
caption: event.currentTarget.value,
diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx
index 9e2272c594..a46f161a6e 100644
--- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx
+++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx
@@ -54,6 +54,7 @@ export const FileDeleteButton = () => {
const onClick = useCallback(() => {
if (block !== undefined) {
editor.focus();
+ // untracked delete on a possibly suggested file block
editor.removeBlocks([block.id]);
}
}, [block, editor]);
diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx
index 3902651e30..b1522cd235 100644
--- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx
+++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx
@@ -60,6 +60,7 @@ export const FilePreviewButton = () => {
showPreview: "boolean",
})
) {
+ // untracked prop edit on a possibly suggested file block
editor.updateBlock(block.id, {
props: {
showPreview: !block.props.showPreview,
diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx
index 87c30fef22..7b5808996d 100644
--- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx
+++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx
@@ -68,6 +68,7 @@ export const FileRenameButton = () => {
name: "string",
})
) {
+ // untracked prop edit on a possibly suggested file block
editor.updateBlock(block.id, {
props: {
name: event.currentTarget.value,
diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx
index a21f1006cb..c80bf1435d 100644
--- a/packages/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx
+++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx
@@ -108,6 +108,7 @@ export const TextAlignButton = (props: { textAlignment: TextAlignment }) => {
textAlignment: defaultProps.textAlignment,
})
) {
+ // untracked prop edit; block may be suggested-deleted/modified
editor.updateBlock(block, {
props: { textAlignment: textAlignment },
});
@@ -135,6 +136,7 @@ export const TextAlignButton = (props: { textAlignment: TextAlignment }) => {
newTable[row].cells[col].props.textAlignment = textAlignment;
});
+ // untracked content edit on a possibly suggested table block
editor.updateBlock(block, {
type: "table",
content: {
diff --git a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx
index d9bb12a00a..ae2a98e4e3 100644
--- a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx
+++ b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx
@@ -167,6 +167,7 @@ export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => {
return filteredItems.map((item) => {
const Icon = item.icon;
+ // reads suggestion-stripped type; wrong current type for suggested-modify
const typesMatch = item.type === firstSelectedBlock.type;
const propsMatch =
Object.entries(item.props || {}).filter(
@@ -181,6 +182,7 @@ export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => {
editor.focus();
editor.transact(() => {
for (const block of selectedBlocks) {
+ // untracked type change; block may be suggested-deleted/modified
editor.updateBlock(block, {
type: item.type as any,
props: item.props as any,
diff --git a/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx b/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx
index a10469eab1..4a7f0eabd8 100644
--- a/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx
+++ b/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx
@@ -69,6 +69,7 @@ export const FormattingToolbarController = (props: {
const placement = useEditorState({
editor,
selector: ({ editor }) => {
+ // resolves active block with no concept of suggestion state
const block = editor.getTextCursorPosition().block;
if (
diff --git a/packages/react/src/components/SideMenu/DefaultButtons/AddBlockButton.tsx b/packages/react/src/components/SideMenu/DefaultButtons/AddBlockButton.tsx
index ac1209fad6..d09d80fa94 100644
--- a/packages/react/src/components/SideMenu/DefaultButtons/AddBlockButton.tsx
+++ b/packages/react/src/components/SideMenu/DefaultButtons/AddBlockButton.tsx
@@ -27,6 +27,7 @@ export const AddBlockButton = () => {
return;
}
+ // untracked insert on a possibly suggested-deleted block
const blockContent = block.content;
const isBlockEmpty =
blockContent !== undefined &&
diff --git a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/BlockColorsItem.tsx b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/BlockColorsItem.tsx
index 97694194f7..3ff9536405 100644
--- a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/BlockColorsItem.tsx
+++ b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/BlockColorsItem.tsx
@@ -55,6 +55,7 @@ export const BlockColorsItem = (props: { children: ReactNode }) => {
})
? {
color: block.props.textColor,
+ // untracked prop edit; block may be suggested-deleted/modified
setColor: (color) =>
editor.updateBlock(block, {
type: block.type,
diff --git a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/RemoveBlockItem.tsx b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/RemoveBlockItem.tsx
index 12ade8b4ae..84ae65429e 100644
--- a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/RemoveBlockItem.tsx
+++ b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/RemoveBlockItem.tsx
@@ -28,6 +28,7 @@ export const RemoveBlockItem = (props: { children: ReactNode }) => {
selectedBlocks && selectedBlocks.some((b) => b.id === block.id)
? selectedBlocks
: [block];
+ // untracked delete; bypasses suggestion system on a possibly suggested block
editor.removeBlocks(blocksToRemove);
}}
>
diff --git a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx
index 51211b9e06..2d0a5e4a23 100644
--- a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx
+++ b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx
@@ -44,6 +44,7 @@ export const TableRowHeaderItem = (props: { children: ReactNode }) => {
className={"bn-menu-item"}
checked={isHeaderRow}
onClick={() => {
+ // untracked prop edit on a possibly suggested table block
editor.updateBlock(block, {
content: {
...block.content,
@@ -95,6 +96,7 @@ export const TableColumnHeaderItem = (props: { children: ReactNode }) => {
className={"bn-menu-item"}
checked={isHeaderColumn}
onClick={() => {
+ // untracked prop edit on a possibly suggested table block
editor.updateBlock(block, {
content: {
...block.content,
diff --git a/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx b/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx
index 9b863bde47..85a1f70952 100644
--- a/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx
+++ b/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx
@@ -99,6 +99,7 @@ export const ExtendButton = (
return;
}
+ // untracked content edit on a possibly suggested table block
editor.updateBlock(block, {
type: "table",
content: {
@@ -159,6 +160,7 @@ export const ExtendButton = (
newNumCells > 0 &&
newNumCells !== currentNumCells
) {
+ // untracked content edit on a possibly suggested table block
editor.updateBlock(block, {
type: "table",
content: {
diff --git a/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/ColorPicker.tsx b/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/ColorPicker.tsx
index 36e1ecca2f..f20f3983d3 100644
--- a/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/ColorPicker.tsx
+++ b/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/ColorPicker.tsx
@@ -33,6 +33,7 @@ export const ColorPickerButton = (props: { children?: ReactNode }) => {
return;
}
+ // reads only new content; suggested-modify old content is invisible
const newTable = block.content.rows.map((row) => {
return {
...row,
@@ -46,6 +47,7 @@ export const ColorPickerButton = (props: { children?: ReactNode }) => {
newTable[rowIndex].cells[colIndex].props.backgroundColor = color;
}
+ // untracked content edit on a possibly suggested table block
editor.updateBlock(block, {
type: "table",
content: {
diff --git a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/ColorPicker.tsx b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/ColorPicker.tsx
index 812a44ec35..72399b2b48 100644
--- a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/ColorPicker.tsx
+++ b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/ColorPicker.tsx
@@ -63,6 +63,7 @@ export const ColorPickerButton = <
return;
}
+ // reads only new content; suggested-modify old content is invisible
const newTable = block.content.rows.map((row) => {
return {
...row,
@@ -78,6 +79,7 @@ export const ColorPickerButton = <
}
});
+ // untracked content edit on a possibly suggested table block
editor.updateBlock(block, {
type: "table",
content: {
diff --git a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/TableHeaderButton.tsx b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/TableHeaderButton.tsx
index 285f251b48..8cd08ee3a3 100644
--- a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/TableHeaderButton.tsx
+++ b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/TableHeaderButton.tsx
@@ -55,6 +55,7 @@ export const TableHeaderRowButton = <
className={"bn-menu-item"}
checked={isHeaderRow}
onClick={() => {
+ // untracked prop edit on a possibly suggested table block
editor.updateBlock(block, {
...block,
content: {
@@ -110,6 +111,7 @@ export const TableHeaderColumnButton = <
className={"bn-menu-item"}
checked={isHeaderColumn}
onClick={() => {
+ // untracked prop edit on a possibly suggested table block
editor.updateBlock(block, {
...block,
content: {
diff --git a/packages/react/src/schema/ReactBlockSpec.tsx b/packages/react/src/schema/ReactBlockSpec.tsx
index da722395b3..8fbbe8692c 100644
--- a/packages/react/src/schema/ReactBlockSpec.tsx
+++ b/packages/react/src/schema/ReactBlockSpec.tsx
@@ -266,6 +266,7 @@ export function createReactBlockSpec<
return output;
},
render(block, editor) {
+ // nodeView wires real node only, shadow stays vanilla editable
if (this.renderType === "nodeView") {
return ReactNodeViewRenderer(
(props: NodeViewProps) => {
diff --git a/packages/xl-ai/src/prosemirror/agent.ts b/packages/xl-ai/src/prosemirror/agent.ts
index f0c5f0063a..3866831f86 100644
--- a/packages/xl-ai/src/prosemirror/agent.ts
+++ b/packages/xl-ai/src/prosemirror/agent.ts
@@ -186,6 +186,7 @@ export function getStepsAsAgent(inputTr: Transform) {
const stepIndex = tr.steps.length;
if (isReplacing) {
const $pos = tr.doc.resolve(tr.mapping.map(from));
+ // isBlock true for shadow nodes, may mark suggestion content
if ($pos.nodeAfter?.isBlock) {
// mark the entire node as deleted. This can be needed for inline nodes or table cells
tr.addNodeMark($pos.pos, pmSchema.mark("y-attributed-delete", {}));
@@ -216,6 +217,7 @@ export function getStepsAsAgent(inputTr: Transform) {
) {
return true;
}
+ // isBlock true for shadow nodes, may mark suggestion content
if (node.isBlock) {
tr.addNodeMark(pos, pmSchema.mark("y-attributed-insert", {}));
}
diff --git a/packages/xl-ai/src/prosemirror/changeset.ts b/packages/xl-ai/src/prosemirror/changeset.ts
index 7f40964016..eab66ce5eb 100644
--- a/packages/xl-ai/src/prosemirror/changeset.ts
+++ b/packages/xl-ai/src/prosemirror/changeset.ts
@@ -128,6 +128,7 @@ function addMissingChanges(
}
const createEncoder = (doc: Node, updatedDoc: Node) => {
+ // encoder ignores --attributed shadow nodes, diffs suggestion content
// this encoder makes sure unchanged table cells stay intact,
// without this, prosemirror-changeset would too eagerly
// return changes across table cells (this is covered in test cases).