@wordpress/block-editor
Version:
443 lines (396 loc) • 14.6 kB
JavaScript
import { createElement, Fragment } from "@wordpress/element";
/**
* External dependencies
*/
import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { useMemo, useCallback, RawHTML } from '@wordpress/element';
import { getBlockType, getSaveContent, isUnmodifiedDefaultBlock, serializeRawBlock, switchToBlockType, getDefaultBlockName, isUnmodifiedBlock } from '@wordpress/blocks';
import { withFilters } from '@wordpress/components';
import { withDispatch, withSelect, useDispatch, useSelect } from '@wordpress/data';
import { compose, pure, ifCondition } from '@wordpress/compose';
import { safeHTML } from '@wordpress/dom';
/**
* Internal dependencies
*/
import BlockEdit from '../block-edit';
import BlockInvalidWarning from './block-invalid-warning';
import BlockCrashWarning from './block-crash-warning';
import BlockCrashBoundary from './block-crash-boundary';
import BlockHtml from './block-html';
import { useBlockProps } from './use-block-props';
import { store as blockEditorStore } from '../../store';
import { useLayout } from './layout';
import { unlock } from '../../lock-unlock';
import { BlockListBlockContext } from './block-list-block-context';
/**
* Merges wrapper props with special handling for classNames and styles.
*
* @param {Object} propsA
* @param {Object} propsB
*
* @return {Object} Merged props.
*/
function mergeWrapperProps(propsA, propsB) {
const newProps = { ...propsA,
...propsB
};
if (propsA?.className && propsB?.className) {
newProps.className = classnames(propsA.className, propsB.className);
}
if (propsA?.style && propsB?.style) {
newProps.style = { ...propsA.style,
...propsB.style
};
}
return newProps;
}
function Block({
children,
isHtml,
...props
}) {
return createElement("div", useBlockProps(props, {
__unstableIsHtml: isHtml
}), children);
}
function BlockListBlock({
block: {
__unstableBlockSource
},
mode,
isLocked,
canRemove,
clientId,
isSelected,
isSelectionEnabled,
className,
__unstableLayoutClassNames: layoutClassNames,
name,
isValid,
attributes,
wrapperProps,
setAttributes,
onReplace,
onInsertBlocksAfter,
onMerge,
toggleSelection
}) {
var _wrapperProps;
const {
themeSupportsLayout,
isTemporarilyEditingAsBlocks,
blockEditingMode
} = useSelect(select => {
const {
getSettings,
__unstableGetTemporarilyEditingAsBlocks,
getBlockEditingMode
} = unlock(select(blockEditorStore));
return {
themeSupportsLayout: getSettings().supportsLayout,
isTemporarilyEditingAsBlocks: __unstableGetTemporarilyEditingAsBlocks() === clientId,
blockEditingMode: getBlockEditingMode(clientId)
};
}, [clientId]);
const {
removeBlock
} = useDispatch(blockEditorStore);
const onRemove = useCallback(() => removeBlock(clientId), [clientId]);
const parentLayout = useLayout() || {}; // We wrap the BlockEdit component in a div that hides it when editing in
// HTML mode. This allows us to render all of the ancillary pieces
// (InspectorControls, etc.) which are inside `BlockEdit` but not
// `BlockHTML`, even in HTML mode.
let blockEdit = createElement(BlockEdit, {
name: name,
isSelected: isSelected,
attributes: attributes,
setAttributes: setAttributes,
insertBlocksAfter: isLocked ? undefined : onInsertBlocksAfter,
onReplace: canRemove ? onReplace : undefined,
onRemove: canRemove ? onRemove : undefined,
mergeBlocks: canRemove ? onMerge : undefined,
clientId: clientId,
isSelectionEnabled: isSelectionEnabled,
toggleSelection: toggleSelection,
__unstableLayoutClassNames: layoutClassNames,
__unstableParentLayout: Object.keys(parentLayout).length ? parentLayout : undefined
});
const blockType = getBlockType(name);
if (blockEditingMode === 'disabled') {
wrapperProps = { ...wrapperProps,
tabIndex: -1
};
} // Determine whether the block has props to apply to the wrapper.
if (blockType?.getEditWrapperProps) {
wrapperProps = mergeWrapperProps(wrapperProps, blockType.getEditWrapperProps(attributes));
}
const isAligned = wrapperProps && !!wrapperProps['data-align'] && !themeSupportsLayout; // For aligned blocks, provide a wrapper element so the block can be
// positioned relative to the block column.
// This is only kept for classic themes that don't support layout
// Historically we used to rely on extra divs and data-align to
// provide the alignments styles in the editor.
// Due to the differences between frontend and backend, we migrated
// to the layout feature, and we're now aligning the markup of frontend
// and backend.
if (isAligned) {
blockEdit = createElement("div", {
className: "wp-block",
"data-align": wrapperProps['data-align']
}, blockEdit);
}
let block;
if (!isValid) {
const saveContent = __unstableBlockSource ? serializeRawBlock(__unstableBlockSource) : getSaveContent(blockType, attributes);
block = createElement(Block, {
className: "has-warning"
}, createElement(BlockInvalidWarning, {
clientId: clientId
}), createElement(RawHTML, null, safeHTML(saveContent)));
} else if (mode === 'html') {
// Render blockEdit so the inspector controls don't disappear.
// See #8969.
block = createElement(Fragment, null, createElement("div", {
style: {
display: 'none'
}
}, blockEdit), createElement(Block, {
isHtml: true
}, createElement(BlockHtml, {
clientId: clientId
})));
} else if (blockType?.apiVersion > 1) {
block = blockEdit;
} else {
block = createElement(Block, wrapperProps, blockEdit);
}
const {
'data-align': dataAlign,
...restWrapperProps
} = (_wrapperProps = wrapperProps) !== null && _wrapperProps !== void 0 ? _wrapperProps : {};
const value = {
clientId,
className: classnames({
'is-editing-disabled': blockEditingMode === 'disabled',
'is-content-locked-temporarily-editing-as-blocks': isTemporarilyEditingAsBlocks
}, dataAlign && themeSupportsLayout && `align${dataAlign}`, className),
wrapperProps: restWrapperProps,
isAligned
};
const memoizedValue = useMemo(() => value, Object.values(value));
return createElement(BlockListBlockContext.Provider, {
value: memoizedValue
}, createElement(BlockCrashBoundary, {
fallback: createElement(Block, {
className: "has-warning"
}, createElement(BlockCrashWarning, null))
}, block));
}
const applyWithSelect = withSelect((select, {
clientId,
rootClientId
}) => {
const {
isBlockSelected,
getBlockMode,
isSelectionEnabled,
getTemplateLock,
__unstableGetBlockWithoutInnerBlocks,
canRemoveBlock,
canMoveBlock
} = select(blockEditorStore);
const block = __unstableGetBlockWithoutInnerBlocks(clientId);
const isSelected = isBlockSelected(clientId);
const templateLock = getTemplateLock(rootClientId);
const canRemove = canRemoveBlock(clientId, rootClientId);
const canMove = canMoveBlock(clientId, rootClientId); // The fallback to `{}` is a temporary fix.
// This function should never be called when a block is not present in
// the state. It happens now because the order in withSelect rendering
// is not correct.
const {
name,
attributes,
isValid
} = block || {}; // Do not add new properties here, use `useSelect` instead to avoid
// leaking new props to the public API (editor.BlockListBlock filter).
return {
mode: getBlockMode(clientId),
isSelectionEnabled: isSelectionEnabled(),
isLocked: !!templateLock,
canRemove,
canMove,
// Users of the editor.BlockListBlock filter used to be able to
// access the block prop.
// Ideally these blocks would rely on the clientId prop only.
// This is kept for backward compatibility reasons.
block,
name,
attributes,
isValid,
isSelected
};
});
const applyWithDispatch = withDispatch((dispatch, ownProps, registry) => {
const {
updateBlockAttributes,
insertBlocks,
mergeBlocks,
replaceBlocks,
toggleSelection,
__unstableMarkLastChangeAsPersistent,
moveBlocksToPosition,
removeBlock
} = dispatch(blockEditorStore); // Do not add new properties here, use `useDispatch` instead to avoid
// leaking new props to the public API (editor.BlockListBlock filter).
return {
setAttributes(newAttributes) {
const {
getMultiSelectedBlockClientIds
} = registry.select(blockEditorStore);
const multiSelectedBlockClientIds = getMultiSelectedBlockClientIds();
const {
clientId
} = ownProps;
const clientIds = multiSelectedBlockClientIds.length ? multiSelectedBlockClientIds : [clientId];
updateBlockAttributes(clientIds, newAttributes);
},
onInsertBlocks(blocks, index) {
const {
rootClientId
} = ownProps;
insertBlocks(blocks, index, rootClientId);
},
onInsertBlocksAfter(blocks) {
const {
clientId,
rootClientId
} = ownProps;
const {
getBlockIndex
} = registry.select(blockEditorStore);
const index = getBlockIndex(clientId);
insertBlocks(blocks, index + 1, rootClientId);
},
onMerge(forward) {
const {
clientId,
rootClientId
} = ownProps;
const {
getPreviousBlockClientId,
getNextBlockClientId,
getBlock,
getBlockAttributes,
getBlockName,
getBlockOrder,
getBlockIndex,
getBlockRootClientId,
canInsertBlockType
} = registry.select(blockEditorStore);
/**
* Moves the block with clientId up one level. If the block type
* cannot be inserted at the new location, it will be attempted to
* convert to the default block type.
*
* @param {string} _clientId The block to move.
* @param {boolean} changeSelection Whether to change the selection
* to the moved block.
*/
function moveFirstItemUp(_clientId, changeSelection = true) {
const targetRootClientId = getBlockRootClientId(_clientId);
const blockOrder = getBlockOrder(_clientId);
const [firstClientId] = blockOrder;
if (blockOrder.length === 1 && isUnmodifiedBlock(getBlock(firstClientId))) {
removeBlock(_clientId);
} else {
registry.batch(() => {
if (canInsertBlockType(getBlockName(firstClientId), targetRootClientId)) {
moveBlocksToPosition([firstClientId], _clientId, targetRootClientId, getBlockIndex(_clientId));
} else {
const replacement = switchToBlockType(getBlock(firstClientId), getDefaultBlockName());
if (replacement && replacement.length) {
insertBlocks(replacement, getBlockIndex(_clientId), targetRootClientId, changeSelection);
removeBlock(firstClientId, false);
}
}
if (!getBlockOrder(_clientId).length && isUnmodifiedBlock(getBlock(_clientId))) {
removeBlock(_clientId, false);
}
});
}
} // For `Delete` or forward merge, we should do the exact same thing
// as `Backspace`, but from the other block.
if (forward) {
if (rootClientId) {
const nextRootClientId = getNextBlockClientId(rootClientId);
if (nextRootClientId) {
// If there is a block that follows with the same parent
// block name and the same attributes, merge the inner
// blocks.
if (getBlockName(rootClientId) === getBlockName(nextRootClientId)) {
const rootAttributes = getBlockAttributes(rootClientId);
const previousRootAttributes = getBlockAttributes(nextRootClientId);
if (Object.keys(rootAttributes).every(key => rootAttributes[key] === previousRootAttributes[key])) {
registry.batch(() => {
moveBlocksToPosition(getBlockOrder(nextRootClientId), nextRootClientId, rootClientId);
removeBlock(nextRootClientId, false);
});
return;
}
} else {
mergeBlocks(rootClientId, nextRootClientId);
return;
}
}
}
const nextBlockClientId = getNextBlockClientId(clientId);
if (!nextBlockClientId) {
return;
}
if (getBlockOrder(nextBlockClientId).length) {
moveFirstItemUp(nextBlockClientId, false);
} else {
mergeBlocks(clientId, nextBlockClientId);
}
} else {
const previousBlockClientId = getPreviousBlockClientId(clientId);
if (previousBlockClientId) {
mergeBlocks(previousBlockClientId, clientId);
} else if (rootClientId) {
const previousRootClientId = getPreviousBlockClientId(rootClientId); // If there is a preceding block with the same parent block
// name and the same attributes, merge the inner blocks.
if (previousRootClientId && getBlockName(rootClientId) === getBlockName(previousRootClientId)) {
const rootAttributes = getBlockAttributes(rootClientId);
const previousRootAttributes = getBlockAttributes(previousRootClientId);
if (Object.keys(rootAttributes).every(key => rootAttributes[key] === previousRootAttributes[key])) {
registry.batch(() => {
moveBlocksToPosition(getBlockOrder(rootClientId), rootClientId, previousRootClientId);
removeBlock(rootClientId, false);
});
return;
}
}
moveFirstItemUp(rootClientId);
}
}
},
onReplace(blocks, indexToSelect, initialPosition) {
if (blocks.length && !isUnmodifiedDefaultBlock(blocks[blocks.length - 1])) {
__unstableMarkLastChangeAsPersistent();
}
replaceBlocks([ownProps.clientId], blocks, indexToSelect, initialPosition);
},
toggleSelection(selectionEnabled) {
toggleSelection(selectionEnabled);
}
};
});
export default compose(pure, applyWithSelect, applyWithDispatch, // Block is sometimes not mounted at the right time, causing it be undefined
// see issue for more info
// https://github.com/WordPress/gutenberg/issues/17013
ifCondition(({
block
}) => !!block), withFilters('editor.BlockListBlock'))(BlockListBlock);
//# sourceMappingURL=block.js.map