@wordpress/block-editor
Version:
307 lines (285 loc) • 7.94 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,
useEffect,
useMemo,
useCallback,
} 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();
IntersectionObserver.displayName = 'IntersectionObserverContext';
const pendingBlockVisibilityUpdatesPerRegistry = new WeakMap();
const delayedBlockVisibilityDebounceOptions = {
trailing: true,
};
function Root( { className, ...settings } ) {
const { isOutlineMode, isFocusMode, editedContentOnlySection } = useSelect(
( select ) => {
const {
getSettings,
isTyping,
hasBlockSpotlight,
getEditedContentOnlySection,
} = unlock( select( blockEditorStore ) );
const { outlineMode, focusMode } = getSettings();
return {
isOutlineMode: outlineMode && ! isTyping(),
isFocusMode: focusMode || hasBlockSpotlight(),
editedContentOnlySection: getEditedContentOnlySection(),
};
},
[]
);
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,
delayedBlockVisibilityDebounceOptions
);
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 } />
{ !! editedContentOnlySection && (
<StopEditingContentOnlySectionOnOutsideSelect
clientId={ editedContentOnlySection }
/>
) }
</IntersectionObserver.Provider>
);
}
function StopEditingContentOnlySectionOnOutsideSelect( { clientId } ) {
const { stopEditingContentOnlySection } = unlock(
useDispatch( blockEditorStore )
);
const isBlockOrDescendantSelected = useSelect(
( select ) => {
const {
isBlockSelected,
hasSelectedInnerBlock,
getBlockSelectionStart,
} = select( blockEditorStore );
return (
! getBlockSelectionStart() ||
isBlockSelected( clientId ) ||
hasSelectedInnerBlock( clientId, true )
);
},
[ clientId ]
);
useEffect( () => {
if ( ! isBlockOrDescendantSelected ) {
stopEditingContentOnlySection();
}
}, [ isBlockOrDescendantSelected, stopEditingContentOnlySection ] );
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,
isContainerInsertableToInContentOnlyMode,
getBlockName,
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
);
const templateLock = getTemplateLock( rootClientId );
return {
order: _order,
selectedBlocks: selectedBlockClientIds,
visibleBlocks: __unstableGetVisibleBlocks(),
isZoomOut: _isZoomOut(),
shouldRenderAppender:
( ! isSectionBlock( rootClientId ) ||
isContainerInsertableToInContentOnlyMode(
getBlockName( selectedBlockClientId ),
rootClientId
) ) &&
getBlockEditingMode( rootClientId ) !== 'disabled' &&
( ! templateLock || templateLock === 'contentOnly' ) &&
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>
);
}