UNPKG

@wordpress/block-editor

Version:
443 lines (396 loc) 14.6 kB
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