@wordpress/block-editor
Version:
545 lines (523 loc) • 21.3 kB
JavaScript
"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