{ additionalControls }
diff --git a/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js b/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js
index 8528655c1dcc9..43e887888dbd1 100644
--- a/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js
+++ b/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js
@@ -5,7 +5,7 @@ import {
documentHasSelection,
documentHasUncollapsedSelection,
} from '@wordpress/dom';
-import { useDispatch, useSelect } from '@wordpress/data';
+import { useDispatch, useRegistry, useSelect } from '@wordpress/data';
import { useRefEffect } from '@wordpress/compose';
/**
@@ -16,6 +16,7 @@ import { useNotifyCopy } from '../../utils/use-notify-copy';
import { getPasteBlocks, setClipboardBlocks } from './utils';
export default function useClipboardHandler() {
+ const registry = useRegistry();
const {
getBlocksByClientId,
getSelectedBlockClientIds,
@@ -104,7 +105,7 @@ export default function useClipboardHandler() {
blocks = [ head, ...inBetweenBlocks, tail ];
}
- setClipboardBlocks( event, blocks );
+ setClipboardBlocks( event, blocks, registry );
}
}
diff --git a/packages/block-editor/src/components/writing-flow/utils.js b/packages/block-editor/src/components/writing-flow/utils.js
index ef1827077ccbf..2a2010854ed20 100644
--- a/packages/block-editor/src/components/writing-flow/utils.js
+++ b/packages/block-editor/src/components/writing-flow/utils.js
@@ -8,36 +8,51 @@ import {
pasteHandler,
findTransform,
getBlockTransforms,
+ store as blocksStore,
} from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { getPasteEventData } from '../../utils/pasting';
+import { store as blockEditorStore } from '../../store';
+
+export const requiresWrapperOnCopy = Symbol( 'requiresWrapperOnCopy' );
/**
* Sets the clipboard data for the provided blocks, with both HTML and plain
* text representations.
*
- * @param {ClipboardEvent} event Clipboard event.
- * @param {WPBlock[]} blocks Blocks to set as clipboard data.
+ * @param {ClipboardEvent} event Clipboard event.
+ * @param {WPBlock[]} blocks Blocks to set as clipboard data.
+ * @param {Object} registry The registry to select from.
*/
-export function setClipboardBlocks( event, blocks ) {
+export function setClipboardBlocks( event, blocks, registry ) {
let _blocks = blocks;
- const wrapperBlockName = event.clipboardData.getData(
- '__unstableWrapperBlockName'
- );
- if ( wrapperBlockName ) {
- _blocks = createBlock(
- wrapperBlockName,
- JSON.parse(
- event.clipboardData.getData(
- '__unstableWrapperBlockAttributes'
- )
- ),
- _blocks
- );
+ const [ firstBlock ] = blocks;
+
+ if ( firstBlock ) {
+ const firstBlockType = registry
+ .select( blocksStore )
+ .getBlockType( firstBlock.name );
+
+ if ( firstBlockType[ requiresWrapperOnCopy ] ) {
+ const { getBlockRootClientId, getBlockName, getBlockAttributes } =
+ registry.select( blockEditorStore );
+ const wrapperBlockClientId = getBlockRootClientId(
+ firstBlock.clientId
+ );
+ const wrapperBlockName = getBlockName( wrapperBlockClientId );
+
+ if ( wrapperBlockName ) {
+ _blocks = createBlock(
+ wrapperBlockName,
+ getBlockAttributes( wrapperBlockClientId ),
+ _blocks
+ );
+ }
+ }
}
const serialized = serialize( _blocks );
diff --git a/packages/block-editor/src/hooks/block-hooks.js b/packages/block-editor/src/hooks/block-hooks.js
index eb84352ab62f0..93bf87f42124b 100644
--- a/packages/block-editor/src/hooks/block-hooks.js
+++ b/packages/block-editor/src/hooks/block-hooks.js
@@ -19,18 +19,28 @@ import { store as blockEditorStore } from '../store';
const EMPTY_OBJECT = {};
-function BlockHooksControlPure( { name, clientId } ) {
+function BlockHooksControlPure( {
+ name,
+ clientId,
+ metadata: { ignoredHookedBlocks = [] } = {},
+} ) {
const blockTypes = useSelect(
( select ) => select( blocksStore ).getBlockTypes(),
[]
);
+ // A hooked block added via a filter will not be exposed through a block
+ // type's `blockHooks` property; however, if the containing layout has been
+ // modified, it will be present in the anchor block's `ignoredHookedBlocks`
+ // metadata.
const hookedBlocksForCurrentBlock = useMemo(
() =>
blockTypes?.filter(
- ( { blockHooks } ) => blockHooks && name in blockHooks
+ ( { name: blockName, blockHooks } ) =>
+ ( blockHooks && name in blockHooks ) ||
+ ignoredHookedBlocks.includes( blockName )
),
- [ blockTypes, name ]
+ [ blockTypes, name, ignoredHookedBlocks ]
);
const { blockIndex, rootClientId, innerBlocksLength } = useSelect(
@@ -79,6 +89,16 @@ function BlockHooksControlPure( { name, clientId } ) {
// inserted and then moved around a bit by the user.
candidates = getBlocks( clientId );
break;
+
+ case undefined:
+ // If we haven't found a blockHooks field with a relative position for the hooked
+ // block, it means that it was added by a filter. In this case, we look for the block
+ // both among the current block's siblings and its children.
+ candidates = [
+ ...getBlocks( rootClientId ),
+ ...getBlocks( clientId ),
+ ];
+ break;
}
const hookedBlock = candidates?.find(
@@ -151,6 +171,18 @@ function BlockHooksControlPure( { name, clientId } ) {
false
);
break;
+
+ case undefined:
+ // If we do not know the relative position, it is because the block was
+ // added via a filter. In this case, we default to inserting it after the
+ // current block.
+ insertBlock(
+ block,
+ blockIndex + 1,
+ rootClientId, // Insert as a child of the current block's parent
+ false
+ );
+ break;
}
};
@@ -219,6 +251,7 @@ function BlockHooksControlPure( { name, clientId } ) {
export default {
edit: BlockHooksControlPure,
+ attributeKeys: [ 'metadata' ],
hasSupport() {
return true;
},
diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js
index 0e5b6614f07cb..5cd8cb46b3b7e 100644
--- a/packages/block-editor/src/hooks/use-bindings-attributes.js
+++ b/packages/block-editor/src/hooks/use-bindings-attributes.js
@@ -4,12 +4,13 @@
import { getBlockType, store as blocksStore } from '@wordpress/blocks';
import { createHigherOrderComponent } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
+import { useLayoutEffect, useCallback, useState } from '@wordpress/element';
import { addFilter } from '@wordpress/hooks';
+import { RichTextData } from '@wordpress/rich-text';
+
/**
* Internal dependencies
*/
-import { store as blockEditorStore } from '../store';
-import { useBlockEditContext } from '../components/block-edit/context';
import { unlock } from '../lock-unlock';
/** @typedef {import('@wordpress/compose').WPHigherOrderComponent} WPHigherOrderComponent */
@@ -22,87 +23,238 @@ import { unlock } from '../lock-unlock';
* @return {WPHigherOrderComponent} Higher-order component.
*/
-export const BLOCK_BINDINGS_ALLOWED_BLOCKS = {
+const BLOCK_BINDINGS_ALLOWED_BLOCKS = {
'core/paragraph': [ 'content' ],
'core/heading': [ 'content' ],
'core/image': [ 'url', 'title', 'alt' ],
'core/button': [ 'url', 'text', 'linkTarget' ],
};
-const createEditFunctionWithBindingsAttribute = () =>
- createHigherOrderComponent(
- ( BlockEdit ) => ( props ) => {
- const { clientId, name: blockName } = useBlockEditContext();
- const blockBindingsSources = unlock(
- useSelect( blocksStore )
- ).getAllBlockBindingsSources();
- const { getBlockAttributes } = useSelect( blockEditorStore );
-
- const updatedAttributes = getBlockAttributes( clientId );
- if ( updatedAttributes?.metadata?.bindings ) {
- Object.entries( updatedAttributes.metadata.bindings ).forEach(
- ( [ attributeName, settings ] ) => {
- const source = blockBindingsSources[ settings.source ];
-
- if ( source && source.useSource ) {
- // Second argument (`updateMetaValue`) will be used to update the value in the future.
- const {
- placeholder,
- useValue: [ metaValue = null ] = [],
- } = source.useSource( props, settings.args );
-
- if ( placeholder && ! metaValue ) {
- // If the attribute is `src` or `href`, a placeholder can't be used because it is not a valid url.
- // Adding this workaround until attributes and metadata fields types are improved and include `url`.
- const htmlAttribute =
- getBlockType( blockName ).attributes[
- attributeName
- ].attribute;
- if (
- htmlAttribute === 'src' ||
- htmlAttribute === 'href'
- ) {
- updatedAttributes[ attributeName ] = null;
- } else {
- updatedAttributes[ attributeName ] =
- placeholder;
- }
- }
-
- if ( metaValue ) {
- updatedAttributes[ attributeName ] = metaValue;
- }
- }
- }
- );
+/**
+ * Based on the given block name,
+ * check if it is possible to bind the block.
+ *
+ * @param {string} blockName - The block name.
+ * @return {boolean} Whether it is possible to bind the block to sources.
+ */
+export function canBindBlock( blockName ) {
+ return blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS;
+}
+
+/**
+ * Based on the given block name and attribute name,
+ * check if it is possible to bind the block attribute.
+ *
+ * @param {string} blockName - The block name.
+ * @param {string} attributeName - The attribute name.
+ * @return {boolean} Whether it is possible to bind the block attribute.
+ */
+export function canBindAttribute( blockName, attributeName ) {
+ return (
+ canBindBlock( blockName ) &&
+ BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ].includes( attributeName )
+ );
+}
+
+/**
+ * This component is responsible for detecting and
+ * propagating data changes from the source to the block.
+ *
+ * @param {Object} props - The component props.
+ * @param {string} props.attrName - The attribute name.
+ * @param {Object} props.blockProps - The block props with bound attribute.
+ * @param {Object} props.source - Source handler.
+ * @param {Object} props.args - The arguments to pass to the source.
+ * @param {Function} props.onPropValueChange - The function to call when the attribute value changes.
+ * @return {null} Data-handling component. Render nothing.
+ */
+const BindingConnector = ( {
+ args,
+ attrName,
+ blockProps,
+ source,
+ onPropValueChange,
+} ) => {
+ const { placeholder, value: propValue } = source.useSource(
+ blockProps,
+ args
+ );
+
+ const { name: blockName } = blockProps;
+ const attrValue = blockProps.attributes[ attrName ];
+
+ const updateBoundAttibute = useCallback(
+ ( newAttrValue, prevAttrValue ) => {
+ /*
+ * If the attribute is a RichTextData instance,
+ * (core/paragraph, core/heading, core/button, etc.)
+ * compare its HTML representation with the new value.
+ *
+ * To do: it looks like a workaround.
+ * Consider improving the attribute and metadata fields types.
+ */
+ if ( prevAttrValue instanceof RichTextData ) {
+ // Bail early if the Rich Text value is the same.
+ if ( prevAttrValue.toHTMLString() === newAttrValue ) {
+ return;
+ }
+
+ /*
+ * To preserve the value type,
+ * convert the new value to a RichTextData instance.
+ */
+ newAttrValue = RichTextData.fromHTMLString( newAttrValue );
+ }
+
+ if ( prevAttrValue === newAttrValue ) {
+ return;
}
- return (
+ onPropValueChange( { [ attrName ]: newAttrValue } );
+ },
+ [ attrName, onPropValueChange ]
+ );
+
+ useLayoutEffect( () => {
+ if ( typeof propValue !== 'undefined' ) {
+ updateBoundAttibute( propValue, attrValue );
+ } else if ( placeholder ) {
+ /*
+ * Placeholder fallback.
+ * If the attribute is `src` or `href`,
+ * a placeholder can't be used because it is not a valid url.
+ * Adding this workaround until
+ * attributes and metadata fields types are improved and include `url`.
+ */
+ const htmlAttribute =
+ getBlockType( blockName ).attributes[ attrName ].attribute;
+
+ if ( htmlAttribute === 'src' || htmlAttribute === 'href' ) {
+ updateBoundAttibute( null );
+ return;
+ }
+
+ updateBoundAttibute( placeholder );
+ }
+ }, [
+ updateBoundAttibute,
+ propValue,
+ attrValue,
+ placeholder,
+ blockName,
+ attrName,
+ ] );
+
+ return null;
+};
+
+/**
+ * BlockBindingBridge acts like a component wrapper
+ * that connects the bound attributes of a block
+ * to the source handlers.
+ * For this, it creates a BindingConnector for each bound attribute.
+ *
+ * @param {Object} props - The component props.
+ * @param {Object} props.blockProps - The BlockEdit props object.
+ * @param {Object} props.bindings - The block bindings settings.
+ * @param {Function} props.onPropValueChange - The function to call when the attribute value changes.
+ * @return {null} Data-handling component. Render nothing.
+ */
+function BlockBindingBridge( { blockProps, bindings, onPropValueChange } ) {
+ const blockBindingsSources = unlock(
+ useSelect( blocksStore )
+ ).getAllBlockBindingsSources();
+
+ return (
+ <>
+ { Object.entries( bindings ).map(
+ ( [ attrName, boundAttribute ] ) => {
+ // Bail early if the block doesn't have a valid source handler.
+ const source =
+ blockBindingsSources[ boundAttribute.source ];
+ if ( ! source?.useSource ) {
+ return null;
+ }
+
+ return (
+
+ );
+ }
+ ) }
+ >
+ );
+}
+
+const withBlockBindingSupport = createHigherOrderComponent(
+ ( BlockEdit ) => ( props ) => {
+ /*
+ * Collect and update the bound attributes
+ * in a separate state.
+ */
+ const [ boundAttributes, setBoundAttributes ] = useState( {} );
+ const updateBoundAttributes = useCallback(
+ ( newAttributes ) =>
+ setBoundAttributes( ( prev ) => ( {
+ ...prev,
+ ...newAttributes,
+ } ) ),
+ []
+ );
+
+ /*
+ * Create binding object filtering
+ * only the attributes that can be bound.
+ */
+ const bindings = Object.fromEntries(
+ Object.entries( props.attributes.metadata?.bindings || {} ).filter(
+ ( [ attrName ] ) => canBindAttribute( props.name, attrName )
+ )
+ );
+
+ return (
+ <>
+ { Object.keys( bindings ).length > 0 && (
+
+ ) }
+
- );
- },
- 'useBoundAttributes'
- );
+ >
+ );
+ },
+ 'withBlockBindingSupport'
+);
/**
* Filters a registered block's settings to enhance a block's `edit` component
* to upgrade bound attributes.
*
- * @param {WPBlockSettings} settings Registered block settings.
- *
+ * @param {WPBlockSettings} settings - Registered block settings.
+ * @param {string} name - Block name.
* @return {WPBlockSettings} Filtered block settings.
*/
-function shimAttributeSource( settings ) {
- if ( ! ( settings.name in BLOCK_BINDINGS_ALLOWED_BLOCKS ) ) {
+function shimAttributeSource( settings, name ) {
+ if ( ! canBindBlock( name ) ) {
return settings;
}
- settings.edit = createEditFunctionWithBindingsAttribute()( settings.edit );
- return settings;
+ return {
+ ...settings,
+ edit: withBlockBindingSupport( settings.edit ),
+ };
}
addFilter(
diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js
index ec6843ead2489..6862d2a542457 100644
--- a/packages/block-editor/src/private-apis.js
+++ b/packages/block-editor/src/private-apis.js
@@ -27,6 +27,7 @@ import { ExperimentalBlockCanvas } from './components/block-canvas';
import { getDuotoneFilter } from './components/duotone/utils';
import { useFlashEditableBlocks } from './components/use-flash-editable-blocks';
import { selectBlockPatternsKey } from './store/private-keys';
+import { requiresWrapperOnCopy } from './components/writing-flow/utils';
import { PrivateRichText } from './components/rich-text/';
/**
@@ -59,5 +60,6 @@ lock( privateApis, {
usesContextKey,
useFlashEditableBlocks,
selectBlockPatternsKey,
+ requiresWrapperOnCopy,
PrivateRichText,
} );
diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js
index 6adbafe28341c..4edf075e712ac 100644
--- a/packages/block-editor/src/store/actions.js
+++ b/packages/block-editor/src/store/actions.js
@@ -1575,7 +1575,7 @@ export const duplicateBlocks =
};
/**
- * Action that inserts an empty block before a given block.
+ * Action that inserts a default block before a given block.
*
* @param {string} clientId
*/
@@ -1591,16 +1591,34 @@ export const insertBeforeBlock =
return;
}
- const firstSelectedIndex = select.getBlockIndex( clientId );
- return dispatch.insertDefaultBlock(
- {},
- rootClientId,
- firstSelectedIndex
- );
+ const blockIndex = select.getBlockIndex( clientId );
+ const directInsertBlock = rootClientId
+ ? select.getDirectInsertBlock( rootClientId )
+ : null;
+
+ if ( ! directInsertBlock ) {
+ return dispatch.insertDefaultBlock( {}, rootClientId, blockIndex );
+ }
+
+ const copiedAttributes = {};
+ if ( directInsertBlock.attributesToCopy ) {
+ const attributes = select.getBlockAttributes( clientId );
+ directInsertBlock.attributesToCopy.forEach( ( key ) => {
+ if ( attributes[ key ] ) {
+ copiedAttributes[ key ] = attributes[ key ];
+ }
+ } );
+ }
+
+ const block = createBlock( directInsertBlock.name, {
+ ...directInsertBlock.attributes,
+ ...copiedAttributes,
+ } );
+ return dispatch.insertBlock( block, blockIndex, rootClientId );
};
/**
- * Action that inserts an empty block after a given block.
+ * Action that inserts a default block after a given block.
*
* @param {string} clientId
*/
@@ -1616,12 +1634,34 @@ export const insertAfterBlock =
return;
}
- const firstSelectedIndex = select.getBlockIndex( clientId );
- return dispatch.insertDefaultBlock(
- {},
- rootClientId,
- firstSelectedIndex + 1
- );
+ const blockIndex = select.getBlockIndex( clientId );
+ const directInsertBlock = rootClientId
+ ? select.getDirectInsertBlock( rootClientId )
+ : null;
+
+ if ( ! directInsertBlock ) {
+ return dispatch.insertDefaultBlock(
+ {},
+ rootClientId,
+ blockIndex + 1
+ );
+ }
+
+ const copiedAttributes = {};
+ if ( directInsertBlock.attributesToCopy ) {
+ const attributes = select.getBlockAttributes( clientId );
+ directInsertBlock.attributesToCopy.forEach( ( key ) => {
+ if ( attributes[ key ] ) {
+ copiedAttributes[ key ] = attributes[ key ];
+ }
+ } );
+ }
+
+ const block = createBlock( directInsertBlock.name, {
+ ...directInsertBlock.attributes,
+ ...copiedAttributes,
+ } );
+ return dispatch.insertBlock( block, blockIndex + 1, rootClientId );
};
/**
diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss
index 43fb047710b8d..bb10ace5ba5b4 100644
--- a/packages/block-editor/src/style.scss
+++ b/packages/block-editor/src/style.scss
@@ -1,5 +1,6 @@
@import "./autocompleters/style.scss";
@import "./components/block-alignment-control/style.scss";
+@import "./components/block-bindings-toolbar-indicator/style.scss";
@import "./components/block-canvas/style.scss";
@import "./components/block-icon/style.scss";
@import "./components/block-inspector/style.scss";
diff --git a/packages/block-library/src/block/deprecated.js b/packages/block-library/src/block/deprecated.js
index 7bc243bbf4ce9..f820867fff627 100644
--- a/packages/block-library/src/block/deprecated.js
+++ b/packages/block-library/src/block/deprecated.js
@@ -1,4 +1,75 @@
-// v1: Migrate and rename the `overrides` attribute to the `content` attribute.
+const isObject = ( obj ) =>
+ typeof obj === 'object' && ! Array.isArray( obj ) && obj !== null;
+
+// v2: Migrate to a more condensed version of the 'content' attribute attribute.
+const v2 = {
+ attributes: {
+ ref: {
+ type: 'number',
+ },
+ content: {
+ type: 'object',
+ },
+ },
+ supports: {
+ customClassName: false,
+ html: false,
+ inserter: false,
+ renaming: false,
+ },
+ // Force this deprecation to run whenever there's a values sub-property that's an object.
+ //
+ // This could fail in the future if a block ever has binding to a `values` attribute.
+ // Some extra protection is added to ensure `values` is an object, but this only reduces
+ // the likelihood, it doesn't solve it completely.
+ isEligible( { content } ) {
+ return (
+ !! content &&
+ Object.keys( content ).every(
+ ( contentKey ) =>
+ content[ contentKey ].values &&
+ isObject( content[ contentKey ].values )
+ )
+ );
+ },
+ /*
+ * Old attribute format:
+ * content: {
+ * "V98q_x": {
+ * // The attribute values are now stored as a 'values' sub-property.
+ * values: { content: 'My content value' },
+ * // ... additional metadata, like the block name can be stored here.
+ * }
+ * }
+ *
+ * New attribute format:
+ * content: {
+ * "V98q_x": {
+ * content: 'My content value',
+ * }
+ * }
+ */
+ migrate( attributes ) {
+ const { content, ...retainedAttributes } = attributes;
+
+ if ( content && Object.keys( content ).length ) {
+ const updatedContent = { ...content };
+
+ for ( const contentKey in content ) {
+ updatedContent[ contentKey ] = content[ contentKey ].values;
+ }
+
+ return {
+ ...retainedAttributes,
+ content: updatedContent,
+ };
+ }
+
+ return attributes;
+ },
+};
+
+// v1: Rename the `overrides` attribute to the `content` attribute.
const v1 = {
attributes: {
ref: {
@@ -23,16 +94,12 @@ const v1 = {
* overrides: {
* // An key is an id that represents a block.
* // The values are the attribute values of the block.
- * "V98q_x": { content: 'dwefwefwefwe' }
+ * "V98q_x": { content: 'My content value' }
* }
*
* New attribute format:
* content: {
- * "V98q_x": {
- * // The attribute values are now stored as a 'values' sub-property.
- * values: { content: 'dwefwefwefwe' },
- * // ... additional metadata, like the block name can be stored here.
- * }
+ * "V98q_x": { content: 'My content value' }
* }
*
*/
@@ -42,9 +109,7 @@ const v1 = {
const content = {};
Object.keys( overrides ).forEach( ( id ) => {
- content[ id ] = {
- values: overrides[ id ],
- };
+ content[ id ] = overrides[ id ];
} );
return {
@@ -54,4 +119,4 @@ const v1 = {
},
};
-export default [ v1 ];
+export default [ v2, v1 ];
diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js
index 5efe245c935fc..ddacc47dbd039 100644
--- a/packages/block-library/src/block/edit.js
+++ b/packages/block-library/src/block/edit.js
@@ -42,6 +42,25 @@ const { PARTIAL_SYNCING_SUPPORTED_BLOCKS } = unlock( patternsPrivateApis );
const fullAlignments = [ 'full', 'wide', 'left', 'right' ];
+function getLegacyIdMap( blocks, content, nameCount = {} ) {
+ let idToClientIdMap = {};
+ for ( const block of blocks ) {
+ if ( block?.innerBlocks?.length ) {
+ idToClientIdMap = {
+ ...idToClientIdMap,
+ ...getLegacyIdMap( block.innerBlocks, content, nameCount ),
+ };
+ }
+
+ const id = block.attributes.metadata?.id;
+ const clientId = block.clientId;
+ if ( id && content?.[ id ] ) {
+ idToClientIdMap[ clientId ] = id;
+ }
+ }
+ return idToClientIdMap;
+}
+
const useInferredLayout = ( blocks, parentLayout ) => {
const initialInferredAlignmentRef = useRef();
@@ -101,25 +120,31 @@ function getOverridableAttributes( block ) {
function applyInitialContentValuesToInnerBlocks(
blocks,
content = {},
- defaultValues
+ defaultValues,
+ legacyIdMap
) {
return blocks.map( ( block ) => {
const innerBlocks = applyInitialContentValuesToInnerBlocks(
block.innerBlocks,
content,
- defaultValues
+ defaultValues,
+ legacyIdMap
);
- const blockId = block.attributes.metadata?.id;
- if ( ! hasOverridableAttributes( block ) || ! blockId )
+ const metadataName =
+ legacyIdMap?.[ block.clientId ] ?? block.attributes.metadata?.name;
+
+ if ( ! metadataName || ! hasOverridableAttributes( block ) ) {
return { ...block, innerBlocks };
+ }
+
const attributes = getOverridableAttributes( block );
const newAttributes = { ...block.attributes };
for ( const attributeKey of attributes ) {
- defaultValues[ blockId ] ??= {};
- defaultValues[ blockId ][ attributeKey ] =
+ defaultValues[ metadataName ] ??= {};
+ defaultValues[ metadataName ][ attributeKey ] =
block.attributes[ attributeKey ];
- const contentValues = content[ blockId ]?.values;
+ const contentValues = content[ metadataName ];
if ( contentValues?.[ attributeKey ] !== undefined ) {
newAttributes[ attributeKey ] = contentValues[ attributeKey ];
}
@@ -142,29 +167,40 @@ function isAttributeEqual( attribute1, attribute2 ) {
return attribute1 === attribute2;
}
-function getContentValuesFromInnerBlocks( blocks, defaultValues ) {
+function getContentValuesFromInnerBlocks( blocks, defaultValues, legacyIdMap ) {
/** @type {Record}>} */
const content = {};
for ( const block of blocks ) {
if ( block.name === patternBlockName ) continue;
- Object.assign(
- content,
- getContentValuesFromInnerBlocks( block.innerBlocks, defaultValues )
- );
- const blockId = block.attributes.metadata?.id;
- if ( ! hasOverridableAttributes( block ) || ! blockId ) continue;
+ if ( block.innerBlocks.length ) {
+ Object.assign(
+ content,
+ getContentValuesFromInnerBlocks(
+ block.innerBlocks,
+ defaultValues,
+ legacyIdMap
+ )
+ );
+ }
+ const metadataName =
+ legacyIdMap?.[ block.clientId ] ?? block.attributes.metadata?.name;
+ if ( ! metadataName || ! hasOverridableAttributes( block ) ) {
+ continue;
+ }
+
const attributes = getOverridableAttributes( block );
+
for ( const attributeKey of attributes ) {
if (
! isAttributeEqual(
block.attributes[ attributeKey ],
- defaultValues[ blockId ][ attributeKey ]
+ defaultValues?.[ metadataName ]?.[ attributeKey ]
)
) {
- content[ blockId ] ??= { values: {}, blockName: block.name };
+ content[ metadataName ] ??= {};
// TODO: We need a way to represent `undefined` in the serialized overrides.
// Also see: https://github.com/WordPress/gutenberg/pull/57249#discussion_r1452987871
- content[ blockId ].values[ attributeKey ] =
+ content[ metadataName ][ attributeKey ] =
block.attributes[ attributeKey ] === undefined
? // TODO: We use an empty string to represent undefined for now until
// we support a richer format for overrides and the block binding API.
@@ -278,8 +314,15 @@ export default function ReusableBlockEdit( {
[ editedRecord.blocks, editedRecord.content ]
);
+ const legacyIdMap = useRef( {} );
+
// Apply the initial overrides from the pattern block to the inner blocks.
useEffect( () => {
+ // Build a map of clientIds to the old nano id system to provide back compat.
+ legacyIdMap.current = getLegacyIdMap(
+ initialBlocks,
+ initialContent.current
+ );
defaultContent.current = {};
const originalEditingMode = getBlockEditingMode( patternClientId );
// Replace the contents of the blocks with the overrides.
@@ -291,7 +334,8 @@ export default function ReusableBlockEdit( {
applyInitialContentValuesToInnerBlocks(
initialBlocks,
initialContent.current,
- defaultContent.current
+ defaultContent.current,
+ legacyIdMap.current
)
);
} );
@@ -343,7 +387,8 @@ export default function ReusableBlockEdit( {
setAttributes( {
content: getContentValuesFromInnerBlocks(
blocks,
- defaultContent.current
+ defaultContent.current,
+ legacyIdMap.current
),
} );
} );
diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php
index 8e24317501d9f..4886373b5885a 100644
--- a/packages/block-library/src/block/index.php
+++ b/packages/block-library/src/block/index.php
@@ -46,26 +46,35 @@ function render_block_core_block( $attributes ) {
$content = $wp_embed->run_shortcode( $reusable_block->post_content );
$content = $wp_embed->autoembed( $content );
- // Back compat, the content attribute was previously named overrides and
- // had a slightly different format. For blocks that have not been migrated,
- // also convert the format here so that the provided `pattern/overrides`
- // context is correct.
- if ( isset( $attributes['overrides'] ) && ! isset( $attributes['content'] ) ) {
- $migrated_content = array();
- foreach ( $attributes['overrides'] as $id => $values ) {
- $migrated_content[ $id ] = array(
- 'values' => $values,
- );
+ // Back compat.
+ // For blocks that have not been migrated in the editor, add some back compat
+ // so that front-end rendering continues to work.
+
+ // This matches the `v2` deprecation. Removes the inner `values` property
+ // from every item.
+ if ( isset( $attributes['content'] ) ) {
+ foreach ( $attributes['content'] as &$content_data ) {
+ if ( isset( $content_data['values'] ) ) {
+ $is_assoc_array = is_array( $content_data['values'] ) && ! wp_is_numeric_array( $content_data['values'] );
+
+ if ( $is_assoc_array ) {
+ $content_data = $content_data['values'];
+ }
+ }
}
- $attributes['content'] = $migrated_content;
}
- $has_pattern_overrides = isset( $attributes['content'] );
+
+ // This matches the `v1` deprecation. Rename `overrides` to `content`.
+ if ( isset( $attributes['overrides'] ) && ! isset( $attributes['content'] ) ) {
+ $attributes['content'] = $attributes['overrides'];
+ }
/**
* We set the `pattern/overrides` context through the `render_block_context`
* filter so that it is available when a pattern's inner blocks are
* rendering via do_blocks given it only receives the inner content.
*/
+ $has_pattern_overrides = isset( $attributes['content'] );
if ( $has_pattern_overrides ) {
$filter_block_context = static function ( $context ) use ( $attributes ) {
$context['pattern/overrides'] = $attributes['content'];
diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js
index e1e221bc9575a..489343e1dfef7 100644
--- a/packages/block-library/src/image/edit.js
+++ b/packages/block-library/src/image/edit.js
@@ -20,7 +20,7 @@ import {
useBlockEditingMode,
} from '@wordpress/block-editor';
import { useEffect, useRef, useState } from '@wordpress/element';
-import { __ } from '@wordpress/i18n';
+import { __, sprintf } from '@wordpress/i18n';
import { image as icon, plugins as pluginsIcon } from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';
@@ -336,7 +336,7 @@ export function ImageEdit( {
} );
// Much of this description is duplicated from MediaPlaceholder.
- const { lockUrlControls = false } = useSelect(
+ const { lockUrlControls = false, lockUrlControlsMessage } = useSelect(
( select ) => {
if ( ! isSingleSelected ) {
return {};
@@ -351,6 +351,13 @@ export function ImageEdit( {
!! metadata?.bindings?.url &&
( ! blockBindingsSource ||
blockBindingsSource?.lockAttributesEditing ),
+ lockUrlControlsMessage: blockBindingsSource?.label
+ ? sprintf(
+ /* translators: %s: Label of the bindings source. */
+ __( 'Connected to %s' ),
+ blockBindingsSource.label
+ )
+ : __( 'Connected to dynamic data' ),
};
},
[ isSingleSelected ]
@@ -387,7 +394,7 @@ export function ImageEdit( {
- { __( 'Connected to a custom field' ) }
+ { lockUrlControlsMessage }
) : (
content
diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js
index ea0f82a2e1986..e55f6bfa03d73 100644
--- a/packages/block-library/src/image/image.js
+++ b/packages/block-library/src/image/image.js
@@ -410,7 +410,9 @@ export default function Image( {
lockUrlControls = false,
lockHrefControls = false,
lockAltControls = false,
+ lockAltControlsMessage,
lockTitleControls = false,
+ lockTitleControlsMessage,
lockCaption = false,
} = useSelect(
( select ) => {
@@ -454,10 +456,24 @@ export default function Image( {
!! altBinding &&
( ! altBindingSource ||
altBindingSource?.lockAttributesEditing ),
+ lockAltControlsMessage: altBindingSource?.label
+ ? sprintf(
+ /* translators: %s: Label of the bindings source. */
+ __( 'Connected to %s' ),
+ altBindingSource.label
+ )
+ : __( 'Connected to dynamic data' ),
lockTitleControls:
!! titleBinding &&
( ! titleBindingSource ||
titleBindingSource?.lockAttributesEditing ),
+ lockTitleControlsMessage: titleBindingSource?.label
+ ? sprintf(
+ /* translators: %s: Label of the bindings source. */
+ __( 'Connected to %s' ),
+ titleBindingSource.label
+ )
+ : __( 'Connected to dynamic data' ),
};
},
[ clientId, isSingleSelected, metadata?.bindings ]
@@ -557,11 +573,7 @@ export default function Image( {
disabled={ lockAltControls }
help={
lockAltControls ? (
- <>
- { __(
- 'Connected to a custom field'
- ) }
- >
+ <>{ lockAltControlsMessage }>
) : (
<>
@@ -607,11 +619,7 @@ export default function Image( {
disabled={ lockTitleControls }
help={
lockTitleControls ? (
- <>
- { __(
- 'Connected to a custom field'
- ) }
- >
+ <>{ lockTitleControlsMessage }>
) : (
<>
{ __(
@@ -652,11 +660,7 @@ export default function Image( {
readOnly={ lockAltControls }
help={
lockAltControls ? (
- <>
- { __(
- 'Connected to a custom field'
- ) }
- >
+ <>{ lockAltControlsMessage }>
) : (
<>
@@ -694,7 +698,7 @@ export default function Image( {
readOnly={ lockTitleControls }
help={
lockTitleControls ? (
- <>{ __( 'Connected to a custom field' ) }>
+ <>{ lockTitleControlsMessage }>
) : (
<>
{ __(
diff --git a/packages/block-library/src/list-item/edit.js b/packages/block-library/src/list-item/edit.js
index 46cbd3a94831d..467154f76992e 100644
--- a/packages/block-library/src/list-item/edit.js
+++ b/packages/block-library/src/list-item/edit.js
@@ -29,7 +29,6 @@ import {
useOutdentListItem,
useSplit,
useMerge,
- useCopy,
} from './hooks';
import { convertToListItems } from './utils';
@@ -79,7 +78,7 @@ export default function ListItemEdit( {
mergeBlocks,
} ) {
const { placeholder, content } = attributes;
- const blockProps = useBlockProps( { ref: useCopy( clientId ) } );
+ const blockProps = useBlockProps();
const innerBlocksProps = useInnerBlocksProps( blockProps, {
renderAppender: false,
__unstableDisableDropZone: true,
diff --git a/packages/block-library/src/list-item/hooks/index.js b/packages/block-library/src/list-item/hooks/index.js
index 3bbc3167abed3..1687adbe740d0 100644
--- a/packages/block-library/src/list-item/hooks/index.js
+++ b/packages/block-library/src/list-item/hooks/index.js
@@ -4,4 +4,3 @@ export { default as useEnter } from './use-enter';
export { default as useSpace } from './use-space';
export { default as useSplit } from './use-split';
export { default as useMerge } from './use-merge';
-export { default as useCopy } from './use-copy';
diff --git a/packages/block-library/src/list-item/hooks/use-copy.js b/packages/block-library/src/list-item/hooks/use-copy.js
deleted file mode 100644
index 7a76019ad11a4..0000000000000
--- a/packages/block-library/src/list-item/hooks/use-copy.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { useRefEffect } from '@wordpress/compose';
-import { store as blockEditorStore } from '@wordpress/block-editor';
-import { useSelect } from '@wordpress/data';
-
-export default function useCopy( clientId ) {
- const { getBlockRootClientId, getBlockName, getBlockAttributes } =
- useSelect( blockEditorStore );
-
- return useRefEffect( ( node ) => {
- function onCopy( event ) {
- // The event propagates through all nested lists, so don't override
- // when copying nested list items.
- if ( event.clipboardData.getData( '__unstableWrapperBlockName' ) ) {
- return;
- }
-
- const rootClientId = getBlockRootClientId( clientId );
- event.clipboardData.setData(
- '__unstableWrapperBlockName',
- getBlockName( rootClientId )
- );
- event.clipboardData.setData(
- '__unstableWrapperBlockAttributes',
- JSON.stringify( getBlockAttributes( rootClientId ) )
- );
- }
-
- node.addEventListener( 'copy', onCopy );
- node.addEventListener( 'cut', onCopy );
- return () => {
- node.removeEventListener( 'copy', onCopy );
- node.removeEventListener( 'cut', onCopy );
- };
- }, [] );
-}
diff --git a/packages/block-library/src/list-item/index.js b/packages/block-library/src/list-item/index.js
index 00adc1c2c4026..07c5bb7fda901 100644
--- a/packages/block-library/src/list-item/index.js
+++ b/packages/block-library/src/list-item/index.js
@@ -2,6 +2,7 @@
* WordPress dependencies
*/
import { listItem as icon } from '@wordpress/icons';
+import { privateApis } from '@wordpress/block-editor';
/**
* Internal dependencies
@@ -11,6 +12,7 @@ import metadata from './block.json';
import edit from './edit';
import save from './save';
import transforms from './transforms';
+import { unlock } from '../lock-unlock';
const { name } = metadata;
@@ -27,6 +29,7 @@ export const settings = {
};
},
transforms,
+ [ unlock( privateApis ).requiresWrapperOnCopy ]: true,
};
export const init = () => initBlock( { name, metadata, settings } );
diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php
index 3a3a654aee612..1d73d09bbd1fb 100644
--- a/packages/block-library/src/navigation/index.php
+++ b/packages/block-library/src/navigation/index.php
@@ -552,7 +552,7 @@ private static function get_nav_element_directives( $is_interactive ) {
return '';
}
// When adding to this array be mindful of security concerns.
- $nav_element_context = data_wp_context(
+ $nav_element_context = wp_interactivity_data_wp_context(
array(
'overlayOpenedBy' => array(
'click' => false,
diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php
index c368c2ab03dbf..ca8c70edfa907 100644
--- a/packages/block-library/src/search/index.php
+++ b/packages/block-library/src/search/index.php
@@ -179,7 +179,7 @@ function render_block_core_search( $attributes ) {
if ( $is_expandable_searchfield ) {
$aria_label_expanded = __( 'Submit Search' );
$aria_label_collapsed = __( 'Expand search field' );
- $form_context = data_wp_context(
+ $form_context = wp_interactivity_data_wp_context(
array(
'isSearchInputVisible' => $open_by_default,
'inputId' => $input_id,
diff --git a/packages/block-library/src/site-logo/edit.js b/packages/block-library/src/site-logo/edit.js
index 0c146400cd74f..079811f0aae95 100644
--- a/packages/block-library/src/site-logo/edit.js
+++ b/packages/block-library/src/site-logo/edit.js
@@ -268,6 +268,14 @@ const SiteLogo = ( {
);
+ // Support the previous location for the Site Icon settings. To be removed
+ // when the required WP core version for Gutenberg is >= 6.5.0.
+ const shouldUseNewUrl = ! window?.__experimentalUseCustomizerSiteLogoUrl;
+
+ const siteIconSettingsUrl = shouldUseNewUrl
+ ? siteUrl + '/wp-admin/options-general.php'
+ : siteUrl + '/wp-admin/customize.php?autofocus[section]=title_tagline';
+
const syncSiteIconHelpText = createInterpolateElement(
__(
'Site Icons are what you see in browser tabs, bookmark bars, and within the WordPress mobile apps. To use a custom icon that is different from your site logo, use the Site Icon settings.'
@@ -276,10 +284,7 @@ const SiteLogo = ( {
a: (
// eslint-disable-next-line jsx-a11y/anchor-has-content
@@ -331,7 +336,7 @@ const SiteLogo = ( {
<>
{
setAttributes( { shouldSyncIcon: value } );
setIcon( value ? logoId : undefined );
diff --git a/packages/dataviews/src/single-selection-checkbox.js b/packages/dataviews/src/single-selection-checkbox.js
index ce21419f969ce..f55fc58b908d8 100644
--- a/packages/dataviews/src/single-selection-checkbox.js
+++ b/packages/dataviews/src/single-selection-checkbox.js
@@ -32,10 +32,14 @@ export default function SingleSelectionCheckbox( {
{
+ if ( disabled ) {
+ return;
+ }
+
if ( ! isSelected ) {
onSelectionChange(
data.filter( ( _item ) => {
diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss
index 10e3b511cc58a..95e685f3cb905 100644
--- a/packages/dataviews/src/style.scss
+++ b/packages/dataviews/src/style.scss
@@ -219,7 +219,6 @@
color: $gray-900;
text-overflow: ellipsis;
white-space: nowrap;
- overflow: hidden;
display: block;
width: 100%;
@@ -235,6 +234,7 @@
&:hover {
color: $gray-900;
}
+ @include link-reset();
}
button.components-button.is-link {
diff --git a/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts b/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts
index 3b187625fd47c..11bc11c43f603 100644
--- a/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts
+++ b/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts
@@ -55,18 +55,26 @@ async function emulateClipboard( page: Page, type: 'copy' | 'cut' | 'paste' ) {
const canvasDoc =
// @ts-ignore
document.activeElement?.contentDocument ?? document;
- const clipboardDataTransfer = new DataTransfer();
+ const event = new ClipboardEvent( _type, {
+ bubbles: true,
+ cancelable: true,
+ clipboardData: new DataTransfer(),
+ } );
+
+ if ( ! event.clipboardData ) {
+ throw new Error( 'ClipboardEvent.clipboardData is null' );
+ }
if ( _type === 'paste' ) {
- clipboardDataTransfer.setData(
+ event.clipboardData.setData(
'text/plain',
_clipboardData[ 'text/plain' ]
);
- clipboardDataTransfer.setData(
+ event.clipboardData.setData(
'text/html',
_clipboardData[ 'text/html' ]
);
- clipboardDataTransfer.setData(
+ event.clipboardData.setData(
'rich-text',
_clipboardData[ 'rich-text' ]
);
@@ -85,22 +93,16 @@ async function emulateClipboard( page: Page, type: 'copy' | 'cut' | 'paste' ) {
)
.join( '' );
}
- clipboardDataTransfer.setData( 'text/plain', plainText );
- clipboardDataTransfer.setData( 'text/html', html );
+ event.clipboardData.setData( 'text/plain', plainText );
+ event.clipboardData.setData( 'text/html', html );
}
- canvasDoc.activeElement?.dispatchEvent(
- new ClipboardEvent( _type, {
- bubbles: true,
- cancelable: true,
- clipboardData: clipboardDataTransfer,
- } )
- );
+ canvasDoc.activeElement.dispatchEvent( event );
return {
- 'text/plain': clipboardDataTransfer.getData( 'text/plain' ),
- 'text/html': clipboardDataTransfer.getData( 'text/html' ),
- 'rich-text': clipboardDataTransfer.getData( 'rich-text' ),
+ 'text/plain': event.clipboardData.getData( 'text/plain' ),
+ 'text/html': event.clipboardData.getData( 'text/html' ),
+ 'rich-text': event.clipboardData.getData( 'rich-text' ),
};
},
[ type, clipboardDataHolder ] as const
diff --git a/packages/e2e-tests/plugins/block-bindings.php b/packages/e2e-tests/plugins/block-bindings.php
index c686b40006a06..74aec2adb500f 100644
--- a/packages/e2e-tests/plugins/block-bindings.php
+++ b/packages/e2e-tests/plugins/block-bindings.php
@@ -21,7 +21,6 @@ function gutenberg_test_block_bindings_register_custom_fields() {
'default' => 'Value of the text_custom_field',
)
);
- // TODO: Change url.
register_meta(
'post',
'url_custom_field',
@@ -32,5 +31,24 @@ function gutenberg_test_block_bindings_register_custom_fields() {
'default' => '#url-custom-field',
)
);
+ register_meta(
+ 'post',
+ '_protected_field',
+ array(
+ 'type' => 'string',
+ 'single' => true,
+ 'default' => 'protected field value',
+ )
+ );
+ register_meta(
+ 'post',
+ 'show_in_rest_false_field',
+ array(
+ 'show_in_rest' => false,
+ 'type' => 'string',
+ 'single' => true,
+ 'default' => 'show_in_rest false field value',
+ )
+ );
}
add_action( 'init', 'gutenberg_test_block_bindings_register_custom_fields' );
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js
index 35276b0ad8b2b..2f46e0cdec7e0 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js
@@ -162,16 +162,6 @@ function FontLibraryProvider( { children } ) {
// Demo
const [ loadedFontUrls ] = useState( new Set() );
- // Theme data
- const { site, currentTheme } = useSelect( ( select ) => {
- return {
- site: select( coreStore ).getSite(),
- currentTheme: select( coreStore ).getCurrentTheme(),
- };
- } );
- const themeUrl =
- site?.url + '/wp-content/themes/' + currentTheme?.stylesheet;
-
const getAvailableFontsOutline = ( availableFontFamilies ) => {
const outline = availableFontFamilies.reduce( ( acc, font ) => {
const availableFontFaces =
@@ -210,113 +200,115 @@ function FontLibraryProvider( { children } ) {
return getActivatedFontsOutline( source )[ slug ] || [];
};
- async function installFont( fontFamilyToInstall ) {
+ async function installFonts( fontFamiliesToInstall ) {
setIsInstalling( true );
try {
- // Get the font family if it already exists.
- let installedFontFamily = await fetchGetFontFamilyBySlug(
- fontFamilyToInstall.slug
- );
+ const fontFamiliesToActivate = [];
+ let installationErrors = [];
+
+ for ( const fontFamilyToInstall of fontFamiliesToInstall ) {
+ let isANewFontFamily = false;
- // Otherwise create it.
- if ( ! installedFontFamily ) {
- // Prepare font family form data to install.
- installedFontFamily = await fetchInstallFontFamily(
- makeFontFamilyFormData( fontFamilyToInstall )
+ // Get the font family if it already exists.
+ let installedFontFamily = await fetchGetFontFamilyBySlug(
+ fontFamilyToInstall.slug
);
- }
- // Collect font faces that have already been installed (to be activated later)
- const alreadyInstalledFontFaces =
- installedFontFamily.fontFace && fontFamilyToInstall.fontFace
- ? installedFontFamily.fontFace.filter(
+ // Otherwise create it.
+ if ( ! installedFontFamily ) {
+ isANewFontFamily = true;
+ // Prepare font family form data to install.
+ installedFontFamily = await fetchInstallFontFamily(
+ makeFontFamilyFormData( fontFamilyToInstall )
+ );
+ }
+
+ // Collect font faces that have already been installed (to be activated later)
+ const alreadyInstalledFontFaces =
+ installedFontFamily.fontFace && fontFamilyToInstall.fontFace
+ ? installedFontFamily.fontFace.filter(
+ ( fontFaceToInstall ) =>
+ checkFontFaceInstalled(
+ fontFaceToInstall,
+ fontFamilyToInstall.fontFace
+ )
+ )
+ : [];
+
+ // Filter out Font Faces that have already been installed (so that they are not re-installed)
+ if (
+ installedFontFamily.fontFace &&
+ fontFamilyToInstall.fontFace
+ ) {
+ fontFamilyToInstall.fontFace =
+ fontFamilyToInstall.fontFace.filter(
( fontFaceToInstall ) =>
- checkFontFaceInstalled(
+ ! checkFontFaceInstalled(
fontFaceToInstall,
- fontFamilyToInstall.fontFace
+ installedFontFamily.fontFace
)
- )
- : [];
-
- // Filter out Font Faces that have already been installed (so that they are not re-installed)
- if (
- installedFontFamily.fontFace &&
- fontFamilyToInstall.fontFace
- ) {
- fontFamilyToInstall.fontFace =
- fontFamilyToInstall.fontFace.filter(
- ( fontFaceToInstall ) =>
- ! checkFontFaceInstalled(
- fontFaceToInstall,
- installedFontFamily.fontFace
- )
+ );
+ }
+
+ // Install the fonts (upload the font files to the server and create the post in the database).
+ let sucessfullyInstalledFontFaces = [];
+ let unsucessfullyInstalledFontFaces = [];
+ if ( fontFamilyToInstall?.fontFace?.length > 0 ) {
+ const response = await batchInstallFontFaces(
+ installedFontFamily.id,
+ makeFontFacesFormData( fontFamilyToInstall )
);
- }
-
- // Install the fonts (upload the font files to the server and create the post in the database).
- let sucessfullyInstalledFontFaces = [];
- let unsucessfullyInstalledFontFaces = [];
- if ( fontFamilyToInstall?.fontFace?.length > 0 ) {
- const response = await batchInstallFontFaces(
- installedFontFamily.id,
- makeFontFacesFormData( fontFamilyToInstall )
+ sucessfullyInstalledFontFaces = response?.successes;
+ unsucessfullyInstalledFontFaces = response?.errors;
+ }
+
+ // Use the sucessfully installed font faces
+ // As well as any font faces that were already installed (those will be activated)
+ if (
+ sucessfullyInstalledFontFaces?.length > 0 ||
+ alreadyInstalledFontFaces?.length > 0
+ ) {
+ fontFamilyToInstall.fontFace = [
+ ...sucessfullyInstalledFontFaces,
+ ...alreadyInstalledFontFaces,
+ ];
+ fontFamiliesToActivate.push( fontFamilyToInstall );
+ } else if ( isANewFontFamily ) {
+ // If the font family is new, delete it to avoid having font families without font faces.
+ await fetchUninstallFontFamily( installedFontFamily.id );
+ }
+
+ installationErrors = installationErrors.concat(
+ unsucessfullyInstalledFontFaces
);
- sucessfullyInstalledFontFaces = response?.successes;
- unsucessfullyInstalledFontFaces = response?.errors;
}
- const detailedErrorMessage = unsucessfullyInstalledFontFaces.reduce(
- ( errorMessageCollection, error ) => {
- return `${ errorMessageCollection } ${ error.message }`;
- },
- ''
- );
+ if ( fontFamiliesToActivate.length > 0 ) {
+ // Activate the font family (add the font family to the global styles).
+ activateCustomFontFamilies( fontFamiliesToActivate );
- // If there were no successes and nothing already installed then we don't need to activate anything and can bounce now.
- if (
- fontFamilyToInstall?.fontFace?.length > 0 &&
- sucessfullyInstalledFontFaces.length === 0 &&
- alreadyInstalledFontFaces.length === 0
- ) {
- throw new Error(
- sprintf(
- /* translators: %s: Specific error message returned from server. */
- __( 'No font faces were installed. %s' ),
- detailedErrorMessage
- )
+ // Save the global styles to the database.
+ await saveSpecifiedEntityEdits(
+ 'root',
+ 'globalStyles',
+ globalStylesId,
+ [ 'settings.typography.fontFamilies' ]
);
- }
- // Use the sucessfully installed font faces
- // As well as any font faces that were already installed (those will be activated)
- if (
- sucessfullyInstalledFontFaces?.length > 0 ||
- alreadyInstalledFontFaces?.length > 0
- ) {
- fontFamilyToInstall.fontFace = [
- ...sucessfullyInstalledFontFaces,
- ...alreadyInstalledFontFaces,
- ];
+ refreshLibrary();
}
- // Activate the font family (add the font family to the global styles).
- activateCustomFontFamilies( [ fontFamilyToInstall ] );
-
- // Save the global styles to the database.
- saveSpecifiedEntityEdits( 'root', 'globalStyles', globalStylesId, [
- 'settings.typography.fontFamilies',
- ] );
-
- refreshLibrary();
-
- if ( unsucessfullyInstalledFontFaces.length > 0 ) {
+ if ( installationErrors.length > 0 ) {
throw new Error(
sprintf(
/* translators: %s: Specific error message returned from server. */
- __(
- 'Some font faces were installed. There were some errors. %s'
- ),
- detailedErrorMessage
+ __( 'There were some errors installing fonts. %s' ),
+ installationErrors.reduce(
+ ( errorMessageCollection, error ) => {
+ return `${ errorMessageCollection } ${ error.message }`;
+ },
+ ''
+ )
)
);
}
@@ -375,14 +367,10 @@ function FontLibraryProvider( { children } ) {
const activateCustomFontFamilies = ( fontsToAdd ) => {
// Merge the existing custom fonts with the new fonts.
- const newCustomFonts = mergeFontFamilies(
- fontFamilies?.custom,
- fontsToAdd
- );
// Activate the fonts by set the new custom fonts array.
setFontFamilies( {
...fontFamilies,
- custom: newCustomFonts,
+ custom: mergeFontFamilies( fontFamilies?.custom, fontsToAdd ),
} );
// Add custom fonts to the browser.
fontsToAdd.forEach( ( font ) => {
@@ -416,7 +404,7 @@ function FontLibraryProvider( { children } ) {
// If the font doesn't have a src, don't load it.
if ( ! fontFace.src ) return;
// Get the src of the font.
- const src = getDisplaySrcFromFontFace( fontFace.src, themeUrl );
+ const src = getDisplaySrcFromFontFace( fontFace.src );
// If the font is already loaded, don't load it again.
if ( ! src || loadedFontUrls.has( src ) ) return;
// Load the font in the browser.
@@ -467,7 +455,7 @@ function FontLibraryProvider( { children } ) {
isFontActivated,
getFontFacesActivated,
loadFontFaceAsset,
- installFont,
+ installFonts,
uninstallFontFamily,
toggleActivateFont,
getAvailableFontsOutline,
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js
index 01f7a90357c8b..78c042f3fa405 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js
@@ -77,7 +77,7 @@ function FontCollection( { slug } ) {
const {
collections,
getFontCollection,
- installFont,
+ installFonts,
isInstalling,
notice,
setNotice,
@@ -214,7 +214,7 @@ function FontCollection( { slug } ) {
}
try {
- await installFont( fontFamily );
+ await installFonts( [ fontFamily ] );
setNotice( {
type: 'success',
message: __( 'Fonts were installed successfully.' ),
@@ -277,12 +277,14 @@ function FontCollection( { slug } ) {
>
-
- { selectedCollection.name }
-
+
+
+ { selectedCollection.name }
+
+ { selectedCollection.description }
+
- { selectedCollection.description }
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/index.js
index 71966449eb616..979dbece9fad2 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/index.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/index.js
@@ -6,6 +6,8 @@ import {
Modal,
privateApis as componentsPrivateApis,
} from '@wordpress/components';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
import { useContext } from '@wordpress/element';
/**
@@ -19,16 +21,15 @@ import { unlock } from '../../../lock-unlock';
const { Tabs } = unlock( componentsPrivateApis );
-const DEFAULT_TABS = [
- {
- id: 'installed-fonts',
- title: __( 'Library' ),
- },
- {
- id: 'upload-fonts',
- title: __( 'Upload' ),
- },
-];
+const DEFAULT_TAB = {
+ id: 'installed-fonts',
+ title: __( 'Library' ),
+};
+
+const UPLOAD_TAB = {
+ id: 'upload-fonts',
+ title: __( 'Upload' ),
+};
const tabsFromCollections = ( collections ) =>
collections.map( ( { slug, name } ) => ( {
@@ -44,11 +45,17 @@ function FontLibraryModal( {
initialTabId = 'installed-fonts',
} ) {
const { collections, setNotice } = useContext( FontLibraryContext );
+ const canUserCreate = useSelect( ( select ) => {
+ const { canUser } = select( coreStore );
+ return canUser( 'create', 'font-families' );
+ }, [] );
+
+ const tabs = [ DEFAULT_TAB ];
- const tabs = [
- ...DEFAULT_TABS,
- ...tabsFromCollections( collections || [] ),
- ];
+ if ( canUserCreate ) {
+ tabs.push( UPLOAD_TAB );
+ tabs.push( ...tabsFromCollections( collections || [] ) );
+ }
// Reset notice when new tab is selected.
const onSelect = () => {
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js
index 9425cb9c2d27b..226e9cf5a3eef 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js
@@ -18,6 +18,8 @@ import {
Spinner,
privateApis as componentsPrivateApis,
} from '@wordpress/components';
+import { store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
import { useContext, useEffect, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { chevronLeft } from '@wordpress/icons';
@@ -49,9 +51,24 @@ function InstalledFonts() {
setNotice,
} = useContext( FontLibraryContext );
const [ isConfirmDeleteOpen, setIsConfirmDeleteOpen ] = useState( false );
+ const customFontFamilyId =
+ libraryFontSelected?.source === 'custom' && libraryFontSelected?.id;
+
+ const canUserDelete = useSelect(
+ ( select ) => {
+ const { canUser } = select( coreStore );
+ return (
+ customFontFamilyId &&
+ canUser( 'delete', 'font-families', customFontFamilyId )
+ );
+ },
+ [ customFontFamilyId ]
+ );
const shouldDisplayDeleteButton =
- !! libraryFontSelected && libraryFontSelected?.source !== 'theme';
+ !! libraryFontSelected &&
+ libraryFontSelected?.source !== 'theme' &&
+ canUserDelete;
const handleUninstallClick = () => {
setIsConfirmDeleteOpen( true );
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/upload-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/upload-fonts.js
index 8b53ddcdbb38a..e7010355eed5c 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/upload-fonts.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/upload-fonts.js
@@ -28,7 +28,8 @@ import { unlock } from '../../../lock-unlock';
const { ProgressBar } = unlock( componentsPrivateApis );
function UploadFonts() {
- const { installFont, notice, setNotice } = useContext( FontLibraryContext );
+ const { installFonts, notice, setNotice } =
+ useContext( FontLibraryContext );
const [ isUploading, setIsUploading ] = useState( false );
const handleDropZone = ( files ) => {
@@ -143,19 +144,8 @@ function UploadFonts() {
const handleInstall = async ( fontFaces ) => {
const fontFamilies = makeFamiliesFromFaces( fontFaces );
- if ( fontFamilies.length > 1 ) {
- setNotice( {
- type: 'error',
- message: __(
- 'Variants from only one font family can be uploaded at a time.'
- ),
- } );
- setIsUploading( false );
- return;
- }
-
try {
- await installFont( fontFamilies[ 0 ] );
+ await installFonts( fontFamilies );
setNotice( {
type: 'success',
message: __( 'Fonts were installed successfully.' ),
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js
index 011f09b12a841..1458b47cd010a 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js
@@ -121,7 +121,13 @@ export async function loadFontFaceInBrowser( fontFace, source, addTo = 'all' ) {
}
}
-export function getDisplaySrcFromFontFace( input, urlPrefix ) {
+/**
+ * Retrieves the display source from a font face src.
+ *
+ * @param {string|string[]} input - The font face src.
+ * @return {string|undefined} The display source or undefined if the input is invalid.
+ */
+export function getDisplaySrcFromFontFace( input ) {
if ( ! input ) {
return;
}
@@ -132,9 +138,9 @@ export function getDisplaySrcFromFontFace( input, urlPrefix ) {
} else {
src = input;
}
- // If it is a theme font, we need to make the url absolute
- if ( src.startsWith( 'file:.' ) && urlPrefix ) {
- src = src.replace( 'file:.', urlPrefix );
+ // It's expected theme fonts will already be loaded in the browser.
+ if ( src.startsWith( 'file:.' ) ) {
+ return;
}
if ( ! isUrlEncoded( src ) ) {
src = encodeURI( src );
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getDisplaySrcFromFontFace.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getDisplaySrcFromFontFace.spec.js
index 9c6235443a099..3cbdc0283f1a9 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getDisplaySrcFromFontFace.spec.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getDisplaySrcFromFontFace.spec.js
@@ -21,33 +21,22 @@ describe( 'getDisplaySrcFromFontFace', () => {
);
} );
- it( 'makes URL absolute when it starts with file:. and urlPrefix is given', () => {
- const input = 'file:./font1';
- const urlPrefix = 'http://example.com';
- expect( getDisplaySrcFromFontFace( input, urlPrefix ) ).toBe(
- 'http://example.com/font1'
- );
- } );
-
- it( 'does not modify URL if it does not start with file:.', () => {
- const input = [ 'http://some-other-place.com/font1' ];
- const urlPrefix = 'http://example.com';
- expect( getDisplaySrcFromFontFace( input, urlPrefix ) ).toBe(
- 'http://some-other-place.com/font1'
- );
+ it( 'return undefined when the url starts with file:', () => {
+ const input = 'file:./theme/assets/font1.ttf';
+ expect( getDisplaySrcFromFontFace( input ) ).toBe( undefined );
} );
it( 'encodes the URL if it is not encoded', () => {
- const input = 'file:./assets/font one with spaces.ttf';
+ const input = 'https://example.org/font one with spaces.ttf';
expect( getDisplaySrcFromFontFace( input ) ).toBe(
- 'file:./assets/font%20one%20with%20spaces.ttf'
+ 'https://example.org/font%20one%20with%20spaces.ttf'
);
} );
it( 'does not encode the URL if it is already encoded', () => {
- const input = 'file:./font%20one';
+ const input = 'https://example.org/fonts/font%20one.ttf';
expect( getDisplaySrcFromFontFace( input ) ).toBe(
- 'file:./font%20one'
+ 'https://example.org/fonts/font%20one.ttf'
);
} );
} );
diff --git a/packages/edit-site/src/components/resizable-frame/index.js b/packages/edit-site/src/components/resizable-frame/index.js
index ee5c98da09033..e8d0588c420b2 100644
--- a/packages/edit-site/src/components/resizable-frame/index.js
+++ b/packages/edit-site/src/components/resizable-frame/index.js
@@ -300,6 +300,7 @@ function ResizableFrame( {
className={ classnames( 'edit-site-resizable-frame__inner', {
'is-resizing': isResizing,
} ) }
+ showHandle={ false } // Do not show the default handle, as we're using a custom one.
>
{
setMeta( { ...meta, [ metaKey ]: newValue } );
};
+
return {
placeholder: metaKey,
- useValue: [ metaValue, updateMetaValue ],
+ value: metaValue,
+ updateValue: updateMetaValue,
};
},
};
diff --git a/packages/editor/src/hooks/index.js b/packages/editor/src/hooks/index.js
index 5a48ec1bf4956..75bb34abf6cfa 100644
--- a/packages/editor/src/hooks/index.js
+++ b/packages/editor/src/hooks/index.js
@@ -3,4 +3,4 @@
*/
import './custom-sources-backwards-compatibility';
import './default-autocompleters';
-import './pattern-partial-syncing';
+import './pattern-overrides';
diff --git a/packages/editor/src/hooks/pattern-partial-syncing.js b/packages/editor/src/hooks/pattern-overrides.js
similarity index 81%
rename from packages/editor/src/hooks/pattern-partial-syncing.js
rename to packages/editor/src/hooks/pattern-overrides.js
index f86268cb49546..442ce70a2bf71 100644
--- a/packages/editor/src/hooks/pattern-partial-syncing.js
+++ b/packages/editor/src/hooks/pattern-overrides.js
@@ -14,7 +14,7 @@ import { store as editorStore } from '../store';
import { unlock } from '../lock-unlock';
const {
- PartialSyncingControls,
+ useSetPatternBindings,
ResetOverridesControl,
PATTERN_TYPES,
PARTIAL_SYNCING_SUPPORTED_BLOCKS,
@@ -29,7 +29,7 @@ const {
*
* @return {Component} Wrapped component.
*/
-const withPartialSyncingControls = createHigherOrderComponent(
+const withPatternOverrideControls = createHigherOrderComponent(
( BlockEdit ) => ( props ) => {
const isSupportedBlock = Object.keys(
PARTIAL_SYNCING_SUPPORTED_BLOCKS
@@ -38,6 +38,7 @@ const withPartialSyncingControls = createHigherOrderComponent(
return (
<>
+ { isSupportedBlock && }
{ props.isSelected && isSupportedBlock && (
) }
@@ -46,6 +47,15 @@ const withPartialSyncingControls = createHigherOrderComponent(
}
);
+function BindingUpdater( props ) {
+ const postType = useSelect(
+ ( select ) => select( editorStore ).getCurrentPostType(),
+ []
+ );
+ useSetPatternBindings( props, postType );
+ return null;
+}
+
// Split into a separate component to avoid a store subscription
// on every block.
function ControlsWithStoreSubscription( props ) {
@@ -55,6 +65,7 @@ function ControlsWithStoreSubscription( props ) {
select( editorStore ).getCurrentPostType() === PATTERN_TYPES.user,
[]
);
+
const bindings = props.attributes.metadata?.bindings;
const hasPatternBindings =
!! bindings &&
@@ -62,19 +73,14 @@ function ControlsWithStoreSubscription( props ) {
( binding ) => binding.source === 'core/pattern-overrides'
);
- const shouldShowPartialSyncingControls =
- isEditingPattern && blockEditingMode === 'default';
const shouldShowResetOverridesControl =
! isEditingPattern &&
- !! props.attributes.metadata?.id &&
+ !! props.attributes.metadata?.name &&
blockEditingMode !== 'disabled' &&
hasPatternBindings;
return (
<>
- { shouldShowPartialSyncingControls && (
-
- ) }
{ shouldShowResetOverridesControl && (
) }
@@ -84,6 +90,6 @@ function ControlsWithStoreSubscription( props ) {
addFilter(
'editor.BlockEdit',
- 'core/editor/with-partial-syncing-controls',
- withPartialSyncingControls
+ 'core/editor/with-pattern-override-controls',
+ withPatternOverrideControls
);
diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js
index 1d3c8c24c5cfd..788fc0152ba1d 100644
--- a/packages/icons/src/index.js
+++ b/packages/icons/src/index.js
@@ -59,6 +59,7 @@ export { default as commentAuthorName } from './library/comment-author-name';
export { default as commentContent } from './library/comment-content';
export { default as commentReplyLink } from './library/comment-reply-link';
export { default as commentEditLink } from './library/comment-edit-link';
+export { default as connection } from './library/connection';
export { default as cover } from './library/cover';
export { default as create } from './library/create';
export { default as crop } from './library/crop';
diff --git a/packages/icons/src/library/connection.js b/packages/icons/src/library/connection.js
new file mode 100644
index 0000000000000..47cee6c66a3ef
--- /dev/null
+++ b/packages/icons/src/library/connection.js
@@ -0,0 +1,25 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path, G } from '@wordpress/primitives';
+
+const connection = (
+
+);
+
+export default connection;
diff --git a/packages/interactivity/docs/2-api-reference.md b/packages/interactivity/docs/2-api-reference.md
index bae15e9a7fcf2..64033107a8a55 100644
--- a/packages/interactivity/docs/2-api-reference.md
+++ b/packages/interactivity/docs/2-api-reference.md
@@ -969,3 +969,150 @@ const { state } = store(
// The following call works as expected.
store( "myPlugin/private", { /* store part */ }, { lock: PRIVATE_LOCK } );
```
+
+### Store client methods
+
+Apart from the store function, there are also some methods that allows the developer to access data on their store functions.
+
+ - getContext()
+ - getElement()
+
+#### getContext()
+
+Retrieves the context inherited by the element evaluating a function from the store. The returned value depends on the element and the namespace where the function calling `getContext()` exists.
+
+```php
+// render.php
+
+
+
+```
+
+```js
+// store
+import { store, getContext } from '@wordpress/interactivity';
+
+store( "myPlugin", {
+ actions: {
+ log: () => {
+ const context = getContext();
+ // Logs "false"
+ console.log('context => ', context.isOpen)
+ },
+ },
+});
+```
+
+#### getElement()
+
+Retrieves a representation of the element that the action is bound to or called from. Such representation is read-only, and contains a reference to the DOM element, its props and a local reactive state.
+It returns an object with two keys:
+
+##### ref
+
+`ref` is the reference to the DOM element as an (HTMLElement)[https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement]
+
+##### attributes
+
+`attributes` contains a (Proxy)[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy], which adds a getter that allows to reference other store namespaces. Feel free to check the getter in the code. [Link](https://github.com/WordPress/gutenberg/blob/8cb23964d58f3ce5cf6ae1b6f967a4b8d4939a8e/packages/interactivity/src/store.ts#L70)
+
+Those attributes will contain the directives of that element. In the button example:
+
+```js
+// store
+import { store, getContext } from '@wordpress/interactivity';
+
+store( "myPlugin", {
+ actions: {
+ log: () => {
+ const element = getElement();
+ // Logs "false"
+ console.log('element attributes => ', element.attributes)
+ },
+ },
+});
+```
+
+The code will log:
+
+```json
+{
+ "data-wp-on--click": 'actions.increaseCounter',
+ "children": ['Log'],
+ "onclick": event => { evaluate(entry, event); }
+}
+```
+
+## Server functions
+
+The Interactivity API comes with handy functions on the PHP part. Apart from [setting the store via server](#on-the-server-side), there is also a function to get and set Interactivity related config variables.
+
+### wp_interactivity_config
+
+`wp_interactivity_config` allows to set or get a configuration array, referenced to a store namespace.
+The configuration is also available on the client, but it is static information.
+
+Consider it a global setting for interactions of a site, that won't be updated on user interactions.
+
+An example of setting:
+
+```php
+ wp_interactivity_config( 'myPlugin', array( 'showLikeButton' => is_user_logged_in() ) );
+```
+
+An example of getting:
+
+```php
+ wp_interactivity_config( 'myPlugin' );
+```
+
+This config can be retrieved on the client:
+
+```js
+// view.js
+
+const { showLikeButton } = getConfig();
+```
+
+### wp_interactivity_process_directives
+
+`wp_interactivity_process_directives` returns the updated HTML after the directives have been processed.
+
+It is the Core function of the Interactivity API server side rendering part, and is public so any HTML can be processed, whether is a block or not.
+
+This code
+
+```php
+wp_interactivity_state( 'myPlugin', array( 'greeting' => 'Hello, World!' ) );
+$html = '';
+$processed_html = wp_interactivity_process_directives( $html_content );
+echo $processed_html;
+```
+
+will output:
+```html
+
Hello, World!
+```
+
+### wp_interactivity_data_wp_context
+
+`wp_interactivity_data_wp_context` returns a stringified JSON of a context directive.
+This function is the recommended way to print the `data-wp-context` attribute in the server side rendedered markup.
+
+```php
+
+$my_context = array(
+ 'counter' => 0,
+ 'isOpen' => true,
+);
+
+
+```
+
+will output:
+
+```html
+
+```
diff --git a/packages/patterns/package.json b/packages/patterns/package.json
index e90e9b891b192..385c389fa14de 100644
--- a/packages/patterns/package.json
+++ b/packages/patterns/package.json
@@ -44,8 +44,7 @@
"@wordpress/icons": "file:../icons",
"@wordpress/notices": "file:../notices",
"@wordpress/private-apis": "file:../private-apis",
- "@wordpress/url": "file:../url",
- "nanoid": "^3.3.4"
+ "@wordpress/url": "file:../url"
},
"peerDependencies": {
"react": "^18.0.0",
diff --git a/packages/patterns/src/components/partial-syncing-controls.js b/packages/patterns/src/components/partial-syncing-controls.js
deleted file mode 100644
index 7b3e5cb312e82..0000000000000
--- a/packages/patterns/src/components/partial-syncing-controls.js
+++ /dev/null
@@ -1,108 +0,0 @@
-/**
- * External dependencies
- */
-import { nanoid } from 'nanoid';
-
-/**
- * WordPress dependencies
- */
-import { InspectorControls } from '@wordpress/block-editor';
-import { BaseControl, CheckboxControl } from '@wordpress/components';
-import { __ } from '@wordpress/i18n';
-
-/**
- * Internal dependencies
- */
-import { PARTIAL_SYNCING_SUPPORTED_BLOCKS } from '../constants';
-
-function PartialSyncingControls( { name, attributes, setAttributes } ) {
- const syncedAttributes = PARTIAL_SYNCING_SUPPORTED_BLOCKS[ name ];
- const attributeSources = syncedAttributes.map(
- ( attributeName ) =>
- attributes.metadata?.bindings?.[ attributeName ]?.source
- );
- const isConnectedToOtherSources = attributeSources.every(
- ( source ) => source && source !== 'core/pattern-overrides'
- );
-
- // Render nothing if all supported attributes are connected to other sources.
- if ( isConnectedToOtherSources ) {
- return null;
- }
-
- function updateBindings( isChecked ) {
- let updatedBindings = {
- ...attributes?.metadata?.bindings,
- };
-
- if ( ! isChecked ) {
- for ( const attributeName of syncedAttributes ) {
- if (
- updatedBindings[ attributeName ]?.source ===
- 'core/pattern-overrides'
- ) {
- delete updatedBindings[ attributeName ];
- }
- }
- if ( ! Object.keys( updatedBindings ).length ) {
- updatedBindings = undefined;
- }
- setAttributes( {
- metadata: {
- ...attributes.metadata,
- bindings: updatedBindings,
- },
- } );
- return;
- }
-
- for ( const attributeName of syncedAttributes ) {
- if ( ! updatedBindings[ attributeName ] ) {
- updatedBindings[ attributeName ] = {
- source: 'core/pattern-overrides',
- };
- }
- }
-
- if ( typeof attributes.metadata?.id === 'string' ) {
- setAttributes( {
- metadata: {
- ...attributes.metadata,
- bindings: updatedBindings,
- },
- } );
- return;
- }
-
- const id = nanoid( 6 );
- setAttributes( {
- metadata: {
- ...attributes.metadata,
- id,
- bindings: updatedBindings,
- },
- } );
- }
-
- return (
-
-
-
- { __( 'Pattern overrides' ) }
-
- source === 'core/pattern-overrides'
- ) }
- onChange={ ( isChecked ) => {
- updateBindings( isChecked );
- } }
- />
-
-
- );
-}
-
-export default PartialSyncingControls;
diff --git a/packages/patterns/src/components/reset-overrides-control.js b/packages/patterns/src/components/reset-overrides-control.js
index 586f460835234..1d3ae013addd3 100644
--- a/packages/patterns/src/components/reset-overrides-control.js
+++ b/packages/patterns/src/components/reset-overrides-control.js
@@ -11,13 +11,13 @@ import { store as coreStore } from '@wordpress/core-data';
import { parse } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
-function recursivelyFindBlockWithId( blocks, id ) {
+function recursivelyFindBlockWithName( blocks, name ) {
for ( const block of blocks ) {
- if ( block.attributes.metadata?.id === id ) {
+ if ( block.attributes.metadata?.name === name ) {
return block;
}
- const found = recursivelyFindBlockWithId( block.innerBlocks, id );
+ const found = recursivelyFindBlockWithName( block.innerBlocks, name );
if ( found ) {
return found;
}
@@ -26,10 +26,10 @@ function recursivelyFindBlockWithId( blocks, id ) {
export default function ResetOverridesControl( props ) {
const registry = useRegistry();
- const id = props.attributes.metadata?.id;
+ const name = props.attributes.metadata?.name;
const patternWithOverrides = useSelect(
( select ) => {
- if ( ! id ) {
+ if ( ! name ) {
return undefined;
}
@@ -39,13 +39,13 @@ export default function ResetOverridesControl( props ) {
getBlockParentsByBlockName( props.clientId, 'core/block' )
)[ 0 ];
- if ( ! patternBlock?.attributes.content?.[ id ] ) {
+ if ( ! patternBlock?.attributes.content?.[ name ] ) {
return undefined;
}
return patternBlock;
},
- [ props.clientId, id ]
+ [ props.clientId, name ]
);
const resetOverrides = async () => {
@@ -57,7 +57,7 @@ export default function ResetOverridesControl( props ) {
patternWithOverrides.attributes.ref
);
const blocks = editedRecord.blocks ?? parse( editedRecord.content );
- const block = recursivelyFindBlockWithId( blocks, id );
+ const block = recursivelyFindBlockWithName( blocks, name );
const newAttributes = Object.assign(
// Reset every existing attribute to undefined.
diff --git a/packages/patterns/src/components/use-set-pattern-bindings.js b/packages/patterns/src/components/use-set-pattern-bindings.js
new file mode 100644
index 0000000000000..df16d2b2b0591
--- /dev/null
+++ b/packages/patterns/src/components/use-set-pattern-bindings.js
@@ -0,0 +1,106 @@
+/**
+ * WordPress dependencies
+ */
+import { usePrevious } from '@wordpress/compose';
+import { useEffect } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { PARTIAL_SYNCING_SUPPORTED_BLOCKS } from '../constants';
+
+function removeBindings( bindings, syncedAttributes ) {
+ let updatedBindings = {};
+ for ( const attributeName of syncedAttributes ) {
+ // Omit any pattern override bindings from the `updatedBindings` object.
+ if (
+ bindings?.[ attributeName ]?.source !== 'core/pattern-overrides' &&
+ bindings?.[ attributeName ]?.source !== undefined
+ ) {
+ updatedBindings[ attributeName ] = bindings[ attributeName ];
+ }
+ }
+ if ( ! Object.keys( updatedBindings ).length ) {
+ updatedBindings = undefined;
+ }
+ return updatedBindings;
+}
+
+function addBindings( bindings, syncedAttributes ) {
+ const updatedBindings = { ...bindings };
+ for ( const attributeName of syncedAttributes ) {
+ if ( ! bindings?.[ attributeName ] ) {
+ updatedBindings[ attributeName ] = {
+ source: 'core/pattern-overrides',
+ };
+ }
+ }
+ return updatedBindings;
+}
+
+export default function useSetPatternBindings(
+ { name, attributes, setAttributes },
+ currentPostType
+) {
+ const metadataName = attributes?.metadata?.name ?? '';
+ const prevMetadataName = usePrevious( metadataName ) ?? '';
+ const bindings = attributes?.metadata?.bindings;
+
+ useEffect( () => {
+ // Bindings should only be created when editing a wp_block post type,
+ // and also when there's a change to the user-given name for the block.
+ if (
+ currentPostType !== 'wp_block' ||
+ metadataName === prevMetadataName
+ ) {
+ return;
+ }
+
+ const syncedAttributes = PARTIAL_SYNCING_SUPPORTED_BLOCKS[ name ];
+ const attributeSources = syncedAttributes.map(
+ ( attributeName ) =>
+ attributes.metadata?.bindings?.[ attributeName ]?.source
+ );
+ const isConnectedToOtherSources = attributeSources.every(
+ ( source ) => source && source !== 'core/pattern-overrides'
+ );
+
+ // Avoid overwriting other (e.g. meta) bindings.
+ if ( isConnectedToOtherSources ) {
+ return;
+ }
+
+ // The user-given name for the block was deleted, remove the bindings.
+ if ( ! metadataName?.length && prevMetadataName?.length ) {
+ const updatedBindings = removeBindings(
+ bindings,
+ syncedAttributes
+ );
+ setAttributes( {
+ metadata: {
+ ...attributes.metadata,
+ bindings: updatedBindings,
+ },
+ } );
+ }
+
+ // The user-given name for the block was set, set the bindings.
+ if ( ! prevMetadataName?.length && metadataName.length ) {
+ const updatedBindings = addBindings( bindings, syncedAttributes );
+ setAttributes( {
+ metadata: {
+ ...attributes.metadata,
+ bindings: updatedBindings,
+ },
+ } );
+ }
+ }, [
+ bindings,
+ prevMetadataName,
+ metadataName,
+ currentPostType,
+ name,
+ attributes.metadata,
+ setAttributes,
+ ] );
+}
diff --git a/packages/patterns/src/private-apis.js b/packages/patterns/src/private-apis.js
index a5fbddb62fd62..54ad5a4aa47d1 100644
--- a/packages/patterns/src/private-apis.js
+++ b/packages/patterns/src/private-apis.js
@@ -13,7 +13,7 @@ import {
import RenamePatternModal from './components/rename-pattern-modal';
import PatternsMenuItems from './components';
import RenamePatternCategoryModal from './components/rename-pattern-category-modal';
-import PartialSyncingControls from './components/partial-syncing-controls';
+import useSetPatternBindings from './components/use-set-pattern-bindings';
import ResetOverridesControl from './components/reset-overrides-control';
import { useAddPatternCategory } from './private-hooks';
import {
@@ -34,7 +34,7 @@ lock( privateApis, {
RenamePatternModal,
PatternsMenuItems,
RenamePatternCategoryModal,
- PartialSyncingControls,
+ useSetPatternBindings,
ResetOverridesControl,
useAddPatternCategory,
PATTERN_TYPES,
diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js
index fce6d5a55b202..3629ff8c60a74 100644
--- a/packages/rich-text/src/create.js
+++ b/packages/rich-text/src/create.js
@@ -472,11 +472,9 @@ function createFromElement( { element, range, isEditableTree } ) {
if (
isEditableTree &&
- // Ignore any placeholders.
- ( node.getAttribute( 'data-rich-text-placeholder' ) ||
- // Ignore any line breaks that are not inserted by us.
- ( tagName === 'br' &&
- ! node.getAttribute( 'data-rich-text-line-break' ) ) )
+ // Ignore any line breaks that are not inserted by us.
+ tagName === 'br' &&
+ ! node.getAttribute( 'data-rich-text-line-break' )
) {
accumulateSelection( accumulator, node, range, createEmptyValue() );
continue;
@@ -541,7 +539,9 @@ function createFromElement( { element, range, isEditableTree } ) {
accumulateSelection( accumulator, node, range, value );
- if ( ! format ) {
+ // Ignore any placeholders, but keep their content since the browser
+ // might insert text inside them when the editable element is flex.
+ if ( ! format || node.getAttribute( 'data-rich-text-placeholder' ) ) {
mergePair( accumulator, value );
} else if ( value.text.length === 0 ) {
if ( format.attributes ) {
diff --git a/test/e2e/specs/editor/blocks/list.spec.js b/test/e2e/specs/editor/blocks/list.spec.js
index 48d50a862a50b..10f25d6b3609f 100644
--- a/test/e2e/specs/editor/blocks/list.spec.js
+++ b/test/e2e/specs/editor/blocks/list.spec.js
@@ -8,6 +8,36 @@ test.describe( 'List (@firefox)', () => {
await admin.createNewPost();
} );
+ test( 'can be copied from multi selection', async ( {
+ editor,
+ page,
+ pageUtils,
+ } ) => {
+ await editor.insertBlock( { name: 'core/list' } );
+ await page.keyboard.type( 'one' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( 'two' );
+ await pageUtils.pressKeys( 'primary+a' );
+ await pageUtils.pressKeys( 'primary+a' );
+ await pageUtils.pressKeys( 'primary+c' );
+ await editor.insertBlock( { name: 'core/paragraph' } );
+ await pageUtils.pressKeys( 'primary+v' );
+
+ const copied = `
+
+
one
+
+
+
+
two
+
+`;
+
+ expect( await editor.getEditedPostContent() ).toBe(
+ copied + '\n\n' + copied
+ );
+ } );
+
test( 'can be created by using an asterisk at the start of a paragraph block', async ( {
editor,
page,
diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js
index fc315e522b81a..8cd3423290e3f 100644
--- a/test/e2e/specs/editor/various/block-bindings.spec.js
+++ b/test/e2e/specs/editor/various/block-bindings.spec.js
@@ -47,7 +47,7 @@ test.describe( 'Block bindings', () => {
} );
test.describe( 'Paragraph', () => {
- test( 'Should show the value of the custom field', async ( {
+ test( 'should show the value of the custom field', async ( {
editor,
} ) => {
await editor.insertBlock( {
@@ -72,7 +72,7 @@ test.describe( 'Block bindings', () => {
);
} );
- test( 'Should lock the appropriate controls with a registered source', async ( {
+ test( 'should lock the appropriate controls with a registered source', async ( {
editor,
page,
} ) => {
@@ -118,7 +118,7 @@ test.describe( 'Block bindings', () => {
);
} );
- test( 'Should lock the appropriate controls when source is not defined', async ( {
+ test( 'should lock the appropriate controls when source is not defined', async ( {
editor,
page,
} ) => {
@@ -166,7 +166,7 @@ test.describe( 'Block bindings', () => {
} );
test.describe( 'Heading', () => {
- test( 'Should show the key of the custom field', async ( {
+ test( 'should show the key of the custom field', async ( {
editor,
} ) => {
await editor.insertBlock( {
@@ -189,7 +189,7 @@ test.describe( 'Block bindings', () => {
await expect( headingBlock ).toHaveText( 'text_custom_field' );
} );
- test( 'Should lock the appropriate controls with a registered source', async ( {
+ test( 'should lock the appropriate controls with a registered source', async ( {
editor,
page,
} ) => {
@@ -235,7 +235,7 @@ test.describe( 'Block bindings', () => {
);
} );
- test( 'Should lock the appropriate controls when source is not defined', async ( {
+ test( 'should lock the appropriate controls when source is not defined', async ( {
editor,
page,
} ) => {
@@ -283,7 +283,7 @@ test.describe( 'Block bindings', () => {
} );
test.describe( 'Button', () => {
- test( 'Should show the key of the custom field when text is bound', async ( {
+ test( 'should show the key of the custom field when text is bound', async ( {
editor,
} ) => {
await editor.insertBlock( {
@@ -313,7 +313,7 @@ test.describe( 'Block bindings', () => {
await expect( buttonBlock ).toHaveText( 'text_custom_field' );
} );
- test( 'Should lock text controls when text is bound to a registered source', async ( {
+ test( 'should lock text controls when text is bound to a registered source', async ( {
editor,
page,
} ) => {
@@ -375,7 +375,7 @@ test.describe( 'Block bindings', () => {
).toBeVisible();
} );
- test( 'Should lock text controls when text is bound to an undefined source', async ( {
+ test( 'should lock text controls when text is bound to an undefined source', async ( {
editor,
page,
} ) => {
@@ -437,7 +437,7 @@ test.describe( 'Block bindings', () => {
).toBeVisible();
} );
- test( 'Should lock url controls when url is bound to a registered source', async ( {
+ test( 'should lock url controls when url is bound to a registered source', async ( {
editor,
page,
} ) => {
@@ -497,7 +497,7 @@ test.describe( 'Block bindings', () => {
).toBeHidden();
} );
- test( 'Should lock url controls when url is bound to an undefined source', async ( {
+ test( 'should lock url controls when url is bound to an undefined source', async ( {
editor,
page,
} ) => {
@@ -557,7 +557,7 @@ test.describe( 'Block bindings', () => {
).toBeHidden();
} );
- test( 'Should lock url and text controls when both are bound', async ( {
+ test( 'should lock url and text controls when both are bound', async ( {
editor,
page,
} ) => {
@@ -630,7 +630,7 @@ test.describe( 'Block bindings', () => {
} );
test.describe( 'Image', () => {
- test( 'Should show the upload form when url is not bound', async ( {
+ test( 'should show the upload form when url is not bound', async ( {
editor,
} ) => {
await editor.insertBlock( { name: 'core/image' } );
@@ -643,7 +643,7 @@ test.describe( 'Block bindings', () => {
).toBeVisible();
} );
- test( 'Should NOT show the upload form when url is bound to a registered source', async ( {
+ test( 'should NOT show the upload form when url is bound to a registered source', async ( {
editor,
} ) => {
await editor.insertBlock( {
@@ -671,7 +671,7 @@ test.describe( 'Block bindings', () => {
).toBeHidden();
} );
- test( 'Should NOT show the upload form when url is bound to an undefined source', async ( {
+ test( 'should NOT show the upload form when url is bound to an undefined source', async ( {
editor,
} ) => {
await editor.insertBlock( {
@@ -699,7 +699,7 @@ test.describe( 'Block bindings', () => {
).toBeHidden();
} );
- test( 'Should lock url controls when url is bound to a registered source', async ( {
+ test( 'should lock url controls when url is bound to a registered source', async ( {
editor,
page,
} ) => {
@@ -768,7 +768,7 @@ test.describe( 'Block bindings', () => {
expect( titleValue ).toBe( 'default title value' );
} );
- test( 'Should lock url controls when url is bound to an undefined source', async ( {
+ test( 'should lock url controls when url is bound to an undefined source', async ( {
editor,
page,
} ) => {
@@ -837,7 +837,7 @@ test.describe( 'Block bindings', () => {
expect( titleValue ).toBe( 'default title value' );
} );
- test( 'Should disable alt textarea when alt is bound to a registered source', async ( {
+ test( 'should disable alt textarea when alt is bound to a registered source', async ( {
editor,
page,
} ) => {
@@ -900,7 +900,7 @@ test.describe( 'Block bindings', () => {
expect( titleValue ).toBe( 'default title value' );
} );
- test( 'Should disable alt textarea when alt is bound to an undefined source', async ( {
+ test( 'should disable alt textarea when alt is bound to an undefined source', async ( {
editor,
page,
} ) => {
@@ -963,7 +963,7 @@ test.describe( 'Block bindings', () => {
expect( titleValue ).toBe( 'default title value' );
} );
- test( 'Should disable title input when title is bound to a registered source', async ( {
+ test( 'should disable title input when title is bound to a registered source', async ( {
editor,
page,
} ) => {
@@ -1026,7 +1026,7 @@ test.describe( 'Block bindings', () => {
expect( titleValue ).toBe( 'text_custom_field' );
} );
- test( 'Should disable title input when title is bound to an undefined source', async ( {
+ test( 'should disable title input when title is bound to an undefined source', async ( {
editor,
page,
} ) => {
@@ -1168,7 +1168,7 @@ test.describe( 'Block bindings', () => {
await admin.createNewPost( { title: 'Test bindings' } );
} );
test.describe( 'Paragraph', () => {
- test( 'Should show the value of the custom field when exists', async ( {
+ test( 'should show the value of the custom field when exists', async ( {
editor,
page,
} ) => {
@@ -1210,7 +1210,7 @@ test.describe( 'Block bindings', () => {
);
} );
- test( "Should show the value of the key when custom field doesn't exists", async ( {
+ test( "should show the value of the key when custom field doesn't exist", async ( {
editor,
page,
} ) => {
@@ -1218,7 +1218,7 @@ test.describe( 'Block bindings', () => {
name: 'core/paragraph',
attributes: {
anchor: 'paragraph-binding',
- content: 'p',
+ content: 'fallback value',
metadata: {
bindings: {
content: {
@@ -1244,54 +1244,192 @@ test.describe( 'Block bindings', () => {
// Check the frontend doesn't show the content.
const postId = await editor.publishPost();
await page.goto( `/?p=${ postId }` );
- await expect(
- page.locator( '#paragraph-binding' )
- ).toBeHidden();
+ await expect( page.locator( '#paragraph-binding' ) ).toHaveText(
+ 'fallback value'
+ );
+ } );
+
+ test( 'should not show the value of a protected meta field', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/paragraph',
+ attributes: {
+ anchor: 'paragraph-binding',
+ content: 'fallback value',
+ metadata: {
+ bindings: {
+ content: {
+ source: 'core/post-meta',
+ args: { key: '_protected_field' },
+ },
+ },
+ },
+ },
+ } );
+ const paragraphBlock = editor.canvas.getByRole( 'document', {
+ name: 'Block: Paragraph',
+ } );
+ await expect( paragraphBlock ).toHaveText( '_protected_field' );
+ // Check the frontend doesn't show the content.
+ const postId = await editor.publishPost();
+ await page.goto( `/?p=${ postId }` );
+ await expect( page.locator( '#paragraph-binding' ) ).toHaveText(
+ 'fallback value'
+ );
+ } );
+
+ test( 'should not show the value of a meta field with `show_in_rest` false', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/paragraph',
+ attributes: {
+ anchor: 'paragraph-binding',
+ content: 'fallback value',
+ metadata: {
+ bindings: {
+ content: {
+ source: 'core/post-meta',
+ args: { key: 'show_in_rest_false_field' },
+ },
+ },
+ },
+ },
+ } );
+ const paragraphBlock = editor.canvas.getByRole( 'document', {
+ name: 'Block: Paragraph',
+ } );
+ await expect( paragraphBlock ).toHaveText(
+ 'show_in_rest_false_field'
+ );
+ // Check the frontend doesn't show the content.
+ const postId = await editor.publishPost();
+ await page.goto( `/?p=${ postId }` );
+ await expect( page.locator( '#paragraph-binding' ) ).toHaveText(
+ 'fallback value'
+ );
+ } );
+
+ test( 'should add empty paragraph block when pressing enter', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/paragraph',
+ attributes: {
+ content: 'paragraph default content',
+ metadata: {
+ bindings: {
+ content: {
+ source: 'core/post-meta',
+ args: { key: 'text_custom_field' },
+ },
+ },
+ },
+ },
+ } );
+ await page.keyboard.press( 'Enter' );
+ const [ initialParagraph, newEmptyParagraph ] =
+ await editor.canvas
+ .locator( '[data-type="core/paragraph"]' )
+ .all();
+ await expect( initialParagraph ).toHaveText(
+ 'Value of the text_custom_field'
+ );
+ await expect( newEmptyParagraph ).toHaveText( '' );
+ await expect( newEmptyParagraph ).toBeEditable();
} );
} );
- test( 'Heading - should show the value of the custom field', async ( {
- editor,
- page,
- } ) => {
- await editor.insertBlock( {
- name: 'core/heading',
- attributes: {
- anchor: 'heading-binding',
- content: 'heading default content',
- metadata: {
- bindings: {
- content: {
- source: 'core/post-meta',
- args: { key: 'text_custom_field' },
+ test.describe( 'Heading', () => {
+ test( 'should show the value of the custom field', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/heading',
+ attributes: {
+ anchor: 'heading-binding',
+ content: 'heading default content',
+ metadata: {
+ bindings: {
+ content: {
+ source: 'core/post-meta',
+ args: { key: 'text_custom_field' },
+ },
},
},
},
- },
+ } );
+ const headingBlock = editor.canvas.getByRole( 'document', {
+ name: 'Block: Heading',
+ } );
+ await expect( headingBlock ).toHaveText(
+ 'Value of the text_custom_field'
+ );
+ // Heading is not editable.
+ await expect( headingBlock ).toHaveAttribute(
+ 'contenteditable',
+ 'false'
+ );
+
+ // Check the frontend shows the value of the custom field.
+ const postId = await editor.publishPost();
+ await page.goto( `/?p=${ postId }` );
+ await expect(
+ page.locator( '#heading-binding' )
+ ).toBeVisible();
+ await expect( page.locator( '#heading-binding' ) ).toHaveText(
+ 'Value of the text_custom_field'
+ );
} );
- const headingBlock = editor.canvas.getByRole( 'document', {
- name: 'Block: Heading',
+
+ test( 'should add empty paragraph block when pressing enter', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/heading',
+ attributes: {
+ anchor: 'heading-binding',
+ content: 'heading default content',
+ metadata: {
+ bindings: {
+ content: {
+ source: 'core/post-meta',
+ args: { key: 'text_custom_field' },
+ },
+ },
+ },
+ },
+ } );
+ await page.keyboard.press( 'Enter' );
+ // Can't use `editor.getBlocks` because it doesn't return the meta value shown in the editor.
+ const [ initialHeading, newEmptyParagraph ] =
+ await editor.canvas.locator( '[data-block]' ).all();
+ // First block should be the original block.
+ await expect( initialHeading ).toHaveAttribute(
+ 'data-type',
+ 'core/heading'
+ );
+ await expect( initialHeading ).toHaveText(
+ 'Value of the text_custom_field'
+ );
+ // Second block should be an empty paragraph block.
+ await expect( newEmptyParagraph ).toHaveAttribute(
+ 'data-type',
+ 'core/paragraph'
+ );
+ await expect( newEmptyParagraph ).toHaveText( '' );
+ await expect( newEmptyParagraph ).toBeEditable();
} );
- await expect( headingBlock ).toHaveText(
- 'Value of the text_custom_field'
- );
- // Heading is not editable.
- await expect( headingBlock ).toHaveAttribute(
- 'contenteditable',
- 'false'
- );
-
- // Check the frontend shows the value of the custom field.
- const postId = await editor.publishPost();
- await page.goto( `/?p=${ postId }` );
- await expect( page.locator( '#heading-binding' ) ).toBeVisible();
- await expect( page.locator( '#heading-binding' ) ).toHaveText(
- 'Value of the text_custom_field'
- );
} );
test.describe( 'Button', () => {
- test( 'Should show the value of the custom field when text is bound', async ( {
+ test( 'should show the value of the custom field when text is bound', async ( {
editor,
page,
} ) => {
@@ -1347,7 +1485,7 @@ test.describe( 'Block bindings', () => {
);
} );
- test( 'Should use the value of the custom field when url is bound', async ( {
+ test( 'should use the value of the custom field when url is bound', async ( {
editor,
page,
} ) => {
@@ -1385,7 +1523,7 @@ test.describe( 'Block bindings', () => {
);
} );
- test( 'Should use the values of the custom fields when text and url are bound', async ( {
+ test( 'should use the values of the custom fields when text and url are bound', async ( {
editor,
page,
} ) => {
@@ -1428,6 +1566,51 @@ test.describe( 'Block bindings', () => {
'#url-custom-field'
);
} );
+
+ test( 'should add empty button block when pressing enter', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/buttons',
+ innerBlocks: [
+ {
+ name: 'core/button',
+ attributes: {
+ anchor: 'button-text-binding',
+ text: 'button default text',
+ url: '#default-url',
+ metadata: {
+ bindings: {
+ text: {
+ source: 'core/post-meta',
+ args: { key: 'text_custom_field' },
+ },
+ },
+ },
+ },
+ },
+ ],
+ } );
+ await editor.canvas
+ .getByRole( 'document', {
+ name: 'Block: Button',
+ exact: true,
+ } )
+ .getByRole( 'textbox' )
+ .click();
+ await page.keyboard.press( 'Enter' );
+ const [ initialButton, newEmptyButton ] = await editor.canvas
+ .locator( '[data-type="core/button"]' )
+ .all();
+ // First block should be the original block.
+ await expect( initialButton ).toHaveText(
+ 'Value of the text_custom_field'
+ );
+ // Second block should be an empty paragraph block.
+ await expect( newEmptyButton ).toHaveText( '' );
+ await expect( newEmptyButton ).toBeEditable();
+ } );
} );
test.describe( 'Image', () => {
@@ -1454,7 +1637,7 @@ test.describe( 'Block bindings', () => {
} );
await page.reload();
} );
- test( 'Should show the value of the custom field when url is bound', async ( {
+ test( 'should show the value of the custom field when url is bound', async ( {
editor,
page,
BlockBindingsUtils,
@@ -1505,7 +1688,7 @@ test.describe( 'Block bindings', () => {
);
} );
- test( 'Should show value of the custom field in the alt textarea when alt is bound', async ( {
+ test( 'should show value of the custom field in the alt textarea when alt is bound', async ( {
editor,
page,
BlockBindingsUtils,
@@ -1571,7 +1754,7 @@ test.describe( 'Block bindings', () => {
);
} );
- test( 'Should show value of the custom field in the title input when title is bound', async ( {
+ test( 'should show value of the custom field in the title input when title is bound', async ( {
editor,
page,
BlockBindingsUtils,
diff --git a/test/e2e/specs/editor/various/pattern-overrides.spec.js b/test/e2e/specs/editor/various/pattern-overrides.spec.js
index 86d7b9117bef5..4542e8c789ad1 100644
--- a/test/e2e/specs/editor/various/pattern-overrides.spec.js
+++ b/test/e2e/specs/editor/various/pattern-overrides.spec.js
@@ -32,7 +32,7 @@ test.describe( 'Pattern Overrides', () => {
editor,
} ) => {
let patternId;
- let editableParagraphId;
+ const editableParagraphName = 'Editable Paragraph';
await test.step( 'Create a synced pattern and assign blocks to allow overrides', async () => {
await admin.visitSiteEditor( { path: '/patterns' } );
@@ -85,8 +85,8 @@ test.describe( 'Pattern Overrides', () => {
await advancedPanel.click();
}
await editorSettings
- .getByRole( 'checkbox', { name: 'Allow instance overrides' } )
- .setChecked( true );
+ .getByRole( 'textbox', { name: 'Block Name' } )
+ .fill( editableParagraphName );
await expect.poll( editor.getBlocks ).toMatchObject( [
{
@@ -94,7 +94,7 @@ test.describe( 'Pattern Overrides', () => {
attributes: {
content: 'This paragraph can be edited',
metadata: {
- id: expect.any( String ),
+ name: editableParagraphName,
bindings: {
content: {
source: 'core/pattern-overrides',
@@ -123,8 +123,6 @@ test.describe( 'Pattern Overrides', () => {
).toBeVisible();
patternId = new URL( page.url() ).searchParams.get( 'postId' );
- const blocks = await editor.getBlocks();
- editableParagraphId = blocks[ 0 ].attributes.metadata.id;
} );
await test.step( 'Create a post and insert the pattern with overrides', async () => {
@@ -176,10 +174,8 @@ test.describe( 'Pattern Overrides', () => {
attributes: {
ref: patternId,
content: {
- [ editableParagraphId ]: {
- values: {
- content: 'I would word it this way',
- },
+ [ editableParagraphName ]: {
+ content: 'I would word it this way',
},
},
},
@@ -189,10 +185,8 @@ test.describe( 'Pattern Overrides', () => {
attributes: {
ref: patternId,
content: {
- [ editableParagraphId ]: {
- values: {
- content: 'This one is different',
- },
+ [ editableParagraphName ]: {
+ content: 'This one is different',
},
},
},
@@ -276,11 +270,11 @@ test.describe( 'Pattern Overrides', () => {
editor,
context,
} ) => {
- const buttonId = 'button-id';
+ const buttonName = 'Editable button';
const { id } = await requestUtils.createBlock( {
title: 'Button with target',
content: `
-