UNPKG

@gechiui/block-editor

Version:
359 lines (323 loc) 9.87 kB
/** * External dependencies */ import { find } from 'lodash'; import classnames from 'classnames'; /** * GeChiUI dependencies */ import { useState, useRef, useEffect } from '@gechiui/element'; import { isUnmodifiedDefaultBlock } from '@gechiui/blocks'; import { Popover } from '@gechiui/components'; import { useDispatch, useSelect } from '@gechiui/data'; import { useShortcut } from '@gechiui/keyboard-shortcuts'; import { useViewportMatch } from '@gechiui/compose'; import { getScrollContainer } from '@gechiui/dom'; /** * Internal dependencies */ import BlockSelectionButton from './block-selection-button'; import BlockContextualToolbar from './block-contextual-toolbar'; import Inserter from '../inserter'; import { store as blockEditorStore } from '../../store'; import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; import { usePopoverScroll } from './use-popover-scroll'; function selector( select ) { const { isNavigationMode, isMultiSelecting, hasMultiSelection, isTyping, isCaretWithinFormattedText, getSettings, getLastMultiSelectedBlockClientId, } = select( blockEditorStore ); return { isNavigationMode: isNavigationMode(), isMultiSelecting: isMultiSelecting(), isTyping: isTyping(), isCaretWithinFormattedText: isCaretWithinFormattedText(), hasMultiSelection: hasMultiSelection(), hasFixedToolbar: getSettings().hasFixedToolbar, lastClientId: getLastMultiSelectedBlockClientId(), }; } function BlockPopover( { clientId, rootClientId, isValid, isEmptyDefaultBlock, capturingClientId, __unstablePopoverSlot, __unstableContentRef, } ) { const { isNavigationMode, isMultiSelecting, isTyping, isCaretWithinFormattedText, hasMultiSelection, hasFixedToolbar, lastClientId, } = useSelect( selector, [] ); const isInsertionPointVisible = useSelect( ( select ) => { const { isBlockInsertionPointVisible, getBlockInsertionPoint, getBlockOrder, } = select( blockEditorStore ); if ( ! isBlockInsertionPointVisible() ) { return false; } const insertionPoint = getBlockInsertionPoint(); const order = getBlockOrder( insertionPoint.rootClientId ); return order[ insertionPoint.index ] === clientId; }, [ clientId ] ); const isLargeViewport = useViewportMatch( 'medium' ); const [ isToolbarForced, setIsToolbarForced ] = useState( false ); const [ isInserterShown, setIsInserterShown ] = useState( false ); const { stopTyping } = useDispatch( blockEditorStore ); // Controls when the side inserter on empty lines should // be shown, including writing and selection modes. const showEmptyBlockSideInserter = ! isTyping && ! isNavigationMode && isEmptyDefaultBlock && isValid; const shouldShowBreadcrumb = isNavigationMode; const shouldShowContextualToolbar = ! isNavigationMode && ! hasFixedToolbar && isLargeViewport && ! showEmptyBlockSideInserter && ! isMultiSelecting && ( ! isTyping || isCaretWithinFormattedText ); const canFocusHiddenToolbar = ! isNavigationMode && ! shouldShowContextualToolbar && ! hasFixedToolbar && ! isEmptyDefaultBlock; useShortcut( 'core/block-editor/focus-toolbar', () => { setIsToolbarForced( true ); stopTyping( true ); }, { isDisabled: ! canFocusHiddenToolbar, } ); useEffect( () => { if ( ! shouldShowContextualToolbar ) { setIsToolbarForced( false ); } }, [ shouldShowContextualToolbar ] ); // Stores the active toolbar item index so the block toolbar can return focus // to it when re-mounting. const initialToolbarItemIndexRef = useRef(); const selectedElement = useBlockElement( clientId ); const lastSelectedElement = useBlockElement( lastClientId ); const capturingElement = useBlockElement( capturingClientId ); const popoverScrollRef = usePopoverScroll( __unstableContentRef ); if ( ! shouldShowBreadcrumb && ! shouldShowContextualToolbar && ! isToolbarForced && ! showEmptyBlockSideInserter ) { return null; } let node = selectedElement; if ( ! node ) { return null; } if ( capturingClientId ) { node = capturingElement; } let anchorRef = node; if ( hasMultiSelection ) { // Wait to render the popover until the bottom reference is available // as well. if ( ! lastSelectedElement ) { return null; } anchorRef = { top: node, bottom: lastSelectedElement, }; } function onFocus() { setIsInserterShown( true ); } function onBlur() { setIsInserterShown( false ); } // Position above the anchor, pop out towards the right, and position in the // left corner. For the side inserter, pop out towards the left, and // position in the right corner. // To do: refactor `Popover` to make this prop clearer. const popoverPosition = showEmptyBlockSideInserter ? 'top left right' : 'top right left'; const { ownerDocument } = node; const stickyBoundaryElement = showEmptyBlockSideInserter ? undefined : // The sticky boundary element should be the boundary at which the // the block toolbar becomes sticky when the block scolls out of view. // In case of an iframe, this should be the iframe boundary, otherwise // the scroll container. ownerDocument.defaultView.frameElement || getScrollContainer( node ) || ownerDocument.body; return ( <Popover ref={ popoverScrollRef } noArrow animate={ false } position={ popoverPosition } focusOnMount={ false } anchorRef={ anchorRef } className={ classnames( 'block-editor-block-list__block-popover', { 'is-insertion-point-visible': isInsertionPointVisible, } ) } __unstableStickyBoundaryElement={ stickyBoundaryElement } // Render in the old slot if needed for backward compatibility, // otherwise render in place (not in the the default popover slot). __unstableSlotName={ __unstablePopoverSlot || null } __unstableBoundaryParent // Observe movement for block animations (especially horizontal). __unstableObserveElement={ node } shouldAnchorIncludePadding // Used to safeguard sticky position behavior against cases where it would permanently // obscure specific sections of a block. __unstableEditorCanvasWrapper={ __unstableContentRef?.current } > { ( shouldShowContextualToolbar || isToolbarForced ) && ( <div onFocus={ onFocus } onBlur={ onBlur } // While ideally it would be enough to capture the // bubbling focus event from the Inserter, due to the // characteristics of click focusing of `button`s in // Firefox and Safari, it is not reliable. // // See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus tabIndex={ -1 } className={ classnames( 'block-editor-block-list__block-popover-inserter', { 'is-visible': isInserterShown, } ) } > <Inserter clientId={ clientId } rootClientId={ rootClientId } __experimentalIsQuick /> </div> ) } { ( shouldShowContextualToolbar || isToolbarForced ) && ( <BlockContextualToolbar // If the toolbar is being shown because of being forced // it should focus the toolbar right after the mount. focusOnMount={ isToolbarForced } __experimentalInitialIndex={ initialToolbarItemIndexRef.current } __experimentalOnIndexChange={ ( index ) => { initialToolbarItemIndexRef.current = index; } } // Resets the index whenever the active block changes so // this is not persisted. See https://github.com/GeChiUI/gutenberg/pull/25760#issuecomment-717906169 key={ clientId } /> ) } { shouldShowBreadcrumb && ( <BlockSelectionButton clientId={ clientId } rootClientId={ rootClientId } blockElement={ node } /> ) } { showEmptyBlockSideInserter && ( <div className="block-editor-block-list__empty-block-inserter"> <Inserter position="bottom right" rootClientId={ rootClientId } clientId={ clientId } __experimentalIsQuick /> </div> ) } </Popover> ); } function wrapperSelector( select ) { const { getSelectedBlockClientId, getFirstMultiSelectedBlockClientId, getBlockRootClientId, getBlock, getBlockParents, __experimentalGetBlockListSettingsForBlocks, } = select( blockEditorStore ); const clientId = getSelectedBlockClientId() || getFirstMultiSelectedBlockClientId(); if ( ! clientId ) { return; } const { name, attributes = {}, isValid } = getBlock( clientId ) || {}; const blockParentsClientIds = getBlockParents( clientId ); // Get Block List Settings for all ancestors of the current Block clientId const parentBlockListSettings = __experimentalGetBlockListSettingsForBlocks( blockParentsClientIds ); // Get the clientId of the topmost parent with the capture toolbars setting. const capturingClientId = find( blockParentsClientIds, ( parentClientId ) => parentBlockListSettings[ parentClientId ] ?.__experimentalCaptureToolbars ); return { clientId, rootClientId: getBlockRootClientId( clientId ), name, isValid, isEmptyDefaultBlock: name && isUnmodifiedDefaultBlock( { name, attributes } ), capturingClientId, }; } export default function WrappedBlockPopover( { __unstablePopoverSlot, __unstableContentRef, } ) { const selected = useSelect( wrapperSelector, [] ); if ( ! selected ) { return null; } const { clientId, rootClientId, name, isValid, isEmptyDefaultBlock, capturingClientId, } = selected; if ( ! name ) { return null; } return ( <BlockPopover clientId={ clientId } rootClientId={ rootClientId } isValid={ isValid } isEmptyDefaultBlock={ isEmptyDefaultBlock } capturingClientId={ capturingClientId } __unstablePopoverSlot={ __unstablePopoverSlot } __unstableContentRef={ __unstableContentRef } /> ); }