@wordpress/block-editor
Version:
394 lines (383 loc) • 13.2 kB
JavaScript
/**
* External dependencies
*/
import clsx from 'clsx';
/**
* WordPress dependencies
*/
import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose';
import { addFilter } from '@wordpress/hooks';
import { getBlockSupport, hasBlockSupport } from '@wordpress/blocks';
import { useSelect } from '@wordpress/data';
import { __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOption as ToggleGroupControlOption, ToggleControl, PanelBody, privateApis as componentsPrivateApis } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../store';
import { InspectorControls } from '../components';
import { useSettings } from '../components/use-settings';
import { getLayoutType, getLayoutTypes } from '../layouts';
import { useBlockEditingMode } from '../components/block-editing-mode';
import { LAYOUT_DEFINITIONS } from '../layouts/definitions';
import { useBlockSettings, useStyleOverride } from './utils';
import { unlock } from '../lock-unlock';
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
const layoutBlockSupportKey = 'layout';
const {
kebabCase
} = unlock(componentsPrivateApis);
function hasLayoutBlockSupport(blockName) {
return hasBlockSupport(blockName, 'layout') || hasBlockSupport(blockName, '__experimentalLayout');
}
/**
* Generates the utility classnames for the given block's layout attributes.
*
* @param { Object } blockAttributes Block attributes.
* @param { string } blockName Block name.
*
* @return { Array } Array of CSS classname strings.
*/
export function useLayoutClasses(blockAttributes = {}, blockName = '') {
const {
layout
} = blockAttributes;
const {
default: defaultBlockLayout
} = getBlockSupport(blockName, layoutBlockSupportKey) || {};
const usedLayout = layout?.inherit || layout?.contentSize || layout?.wideSize ? {
...layout,
type: 'constrained'
} : layout || defaultBlockLayout || {};
const layoutClassnames = [];
if (LAYOUT_DEFINITIONS[usedLayout?.type || 'default']?.className) {
const baseClassName = LAYOUT_DEFINITIONS[usedLayout?.type || 'default']?.className;
const splitBlockName = blockName.split('/');
const fullBlockName = splitBlockName[0] === 'core' ? splitBlockName.pop() : splitBlockName.join('-');
const compoundClassName = `wp-block-${fullBlockName}-${baseClassName}`;
layoutClassnames.push(baseClassName, compoundClassName);
}
const hasGlobalPadding = useSelect(select => {
return (usedLayout?.inherit || usedLayout?.contentSize || usedLayout?.type === 'constrained') && select(blockEditorStore).getSettings().__experimentalFeatures?.useRootPaddingAwareAlignments;
}, [usedLayout?.contentSize, usedLayout?.inherit, usedLayout?.type]);
if (hasGlobalPadding) {
layoutClassnames.push('has-global-padding');
}
if (usedLayout?.orientation) {
layoutClassnames.push(`is-${kebabCase(usedLayout.orientation)}`);
}
if (usedLayout?.justifyContent) {
layoutClassnames.push(`is-content-justification-${kebabCase(usedLayout.justifyContent)}`);
}
if (usedLayout?.flexWrap && usedLayout.flexWrap === 'nowrap') {
layoutClassnames.push('is-nowrap');
}
return layoutClassnames;
}
/**
* Generates a CSS rule with the given block's layout styles.
*
* @param { Object } blockAttributes Block attributes.
* @param { string } blockName Block name.
* @param { string } selector A selector to use in generating the CSS rule.
*
* @return { string } CSS rule.
*/
export function useLayoutStyles(blockAttributes = {}, blockName, selector) {
const {
layout = {},
style = {}
} = blockAttributes;
// Update type for blocks using legacy layouts.
const usedLayout = layout?.inherit || layout?.contentSize || layout?.wideSize ? {
...layout,
type: 'constrained'
} : layout || {};
const fullLayoutType = getLayoutType(usedLayout?.type || 'default');
const [blockGapSupport] = useSettings('spacing.blockGap');
const hasBlockGapSupport = blockGapSupport !== null;
return fullLayoutType?.getLayoutStyle?.({
blockName,
selector,
layout,
style,
hasBlockGapSupport
});
}
function LayoutPanelPure({
layout,
setAttributes,
name: blockName,
clientId
}) {
const settings = useBlockSettings(blockName);
// Block settings come from theme.json under settings.[blockName].
const {
layout: layoutSettings
} = settings;
const {
themeSupportsLayout
} = useSelect(select => {
const {
getSettings
} = select(blockEditorStore);
return {
themeSupportsLayout: getSettings().supportsLayout
};
}, []);
const blockEditingMode = useBlockEditingMode();
if (blockEditingMode !== 'default') {
return null;
}
// Layout block support comes from the block's block.json.
const layoutBlockSupport = getBlockSupport(blockName, layoutBlockSupportKey, {});
const blockSupportAndThemeSettings = {
...layoutSettings,
...layoutBlockSupport
};
const {
allowSwitching,
allowEditing = true,
allowInheriting = true,
default: defaultBlockLayout
} = blockSupportAndThemeSettings;
if (!allowEditing) {
return null;
}
/*
* Try to find the layout type from either the
* block's layout settings or any saved layout config.
*/
const blockSupportAndLayout = {
...layoutBlockSupport,
...layout
};
const {
type,
default: {
type: defaultType = 'default'
} = {}
} = blockSupportAndLayout;
const blockLayoutType = type || defaultType;
// Only show the inherit toggle if it's supported,
// and either the default / flow or the constrained layout type is in use, as the toggle switches from one to the other.
const showInheritToggle = !!(allowInheriting && (!blockLayoutType || blockLayoutType === 'default' || blockLayoutType === 'constrained' || blockSupportAndLayout.inherit));
const usedLayout = layout || defaultBlockLayout || {};
const {
inherit = false,
contentSize = null
} = usedLayout;
/**
* `themeSupportsLayout` is only relevant to the `default/flow` or
* `constrained` layouts and it should not be taken into account when other
* `layout` types are used.
*/
if ((blockLayoutType === 'default' || blockLayoutType === 'constrained') && !themeSupportsLayout) {
return null;
}
const layoutType = getLayoutType(blockLayoutType);
const constrainedType = getLayoutType('constrained');
const displayControlsForLegacyLayouts = !usedLayout.type && (contentSize || inherit);
const hasContentSizeOrLegacySettings = !!inherit || !!contentSize;
const onChangeType = newType => setAttributes({
layout: {
type: newType
}
});
const onChangeLayout = newLayout => setAttributes({
layout: newLayout
});
return /*#__PURE__*/_jsxs(_Fragment, {
children: [/*#__PURE__*/_jsx(InspectorControls, {
children: /*#__PURE__*/_jsxs(PanelBody, {
title: __('Layout'),
children: [showInheritToggle && /*#__PURE__*/_jsx(_Fragment, {
children: /*#__PURE__*/_jsx(ToggleControl, {
__nextHasNoMarginBottom: true,
label: __('Inner blocks use content width'),
checked: layoutType?.name === 'constrained' || hasContentSizeOrLegacySettings,
onChange: () => setAttributes({
layout: {
type: layoutType?.name === 'constrained' || hasContentSizeOrLegacySettings ? 'default' : 'constrained'
}
}),
help: layoutType?.name === 'constrained' || hasContentSizeOrLegacySettings ? __('Nested blocks use content width with options for full and wide widths.') : __('Nested blocks will fill the width of this container.')
})
}), !inherit && allowSwitching && /*#__PURE__*/_jsx(LayoutTypeSwitcher, {
type: blockLayoutType,
onChange: onChangeType
}), layoutType && layoutType.name !== 'default' && /*#__PURE__*/_jsx(layoutType.inspectorControls, {
layout: usedLayout,
onChange: onChangeLayout,
layoutBlockSupport: blockSupportAndThemeSettings,
name: blockName,
clientId: clientId
}), constrainedType && displayControlsForLegacyLayouts && /*#__PURE__*/_jsx(constrainedType.inspectorControls, {
layout: usedLayout,
onChange: onChangeLayout,
layoutBlockSupport: blockSupportAndThemeSettings,
name: blockName,
clientId: clientId
})]
})
}), !inherit && layoutType && /*#__PURE__*/_jsx(layoutType.toolBarControls, {
layout: usedLayout,
onChange: onChangeLayout,
layoutBlockSupport: layoutBlockSupport,
name: blockName,
clientId: clientId
})]
});
}
export default {
shareWithChildBlocks: true,
edit: LayoutPanelPure,
attributeKeys: ['layout'],
hasSupport(name) {
return hasLayoutBlockSupport(name);
}
};
function LayoutTypeSwitcher({
type,
onChange
}) {
return /*#__PURE__*/_jsx(ToggleGroupControl, {
__next40pxDefaultSize: true,
isBlock: true,
label: __('Layout type'),
__nextHasNoMarginBottom: true,
hideLabelFromVision: true,
isAdaptiveWidth: true,
value: type,
onChange: onChange,
children: getLayoutTypes().map(({
name,
label
}) => {
return /*#__PURE__*/_jsx(ToggleGroupControlOption, {
value: name,
label: label
}, name);
})
});
}
/**
* Filters registered block settings, extending attributes to include `layout`.
*
* @param {Object} settings Original block settings.
*
* @return {Object} Filtered block settings.
*/
export function addAttribute(settings) {
var _settings$attributes$;
if ('type' in ((_settings$attributes$ = settings.attributes?.layout) !== null && _settings$attributes$ !== void 0 ? _settings$attributes$ : {})) {
return settings;
}
if (hasLayoutBlockSupport(settings)) {
settings.attributes = {
...settings.attributes,
layout: {
type: 'object'
}
};
}
return settings;
}
function BlockWithLayoutStyles({
block: BlockListBlock,
props,
blockGapSupport,
layoutClasses
}) {
const {
name,
attributes
} = props;
const id = useInstanceId(BlockListBlock);
const {
layout
} = attributes;
const {
default: defaultBlockLayout
} = getBlockSupport(name, layoutBlockSupportKey) || {};
const usedLayout = layout?.inherit || layout?.contentSize || layout?.wideSize ? {
...layout,
type: 'constrained'
} : layout || defaultBlockLayout || {};
const selectorPrefix = `wp-container-${kebabCase(name)}-is-layout-`;
// Higher specificity to override defaults from theme.json.
const selector = `.${selectorPrefix}${id}`;
const hasBlockGapSupport = blockGapSupport !== null;
// Get CSS string for the current layout type.
// The CSS and `style` element is only output if it is not empty.
const fullLayoutType = getLayoutType(usedLayout?.type || 'default');
const css = fullLayoutType?.getLayoutStyle?.({
blockName: name,
selector,
layout: usedLayout,
style: attributes?.style,
hasBlockGapSupport
});
// Attach a `wp-container-` id-based class name as well as a layout class name such as `is-layout-flex`.
const layoutClassNames = clsx({
[`${selectorPrefix}${id}`]: !!css // Only attach a container class if there is generated CSS to be attached.
}, layoutClasses);
useStyleOverride({
css
});
return /*#__PURE__*/_jsx(BlockListBlock, {
...props,
__unstableLayoutClassNames: layoutClassNames
});
}
/**
* Override the default block element to add the layout styles.
*
* @param {Function} BlockListBlock Original component.
*
* @return {Function} Wrapped component.
*/
export const withLayoutStyles = createHigherOrderComponent(BlockListBlock => props => {
const {
clientId,
name,
attributes
} = props;
const blockSupportsLayout = hasLayoutBlockSupport(name);
const layoutClasses = useLayoutClasses(attributes, name);
const extraProps = useSelect(select => {
// The callback returns early to avoid block editor subscription.
if (!blockSupportsLayout) {
return;
}
const {
getSettings,
getBlockSettings
} = unlock(select(blockEditorStore));
const {
disableLayoutStyles
} = getSettings();
if (disableLayoutStyles) {
return;
}
const [blockGapSupport] = getBlockSettings(clientId, 'spacing.blockGap');
return {
blockGapSupport
};
}, [blockSupportsLayout, clientId]);
if (!extraProps) {
return /*#__PURE__*/_jsx(BlockListBlock, {
...props,
__unstableLayoutClassNames: blockSupportsLayout ? layoutClasses : undefined
});
}
return /*#__PURE__*/_jsx(BlockWithLayoutStyles, {
block: BlockListBlock,
props: props,
layoutClasses: layoutClasses,
...extraProps
});
}, 'withLayoutStyles');
addFilter('blocks.registerBlockType', 'core/layout/addAttribute', addAttribute);
addFilter('editor.BlockListBlock', 'core/editor/layout/with-layout-styles', withLayoutStyles);
//# sourceMappingURL=layout.js.map