@wordpress/block-library
Version:
Block library for the WordPress editor.
148 lines (141 loc) • 3.92 kB
JavaScript
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { FormTokenField } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { useState, useEffect, useMemo } from '@wordpress/element';
import { useDebounce } from '@wordpress/compose';
/**
* Internal dependencies
*/
import { getEntitiesInfo, mapToIHasNameAndId } from '../../utils';
const EMPTY_ARRAY = [];
const BASE_QUERY = {
order: 'asc',
_fields: 'id,title',
context: 'view',
};
function ParentControl( { parents, postType, onChange } ) {
const [ search, setSearch ] = useState( '' );
const [ value, setValue ] = useState( EMPTY_ARRAY );
const [ suggestions, setSuggestions ] = useState( EMPTY_ARRAY );
const debouncedSearch = useDebounce( setSearch, 250 );
const { searchResults, searchHasResolved } = useSelect(
( select ) => {
if ( ! search ) {
return { searchResults: EMPTY_ARRAY, searchHasResolved: true };
}
const { getEntityRecords, hasFinishedResolution } =
select( coreStore );
const selectorArgs = [
'postType',
postType,
{
...BASE_QUERY,
search,
orderby: 'relevance',
exclude: parents,
per_page: 20,
},
];
return {
searchResults: getEntityRecords( ...selectorArgs ),
searchHasResolved: hasFinishedResolution(
'getEntityRecords',
selectorArgs
),
};
},
[ search, postType, parents ]
);
const currentParents = useSelect(
( select ) => {
if ( ! parents?.length ) {
return EMPTY_ARRAY;
}
const { getEntityRecords } = select( coreStore );
return getEntityRecords( 'postType', postType, {
...BASE_QUERY,
include: parents,
per_page: parents.length,
} );
},
[ parents, postType ]
);
// Update the `value` state only after the selectors are resolved
// to avoid emptying the input when we're changing parents.
useEffect( () => {
if ( ! parents?.length ) {
setValue( EMPTY_ARRAY );
}
if ( ! currentParents?.length ) {
return;
}
const currentParentsInfo = getEntitiesInfo(
mapToIHasNameAndId( currentParents, 'title.rendered' )
);
// Returns only the existing entity ids. This prevents the component
// from crashing in the editor, when non existing ids are provided.
const sanitizedValue = parents.reduce( ( accumulator, id ) => {
const entity = currentParentsInfo.mapById[ id ];
if ( entity ) {
accumulator.push( {
id,
value: entity.name,
} );
}
return accumulator;
}, [] );
setValue( sanitizedValue );
}, [ parents, currentParents ] );
const entitiesInfo = useMemo( () => {
if ( ! searchResults?.length ) {
return EMPTY_ARRAY;
}
return getEntitiesInfo(
mapToIHasNameAndId( searchResults, 'title.rendered' )
);
}, [ searchResults ] );
// Update suggestions only when the query has resolved.
useEffect( () => {
if ( ! searchHasResolved ) {
return;
}
setSuggestions( entitiesInfo.names );
}, [ entitiesInfo.names, searchHasResolved ] );
const getIdByValue = ( entitiesMappedByName, entity ) => {
const id = entity?.id || entitiesMappedByName?.[ entity ]?.id;
if ( id ) {
return id;
}
};
const onParentChange = ( newValue ) => {
const ids = Array.from(
newValue.reduce( ( accumulator, entity ) => {
// Verify that new values point to existing entities.
const id = getIdByValue( entitiesInfo.mapByName, entity );
if ( id ) {
accumulator.add( id );
}
return accumulator;
}, new Set() )
);
setSuggestions( EMPTY_ARRAY );
onChange( { parents: ids } );
};
return (
<FormTokenField
__next40pxDefaultSize
label={ __( 'Parents' ) }
value={ value }
onInputChange={ debouncedSearch }
suggestions={ suggestions }
onChange={ onParentChange }
__experimentalShowHowTo={ false }
__nextHasNoMarginBottom
/>
);
}
export default ParentControl;