@@ -283,43 +283,29 @@ export function mergeCrdtBlocks(
283283
284284 Object . entries ( value ) . forEach (
285285 ( [ attributeName , attributeValue ] ) => {
286- if (
287- fastDeepEqual (
288- currentAttributes ?. get ( attributeName ) ,
289- attributeValue
290- )
291- ) {
292- return ;
293- }
294-
295286 const currentAttribute =
296- currentAttributes . get ( attributeName ) ;
297- const isRichText = isRichTextAttribute (
287+ currentAttributes ?. get ( attributeName ) ;
288+
289+ const isExpectedType = isExpectedAttributeType (
298290 block . name ,
299- attributeName
291+ attributeName ,
292+ currentAttribute
300293 ) ;
301294
302- if (
303- isRichText &&
304- 'string' === typeof attributeValue &&
305- currentAttributes . has ( attributeName ) &&
306- currentAttribute instanceof Y . Text
307- ) {
308- // Rich text values are stored as persistent Y.Text instances.
309- // Update the value with a delta in place.
310- mergeRichTextUpdate (
295+ const isAttributeChanged =
296+ ! isExpectedType ||
297+ ! fastDeepEqual (
311298 currentAttribute ,
312- attributeValue ,
313- cursorPosition
299+ attributeValue
314300 ) ;
315- } else {
316- currentAttributes . set (
301+
302+ if ( isAttributeChanged ) {
303+ updateYBlockAttribute (
304+ block . name ,
317305 attributeName ,
318- createNewYAttributeValue (
319- block . name ,
320- attributeName ,
321- attributeValue
322- )
306+ attributeValue ,
307+ currentAttributes ,
308+ cursorPosition
323309 ) ;
324310 }
325311 }
@@ -422,45 +408,125 @@ function shouldBlockBeSynced( block: Block ): boolean {
422408 return true ;
423409}
424410
425- // Cache rich-text attributes for all block types.
426- let cachedRichTextAttributes : Map < string , Map < string , true > > ;
411+ /**
412+ * Update a single attribute on a Yjs block attributes map (currentAttributes).
413+ *
414+ * For rich-text attributes that already exist as Y.Text instances, the update
415+ * is applied as a delta merge so that concurrent edits are preserved. All
416+ * other attributes are replaced wholesale via `createNewYAttributeValue`.
417+ *
418+ * @param blockName The block type name, e.g. 'core/paragraph'.
419+ * @param attributeName The name of the attribute to update, e.g. 'content'.
420+ * @param attributeValue The new value for the attribute.
421+ * @param currentAttributes The Y.Map holding the block's current attributes.
422+ * @param cursorPosition The local cursor position, used when merging rich-text deltas.
423+ */
424+ function updateYBlockAttribute (
425+ blockName : string ,
426+ attributeName : string ,
427+ attributeValue : unknown ,
428+ currentAttributes : YBlockAttributes ,
429+ cursorPosition : number | null
430+ ) : void {
431+ const isRichText = isRichTextAttribute ( blockName , attributeName ) ;
432+ const currentAttribute = currentAttributes . get ( attributeName ) ;
433+
434+ if (
435+ isRichText &&
436+ 'string' === typeof attributeValue &&
437+ currentAttributes . has ( attributeName ) &&
438+ currentAttribute instanceof Y . Text
439+ ) {
440+ // Rich text values are stored as persistent Y.Text instances.
441+ // Update the value with a delta in place.
442+ mergeRichTextUpdate ( currentAttribute , attributeValue , cursorPosition ) ;
443+ } else {
444+ currentAttributes . set (
445+ attributeName ,
446+ createNewYAttributeValue ( blockName , attributeName , attributeValue )
447+ ) ;
448+ }
449+ }
450+
451+ // Cached types for block attributes.
452+ let cachedBlockAttributeTypes : Map < string , Map < string , string > > ;
427453
428454/**
429- * Given a block name and attribute key, return true if the attribute is rich-text typed .
455+ * Get the defined attribute type for a block attribute.
430456 *
431457 * @param blockName The name of the block, e.g. 'core/paragraph'.
432- * @param attributeName The name of the attribute to check , e.g. 'content'.
433- * @return True if the attribute is rich-text typed, false otherwise .
458+ * @param attributeName The name of the attribute, e.g. 'content'.
459+ * @return The type of the attribute, e.g. ' rich-text' or 'string' .
434460 */
435- function isRichTextAttribute (
461+ function getBlockAttributeType (
436462 blockName : string ,
437463 attributeName : string
438- ) : boolean {
439- if ( ! cachedRichTextAttributes ) {
464+ ) : string | undefined {
465+ if ( ! cachedBlockAttributeTypes ) {
440466 // Parse the attributes for all blocks once.
441- cachedRichTextAttributes = new Map < string , Map < string , true > > ( ) ;
467+ cachedBlockAttributeTypes = new Map < string , Map < string , string > > ( ) ;
442468
443469 for ( const blockType of getBlockTypes ( ) as BlockType [ ] ) {
444- const richTextAttributeMap = new Map < string , true > ( ) ;
470+ const blockAttributeTypeMap = new Map < string , string > ( ) ;
445471
446472 for ( const [ name , definition ] of Object . entries (
447473 blockType . attributes ?? { }
448474 ) ) {
449- if ( 'rich-text' === definition . type ) {
450- richTextAttributeMap . set ( name , true ) ;
475+ if ( definition . type ) {
476+ blockAttributeTypeMap . set ( name , definition . type ) ;
451477 }
452478 }
453479
454- cachedRichTextAttributes . set (
480+ cachedBlockAttributeTypes . set (
455481 blockType . name ,
456- richTextAttributeMap
482+ blockAttributeTypeMap
457483 ) ;
458484 }
459485 }
460486
461- return (
462- cachedRichTextAttributes . get ( blockName ) ?. has ( attributeName ) ?? false
487+ return cachedBlockAttributeTypes . get ( blockName ) ?. get ( attributeName ) ;
488+ }
489+
490+ /**
491+ * Check if an attribute value is the expected type.
492+ *
493+ * @param blockName The name of the block, e.g. 'core/paragraph'.
494+ * @param attributeName The name of the attribute, e.g. 'content'.
495+ * @param attributeValue The current attribute value.
496+ * @return True if the attribute type is expected, false otherwise.
497+ */
498+ function isExpectedAttributeType (
499+ blockName : string ,
500+ attributeName : string ,
501+ attributeValue : unknown
502+ ) : boolean {
503+ const expectedAttributeType = getBlockAttributeType (
504+ blockName ,
505+ attributeName
463506 ) ;
507+
508+ if ( expectedAttributeType === 'rich-text' ) {
509+ return attributeValue instanceof Y . Text ;
510+ } else if ( expectedAttributeType === 'string' ) {
511+ return typeof attributeValue === 'string' ;
512+ }
513+
514+ // No other types comparisons use special logic.
515+ return true ;
516+ }
517+
518+ /**
519+ * Given a block name and attribute key, return true if the attribute is rich-text typed.
520+ *
521+ * @param blockName The name of the block, e.g. 'core/paragraph'.
522+ * @param attributeName The name of the attribute to check, e.g. 'content'.
523+ * @return True if the attribute is rich-text typed, false otherwise.
524+ */
525+ function isRichTextAttribute (
526+ blockName : string ,
527+ attributeName : string
528+ ) : boolean {
529+ return 'rich-text' === getBlockAttributeType ( blockName , attributeName ) ;
464530}
465531
466532let localDoc : Y . Doc ;
0 commit comments