UNPKG

@wordpress/core-data

Version:
156 lines (138 loc) 4.47 kB
import { useSelect } from '@wordpress/data'; import { Y } from '@wordpress/sync'; // @ts-expect-error `@wordpress/block-editor` does not expose type declarations for its entry point. import { store as blockEditorStore } from '@wordpress/block-editor'; import type { AbsoluteBlockIndexPath } from '../types'; import { unlock } from '../lock-unlock'; /** * A block as represented in the block-editor store's client ID tree. * * This is a minimal interface covering only the fields used by RTC awareness. */ export type EditorStoreBlock = { clientId: string; innerBlocks: EditorStoreBlock[]; }; /** * Find the block Y.Map that contains a nested Yjs type. * * Rich-text attributes are often stored directly at attributes.content, but * blocks can also store rich text deeper inside object or array attributes. * Walk upward until we find the block map instead of assuming a fixed parent * depth. * * @param yType - The nested Yjs type to start from. * @return The containing block Y.Map, or null if no block ancestor exists. */ export function getContainingBlockYMap( yType: Y.AbstractType< any > ): Y.Map< unknown > | null { let current: Y.AbstractType< any > | null = yType; while ( current ) { const parent = current.parent; if ( parent instanceof Y.Map && parent.parent instanceof Y.Array && parent.get( 'clientId' ) !== undefined && parent.get( 'innerBlocks' ) instanceof Y.Array ) { return parent; } current = parent instanceof Y.AbstractType ? parent : null; } return null; } /** * Given a Y.Map within a Ydoc, traverse up the Yjs block tree to compute the * index path from the root. * * For example, the second inner block of the first root block returns [0, 1]. * * @param yType - The Yjs block Y.Map to start from. * @return The index path from root, or null if traversal fails. */ export function getBlockPathInYdoc( yType: Y.Map< unknown > ): AbsoluteBlockIndexPath | null { const path: AbsoluteBlockIndexPath = []; let current: Y.Map< unknown > = yType; while ( current ) { const parentArray = current.parent; if ( ! parentArray || ! ( parentArray instanceof Y.Array ) ) { return null; } // Find index of current block in its parent array. let index = -1; for ( let i = 0; i < parentArray.length; i++ ) { if ( parentArray.get( i ) === current ) { index = i; break; } } if ( index === -1 ) { return null; } path.unshift( index ); // Walk up: is the parent array's parent a block Y.Map or the root? const grandparent = parentArray.parent; if ( grandparent instanceof Y.Map && grandparent.get( 'clientId' ) !== undefined ) { current = grandparent; // It's a block, keep going. } else { break; // It's the root map, done. } } return path; } /** * Navigate the block-editor store's block tree by an index path * and return the local block's clientId. * * @param path - The index path, e.g. [0, 1] for blocks[0].innerBlocks[1]. * @param blocks - The tree of block-editor store post contentblocks. * @return The local block clientId, or null if the path is invalid. */ export function resolveBlockClientIdByPath( path: AbsoluteBlockIndexPath, blocks: EditorStoreBlock[] ): string | null { if ( path.length === 0 ) { return null; } for ( let i = 0; i < path.length; i++ ) { const block = blocks[ path[ i ] ]; if ( ! block ) { return null; } if ( i === path.length - 1 ) { return block.clientId; } blocks = block.innerBlocks; } return null; } /** * Find the post content blocks to use as the navigation root. * * In template mode, the block tree contains template parts wrapping a * core/post-content block. The Yjs document only stores the post content * blocks, so we need to find the core/post-content block and use * getClientIdsTree(clientId) to retrieve its inner blocks from the store. * * Uses the private getClientIdsTree selector which depends only on * state.blocks.order, avoiding unnecessary re-renders when block * attributes change (which would happen with getBlocks()). * * @return The blocks that correspond to the Yjs document root. */ export function usePostContentBlocks(): EditorStoreBlock[] { return useSelect( ( select ) => { const { getBlocksByName, getClientIdsTree } = unlock( select( blockEditorStore ) ); const [ postContentClientId ] = getBlocksByName( 'core/post-content' ); return getClientIdsTree( postContentClientId ?? '' ); }, [] ); }