UNPKG

@wordpress/block-editor

Version:
269 lines (244 loc) 7 kB
/** * External dependencies */ import clsx from 'clsx'; /** * WordPress dependencies */ import { useSelect, useDispatch } from '@wordpress/data'; import { useRef, createContext, useContext } from '@wordpress/element'; import { __unstableMotion as motion } from '@wordpress/components'; import { useReducedMotion } from '@wordpress/compose'; /** * Internal dependencies */ import Inserter from '../inserter'; import { store as blockEditorStore } from '../../store'; import BlockPopoverInbetween from '../block-popover/inbetween'; import BlockDropZonePopover from '../block-popover/drop-zone'; import { unlock } from '../../lock-unlock'; export const InsertionPointOpenRef = createContext(); function InbetweenInsertionPointPopover( { __unstablePopoverSlot, __unstableContentRef, operation = 'insert', nearestSide = 'right', } ) { const { selectBlock, hideInsertionPoint } = useDispatch( blockEditorStore ); const openRef = useContext( InsertionPointOpenRef ); const ref = useRef(); const { orientation, previousClientId, nextClientId, rootClientId, isInserterShown, isDistractionFree, isZoomOutMode, } = useSelect( ( select ) => { const { getBlockOrder, getBlockListSettings, getBlockInsertionPoint, isBlockBeingDragged, getPreviousBlockClientId, getNextBlockClientId, getSettings, isZoomOut, } = unlock( select( blockEditorStore ) ); const insertionPoint = getBlockInsertionPoint(); const order = getBlockOrder( insertionPoint.rootClientId ); if ( ! order.length ) { return {}; } let _previousClientId = order[ insertionPoint.index - 1 ]; let _nextClientId = order[ insertionPoint.index ]; while ( isBlockBeingDragged( _previousClientId ) ) { _previousClientId = getPreviousBlockClientId( _previousClientId ); } while ( isBlockBeingDragged( _nextClientId ) ) { _nextClientId = getNextBlockClientId( _nextClientId ); } const settings = getSettings(); return { previousClientId: _previousClientId, nextClientId: _nextClientId, orientation: getBlockListSettings( insertionPoint.rootClientId ) ?.orientation || 'vertical', rootClientId: insertionPoint.rootClientId, isDistractionFree: settings.isDistractionFree, isInserterShown: insertionPoint?.__unstableWithInserter, isZoomOutMode: isZoomOut(), }; }, [] ); const { getBlockEditingMode } = useSelect( blockEditorStore ); const disableMotion = useReducedMotion(); function onClick( event ) { if ( event.target === ref.current && nextClientId && getBlockEditingMode( nextClientId ) !== 'disabled' ) { selectBlock( nextClientId, -1 ); } } function maybeHideInserterPoint( event ) { // Only hide the inserter if it's triggered on the wrapper, // and the inserter is not open. if ( event.target === ref.current && ! openRef.current ) { hideInsertionPoint(); } } function onFocus( event ) { // Only handle click on the wrapper specifically, and not an event // bubbled from the inserter itself. if ( event.target !== ref.current ) { openRef.current = true; } } const lineVariants = { // Initial position starts from the center and invisible. start: { opacity: 0, scale: 0, }, // The line expands to fill the container. If the inserter is visible it // is delayed so it appears orchestrated. rest: { opacity: 1, scale: 1, transition: { delay: isInserterShown ? 0.5 : 0, type: 'tween' }, }, hover: { opacity: 1, scale: 1, transition: { delay: 0.5, type: 'tween' }, }, }; const inserterVariants = { start: { scale: disableMotion ? 1 : 0, }, rest: { scale: 1, transition: { delay: 0.4, type: 'tween' }, }, }; if ( isDistractionFree ) { return null; } // Zoom out mode should only show the insertion point for the insert operation. // Other operations such as "group" are when the editor tries to create a row // block by grouping the block being dragged with the block it's being dropped // onto. if ( isZoomOutMode && operation !== 'insert' ) { return null; } const orientationClassname = orientation === 'horizontal' || operation === 'group' ? 'is-horizontal' : 'is-vertical'; const className = clsx( 'block-editor-block-list__insertion-point', orientationClassname ); return ( <BlockPopoverInbetween previousClientId={ previousClientId } nextClientId={ nextClientId } __unstablePopoverSlot={ __unstablePopoverSlot } __unstableContentRef={ __unstableContentRef } operation={ operation } nearestSide={ nearestSide } > <motion.div layout={ ! disableMotion } initial={ disableMotion ? 'rest' : 'start' } animate="rest" whileHover="hover" whileTap="pressed" exit="start" ref={ ref } tabIndex={ -1 } onClick={ onClick } onFocus={ onFocus } className={ clsx( className, { 'is-with-inserter': isInserterShown, } ) } onHoverEnd={ maybeHideInserterPoint } > <motion.div variants={ lineVariants } className="block-editor-block-list__insertion-point-indicator" data-testid="block-list-insertion-point-indicator" /> { isInserterShown && ( <motion.div variants={ inserterVariants } className={ clsx( 'block-editor-block-list__insertion-point-inserter' ) } > <Inserter position="bottom center" clientId={ nextClientId } rootClientId={ rootClientId } __experimentalIsQuick onToggle={ ( isOpen ) => { openRef.current = isOpen; } } onSelectOrClose={ () => { openRef.current = false; } } /> </motion.div> ) } </motion.div> </BlockPopoverInbetween> ); } export default function InsertionPoint( props ) { const { insertionPoint, isVisible, isBlockListEmpty } = useSelect( ( select ) => { const { getBlockInsertionPoint, isBlockInsertionPointVisible, getBlockCount, } = select( blockEditorStore ); const blockInsertionPoint = getBlockInsertionPoint(); return { insertionPoint: blockInsertionPoint, isVisible: isBlockInsertionPointVisible(), isBlockListEmpty: getBlockCount( blockInsertionPoint?.rootClientId ) === 0, }; }, [] ); if ( ! isVisible || // Don't render the insertion point if the block list is empty. // The insertion point will be represented by the appender instead. isBlockListEmpty ) { return null; } /** * Render a popover that overlays the block when the desired operation is to replace it. * Otherwise, render a popover in between blocks for the indication of inserting between them. */ return insertionPoint.operation === 'replace' ? ( <BlockDropZonePopover // Force remount to trigger the animation. key={ `${ insertionPoint.rootClientId }-${ insertionPoint.index }` } { ...props } /> ) : ( <InbetweenInsertionPointPopover operation={ insertionPoint.operation } nearestSide={ insertionPoint.nearestSide } { ...props } /> ); }