@wordpress/block-editor
Version:
547 lines (546 loc) • 18.9 kB
JavaScript
// packages/block-editor/src/components/list-view/block.js
import clsx from "clsx";
import {
hasBlockSupport,
switchToBlockType,
store as blocksStore
} from "@wordpress/blocks";
import {
__experimentalTreeGridCell as TreeGridCell,
__experimentalTreeGridItem as TreeGridItem
} from "@wordpress/components";
import { useInstanceId, useDebounce } from "@wordpress/compose";
import { moreVertical } from "@wordpress/icons";
import {
useCallback,
useMemo,
useState,
useRef,
memo
} from "@wordpress/element";
import { useDispatch, useSelect } from "@wordpress/data";
import { __ } from "@wordpress/i18n";
import { BACKSPACE, DELETE } from "@wordpress/keycodes";
import isShallowEqual from "@wordpress/is-shallow-equal";
import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from "@wordpress/keyboard-shortcuts";
import { speak } from "@wordpress/a11y";
import ListViewLeaf from "./leaf";
import useListViewScrollIntoView from "./use-list-view-scroll-into-view";
import {
BlockMoverUpButton,
BlockMoverDownButton
} from "../block-mover/button";
import ListViewBlockContents from "./block-contents";
import { useListViewContext } from "./context";
import {
getBlockPositionDescription,
getBlockPropertiesDescription,
focusListItem
} from "./utils";
import { store as blockEditorStore } from "../../store";
import useBlockDisplayInformation from "../use-block-display-information";
import { useBlockLock } from "../block-lock";
import AriaReferencedText from "./aria-referenced-text";
import { unlock } from "../../lock-unlock";
import usePasteStyles from "../use-paste-styles";
import { cleanEmptyObject } from "../../hooks/utils";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
function ListViewBlock({
block: { clientId },
displacement,
isAfterDraggedBlocks,
isDragged,
isNesting,
isSelected,
isBranchSelected,
selectBlock,
position,
level,
rowCount,
siblingBlockCount,
showBlockMovers,
path,
isExpanded,
selectedClientIds,
isSyncedBranch
}) {
const cellRef = useRef(null);
const rowRef = useRef(null);
const settingsRef = useRef(null);
const [isHovered, setIsHovered] = useState(false);
const [settingsAnchorRect, setSettingsAnchorRect] = useState();
const { isLocked } = 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,
updateBlockAttributes
} = unlock(useDispatch(blockEditorStore));
const debouncedToggleBlockHighlight = useDebounce(
toggleBlockHighlight,
50
);
const {
canInsertBlockType,
getSelectedBlockClientIds,
getPreviousBlockClientId,
getBlockRootClientId,
getBlockOrder,
getBlockParents,
getBlocksByClientId,
canEditBlock,
canMoveBlock,
canRemoveBlocks,
isGroupable
} = useSelect(blockEditorStore);
const { getGroupingBlockName } = useSelect(blocksStore);
const blockInformation = useBlockDisplayInformation(clientId);
const pasteStyles = usePasteStyles();
const { block, blockName, allowRightClickOverrides, isBlockHidden } = useSelect(
(select) => {
const { getBlock, getBlockName, getSettings } = select(blockEditorStore);
const { isBlockHidden: _isBlockHidden } = unlock(
select(blockEditorStore)
);
return {
block: getBlock(clientId),
blockName: getBlockName(clientId),
allowRightClickOverrides: getSettings().allowRightClickOverrides,
isBlockHidden: _isBlockHidden(clientId)
};
},
[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.
hasBlockSupport(blockName, "__experimentalToolbar", true)
);
const instanceId = useInstanceId(ListViewBlock);
const descriptionId = `list-view-block-select-button__description-${instanceId}`;
const {
expand,
collapse,
collapseAll,
BlockSettingsMenu,
listViewInstanceId,
expandedState,
setInsertedBlock,
treeGridElementRef,
rootClientId
} = useListViewContext();
const isMatch = useShortcutEventMatch();
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
};
}
async function onKeyDown(event) {
if (event.defaultPrevented) {
return;
}
if (event.target.closest("[role=dialog]")) {
return;
}
const isDeleteKey = [BACKSPACE, DELETE].includes(event.keyCode);
if (isMatch("core/block-editor/unselect", event) && selectedClientIds.length > 0) {
event.stopPropagation();
event.preventDefault();
selectBlock(event, void 0);
} else if (isDeleteKey || isMatch("core/block-editor/remove", event)) {
const {
blocksToUpdate: blocksToDelete,
firstBlockClientId,
firstBlockRootClientId,
selectedBlockClientIds
} = getBlocksToUpdate();
if (!canRemoveBlocks(blocksToDelete)) {
return;
}
let blockToFocus = getPreviousBlockClientId(firstBlockClientId) ?? // If the previous block is not found (when the first block is deleted),
// fallback to focus the parent block.
firstBlockRootClientId;
removeBlocks(blocksToDelete, false);
const shouldUpdateSelection = selectedBlockClientIds.length > 0 && getSelectedBlockClientIds().length === 0;
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 && hasBlockSupport(
blockToUpdate.name,
"multiple",
true
) && canInsertBlockType(
blockToUpdate.name,
firstBlockRootClientId
);
}
);
if (canDuplicate) {
const updatedBlocks = await duplicateBlocks(
blocksToUpdate,
false
);
if (updatedBlocks?.length) {
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();
setOpenedBlockSettingsMenu(void 0);
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();
setOpenedBlockSettingsMenu(void 0);
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 (isShallowEqual(selectedBlockClientIds, blockClientIds)) {
if (firstBlockRootClientId && firstBlockRootClientId !== rootClientId) {
updateFocusAndSelection(firstBlockRootClientId, true);
return;
}
}
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);
collapseAll();
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 = switchToBlockType(
blocks,
groupingBlockName
);
replaceBlocks(blocksToUpdate, newBlocks);
speak(__("Selected blocks are grouped."));
const newlySelectedBlocks = getSelectedBlockClientIds();
setOpenedBlockSettingsMenu(void 0);
updateFocusAndSelection(newlySelectedBlocks[0], false);
}
} else if (isMatch("core/block-editor/toggle-block-visibility", event)) {
event.preventDefault();
const { blocksToUpdate } = getBlocksToUpdate();
const blocks = getBlocksByClientId(blocksToUpdate);
const canToggleVisibility = blocks.every(
(blockToUpdate) => hasBlockSupport(blockToUpdate.name, "visibility", true)
);
if (!canToggleVisibility) {
return;
}
const hasHiddenBlock = blocks.some(
(blockToUpdate) => blockToUpdate.attributes.metadata?.blockVisibility === false
);
const attributesByClientId = Object.fromEntries(
blocks.map(({ clientId: mapClientId, attributes }) => [
mapClientId,
{
metadata: cleanEmptyObject({
...attributes?.metadata,
blockVisibility: hasHiddenBlock ? void 0 : false
})
}
])
);
updateBlockAttributes(blocksToUpdate, attributesByClientId, {
uniqueByBlock: true
});
}
}
const onMouseEnter = useCallback(() => {
setIsHovered(true);
debouncedToggleBlockHighlight(clientId, true);
}, [clientId, setIsHovered, debouncedToggleBlockHighlight]);
const onMouseLeave = useCallback(() => {
setIsHovered(false);
debouncedToggleBlockHighlight(clientId, false);
}, [clientId, setIsHovered, debouncedToggleBlockHighlight]);
const selectEditorBlock = useCallback(
(event) => {
selectBlock(event, clientId);
event.preventDefault();
},
[clientId, selectBlock]
);
const updateFocusAndSelection = useCallback(
(focusClientId, shouldSelectBlock) => {
if (shouldSelectBlock) {
selectBlock(void 0, focusClientId, null, null);
}
focusListItem(focusClientId, treeGridElementRef?.current);
},
[selectBlock, treeGridElementRef]
);
const toggleExpanded = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
if (isExpanded === true) {
collapse(clientId);
} else if (isExpanded === false) {
expand(clientId);
}
},
[clientId, expand, collapse, isExpanded]
);
const onContextMenu = useCallback(
(event) => {
if (showBlockActions && allowRightClickOverrides) {
settingsRef.current?.click();
setSettingsAnchorRect(
new window.DOMRect(event.clientX, event.clientY, 0, 0)
);
event.preventDefault();
}
},
[allowRightClickOverrides, settingsRef, showBlockActions]
);
const onMouseDown = useCallback(
(event) => {
if (allowRightClickOverrides && event.button === 2) {
event.preventDefault();
}
},
[allowRightClickOverrides]
);
const settingsPopoverAnchor = useMemo(() => {
const { ownerDocument } = rowRef?.current || {};
if (!settingsAnchorRect || !ownerDocument) {
return void 0;
}
return {
ownerDocument,
getBoundingClientRect() {
return settingsAnchorRect;
}
};
}, [settingsAnchorRect]);
const clearSettingsAnchorRect = useCallback(() => {
setSettingsAnchorRect(void 0);
}, [setSettingsAnchorRect]);
useListViewScrollIntoView({
isSelected,
rowItemRef: rowRef,
selectedClientIds
});
if (!block) {
return null;
}
const blockPositionDescription = getBlockPositionDescription(
position,
siblingBlockCount,
level
);
const blockPropertiesDescription = getBlockPropertiesDescription(
blockInformation,
isLocked
);
const blockVisibilityDescription = isBlockHidden ? __("Block is hidden.") : null;
const hasSiblings = siblingBlockCount > 0;
const hasRenderedMovers = showBlockMovers && hasSiblings;
const moverCellClassName = clsx(
"block-editor-list-view-block__mover-cell",
{ "is-visible": isHovered || isSelected }
);
const listViewBlockSettingsClassName = clsx(
"block-editor-list-view-block__menu-cell",
{ "is-visible": isHovered || isFirstSelectedBlock }
);
let colSpan;
if (hasRenderedMovers) {
colSpan = 2;
} else if (!showBlockActions) {
colSpan = 3;
}
const classes = clsx({
"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": canMoveBlock,
"is-displacement-normal": displacement === "normal",
"is-displacement-up": displacement === "up",
"is-displacement-down": displacement === "down",
"is-after-dragged-blocks": isAfterDraggedBlocks,
"is-nesting": isNesting
});
const dropdownClientIds = selectedClientIds.includes(clientId) ? selectedClientIds : [clientId];
const currentlyEditingBlockInCanvas = isSelected && selectedClientIds.length === 1;
return /* @__PURE__ */ jsxs(
ListViewLeaf,
{
className: classes,
isDragged,
onKeyDown,
onMouseEnter,
onMouseLeave,
onFocus: onMouseEnter,
onBlur: onMouseLeave,
level,
position,
rowCount,
path,
id: `list-view-${listViewInstanceId}-block-${clientId}`,
"data-block": clientId,
"data-expanded": canEditBlock ? isExpanded : void 0,
ref: rowRef,
children: [
/* @__PURE__ */ jsx(
TreeGridCell,
{
className: "block-editor-list-view-block__contents-cell",
colSpan,
ref: cellRef,
"aria-selected": !!isSelected,
children: ({ ref, tabIndex, onFocus }) => /* @__PURE__ */ jsxs("div", { className: "block-editor-list-view-block__contents-container", children: [
/* @__PURE__ */ jsx(
ListViewBlockContents,
{
block,
onClick: selectEditorBlock,
onContextMenu,
onMouseDown,
onToggleExpanded: toggleExpanded,
isSelected,
position,
siblingBlockCount,
level,
ref,
tabIndex: currentlyEditingBlockInCanvas ? 0 : tabIndex,
onFocus,
isExpanded: canEditBlock ? isExpanded : void 0,
selectedClientIds,
ariaDescribedBy: descriptionId
}
),
/* @__PURE__ */ jsx(AriaReferencedText, { id: descriptionId, children: [
blockPositionDescription,
blockPropertiesDescription,
blockVisibilityDescription
].filter(Boolean).join(" ") })
] })
}
),
hasRenderedMovers && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs(
TreeGridCell,
{
className: moverCellClassName,
withoutGridItem: true,
children: [
/* @__PURE__ */ jsx(TreeGridItem, { children: ({ ref, tabIndex, onFocus }) => /* @__PURE__ */ jsx(
BlockMoverUpButton,
{
orientation: "vertical",
clientIds: [clientId],
ref,
tabIndex,
onFocus
}
) }),
/* @__PURE__ */ jsx(TreeGridItem, { children: ({ ref, tabIndex, onFocus }) => /* @__PURE__ */ jsx(
BlockMoverDownButton,
{
orientation: "vertical",
clientIds: [clientId],
ref,
tabIndex,
onFocus
}
) })
]
}
) }),
showBlockActions && BlockSettingsMenu && /* @__PURE__ */ jsx(
TreeGridCell,
{
className: listViewBlockSettingsClassName,
"aria-selected": !!isSelected,
ref: settingsRef,
children: ({ ref, tabIndex, onFocus }) => /* @__PURE__ */ jsx(
BlockSettingsMenu,
{
clientIds: dropdownClientIds,
block,
icon: moreVertical,
label: __("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,
size: "small"
},
disableOpenOnArrowDown: true,
expand,
expandedState,
setInsertedBlock,
__experimentalSelectBlock: updateFocusAndSelection
}
)
}
)
]
}
);
}
var block_default = memo(ListViewBlock);
export {
block_default as default
};
//# sourceMappingURL=block.js.map