@wordpress/core-data
Version:
Access to and manipulation of core WordPress entities.
132 lines (117 loc) • 3.69 kB
text/typescript
/**
* WordPress dependencies
*/
import { Y } from '@wordpress/sync';
/**
* Internal dependencies
*/
import type { YBlock, YBlocks } from './crdt-blocks';
import type { YPostRecord } from './crdt';
import { CRDT_RECORD_MAP_KEY } from '../sync';
/**
* A YMapRecord represents the shape of the data stored in a Y.Map.
*/
export type YMapRecord = Record< string, unknown >;
/**
* A wrapper around Y.Map to provide type safety. The generic type accepted by
* Y.Map represents the union of possible values of the map, which are varied in
* many cases. This type is accurate, but its non-specificity requires aggressive
* type narrowing or type casting / destruction with `as`.
*
* This type provides type enhancements so that the correct value type can be
* inferred based on the provided key. It is just a type wrap / overlay, and
* does not change the runtime behavior of Y.Map.
*
* This interface cannot extend Y.Map directly due to the limitations of
* TypeScript's structural typing. One negative consequence of this is that
* `instanceof` checks against Y.Map continue to work at runtime but will blur
* the type at compile time. To navigate this, use the `isYMap` function below.
*/
export interface YMapWrap< T extends YMapRecord > extends Y.AbstractType< T > {
delete: < K extends keyof T >( key: K ) => void;
forEach: (
callback: (
value: T[ keyof T ],
key: keyof T,
map: YMapWrap< T >
) => void
) => void;
has: < K extends keyof T >( key: K ) => boolean;
get: < K extends keyof T >( key: K ) => T[ K ] | undefined;
set: < K extends keyof T >( key: K, value: T[ K ] ) => void;
toJSON: () => T;
// add types for other Y.Map methods as needed
}
/**
* Get or create a root-level Map for the given Y.Doc. Use this instead of
* doc.getMap() for additional type safety.
*
* @param doc Y.Doc
* @param key Map key
*/
export function getRootMap< T extends YMapRecord >(
doc: Y.Doc,
key: string
): YMapWrap< T > {
return doc.getMap< T >( key ) as unknown as YMapWrap< T >;
}
/**
* Create a new Y.Map (provided with YMapWrap type), optionally initialized with
* data. Use this instead of `new Y.Map()` for additional type safety.
*
* @param partial Partial data to initialize the map with.
*/
export function createYMap< T extends YMapRecord >(
partial: Partial< T > = {}
): YMapWrap< T > {
return new Y.Map( Object.entries( partial ) ) as unknown as YMapWrap< T >;
}
/**
* Type guard to check if a value is a Y.Map without losing type information.
*
* @param value Value to check.
*/
export function isYMap< T extends YMapRecord >(
value: YMapWrap< T > | undefined
): value is YMapWrap< T > {
return value instanceof Y.Map;
}
/**
* Given a block ID and a Y.Doc, find the block in the document.
*
* @param blockId The block ID to find
* @param ydoc The Y.Doc to find the block in
* @return The block, or null if the block is not found
*/
export function findBlockByClientIdInDoc(
blockId: string,
ydoc: Y.Doc
): YBlock | null {
const ymap = getRootMap< YPostRecord >( ydoc, CRDT_RECORD_MAP_KEY );
const blocks = ymap.get( 'blocks' );
if ( ! ( blocks instanceof Y.Array ) ) {
return null;
}
return findBlockByClientIdInBlocks( blockId, blocks );
}
function findBlockByClientIdInBlocks(
blockId: string,
blocks: YBlocks
): YBlock | null {
for ( const block of blocks ) {
if ( block.get( 'clientId' ) === blockId ) {
return block;
}
const innerBlocks = block.get( 'innerBlocks' );
if ( innerBlocks && innerBlocks.length > 0 ) {
const innerBlock = findBlockByClientIdInBlocks(
blockId,
innerBlocks
);
if ( innerBlock ) {
return innerBlock;
}
}
}
return null;
}