UNPKG

@wordpress/block-editor

Version:
545 lines (523 loc) 21.3 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _clsx = _interopRequireDefault(require("clsx")); var _blocks = require("@wordpress/blocks"); var _components = require("@wordpress/components"); var _compose = require("@wordpress/compose"); var _icons = require("@wordpress/icons"); var _element = require("@wordpress/element"); var _data = require("@wordpress/data"); var _i18n = require("@wordpress/i18n"); var _keycodes = require("@wordpress/keycodes"); var _isShallowEqual = _interopRequireDefault(require("@wordpress/is-shallow-equal")); var _keyboardShortcuts = require("@wordpress/keyboard-shortcuts"); var _a11y = require("@wordpress/a11y"); var _leaf = _interopRequireDefault(require("./leaf")); var _useListViewScrollIntoView = _interopRequireDefault(require("./use-list-view-scroll-into-view")); var _button = require("../block-mover/button"); var _blockContents = _interopRequireDefault(require("./block-contents")); var _context = require("./context"); var _utils = require("./utils"); var _store = require("../../store"); var _useBlockDisplayInformation = _interopRequireDefault(require("../use-block-display-information")); var _blockLock = require("../block-lock"); var _ariaReferencedText = _interopRequireDefault(require("./aria-referenced-text")); var _lockUnlock = require("../../lock-unlock"); var _usePasteStyles = _interopRequireDefault(require("../use-paste-styles")); var _jsxRuntime = require("react/jsx-runtime"); /** * External dependencies */ /** * WordPress dependencies */ /** * Internal dependencies */ function ListViewBlock({ block: { clientId }, displacement, isAfterDraggedBlocks, isDragged, isNesting, isSelected, isBranchSelected, selectBlock, position, level, rowCount, siblingBlockCount, showBlockMovers, path, isExpanded, selectedClientIds, isSyncedBranch }) { const cellRef = (0, _element.useRef)(null); const rowRef = (0, _element.useRef)(null); const settingsRef = (0, _element.useRef)(null); const [isHovered, setIsHovered] = (0, _element.useState)(false); const [settingsAnchorRect, setSettingsAnchorRect] = (0, _element.useState)(); const { isLocked, canEdit, canMove } = (0, _blockLock.useBlockLock)(clientId); const isFirstSelectedBlock = isSelected && selectedClientIds[0] === clientId; const isLastSelectedBlock = isSelected && selectedClientIds[selectedClientIds.length - 1] === clientId; const { toggleBlockHighlight, duplicateBlocks, multiSelect, replaceBlocks, removeBlocks, insertAfterBlock, insertBeforeBlock, setOpenedBlockSettingsMenu } = (0, _lockUnlock.unlock)((0, _data.useDispatch)(_store.store)); const { canInsertBlockType, getSelectedBlockClientIds, getPreviousBlockClientId, getBlockRootClientId, getBlockOrder, getBlockParents, getBlocksByClientId, canRemoveBlocks, isGroupable } = (0, _data.useSelect)(_store.store); const { getGroupingBlockName } = (0, _data.useSelect)(_blocks.store); const blockInformation = (0, _useBlockDisplayInformation.default)(clientId); const pasteStyles = (0, _usePasteStyles.default)(); const { block, blockName, allowRightClickOverrides } = (0, _data.useSelect)(select => { const { getBlock, getBlockName, getSettings } = select(_store.store); return { block: getBlock(clientId), blockName: getBlockName(clientId), allowRightClickOverrides: getSettings().allowRightClickOverrides }; }, [clientId]); const showBlockActions = // When a block hides its toolbar it also hides the block settings menu, // since that menu is part of the toolbar in the editor canvas. // List View respects this by also hiding the block settings menu. (0, _blocks.hasBlockSupport)(blockName, '__experimentalToolbar', true); const instanceId = (0, _compose.useInstanceId)(ListViewBlock); const descriptionId = `list-view-block-select-button__description-${instanceId}`; const { expand, collapse, collapseAll, BlockSettingsMenu, listViewInstanceId, expandedState, setInsertedBlock, treeGridElementRef, rootClientId } = (0, _context.useListViewContext)(); const isMatch = (0, _keyboardShortcuts.__unstableUseShortcutEventMatch)(); // Determine which blocks to update: // If the current (focused) block is part of the block selection, use the whole selection. // If the focused block is not part of the block selection, only update the focused block. function getBlocksToUpdate() { const selectedBlockClientIds = getSelectedBlockClientIds(); const isUpdatingSelectedBlocks = selectedBlockClientIds.includes(clientId); const firstBlockClientId = isUpdatingSelectedBlocks ? selectedBlockClientIds[0] : clientId; const firstBlockRootClientId = getBlockRootClientId(firstBlockClientId); const blocksToUpdate = isUpdatingSelectedBlocks ? selectedBlockClientIds : [clientId]; return { blocksToUpdate, firstBlockClientId, firstBlockRootClientId, selectedBlockClientIds }; } /** * @param {KeyboardEvent} event */ async function onKeyDown(event) { if (event.defaultPrevented) { return; } // Do not handle events if it comes from modals; // retain the default behavior for these keys. if (event.target.closest('[role=dialog]')) { return; } const isDeleteKey = [_keycodes.BACKSPACE, _keycodes.DELETE].includes(event.keyCode); // If multiple blocks are selected, deselect all blocks when the user // presses the escape key. if (isMatch('core/block-editor/unselect', event) && selectedClientIds.length > 0) { event.stopPropagation(); event.preventDefault(); selectBlock(event, undefined); } else if (isDeleteKey || isMatch('core/block-editor/remove', event)) { var _getPreviousBlockClie; const { blocksToUpdate: blocksToDelete, firstBlockClientId, firstBlockRootClientId, selectedBlockClientIds } = getBlocksToUpdate(); // Don't update the selection if the blocks cannot be deleted. if (!canRemoveBlocks(blocksToDelete)) { return; } let blockToFocus = (_getPreviousBlockClie = getPreviousBlockClientId(firstBlockClientId)) !== null && _getPreviousBlockClie !== void 0 ? _getPreviousBlockClie : // If the previous block is not found (when the first block is deleted), // fallback to focus the parent block. firstBlockRootClientId; removeBlocks(blocksToDelete, false); // Update the selection if the original selection has been removed. const shouldUpdateSelection = selectedBlockClientIds.length > 0 && getSelectedBlockClientIds().length === 0; // If there's no previous block nor parent block, focus the first block. if (!blockToFocus) { blockToFocus = getBlockOrder()[0]; } updateFocusAndSelection(blockToFocus, shouldUpdateSelection); } else if (isMatch('core/block-editor/paste-styles', event)) { event.preventDefault(); const { blocksToUpdate } = getBlocksToUpdate(); const blocks = getBlocksByClientId(blocksToUpdate); pasteStyles(blocks); } else if (isMatch('core/block-editor/duplicate', event)) { event.preventDefault(); const { blocksToUpdate, firstBlockRootClientId } = getBlocksToUpdate(); const canDuplicate = getBlocksByClientId(blocksToUpdate).every(blockToUpdate => { return !!blockToUpdate && (0, _blocks.hasBlockSupport)(blockToUpdate.name, 'multiple', true) && canInsertBlockType(blockToUpdate.name, firstBlockRootClientId); }); if (canDuplicate) { const updatedBlocks = await duplicateBlocks(blocksToUpdate, false); if (updatedBlocks?.length) { // If blocks have been duplicated, focus the first duplicated block. updateFocusAndSelection(updatedBlocks[0], false); } } } else if (isMatch('core/block-editor/insert-before', event)) { event.preventDefault(); const { blocksToUpdate } = getBlocksToUpdate(); await insertBeforeBlock(blocksToUpdate[0]); const newlySelectedBlocks = getSelectedBlockClientIds(); // Focus the first block of the newly inserted blocks, to keep focus within the list view. setOpenedBlockSettingsMenu(undefined); updateFocusAndSelection(newlySelectedBlocks[0], false); } else if (isMatch('core/block-editor/insert-after', event)) { event.preventDefault(); const { blocksToUpdate } = getBlocksToUpdate(); await insertAfterBlock(blocksToUpdate.at(-1)); const newlySelectedBlocks = getSelectedBlockClientIds(); // Focus the first block of the newly inserted blocks, to keep focus within the list view. setOpenedBlockSettingsMenu(undefined); updateFocusAndSelection(newlySelectedBlocks[0], false); } else if (isMatch('core/block-editor/select-all', event)) { event.preventDefault(); const { firstBlockRootClientId, selectedBlockClientIds } = getBlocksToUpdate(); const blockClientIds = getBlockOrder(firstBlockRootClientId); if (!blockClientIds.length) { return; } // If we have selected all sibling nested blocks, try selecting up a level. // This is a similar implementation to that used by `useSelectAll`. // `isShallowEqual` is used for the list view instead of a length check, // as the array of siblings of the currently focused block may be a different // set of blocks from the current block selection if the user is focused // on a different part of the list view from the block selection. if ((0, _isShallowEqual.default)(selectedBlockClientIds, blockClientIds)) { // Only select up a level if the first block is not the root block. // This ensures that the block selection can't break out of the root block // used by the list view, if the list view is only showing a partial hierarchy. if (firstBlockRootClientId && firstBlockRootClientId !== rootClientId) { updateFocusAndSelection(firstBlockRootClientId, true); return; } } // Select all while passing `null` to skip focusing to the editor canvas, // and retain focus within the list view. multiSelect(blockClientIds[0], blockClientIds[blockClientIds.length - 1], null); } else if (isMatch('core/block-editor/collapse-list-view', event)) { event.preventDefault(); const { firstBlockClientId } = getBlocksToUpdate(); const blockParents = getBlockParents(firstBlockClientId, false); // Collapse all blocks. collapseAll(); // Expand all parents of the current block. expand(blockParents); } else if (isMatch('core/block-editor/group', event)) { const { blocksToUpdate } = getBlocksToUpdate(); if (blocksToUpdate.length > 1 && isGroupable(blocksToUpdate)) { event.preventDefault(); const blocks = getBlocksByClientId(blocksToUpdate); const groupingBlockName = getGroupingBlockName(); const newBlocks = (0, _blocks.switchToBlockType)(blocks, groupingBlockName); replaceBlocks(blocksToUpdate, newBlocks); (0, _a11y.speak)((0, _i18n.__)('Selected blocks are grouped.')); const newlySelectedBlocks = getSelectedBlockClientIds(); // Focus the first block of the newly inserted blocks, to keep focus within the list view. setOpenedBlockSettingsMenu(undefined); updateFocusAndSelection(newlySelectedBlocks[0], false); } } } const onMouseEnter = (0, _element.useCallback)(() => { setIsHovered(true); toggleBlockHighlight(clientId, true); }, [clientId, setIsHovered, toggleBlockHighlight]); const onMouseLeave = (0, _element.useCallback)(() => { setIsHovered(false); toggleBlockHighlight(clientId, false); }, [clientId, setIsHovered, toggleBlockHighlight]); const selectEditorBlock = (0, _element.useCallback)(event => { selectBlock(event, clientId); event.preventDefault(); }, [clientId, selectBlock]); const updateFocusAndSelection = (0, _element.useCallback)((focusClientId, shouldSelectBlock) => { if (shouldSelectBlock) { selectBlock(undefined, focusClientId, null, null); } (0, _utils.focusListItem)(focusClientId, treeGridElementRef?.current); }, [selectBlock, treeGridElementRef]); const toggleExpanded = (0, _element.useCallback)(event => { // Prevent shift+click from opening link in a new window when toggling. event.preventDefault(); event.stopPropagation(); if (isExpanded === true) { collapse(clientId); } else if (isExpanded === false) { expand(clientId); } }, [clientId, expand, collapse, isExpanded]); // Allow right-clicking an item in the List View to open up the block settings dropdown. const onContextMenu = (0, _element.useCallback)(event => { if (showBlockActions && allowRightClickOverrides) { settingsRef.current?.click(); // Ensure the position of the settings dropdown is at the cursor. setSettingsAnchorRect(new window.DOMRect(event.clientX, event.clientY, 0, 0)); event.preventDefault(); } }, [allowRightClickOverrides, settingsRef, showBlockActions]); const onMouseDown = (0, _element.useCallback)(event => { // Prevent right-click from focusing the block, // because focus will be handled when opening the block settings dropdown. if (allowRightClickOverrides && event.button === 2) { event.preventDefault(); } }, [allowRightClickOverrides]); const settingsPopoverAnchor = (0, _element.useMemo)(() => { const { ownerDocument } = rowRef?.current || {}; // If no custom position is set, the settings dropdown will be anchored to the // DropdownMenu toggle button. if (!settingsAnchorRect || !ownerDocument) { return undefined; } // Position the settings dropdown at the cursor when right-clicking a block. return { ownerDocument, getBoundingClientRect() { return settingsAnchorRect; } }; }, [settingsAnchorRect]); const clearSettingsAnchorRect = (0, _element.useCallback)(() => { // Clear the custom position for the settings dropdown so that it is restored back // to being anchored to the DropdownMenu toggle button. setSettingsAnchorRect(undefined); }, [setSettingsAnchorRect]); // Pass in a ref to the row, so that it can be scrolled // into view when selected. For long lists, the placeholder for the // selected block is also observed, within ListViewLeafPlaceholder. (0, _useListViewScrollIntoView.default)({ isSelected, rowItemRef: rowRef, selectedClientIds }); // When switching between rendering modes (such as template preview and content only), // it is possible for a block to temporarily be unavailable. In this case, we should not // render the leaf, to avoid errors further down the tree. if (!block) { return null; } const blockPositionDescription = (0, _utils.getBlockPositionDescription)(position, siblingBlockCount, level); const blockPropertiesDescription = (0, _utils.getBlockPropertiesDescription)(blockInformation, isLocked); const hasSiblings = siblingBlockCount > 0; const hasRenderedMovers = showBlockMovers && hasSiblings; const moverCellClassName = (0, _clsx.default)('block-editor-list-view-block__mover-cell', { 'is-visible': isHovered || isSelected }); const listViewBlockSettingsClassName = (0, _clsx.default)('block-editor-list-view-block__menu-cell', { 'is-visible': isHovered || isFirstSelectedBlock }); let colSpan; if (hasRenderedMovers) { colSpan = 2; } else if (!showBlockActions) { colSpan = 3; } const classes = (0, _clsx.default)({ 'is-selected': isSelected, 'is-first-selected': isFirstSelectedBlock, 'is-last-selected': isLastSelectedBlock, 'is-branch-selected': isBranchSelected, 'is-synced-branch': isSyncedBranch, 'is-dragging': isDragged, 'has-single-cell': !showBlockActions, 'is-synced': blockInformation?.isSynced, 'is-draggable': canMove, 'is-displacement-normal': displacement === 'normal', 'is-displacement-up': displacement === 'up', 'is-displacement-down': displacement === 'down', 'is-after-dragged-blocks': isAfterDraggedBlocks, 'is-nesting': isNesting }); // Only include all selected blocks if the currently clicked on block // is one of the selected blocks. This ensures that if a user attempts // to alter a block that isn't part of the selection, they're still able // to do so. const dropdownClientIds = selectedClientIds.includes(clientId) ? selectedClientIds : [clientId]; // Detect if there is a block in the canvas currently being edited and multi-selection is not happening. const currentlyEditingBlockInCanvas = isSelected && selectedClientIds.length === 1; return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_leaf.default, { className: classes, isDragged: isDragged, onKeyDown: onKeyDown, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, onFocus: onMouseEnter, onBlur: onMouseLeave, level: level, position: position, rowCount: rowCount, path: path, id: `list-view-${listViewInstanceId}-block-${clientId}`, "data-block": clientId, "data-expanded": canEdit ? isExpanded : undefined, ref: rowRef, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalTreeGridCell, { className: "block-editor-list-view-block__contents-cell", colSpan: colSpan, ref: cellRef, "aria-selected": !!isSelected, children: ({ ref, tabIndex, onFocus }) => /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: "block-editor-list-view-block__contents-container", children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_blockContents.default, { block: block, onClick: selectEditorBlock, onContextMenu: onContextMenu, onMouseDown: onMouseDown, onToggleExpanded: toggleExpanded, isSelected: isSelected, position: position, siblingBlockCount: siblingBlockCount, level: level, ref: ref, tabIndex: currentlyEditingBlockInCanvas ? 0 : tabIndex, onFocus: onFocus, isExpanded: canEdit ? isExpanded : undefined, selectedClientIds: selectedClientIds, ariaDescribedBy: descriptionId }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_ariaReferencedText.default, { id: descriptionId, children: [blockPositionDescription, blockPropertiesDescription].filter(Boolean).join(' ') })] }) }), hasRenderedMovers && /*#__PURE__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, { children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_components.__experimentalTreeGridCell, { className: moverCellClassName, withoutGridItem: true, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalTreeGridItem, { children: ({ ref, tabIndex, onFocus }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_button.BlockMoverUpButton, { orientation: "vertical", clientIds: [clientId], ref: ref, tabIndex: tabIndex, onFocus: onFocus }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalTreeGridItem, { children: ({ ref, tabIndex, onFocus }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_button.BlockMoverDownButton, { orientation: "vertical", clientIds: [clientId], ref: ref, tabIndex: tabIndex, onFocus: onFocus }) })] }) }), showBlockActions && BlockSettingsMenu && /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalTreeGridCell, { className: listViewBlockSettingsClassName, "aria-selected": !!isSelected, ref: settingsRef, children: ({ ref, tabIndex, onFocus }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(BlockSettingsMenu, { clientIds: dropdownClientIds, block: block, icon: _icons.moreVertical, label: (0, _i18n.__)('Options'), popoverProps: { anchor: settingsPopoverAnchor // Used to position the settings at the cursor on right-click. }, toggleProps: { ref, className: 'block-editor-list-view-block__menu', tabIndex, onClick: clearSettingsAnchorRect, onFocus }, disableOpenOnArrowDown: true, expand: expand, expandedState: expandedState, setInsertedBlock: setInsertedBlock, __experimentalSelectBlock: updateFocusAndSelection }) })] }); } var _default = exports.default = (0, _element.memo)(ListViewBlock); //# sourceMappingURL=block.js.map