@@ -255,6 +255,10 @@ const fragmentHasNodeView = (
255255 * using BlockNote's `blocksToFullHTML` pipeline when possible, falling back
256256 * to `DOMSerializer`.
257257 *
258+ * Diff attributes (class, data-diff-type, author info, etc.) are applied
259+ * directly to the rendered root element rather than wrapping in an extra
260+ * container, keeping the DOM flat.
261+ *
258262 * For inline fragments the content is first wrapped in a paragraph node so
259263 * it can be converted to blocks; the rendered inline content is then extracted
260264 * from the `.bn-inline-content` wrapper so it stays inline in the document.
@@ -273,20 +277,24 @@ const renderDeletedFragment = (
273277 const tag = opts . isInline ? "span" : "div" ;
274278 const diffType = opts . isInline ? "inline-delete" : "block-delete" ;
275279
276- const container = document . createElement ( tag ) ;
277- container . className = "pm-suggest pm-suggest--delete" ;
278- container . setAttribute ( "data-diff-type" , diffType ) ;
279- if ( opts . authorIds . length ) {
280- container . setAttribute ( "data-diff-user-id" , opts . authorIds . join ( "," ) ) ;
281- }
282- if ( opts . color ) {
283- container . style . setProperty ( "--author-color" , opts . color ) ;
284- }
285- container . setAttribute ( "title" , opts . title ) ;
286- container . contentEditable = "false" ;
280+ /** Apply diff attributes to an element in-place. */
281+ const applyDiffAttrs = ( el : HTMLElement ) => {
282+ el . classList . add ( "pm-suggest" , "pm-suggest--delete" ) ;
283+ el . setAttribute ( "data-diff-type" , diffType ) ;
284+ if ( opts . authorIds . length ) {
285+ el . setAttribute ( "data-diff-user-id" , opts . authorIds . join ( "," ) ) ;
286+ }
287+ if ( opts . color ) {
288+ el . style . setProperty ( "--author-color" , opts . color ) ;
289+ }
290+ el . setAttribute ( "title" , opts . title ) ;
291+ el . contentEditable = "false" ;
292+ } ;
287293
288294 if ( fragment . size === 0 ) {
289- return container ;
295+ const empty = document . createElement ( tag ) ;
296+ applyDiffAttrs ( empty ) ;
297+ return empty ;
290298 }
291299
292300 // For inline content, wrap in a paragraph so it forms a valid block tree.
@@ -298,6 +306,8 @@ const renderDeletedFragment = (
298306 blockFragment = Fragment . from ( paragraphNode ) ;
299307 } else {
300308 // Can't wrap in paragraph — fall back to DOMSerializer
309+ const container = document . createElement ( tag ) ;
310+ applyDiffAttrs ( container ) ;
301311 const serializer = DOMSerializer . fromSchema ( schema ) ;
302312 container . appendChild (
303313 serializer . serializeFragment ( fragment , { document } ) ,
@@ -316,7 +326,7 @@ const renderDeletedFragment = (
316326 : null ;
317327 const isSubBlockContent = wrappingPath && wrappingPath . length > 3 ;
318328
319- let rendered = false ;
329+ let rendered : HTMLElement | null = null ;
320330
321331 if ( ! isSubBlockContent ) {
322332 const ghostDoc = wrapFragmentInDoc ( blockFragment , schema ) ;
@@ -328,23 +338,31 @@ const renderDeletedFragment = (
328338 editor . pmSchema ,
329339 ) ;
330340 const html = editor . blocksToFullHTML ( slicedBlocks . blocks ) ;
341+ const temp = document . createElement ( "div" ) ;
342+ temp . innerHTML = html ;
331343
332344 if ( opts . isInline ) {
333345 // Extract just the inline content from the block wrapper.
334- const temp = document . createElement ( "div" ) ;
335- temp . innerHTML = html ;
336346 const inlineContentEl = temp . querySelector ( ".bn-inline-content" ) ;
337347 if ( inlineContentEl ) {
348+ const span = document . createElement ( "span" ) ;
338349 while ( inlineContentEl . firstChild ) {
339- container . appendChild ( inlineContentEl . firstChild ) ;
350+ span . appendChild ( inlineContentEl . firstChild ) ;
340351 }
352+ rendered = span ;
341353 } else {
342- container . innerHTML = html ;
354+ // No .bn-inline-content found — use the root element
355+ rendered = temp . firstElementChild as HTMLElement | null ;
343356 }
344357 } else {
345- container . innerHTML = html ;
358+ // Extract the .bn-block-outer element so we don't add an extra
359+ // bn-block-group wrapper — the widget is already inserted inside
360+ // an existing block-group in the document.
361+ const blockOuter = temp . querySelector (
362+ ".bn-block-outer" ,
363+ ) as HTMLElement | null ;
364+ rendered = blockOuter ?? ( temp . firstElementChild as HTMLElement | null ) ;
346365 }
347- rendered = true ;
348366 } catch ( e ) {
349367 // prosemirrorSliceToSlicedBlocks doesn't support all node structures.
350368 // Fall through to DOMSerializer fallback.
@@ -360,12 +378,24 @@ const renderDeletedFragment = (
360378 // Fallback: use DOMSerializer for sub-block nodes (tableCell, etc.)
361379 // or when wrapping/conversion failed.
362380 const serializer = DOMSerializer . fromSchema ( schema ) ;
363- container . appendChild (
364- serializer . serializeFragment ( fragment , { document } ) ,
381+ const serialized = serializer . serializeFragment ( fragment , { document } ) ;
382+
383+ // If the fragment serializes to a single element, use it directly
384+ // to avoid an extra wrapper (e.g. <td> stays as <td>, not <div><td>).
385+ const children = Array . from ( serialized . childNodes ) . filter (
386+ ( n ) : n is HTMLElement => n . nodeType === 1 , // ELEMENT_NODE
365387 ) ;
388+ if ( children . length === 1 && serialized . childNodes . length === 1 ) {
389+ rendered = children [ 0 ] ;
390+ } else {
391+ const container = document . createElement ( tag ) ;
392+ container . appendChild ( serialized ) ;
393+ rendered = container ;
394+ }
366395 }
367396
368- return container ;
397+ applyDiffAttrs ( rendered ) ;
398+ return rendered ;
369399} ;
370400
371401/**
0 commit comments