UNPKG

@gechiui/block-editor

Version:
442 lines (415 loc) 12 kB
/** * External dependencies */ import { Platform, findNodeHandle } from 'react-native'; import { partial, first, castArray, last, compact, every } from 'lodash'; /** * GeChiUI dependencies */ import { getClipboard, setClipboard, ToolbarButton, Picker, } from '@gechiui/components'; import { getBlockType, getDefaultBlockName, hasBlockSupport, serialize, rawHandler, createBlock, isUnmodifiedDefaultBlock, isReusableBlock, } from '@gechiui/blocks'; import { __, sprintf } from '@gechiui/i18n'; import { withDispatch, withSelect } from '@gechiui/data'; import { withInstanceId, compose } from '@gechiui/compose'; import { moreHorizontalMobile } from '@gechiui/icons'; import { useRef, useState } from '@gechiui/element'; import { store as noticesStore } from '@gechiui/notices'; import { store as reusableBlocksStore } from '@gechiui/reusable-blocks'; import { store as coreStore } from '@gechiui/core-data'; /** * Internal dependencies */ import { getMoversSetup } from '../block-mover/mover-description'; import { store as blockEditorStore } from '../../store'; import BlockTransformationsMenu from '../block-switcher/block-transformations-menu'; const BlockActionsMenu = ( { // Select blockTitle, canInsertBlockType, getBlocksByClientId, isEmptyDefaultBlock, isLocked, canDuplicate, isFirst, isLast, isReusableBlockType, reusableBlock, rootClientId, selectedBlockClientId, selectedBlockPossibleTransformations, // Dispatch createSuccessNotice, convertToRegularBlocks, duplicateBlock, onMoveDown, onMoveUp, openGeneralSidebar, pasteBlock, removeBlocks, // Passed in anchorNodeRef, isStackedHorizontally, onDelete, wrapBlockMover, wrapBlockSettings, } ) => { const [ clipboard, setCurrentClipboard ] = useState( getClipboard() ); const blockActionsMenuPickerRef = useRef(); const blockTransformationMenuPickerRef = useRef(); const moversOptions = { keys: [ 'icon', 'actionTitle' ] }; const clipboardBlock = clipboard && rawHandler( { HTML: clipboard } )[ 0 ]; const isPasteEnabled = clipboardBlock && canInsertBlockType( clipboardBlock.name, rootClientId ); const { actionTitle: { backward: backwardButtonTitle, forward: forwardButtonTitle, }, } = getMoversSetup( isStackedHorizontally, moversOptions ); const allOptions = { settings: { id: 'settingsOption', label: __( 'Block settings' ), value: 'settingsOption', onSelect: openGeneralSidebar, }, backwardButton: { id: 'backwardButtonOption', label: backwardButtonTitle, value: 'backwardButtonOption', disabled: isFirst, onSelect: onMoveUp, }, forwardButton: { id: 'forwardButtonOption', label: forwardButtonTitle, value: 'forwardButtonOption', disabled: isLast, onSelect: onMoveDown, }, delete: { id: 'deleteOption', label: __( 'Remove block' ), value: 'deleteOption', separated: true, disabled: isEmptyDefaultBlock, onSelect: () => { onDelete(); createSuccessNotice( // translators: displayed right after the block is removed. __( 'Block removed' ) ); }, }, transformButton: { id: 'transformButtonOption', label: __( 'Transform block…' ), value: 'transformButtonOption', onSelect: () => { if ( blockTransformationMenuPickerRef.current ) { blockTransformationMenuPickerRef.current.presentPicker(); } }, }, copyButton: { id: 'copyButtonOption', label: __( 'Copy block' ), value: 'copyButtonOption', onSelect: () => { const serializedBlock = serialize( getBlocksByClientId( selectedBlockClientId ) ); setCurrentClipboard( serializedBlock ); setClipboard( serializedBlock ); createSuccessNotice( // translators: displayed right after the block is copied. __( 'Block copied' ) ); }, }, cutButton: { id: 'cutButtonOption', label: __( 'Cut block' ), value: 'cutButtonOption', onSelect: () => { setClipboard( serialize( getBlocksByClientId( selectedBlockClientId ) ) ); removeBlocks( selectedBlockClientId ); createSuccessNotice( // translators: displayed right after the block is cut. __( 'Block cut' ) ); }, }, pasteButton: { id: 'pasteButtonOption', label: __( 'Paste block after' ), value: 'pasteButtonOption', onSelect: () => { onPasteBlock(); createSuccessNotice( // translators: displayed right after the block is pasted. __( 'Block pasted' ) ); }, }, duplicateButton: { id: 'duplicateButtonOption', label: __( 'Duplicate block' ), value: 'duplicateButtonOption', onSelect: () => { duplicateBlock(); createSuccessNotice( // translators: displayed right after the block is duplicated. __( 'Block duplicated' ) ); }, }, convertToRegularBlocks: { id: 'convertToRegularBlocksOption', label: __( '转换为常规区块' ), value: 'convertToRegularBlocksOption', onSelect: () => { createSuccessNotice( sprintf( /* translators: %s: name of the reusable block */ __( '%s converted to regular blocks' ), reusableBlock?.title?.raw || blockTitle ) ); convertToRegularBlocks(); }, }, }; const options = compact( [ wrapBlockMover && allOptions.backwardButton, wrapBlockMover && allOptions.forwardButton, wrapBlockSettings && allOptions.settings, ! isLocked && selectedBlockPossibleTransformations.length && allOptions.transformButton, canDuplicate && allOptions.copyButton, canDuplicate && allOptions.cutButton, canDuplicate && isPasteEnabled && allOptions.pasteButton, canDuplicate && allOptions.duplicateButton, isReusableBlockType && allOptions.convertToRegularBlocks, ! isLocked && allOptions.delete, ] ); // End early if there are no options to show. if ( ! options.length ) { return ( <ToolbarButton title={ __( 'Open Block Actions Menu' ) } icon={ moreHorizontalMobile } disabled={ true } /> ); } function onPasteBlock() { if ( ! clipboard ) { return; } pasteBlock( rawHandler( { HTML: clipboard } )[ 0 ] ); } function onPickerSelect( value ) { const selectedItem = options.find( ( item ) => item.value === value ); selectedItem.onSelect(); } function onPickerPresent() { if ( blockActionsMenuPickerRef.current ) { blockActionsMenuPickerRef.current.presentPicker(); } } const disabledButtonIndices = options .map( ( option, index ) => option.disabled && index + 1 ) .filter( Boolean ); const accessibilityHint = Platform.OS === 'ios' ? __( 'Double tap to open Action Sheet with available options' ) : __( 'Double tap to open Bottom Sheet with available options' ); const getAnchor = () => anchorNodeRef ? findNodeHandle( anchorNodeRef ) : undefined; return ( <> <ToolbarButton title={ __( 'Open Block Actions Menu' ) } onClick={ onPickerPresent } icon={ moreHorizontalMobile } extraProps={ { hint: accessibilityHint, } } /> <Picker ref={ blockActionsMenuPickerRef } options={ options } onChange={ onPickerSelect } destructiveButtonIndex={ options.length } disabledButtonIndices={ disabledButtonIndices } hideCancelButton={ Platform.OS !== 'ios' } leftAlign={ true } getAnchor={ getAnchor } // translators: %s: block title e.g: "Paragraph". title={ sprintf( __( '%s block options' ), blockTitle ) } /> <BlockTransformationsMenu anchorNodeRef={ anchorNodeRef } blockTitle={ blockTitle } pickerRef={ blockTransformationMenuPickerRef } possibleTransformations={ selectedBlockPossibleTransformations } selectedBlock={ getBlocksByClientId( selectedBlockClientId ) } selectedBlockClientId={ selectedBlockClientId } /> </> ); }; export default compose( withSelect( ( select, { clientIds } ) => { const { getBlockIndex, getBlockRootClientId, getBlockOrder, getBlockName, getBlockTransformItems, getBlock, getBlocksByClientId, getSelectedBlockClientIds, canInsertBlockType, getTemplateLock, } = select( blockEditorStore ); const normalizedClientIds = castArray( clientIds ); const block = getBlock( normalizedClientIds ); const blockName = getBlockName( normalizedClientIds ); const blockType = getBlockType( blockName ); const blockTitle = blockType?.title; const firstClientId = first( normalizedClientIds ); const rootClientId = getBlockRootClientId( firstClientId ); const blockOrder = getBlockOrder( rootClientId ); const firstIndex = getBlockIndex( firstClientId ); const lastIndex = getBlockIndex( last( normalizedClientIds ) ); const innerBlocks = getBlocksByClientId( clientIds ); const canDuplicate = every( innerBlocks, ( innerBlock ) => { return ( !! innerBlock && hasBlockSupport( innerBlock.name, 'multiple', true ) && canInsertBlockType( innerBlock.name, rootClientId ) ); } ); const isDefaultBlock = blockName === getDefaultBlockName(); const isEmptyContent = block?.attributes.content === ''; const isExactlyOneBlock = blockOrder.length === 1; const isEmptyDefaultBlock = isExactlyOneBlock && isDefaultBlock && isEmptyContent; const isLocked = !! getTemplateLock( rootClientId ); const selectedBlockClientId = first( getSelectedBlockClientIds() ); const selectedBlock = selectedBlockClientId ? first( getBlocksByClientId( selectedBlockClientId ) ) : undefined; const selectedBlockPossibleTransformations = selectedBlock ? getBlockTransformItems( [ selectedBlock ], rootClientId ) : []; const isReusableBlockType = block ? isReusableBlock( block ) : false; const reusableBlock = isReusableBlockType ? select( coreStore ).getEntityRecord( 'postType', 'gc_block', block?.attributes.ref ) : undefined; return { blockTitle, canInsertBlockType, currentIndex: firstIndex, getBlocksByClientId, isEmptyDefaultBlock, isLocked, canDuplicate, isFirst: firstIndex === 0, isLast: lastIndex === blockOrder.length - 1, isReusableBlockType, reusableBlock, rootClientId, selectedBlockClientId, selectedBlockPossibleTransformations, }; } ), withDispatch( ( dispatch, { clientIds, rootClientId, currentIndex, selectedBlockClientId }, { select } ) => { const { moveBlocksDown, moveBlocksUp, duplicateBlocks, removeBlocks, insertBlock, replaceBlocks, clearSelectedBlock, } = dispatch( blockEditorStore ); const { openGeneralSidebar } = dispatch( 'core/edit-post' ); const { getBlockSelectionEnd, getBlock } = select( blockEditorStore ); const { createSuccessNotice } = dispatch( noticesStore ); const { __experimentalConvertBlockToStatic: convertBlockToStatic, } = dispatch( reusableBlocksStore ); return { createSuccessNotice, convertToRegularBlocks() { clearSelectedBlock(); // Convert action is executed at the end of the current JavaScript execution block // to prevent issues related to undo/redo actions. setImmediate( () => convertBlockToStatic( selectedBlockClientId ) ); }, duplicateBlock() { return duplicateBlocks( clientIds ); }, onMoveDown: partial( moveBlocksDown, clientIds, rootClientId ), onMoveUp: partial( moveBlocksUp, clientIds, rootClientId ), openGeneralSidebar: () => openGeneralSidebar( 'edit-post/block' ), pasteBlock: ( clipboardBlock ) => { const canReplaceBlock = isUnmodifiedDefaultBlock( getBlock( getBlockSelectionEnd() ) ); if ( ! canReplaceBlock ) { const insertedBlock = createBlock( clipboardBlock.name, clipboardBlock.attributes, clipboardBlock.innerBlocks ); insertBlock( insertedBlock, currentIndex + 1, rootClientId ); } else { replaceBlocks( clientIds, clipboardBlock ); } }, removeBlocks, }; } ), withInstanceId )( BlockActionsMenu );