Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/block-library/src/table/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,7 @@ const Cell = memo( function ( {
) }
>
<RichText
identifier={ `${ name }[${ rowIndex }].cells[${ columnIndex }].content` }
value={ content }
onChange={ onChange }
onFocus={ () => {
Expand Down
68 changes: 59 additions & 9 deletions packages/core-data/src/awareness/post-editor-awareness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
* Resolve a selection state to a text index and block client ID.
*
* For text-based selections, navigates up from the resolved Y.Text via
* AbstractType.parent to find the containing block, then resolves the
* parent chain to find the containing block, then resolves the
* local clientId via the block's tree path.
* For WholeBlock selections, resolves the block's relative position and
* then finds the local clientId via tree path.
Expand All @@ -234,14 +234,19 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
* clientIds (e.g. in "Show Template" mode where blocks are cloned).
*
* @param selection - The selection state.
* @return The rich-text offset and block client ID, or nulls if not resolvable.
* @return The rich-text offset, block client ID, and attribute key path.
*/
public convertSelectionStateToAbsolute( selection: SelectionState ): {
richTextOffset: number | null;
localClientId: string | null;
attributeKey: string | null;
} {
if ( selection.type === SelectionType.None ) {
return { richTextOffset: null, localClientId: null };
return {
richTextOffset: null,
localClientId: null,
attributeKey: null,
};
}

if ( selection.type === SelectionType.WholeBlock ) {
Expand All @@ -264,7 +269,7 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
}
}

return { richTextOffset: null, localClientId };
return { richTextOffset: null, localClientId, attributeKey: null };
}

// Text-based selections: resolve cursor position and navigate up.
Expand All @@ -279,13 +284,57 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
);

if ( ! absolutePosition ) {
return { richTextOffset: null, localClientId: null };
return {
richTextOffset: null,
localClientId: null,
attributeKey: null,
};
}

// Navigate up: Y.Text -> attributes Y.Map -> block Y.Map
const yType = absolutePosition.type.parent?.parent;
const path =
yType instanceof Y.Map ? getBlockPathInYdoc( yType ) : null;
// Navigate up from Y.Text to find the block and its attribute path.
let current: Y.AbstractType< any > | null = absolutePosition.type;
let blockMap: Y.Map< unknown > | null = null;
const pathParts: string[] = [];
let pendingIndex: number | null = null;

while ( current && current.parent ) {
const parent = current.parent;
if ( parent instanceof Y.Map ) {
let foundKey: string | null = null;
for ( const key of parent.keys() ) {
if ( parent.get( key ) === current ) {
foundKey = key;
break;
}
}

if ( foundKey ) {
if (
foundKey === 'attributes' &&
parent.has( 'clientId' )
) {
blockMap = parent;
break;
}
let part = foundKey;
if ( pendingIndex !== null ) {
part += `[${ pendingIndex }]`;
pendingIndex = null;
}
pathParts.unshift( part );
}
} else if ( parent instanceof Y.Array ) {
for ( let i = 0; i < parent.length; i++ ) {
if ( parent.get( i ) === current ) {
pendingIndex = i;
break;
}
}
}
current = parent;
}

const path = blockMap ? getBlockPathInYdoc( blockMap ) : null;
const localClientId = path ? resolveBlockClientIdByPath( path ) : null;

return {
Expand All @@ -294,6 +343,7 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
absolutePosition.index
),
localClientId,
attributeKey: pathParts.join( '.' ) || null,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ describe( 'use-post-editor-awareness-state hooks', () => {
expect( result.current( mockSelection ) ).toEqual( {
richTextOffset: null,
localClientId: null,
attributeKey: null,
} );
} );

Expand All @@ -309,6 +310,7 @@ describe( 'use-post-editor-awareness-state hooks', () => {
mockAwareness.convertSelectionStateToAbsolute.mockReturnValue( {
richTextOffset: 10,
localClientId: 'block-1',
attributeKey: 'content',
} );

const { result } = renderHook( () =>
Expand All @@ -323,6 +325,7 @@ describe( 'use-post-editor-awareness-state hooks', () => {
expect( position ).toEqual( {
richTextOffset: 10,
localClientId: 'block-1',
attributeKey: 'content',
} );
} );
} );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ interface AwarenessState {
const defaultResolvedSelection: ResolvedSelection = {
richTextOffset: null,
localClientId: null,
attributeKey: null,
};

const defaultState: AwarenessState = {
Expand Down
1 change: 1 addition & 0 deletions packages/core-data/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,5 @@ export type SelectionState =
export interface ResolvedSelection {
richTextOffset: number | null;
localClientId: string | null;
attributeKey: string | null;
}
55 changes: 54 additions & 1 deletion packages/core-data/src/utils/crdt-user-selections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,52 @@ export function getSelectionState(
};
}

/**
* Navigate a Yjs type hierarchy by a string path, supporting both Map keys
* and Array indices.
*
* Example: "body[0].cells[0].content"
*
* @param root - The starting Yjs type.
* @param path - The string path to navigate.
* @return The Yjs type at the path, or undefined if not found.
*/
function getYjsValueByPath(
root: Y.AbstractType< any >,
path: string
): Y.AbstractType< any > | undefined {
const parts = path.split( '.' );
let current: any = root;

for ( const part of parts ) {
// Handle array access like "body[0]"
const arrayMatch = part.match( /^(.+)\[(\d+)\]$/ );
if ( arrayMatch ) {
const [ , key, index ] = arrayMatch;
if ( ! ( current instanceof Y.Map ) ) {
return undefined;
}
current = current.get( key );
if ( ! ( current instanceof Y.Array ) ) {
return undefined;
}
current = current.get( parseInt( index, 10 ) );
} else {
// Handle simple Map key
if ( ! ( current instanceof Y.Map ) ) {
return undefined;
}
current = current.get( part );
}

if ( ! current ) {
return undefined;
}
}

return current instanceof Y.AbstractType ? current : undefined;
}

/**
* Get the cursor position from a selection.
*
Expand All @@ -169,7 +215,14 @@ function getCursorPosition(
}

const attributes = block.get( 'attributes' );
const currentYText = attributes?.get( selection.attributeKey );
if ( ! attributes ) {
return null;
}

const currentYText = getYjsValueByPath(
attributes,
selection.attributeKey
);

// If the attribute is not a Y.Text, return null.
if ( ! ( currentYText instanceof Y.Text ) ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ function computeCursorOnly(
start.richTextOffset,
blockElement,
overlayContext.editorDocument,
overlayContext.overlayRect
overlayContext.overlayRect,
start.attributeKey
),
};
}
Expand Down Expand Up @@ -150,7 +151,8 @@ function computeTextSelection(
activeEnd.richTextOffset,
activeEndBlock,
overlayContext.editorDocument,
overlayContext.overlayRect
overlayContext.overlayRect,
activeEnd.attributeKey
),
selectionRects: allRects,
};
Expand All @@ -167,7 +169,8 @@ function computeTextSelection(
start.richTextOffset,
startBlock,
overlayContext.editorDocument,
overlayContext.overlayRect
overlayContext.overlayRect,
start.attributeKey
),
};
}
Expand Down Expand Up @@ -203,7 +206,8 @@ function computeSingleBlockRects(
start.richTextOffset,
end.richTextOffset,
overlayContext.editorDocument,
overlayContext.overlayRect
overlayContext.overlayRect,
start.attributeKey
) ?? [],
blockElement,
};
Expand Down Expand Up @@ -265,7 +269,8 @@ function computeMultiBlockRects(
docFirst.richTextOffset,
Number.MAX_SAFE_INTEGER,
overlayContext.editorDocument,
overlayContext.overlayRect
overlayContext.overlayRect,
docFirst.attributeKey
);
if ( startRects ) {
allRects.push( ...startRects );
Expand All @@ -292,7 +297,8 @@ function computeMultiBlockRects(
0,
docLast.richTextOffset,
overlayContext.editorDocument,
overlayContext.overlayRect
overlayContext.overlayRect,
docLast.attributeKey
);
if ( endRects ) {
allRects.push( ...endRects );
Expand Down
Loading
Loading