@wordpress/block-editor
Version:
282 lines (264 loc) • 7.37 kB
JavaScript
/**
* External dependencies
*/
import clsx from 'clsx';
/**
* WordPress dependencies
*/
import {
AsyncModeProvider,
useSelect,
useDispatch,
useRegistry,
} from '@wordpress/data';
import { useMergeRefs, useDebounce } from '@wordpress/compose';
import {
createContext,
useMemo,
useCallback,
useEffect,
} from '@wordpress/element';
import { getDefaultBlockName } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import BlockListBlock from './block';
import BlockListAppender from '../block-list-appender';
import { useInBetweenInserter } from './use-in-between-inserter';
import { store as blockEditorStore } from '../../store';
import { LayoutProvider, defaultLayout } from './layout';
import { useBlockSelectionClearer } from '../block-selection-clearer';
import { useInnerBlocksProps } from '../inner-blocks';
import {
BlockEditContextProvider,
DEFAULT_BLOCK_EDIT_CONTEXT,
} from '../block-edit/context';
import { useTypingObserver } from '../observe-typing';
import { ZoomOutSeparator } from './zoom-out-separator';
import { unlock } from '../../lock-unlock';
export const IntersectionObserver = createContext();
const pendingBlockVisibilityUpdatesPerRegistry = new WeakMap();
function Root( { className, ...settings } ) {
const { isOutlineMode, isFocusMode, temporarilyEditingAsBlocks } =
useSelect( ( select ) => {
const { getSettings, getTemporarilyEditingAsBlocks, isTyping } =
unlock( select( blockEditorStore ) );
const { outlineMode, focusMode } = getSettings();
return {
isOutlineMode: outlineMode && ! isTyping(),
isFocusMode: focusMode,
temporarilyEditingAsBlocks: getTemporarilyEditingAsBlocks(),
};
}, [] );
const registry = useRegistry();
const { setBlockVisibility } = useDispatch( blockEditorStore );
const delayedBlockVisibilityUpdates = useDebounce(
useCallback( () => {
const updates = {};
pendingBlockVisibilityUpdatesPerRegistry
.get( registry )
.forEach( ( [ id, isIntersecting ] ) => {
updates[ id ] = isIntersecting;
} );
setBlockVisibility( updates );
}, [ registry ] ),
300,
{
trailing: true,
}
);
const intersectionObserver = useMemo( () => {
const { IntersectionObserver: Observer } = window;
if ( ! Observer ) {
return;
}
return new Observer( ( entries ) => {
if ( ! pendingBlockVisibilityUpdatesPerRegistry.get( registry ) ) {
pendingBlockVisibilityUpdatesPerRegistry.set( registry, [] );
}
for ( const entry of entries ) {
const clientId = entry.target.getAttribute( 'data-block' );
pendingBlockVisibilityUpdatesPerRegistry
.get( registry )
.push( [ clientId, entry.isIntersecting ] );
}
delayedBlockVisibilityUpdates();
} );
}, [] );
const innerBlocksProps = useInnerBlocksProps(
{
ref: useMergeRefs( [
useBlockSelectionClearer(),
useInBetweenInserter(),
useTypingObserver(),
] ),
className: clsx( 'is-root-container', className, {
'is-outline-mode': isOutlineMode,
'is-focus-mode': isFocusMode,
} ),
},
settings
);
return (
<IntersectionObserver.Provider value={ intersectionObserver }>
<div { ...innerBlocksProps } />
{ !! temporarilyEditingAsBlocks && (
<StopEditingAsBlocksOnOutsideSelect
clientId={ temporarilyEditingAsBlocks }
/>
) }
</IntersectionObserver.Provider>
);
}
function StopEditingAsBlocksOnOutsideSelect( { clientId } ) {
const { stopEditingAsBlocks } = unlock( useDispatch( blockEditorStore ) );
const isBlockOrDescendantSelected = useSelect(
( select ) => {
const { isBlockSelected, hasSelectedInnerBlock } =
select( blockEditorStore );
return (
isBlockSelected( clientId ) ||
hasSelectedInnerBlock( clientId, true )
);
},
[ clientId ]
);
useEffect( () => {
if ( ! isBlockOrDescendantSelected ) {
stopEditingAsBlocks( clientId );
}
}, [ isBlockOrDescendantSelected, clientId, stopEditingAsBlocks ] );
return null;
}
export default function BlockList( settings ) {
return (
<BlockEditContextProvider value={ DEFAULT_BLOCK_EDIT_CONTEXT }>
<Root { ...settings } />
</BlockEditContextProvider>
);
}
const EMPTY_ARRAY = [];
const EMPTY_SET = new Set();
function Items( {
placeholder,
rootClientId,
renderAppender: CustomAppender,
__experimentalAppenderTagName,
layout = defaultLayout,
} ) {
// Avoid passing CustomAppender to useSelect because it could be a new
// function on every render.
const hasAppender = CustomAppender !== false;
const hasCustomAppender = !! CustomAppender;
const {
order,
isZoomOut,
selectedBlocks,
visibleBlocks,
shouldRenderAppender,
} = useSelect(
( select ) => {
const {
getSettings,
getBlockOrder,
getSelectedBlockClientIds,
__unstableGetVisibleBlocks,
getTemplateLock,
getBlockEditingMode,
isSectionBlock,
isZoomOut: _isZoomOut,
canInsertBlockType,
} = unlock( select( blockEditorStore ) );
const _order = getBlockOrder( rootClientId );
if ( getSettings().isPreviewMode ) {
return {
order: _order,
selectedBlocks: EMPTY_ARRAY,
visibleBlocks: EMPTY_SET,
};
}
const selectedBlockClientIds = getSelectedBlockClientIds();
const selectedBlockClientId = selectedBlockClientIds[ 0 ];
const showRootAppender =
! rootClientId &&
! selectedBlockClientId &&
( ! _order.length ||
! canInsertBlockType(
getDefaultBlockName(),
rootClientId
) );
const hasSelectedRoot = !! (
rootClientId &&
selectedBlockClientId &&
rootClientId === selectedBlockClientId
);
return {
order: _order,
selectedBlocks: selectedBlockClientIds,
visibleBlocks: __unstableGetVisibleBlocks(),
isZoomOut: _isZoomOut(),
shouldRenderAppender:
! isSectionBlock( rootClientId ) &&
getBlockEditingMode( rootClientId ) !== 'disabled' &&
! getTemplateLock( rootClientId ) &&
hasAppender &&
! _isZoomOut() &&
( hasCustomAppender ||
hasSelectedRoot ||
showRootAppender ),
};
},
[ rootClientId, hasAppender, hasCustomAppender ]
);
return (
<LayoutProvider value={ layout }>
{ order.map( ( clientId ) => (
<AsyncModeProvider
key={ clientId }
value={
// Only provide data asynchronously if the block is
// not visible and not selected.
! visibleBlocks.has( clientId ) &&
! selectedBlocks.includes( clientId )
}
>
{ isZoomOut && (
<ZoomOutSeparator
clientId={ clientId }
rootClientId={ rootClientId }
position="top"
/>
) }
<BlockListBlock
rootClientId={ rootClientId }
clientId={ clientId }
/>
{ isZoomOut && (
<ZoomOutSeparator
clientId={ clientId }
rootClientId={ rootClientId }
position="bottom"
/>
) }
</AsyncModeProvider>
) ) }
{ order.length < 1 && placeholder }
{ shouldRenderAppender && (
<BlockListAppender
tagName={ __experimentalAppenderTagName }
rootClientId={ rootClientId }
CustomAppender={ CustomAppender }
/>
) }
</LayoutProvider>
);
}
export function BlockListItems( props ) {
// This component needs to always be synchronous as it's the one changing
// the async mode depending on the block selection.
return (
<AsyncModeProvider value={ false }>
<Items { ...props } />
</AsyncModeProvider>
);
}