@plone/volto
Version:
Volto
384 lines (345 loc) • 10.7 kB
JSX
import React, { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import cloneDeep from 'lodash/cloneDeep';
import map from 'lodash/map';
import EditBlock from './Edit';
import DragDropList from '@plone/volto/components/manage/DragDropList/DragDropList';
import {
getBlocks,
getBlocksFieldname,
getBlocksLayoutFieldname,
applyBlockDefaults,
getBlocksHierarchy,
addBlock,
insertBlock,
changeBlock,
deleteBlock,
moveBlock,
moveBlockEnhanced,
mutateBlock,
nextBlockId,
previousBlockId,
} from '@plone/volto/helpers/Blocks/Blocks';
import { useDetectClickOutside } from '@plone/volto/helpers/Utils/useDetectClickOutside';
import { useEvent } from '@plone/volto/helpers/Utils/useEvent';
import EditBlockWrapper from './EditBlockWrapper';
import { setSidebarTab } from '@plone/volto/actions/sidebar/sidebar';
import { setUIState } from '@plone/volto/actions/form/form';
import { useDispatch } from 'react-redux';
import config from '@plone/volto/registry';
import { createPortal } from 'react-dom';
import Order from './Order/Order';
const BlocksForm = (props) => {
const {
pathname,
onChangeField,
properties,
type,
navRoot,
onChangeFormData,
selectedBlock,
multiSelected,
onSelectBlock,
allowedBlocks,
showRestricted,
title,
description,
metadata,
manage,
children,
isMainForm = true,
isContainer,
stopPropagation,
disableAddBlockOnEnterKey,
blocksConfig = config.blocks.blocksConfig,
editable = true,
direction = 'vertical',
history,
location,
token,
errors,
blocksErrors,
} = props;
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
const blockList = getBlocks(properties);
const dispatch = useDispatch();
const intl = useIntl();
const ClickOutsideListener = () => {
onSelectBlock(null);
dispatch(setSidebarTab(0));
};
const ref = useDetectClickOutside({
onTriggered: ClickOutsideListener,
triggerKeys: ['Escape'],
// Disabled feature for now https://github.com/plone/volto/pull/2389#issuecomment-830027413
disableClick: true,
disableKeys: !isMainForm,
});
const handleKeyDown = (
e,
index,
block,
node,
{
disableEnter = false,
disableArrowUp = false,
disableArrowDown = false,
} = {},
) => {
const isMultipleSelection = e.shiftKey;
if (e.key === 'ArrowUp' && !disableArrowUp) {
onFocusPreviousBlock(block, node, isMultipleSelection);
e.preventDefault();
}
if (e.key === 'ArrowDown' && !disableArrowDown) {
onFocusNextBlock(block, node, isMultipleSelection);
e.preventDefault();
}
const blockConfig =
blocksConfig[properties[getBlocksFieldname(properties)][block]['@type']];
if (e.key === 'Enter' && !disableEnter && !blockConfig.disableEnter) {
if (!disableAddBlockOnEnterKey) {
onSelectBlock(onAddBlock(config.settings.defaultBlockType, index + 1));
}
e.preventDefault();
}
};
const onFocusPreviousBlock = (
currentBlock,
blockNode,
isMultipleSelection,
) => {
const prev = previousBlockId(properties, currentBlock);
if (prev === null) return;
blockNode.blur();
onSelectBlock(prev, isMultipleSelection);
};
const onFocusNextBlock = (currentBlock, blockNode, isMultipleSelection) => {
const next = nextBlockId(properties, currentBlock);
if (next === null) return;
blockNode.blur();
onSelectBlock(next, isMultipleSelection);
};
const onMutateBlock = (id, value) => {
const newFormData = mutateBlock(properties, id, value, {}, intl);
onChangeFormData(newFormData);
};
const onInsertBlock = (id, value, current) => {
const [newId, newFormData] = insertBlock(
properties,
id,
value,
current,
config.experimental.addBlockButton.enabled ? 1 : 0,
{},
intl,
);
const blocksFieldname = getBlocksFieldname(newFormData);
const blockData = newFormData[blocksFieldname][newId];
newFormData[blocksFieldname][newId] = applyBlockDefaults({
data: blockData,
intl,
metadata,
properties,
});
onChangeFormData(newFormData);
return newId;
};
const onAddBlock = (type, index) => {
if (editable) {
const [id, newFormData] = addBlock(properties, type, index, {}, intl);
const blocksFieldname = getBlocksFieldname(newFormData);
const blockData = newFormData[blocksFieldname][id];
newFormData[blocksFieldname][id] = applyBlockDefaults({
data: blockData,
intl,
metadata,
properties,
});
onChangeFormData(newFormData);
return id;
}
};
const onChangeBlock = (id, value) => {
const newFormData = changeBlock(properties, id, value);
onChangeFormData(newFormData);
};
const onDeleteBlock = (id, selectPrev) => {
const previous = previousBlockId(properties, id);
const newFormData = deleteBlock(properties, id, intl);
onChangeFormData(newFormData);
onSelectBlock(selectPrev ? previous : null);
};
const onMoveBlock = (dragIndex, hoverIndex) => {
const newFormData = moveBlock(properties, dragIndex, hoverIndex);
onChangeFormData(newFormData);
};
const onMoveBlockEnhanced = ({ source, destination }) => {
const newFormData = moveBlockEnhanced(cloneDeep(properties), {
source,
destination,
});
const blocksFieldname = getBlocksFieldname(newFormData);
const blocksLayoutFieldname = getBlocksLayoutFieldname(newFormData);
let error = false;
const allowedBlocks = Object.keys(blocksConfig);
map(newFormData[blocksLayoutFieldname].items, (id) => {
const block = newFormData[blocksFieldname][id];
if (!allowedBlocks.includes(block['@type'])) {
error = true;
}
if (Array.isArray(block[blocksLayoutFieldname]?.items)) {
const size = block[blocksLayoutFieldname].items.length;
const allowedSubBlocks = [
...(blocksConfig[block['@type']].allowedBlocks || allowedBlocks),
'empty',
] || ['empty'];
if (size < 1 || size > (blocksConfig[block['@type']].maxLength || 4)) {
error = true;
}
map(block[blocksLayoutFieldname].items, (subId) => {
const subBlock = block[blocksFieldname][subId];
if (!allowedSubBlocks.includes(subBlock['@type'])) {
error = true;
}
});
}
});
if (!error) {
onChangeFormData(newFormData);
dispatch(
setUIState({
selected: null,
multiSelected: [],
gridSelected: null,
}),
);
}
};
const defaultBlockWrapper = ({ draginfo }, editBlock, blockProps) => (
<EditBlockWrapper draginfo={draginfo} blockProps={blockProps}>
{editBlock}
</EditBlockWrapper>
);
const editBlockWrapper = children || defaultBlockWrapper;
// Remove invalid blocks on saving
// Note they are alreaady filtered by DragDropList, but we also want them
// to be removed when the user saves the page next. Otherwise the invalid
// blocks would linger for ever.
for (const [n, v] of blockList) {
if (!v) {
const newFormData = deleteBlock(properties, n, intl);
onChangeFormData(newFormData);
}
}
useEvent('voltoClickBelowContent', () => {
if (!config.experimental.addBlockButton.enabled || !isMainForm) return;
onSelectBlock(
onAddBlock(config.settings.defaultBlockType, blockList.length),
);
});
return (
<>
{isMainForm &&
isClient &&
createPortal(
<div>
<Order
items={getBlocksHierarchy(properties)}
onMoveBlock={onMoveBlockEnhanced}
onDeleteBlock={onDeleteBlock}
onSelectBlock={onSelectBlock}
removable
errors={blocksErrors}
/>
</div>,
document.getElementById('sidebar-order'),
)}
<div
className="blocks-form"
role="presentation"
ref={ref}
onKeyDown={(e) => {
if (stopPropagation) {
e.stopPropagation();
}
}}
>
<fieldset className="invisible" disabled={!editable}>
<DragDropList
childList={blockList}
onMoveItem={(result) => {
const { source, destination } = result;
if (!destination) {
return;
}
const newFormData = moveBlock(
properties,
source.index,
destination.index,
);
onChangeFormData(newFormData);
return true;
}}
direction={direction}
>
{(dragProps) => {
const { child, childId, index } = dragProps;
const blockProps = {
allowedBlocks,
showRestricted,
block: childId,
data: child,
handleKeyDown,
id: childId,
formTitle: title,
formDescription: description,
index,
manage,
onAddBlock,
onInsertBlock,
onChangeBlock,
onChangeField,
onChangeFormData,
onDeleteBlock,
onFocusNextBlock,
onFocusPreviousBlock,
onMoveBlock,
onMutateBlock,
onSelectBlock,
pathname,
metadata,
properties,
contentType: type,
navRoot,
blocksConfig,
selected: selectedBlock === childId,
multiSelected: multiSelected?.includes(childId),
type: child['@type'],
editable,
showBlockChooser: selectedBlock === childId,
detached: isContainer,
// Properties to pass to the BlocksForm to match the View ones
content: properties,
history,
location,
token,
errors,
blocksErrors,
};
return editBlockWrapper(
dragProps,
<EditBlock key={childId} {...blockProps} />,
blockProps,
);
}}
</DragDropList>
</fieldset>
</div>
</>
);
};
export default BlocksForm;