UNPKG

@wordpress/block-editor

Version:
326 lines (316 loc) 12.9 kB
/** * External dependencies */ import clsx from 'clsx'; /** * WordPress dependencies */ import { useInstanceId, useMergeRefs, __experimentalUseFixedWindowList as useFixedWindowList } from '@wordpress/compose'; import { __experimentalTreeGrid as TreeGrid, VisuallyHidden } from '@wordpress/components'; import { AsyncModeProvider, useSelect } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; import { useCallback, useEffect, useMemo, useRef, useReducer, forwardRef, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import ListViewBranch from './branch'; import { ListViewContext } from './context'; import ListViewDropIndicatorPreview from './drop-indicator'; import useBlockSelection from './use-block-selection'; import useListViewBlockIndexes from './use-list-view-block-indexes'; import useListViewClientIds from './use-list-view-client-ids'; import useListViewCollapseItems from './use-list-view-collapse-items'; import useListViewDropZone from './use-list-view-drop-zone'; import useListViewExpandSelectedItem from './use-list-view-expand-selected-item'; import { store as blockEditorStore } from '../../store'; import { BlockSettingsDropdown } from '../block-settings-menu/block-settings-dropdown'; import { focusListItem } from './utils'; import useClipboardHandler from './use-clipboard-handler'; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const expanded = (state, action) => { if (action.type === 'clear') { return {}; } if (Array.isArray(action.clientIds)) { return { ...state, ...action.clientIds.reduce((newState, id) => ({ ...newState, [id]: action.type === 'expand' }), {}) }; } return state; }; export const BLOCK_LIST_ITEM_HEIGHT = 32; /** @typedef {import('react').ComponentType} ComponentType */ /** @typedef {import('react').Ref<HTMLElement>} Ref */ /** * Show a hierarchical list of blocks. * * @param {Object} props Components props. * @param {string} props.id An HTML element id for the root element of ListView. * @param {Array} props.blocks _deprecated_ Custom subset of block client IDs to be used instead of the default hierarchy. * @param {?HTMLElement} props.dropZoneElement Optional element to be used as the drop zone. * @param {?boolean} props.showBlockMovers Flag to enable block movers. Defaults to `false`. * @param {?boolean} props.isExpanded Flag to determine whether nested levels are expanded by default. Defaults to `false`. * @param {?boolean} props.showAppender Flag to show or hide the block appender. Defaults to `false`. * @param {?ComponentType} props.blockSettingsMenu Optional more menu substitution. Defaults to the standard `BlockSettingsDropdown` component. * @param {string} props.rootClientId The client id of the root block from which we determine the blocks to show in the list. * @param {string} props.description Optional accessible description for the tree grid component. * @param {?Function} props.onSelect Optional callback to be invoked when a block is selected. Receives the block object that was selected. * @param {?ComponentType} props.additionalBlockContent Component that renders additional block content UI. * @param {Ref} ref Forwarded ref */ function ListViewComponent({ id, blocks, dropZoneElement, showBlockMovers = false, isExpanded = false, showAppender = false, blockSettingsMenu: BlockSettingsMenu = BlockSettingsDropdown, rootClientId, description, onSelect, additionalBlockContent: AdditionalBlockContent }, ref) { // This can be removed once we no longer need to support the blocks prop. if (blocks) { deprecated('`blocks` property in `wp.blockEditor.__experimentalListView`', { since: '6.3', alternative: '`rootClientId` property' }); } const instanceId = useInstanceId(ListViewComponent); const { clientIdsTree, draggedClientIds, selectedClientIds } = useListViewClientIds({ blocks, rootClientId }); const blockIndexes = useListViewBlockIndexes(clientIdsTree); const { getBlock } = useSelect(blockEditorStore); const { visibleBlockCount } = useSelect(select => { const { getGlobalBlockCount, getClientIdsOfDescendants } = select(blockEditorStore); const draggedBlockCount = draggedClientIds?.length > 0 ? getClientIdsOfDescendants(draggedClientIds).length + 1 : 0; return { visibleBlockCount: getGlobalBlockCount() - draggedBlockCount }; }, [draggedClientIds]); const { updateBlockSelection } = useBlockSelection(); const [expandedState, setExpandedState] = useReducer(expanded, {}); const [insertedBlock, setInsertedBlock] = useState(null); const { setSelectedTreeId } = useListViewExpandSelectedItem({ firstSelectedBlockClientId: selectedClientIds[0], setExpandedState }); const selectEditorBlock = useCallback( /** * @param {MouseEvent | KeyboardEvent | undefined} event * @param {string} blockClientId * @param {null | undefined | -1 | 1} focusPosition */ (event, blockClientId, focusPosition) => { updateBlockSelection(event, blockClientId, null, focusPosition); setSelectedTreeId(blockClientId); if (onSelect) { onSelect(getBlock(blockClientId)); } }, [setSelectedTreeId, updateBlockSelection, onSelect, getBlock]); const { ref: dropZoneRef, target: blockDropTarget } = useListViewDropZone({ dropZoneElement, expandedState, setExpandedState }); const elementRef = useRef(); // Allow handling of copy, cut, and paste events. const clipBoardRef = useClipboardHandler({ selectBlock: selectEditorBlock }); const treeGridRef = useMergeRefs([clipBoardRef, elementRef, dropZoneRef, ref]); useEffect(() => { // If a blocks are already selected when the list view is initially // mounted, shift focus to the first selected block. if (selectedClientIds?.length) { focusListItem(selectedClientIds[0], elementRef?.current); } // Only focus on the selected item when the list view is mounted. }, []); const expand = useCallback(clientId => { if (!clientId) { return; } const clientIds = Array.isArray(clientId) ? clientId : [clientId]; setExpandedState({ type: 'expand', clientIds }); }, [setExpandedState]); const collapse = useCallback(clientId => { if (!clientId) { return; } setExpandedState({ type: 'collapse', clientIds: [clientId] }); }, [setExpandedState]); const collapseAll = useCallback(() => { setExpandedState({ type: 'clear' }); }, [setExpandedState]); const expandRow = useCallback(row => { expand(row?.dataset?.block); }, [expand]); const collapseRow = useCallback(row => { collapse(row?.dataset?.block); }, [collapse]); const focusRow = useCallback((event, startRow, endRow) => { if (event.shiftKey) { updateBlockSelection(event, startRow?.dataset?.block, endRow?.dataset?.block); } }, [updateBlockSelection]); useListViewCollapseItems({ collapseAll, expand }); const firstDraggedBlockClientId = draggedClientIds?.[0]; // Convert a blockDropTarget into indexes relative to the blocks in the list view. // These values are used to determine which blocks should be displaced to make room // for the drop indicator. See `ListViewBranch` and `getDragDisplacementValues`. const { blockDropTargetIndex, blockDropPosition, firstDraggedBlockIndex } = useMemo(() => { let _blockDropTargetIndex, _firstDraggedBlockIndex; if (blockDropTarget?.clientId) { const foundBlockIndex = blockIndexes[blockDropTarget.clientId]; // If dragging below or inside the block, treat the drop target as the next block. _blockDropTargetIndex = foundBlockIndex === undefined || blockDropTarget?.dropPosition === 'top' ? foundBlockIndex : foundBlockIndex + 1; } else if (blockDropTarget === null) { // A `null` value is used to indicate that the user is dragging outside of the list view. _blockDropTargetIndex = null; } if (firstDraggedBlockClientId) { const foundBlockIndex = blockIndexes[firstDraggedBlockClientId]; _firstDraggedBlockIndex = foundBlockIndex === undefined || blockDropTarget?.dropPosition === 'top' ? foundBlockIndex : foundBlockIndex + 1; } return { blockDropTargetIndex: _blockDropTargetIndex, blockDropPosition: blockDropTarget?.dropPosition, firstDraggedBlockIndex: _firstDraggedBlockIndex }; }, [blockDropTarget, blockIndexes, firstDraggedBlockClientId]); const contextValue = useMemo(() => ({ blockDropPosition, blockDropTargetIndex, blockIndexes, draggedClientIds, expandedState, expand, firstDraggedBlockIndex, collapse, collapseAll, BlockSettingsMenu, listViewInstanceId: instanceId, AdditionalBlockContent, insertedBlock, setInsertedBlock, treeGridElementRef: elementRef, rootClientId }), [blockDropPosition, blockDropTargetIndex, blockIndexes, draggedClientIds, expandedState, expand, firstDraggedBlockIndex, collapse, collapseAll, BlockSettingsMenu, instanceId, AdditionalBlockContent, insertedBlock, setInsertedBlock, rootClientId]); // List View renders a fixed number of items and relies on each having a fixed item height of 36px. // If this value changes, we should also change the itemHeight value set in useFixedWindowList. // See: https://github.com/WordPress/gutenberg/pull/35230 for additional context. const [fixedListWindow] = useFixedWindowList(elementRef, BLOCK_LIST_ITEM_HEIGHT, visibleBlockCount, { // Ensure that the windowing logic is recalculated when the expanded state changes. // This is necessary because expanding a collapsed block in a short list view can // switch the list view to a tall list view with a scrollbar, and vice versa. // When this happens, the windowing logic needs to be recalculated to ensure that // the correct number of blocks are rendered, by rechecking for a scroll container. expandedState, useWindowing: true, windowOverscan: 40 }); // If there are no blocks to show and we're not showing the appender, do not render the list view. if (!clientIdsTree.length && !showAppender) { return null; } const describedById = description && `block-editor-list-view-description-${instanceId}`; return /*#__PURE__*/_jsxs(AsyncModeProvider, { value: true, children: [/*#__PURE__*/_jsx(ListViewDropIndicatorPreview, { draggedBlockClientId: firstDraggedBlockClientId, listViewRef: elementRef, blockDropTarget: blockDropTarget }), description && /*#__PURE__*/_jsx(VisuallyHidden, { id: describedById, children: description }), /*#__PURE__*/_jsx(TreeGrid, { id: id, className: clsx('block-editor-list-view-tree', { 'is-dragging': draggedClientIds?.length > 0 && blockDropTargetIndex !== undefined }), "aria-label": __('Block navigation structure'), ref: treeGridRef, onCollapseRow: collapseRow, onExpandRow: expandRow, onFocusRow: focusRow, applicationAriaLabel: __('Block navigation structure'), "aria-describedby": describedById, style: { '--wp-admin--list-view-dragged-items-height': draggedClientIds?.length ? `${BLOCK_LIST_ITEM_HEIGHT * (draggedClientIds.length - 1)}px` : null }, children: /*#__PURE__*/_jsx(ListViewContext.Provider, { value: contextValue, children: /*#__PURE__*/_jsx(ListViewBranch, { blocks: clientIdsTree, parentId: rootClientId, selectBlock: selectEditorBlock, showBlockMovers: showBlockMovers, fixedListWindow: fixedListWindow, selectedClientIds: selectedClientIds, isExpanded: isExpanded, showAppender: showAppender }) }) })] }); } // This is the private API for the ListView component. // It allows access to all props, not just the public ones. export const PrivateListView = forwardRef(ListViewComponent); // This is the public API for the ListView component. // We wrap the PrivateListView component to hide some props from the public API. export default forwardRef((props, ref) => { return /*#__PURE__*/_jsx(PrivateListView, { ref: ref, ...props, showAppender: false, rootClientId: null, onSelect: null, additionalBlockContent: null, blockSettingsMenu: undefined }); }); //# sourceMappingURL=index.js.map