@eightshift/frontend-libs
Version:
A collection of useful frontend utility modules. powered by Eightshift
903 lines (783 loc) • 29.2 kB
JavaScript
import React from 'react';
import { __ } from '@wordpress/i18n';
import { InnerBlocks } from '@wordpress/block-editor';
import { registerBlockType, registerBlockVariation } from '@wordpress/blocks';
import { dispatch, select } from '@wordpress/data';
import { addFilter } from '@wordpress/hooks';
import { createElement } from '@wordpress/element';
import { getUnique } from './css-variables';
import { blockIcons } from '@eightshift/ui-components/icons';
import { STORE_NAME, setStoreGlobalWindow, setStore, setConfigFlags } from './store';
import { camelCase, kebabCase, lowerFirst, upperFirst } from '@eightshift/ui-components/utilities';
/**
* Register all Block Editor blocks using WP `registerBlockType` method.
* Due to restrictions in dynamic import using dynamic names all blocks are registered using `require.context`.
*
* @param {object} globalManifest - Must provide global blocks setting manifest.json.
* @param {function?} [wrapperComponent] - Callback function that returns a `Wrapper`.
* @param {object} wrapperManifest - `Wrapper` manifest.
* @param {function} componentsManifestPath - **Must provide `require.context` for all components `manifest.json`s.**
* @param {function} blocksManifestPath - **Must provide `require.context` for all blocks manifest.json-s.**
* @param {function} blocksEditComponentPath - **Must provide `require.context` for all blocks JavaScript files (unable to add only block edit file due to dynamic naming).**
* @param {function?} [hooksComponentPath] - Function of hooks JavaScript files in a block from `require.context`.
* @param {function?} [transformsComponentPath] - Function of transforms JavaScript files in a block from `require.context`.
* @param {function?} [deprecationsComponentPath] - Function of deprecations JavaScript files in a block from `require.context`.
* @param {function?} [overridesComponentPath] - Function of overrides JavaScript files in a block from `require.context`.
*
* @access public
*
* @returns {mixed}
*
* Usage:
* ```js
* registerBlocks(
* globalSettings,
* Wrapper,
* WrapperManifest,
* require.context('./../../components', true, /manifest.json$/),
* require.context('./../../custom', true, /manifest.json$/),
* require.context('./../../custom', true, /-block.js$/),
* require.context('./../../custom', true, /-hooks.js$/),
* require.context('./../../custom', true, /-transforms.js$/),
* require.context('./../../custom', true, /-deprecations.js$/),
* require.context('./../../custom', true, /-overrides.js$/),
* );
* ```
*/
export const registerBlocks = (
globalManifest = {},
wrapperComponent = null,
wrapperManifest = {},
componentsManifestPath,
blocksManifestPath,
blocksEditComponentPath,
hooksComponentPath = null,
transformsComponentPath = null,
deprecationsComponentPath = null,
overridesComponentPath = null,
) => {
const componentsManifest = componentsManifestPath.keys().map(componentsManifestPath);
const blocksManifests = blocksManifestPath.keys().map(blocksManifestPath);
// Set all store values.
setStore();
dispatch(STORE_NAME).setSettings(globalManifest);
dispatch(STORE_NAME).setBlocks(blocksManifests);
dispatch(STORE_NAME).setComponents(componentsManifest);
setConfigFlags();
if (select(STORE_NAME).getConfigUseWrapper()) {
dispatch(STORE_NAME).setWrapper(wrapperManifest);
}
setStoreGlobalWindow();
// Iterate blocks to register.
blocksManifests.map((blockManifest) => {
const {
active = true,
} = blockManifest;
// If block has active key set to false the block will not show in the block editor.
if (active) {
// Get Block edit component from block name and blocksEditComponentPath.
const blockComponent = getBlockEditComponent(blockManifest.blockName, blocksEditComponentPath, 'block');
// Get Block Transforms component from block name and transformsComponentPath.
if (transformsComponentPath !== null) {
const blockTransformsComponent = getBlockGenericComponent(blockManifest.blockName, transformsComponentPath, 'transforms');
if (blockTransformsComponent !== null) {
blockManifest.transforms = blockTransformsComponent;
}
}
// Get Block Deprecations component from block name and deprecationsComponentPath.
if (deprecationsComponentPath !== null) {
const blockDeprecationsComponent = getBlockGenericComponent(blockManifest.blockName, deprecationsComponentPath, 'deprecations');
if (blockDeprecationsComponent !== null) {
blockManifest.deprecated = blockDeprecationsComponent;
}
}
// Get Block Hooks component from block name and hooksComponentPath.
if (hooksComponentPath !== null) {
const blockHooksComponent = getBlockGenericComponent(blockManifest.blockName, hooksComponentPath, 'hooks');
if (blockHooksComponent !== null) {
blockHooksComponent();
}
}
// Get Block Overrides component from block name and overridesComponentPath.
if (overridesComponentPath !== null) {
const blockOverridesComponent = getBlockGenericComponent(blockManifest.blockName, overridesComponentPath, 'overrides');
if (blockOverridesComponent !== null) {
blockManifest = Object.assign(blockManifest, blockOverridesComponent);
}
}
// Pass data to registerBlock helper to get final output for registerBlockType.
const blockDetails = registerBlock(
globalManifest,
wrapperManifest,
componentsManifest,
blockManifest,
wrapperComponent,
blockComponent
);
// Format the 'deprecated' attribute details to match the format Gutenberg wants.
if (blockDetails?.options?.deprecated) {
blockDetails.options.deprecated = blockDetails.options.deprecated.map((deprecation) => {
if (deprecation?.attributes && deprecation?.migrate) {
return {
...deprecation,
isEligible: deprecation?.isEligible ?? (() => true),
save: blockDetails.options.save,
};
}
return {
attributes: {
...getAttributes(globalManifest, wrapperManifest, componentsManifest, blockManifest),
...deprecation.oldAttributes,
},
migrate: (attributes) => {
return {
...getAttributes(globalManifest, wrapperManifest, componentsManifest, blockManifest),
...attributes,
...deprecation.newAttributes(attributes),
};
},
isEligible: deprecation?.isEligible ?? ((attributes) => Object.keys(deprecation.oldAttributes).every((v) => Object.keys(attributes).includes(v))),
save: blockDetails.options.save,
};
});
}
// Native WP method for block registration.
registerBlockType(blockDetails.blockName, blockDetails.options);
}
return null;
});
// Add icon foreground and background colors as CSS variables for later use.
const {
background: backgroundGlobal,
foreground: foregroundGlobal,
} = globalManifest;
document.documentElement.style.setProperty('--es-admin-block-icon-foreground', foregroundGlobal);
document.documentElement.style.setProperty('--es-admin-block-icon-background', backgroundGlobal);
// Set all data to the dom that is necessary for project.
if (process.env.NODE_ENV !== 'test') {
// Require set like this because some import issue with jest unit tests.
const { blocksFilterHook } = require('./hooks');
addFilter('editor.BlockListBlock', `eightshift/${select(STORE_NAME).getSettingsNamespace()}`, blocksFilterHook);
}
};
/**
* Register all Variations Editor blocks using WP `registerBlockVariation` method.
* Due to restrictions in dynamic import using dynamic names all block are register using `require.context`.
*
* @param {object} globalManifest - **Must provide global blocks setting `manifest.json`.**
* @param {function} variationsManifestPath - **Must provide require.context for all variations `manifest.json`s.**
* @param {function} [blocksManifestPath] - **require.context for all blocks `manifest.json`s.**
* @param {function?} [overridesComponentPath] - Function of overrides JavaScript files in a block from `require.context`.
*
* @access public
*
* @returns {null}
*
* Usage:
* ```js
* registerVariations(
* globalSettings,
* require.context('./../../variations', true, /manifest.json$/),
* require.context('./../../custom', true, /manifest.json$/),
* require.context('./../../variations', true, /-overrides.js$/),
* );
* ```
*/
export const registerVariations = (
globalManifest = {},
variationsManifestPath,
blocksManifestPath = null,
overridesComponentPath = null,
) => {
const variationsManifests = variationsManifestPath.keys().map(variationsManifestPath);
// Set all store values.
dispatch(STORE_NAME).setVariations(variationsManifests);
// Iterate blocks to register.
variationsManifests.map((variationManifest) => {
const {
active = true,
} = variationManifest;
// If variation has active key set to false the variation will not show in the block editor.
if (active) {
// Get Block Overrides component from block name and overridesComponentPath.
if (overridesComponentPath !== null) {
const blockOverridesComponent = getBlockGenericComponent(variationManifest.name, overridesComponentPath, 'overrides');
if (blockOverridesComponent !== null) {
variationManifest = Object.assign(variationManifest, blockOverridesComponent);
}
}
// Pass data to registerVariation helper to get final output for registerBlockVariation.
const blockDetails = registerVariation(
globalManifest,
variationManifest,
(blocksManifestPath !== null) ? blocksManifestPath.keys().map(blocksManifestPath) : []
);
// Native WP method for block registration.
registerBlockVariation(blockDetails.blockName, blockDetails.options);
}
return null;
});
};
//---------------------------------------------------------------
// Private methods
/**
* Filter array of JS paths and get the correct edit components.
*
* @param {string} blockName - Provided block name to find corresponding edit component.
* @param {function} paths - Function of all JavaScript files in a block got from require.context.
* @param {string} fileName - Block partial name.
*
* @access private
*
* @returns {function}
*
*/
export const getBlockEditComponent = (blockName, paths, fileName) => {
// Create an array of all blocks file paths.
const pathsKeys = paths.keys();
// Get Block edit component from block name and pathsKeys.
const editComponent = pathsKeys.filter((filePath) => filePath === `./${blockName}/${blockName}-${fileName}.js`).map(paths)[0];
// If edit component is missing throw and error.
if (typeof editComponent === 'undefined') {
throw Error(`It looks like you are missing block edit component for block: ${blockName}, please check if you have ${blockName}-block.js file in your block folder.`);
}
// No mater if class of functional component is used fetch the first item in an object.
const editCallback = editComponent[Object.keys(editComponent)[0]];
// If edit component callback is missing throw and error.
if (typeof editCallback === 'undefined') {
throw Error(`It looks like you are missing block edit component for block: ${blockName}, please check if you have ${blockName}-block.js file in your block folder.`);
}
return editCallback;
};
/**
* Filter array of JS paths and get the correct transforms, hooks, etc components.
*
* @param {string} blockName - Provided block name to find corresponding edit component.
* @param {function} paths - Function of all JavaScript files in a block got from require.context.
* @param {string} fileName - Block partial name.
*
* @access private
*
* @returns {function}
*
*/
export const getBlockGenericComponent = (blockName, paths, fileName) => {
// Create an array of all blocks file paths.
const pathsKeys = paths.keys();
// Get Block edit component from block name and pathsKeys.
const editComponent = pathsKeys.filter((filePath) => filePath === `./${blockName}/${blockName}-${fileName}.js`).map(paths)[0];
// If edit component is missing throw and error.
if (typeof editComponent === 'undefined') {
return null;
}
// No mater if class of functional component is used fetch the first item in an object.
return editComponent[Object.keys(editComponent)[0]];
};
/**
* Check if namespace is defined in block or in global manifest settings and return namespace.
*
* @param {object} globalManifest - Global manifest.
* @param {object} blockManifest - Block manifest.
*
* @access private
*
* @returns {string?}
*/
export const getNamespace = (globalManifest, blockManifest) => {
return (typeof blockManifest.namespace === 'undefined') ? globalManifest.namespace : blockManifest.namespace;
};
/**
* Return full block name used in Block Editor with correct namespace.
*
* @param {object} globalManifest - Global manifest.
* @param {object} blockManifest - Block manifest.
*
* @access private
*
* @returns {string}
*/
export const getFullBlockName = (globalManifest, blockManifest) => {
return `${getNamespace(globalManifest, blockManifest)}/${blockManifest.blockName}`;
};
/**
* Return full block name used in Block Editor with correct namespace.
*
* @param {object} globalManifest - Global manifest.
* @param {object} blockManifest - Block manifest.
*
* @access private
*
* @returns {string}
*/
export const getFullBlockNameVariation = (globalManifest, blockManifest) => {
return `${getNamespace(globalManifest, blockManifest)}/${blockManifest.parentName}`;
};
/**
* Return save function based on hasInnerBlocks option of block.
*
* @param {object} blockManifest - Block manifest.
*
* @access private
*
* @returns {function} Save callback.
*/
export const getSaveCallback = (blockManifest) => {
const {
hasInnerBlocks,
} = blockManifest;
if (hasInnerBlocks && typeof InnerBlocks !== 'undefined') {
return () => createElement(InnerBlocks.Content);
}
return () => null;
};
/**
* Return merge function based on existence of `mergeableAttributes` option of block.
*
* @access private
*
* @param {object} blockManifest - Block manifest.
*/
export const getMergeCallback = (blockManifest) => {
const {
mergeableAttributes,
} = blockManifest;
if (mergeableAttributes) {
return (receiver, merger) => {
let outputObject = {};
for (const { attribute: attributeName, mergeStrategy } of mergeableAttributes) {
const attribute = Object.keys(receiver).find((k) => {
return k?.toLowerCase()?.includes(attributeName.toLowerCase());
});
switch (mergeStrategy) {
case "append": {
outputObject[attribute] = `${receiver[attribute] ?? ''}${merger[attribute] ?? ''}`;
break;
}
case "useDestinationAttribute": {
outputObject[attribute] = merger[attribute] ?? '';
break;
}
case "addNumericIntValue": {
outputObject[attribute] = parseInt(receiver[attribute] ?? '0') + parseInt(merger[attribute] ?? '0');
break;
}
case "addNumericFloatValue": {
outputObject[attribute] = parseFloat(receiver[attribute] ?? '0') + parseFloat(merger[attribute] ?? '0');
break;
}
case "addNumericPixelValue": {
// Remove numbers
const receiverUnit = (receiver[attribute] ?? '0px').replace(/\d/g, '');
// Remove value labels (= everything but numbers)
const receiverValue = parseInt(receiver[attribute] ?? '0px').replace(/\D/g, '');
const mergerValue = parseInt(receiver[attribute] ?? '0px').replace(/\D/g, '');
const calculatedValue = receiverValue + mergerValue;
outputObject[attribute] = `${calculatedValue}${receiverUnit}`;
break;
}
default: {
// "useSourceAttribute" is default
outputObject[attribute] = receiver[attribute] ?? '';
break;
}
}
}
return outputObject;
};
}
return () => null;
};
/**
* Return edit function wrapped with Wrapper component.
*
* @param {React.Component} Component - Component to render inside the wrapper.
* @param {React.Component} Wrapper - Wrapper component.
*
* @access private
*
* @returns {React.Component}
*/
export const getEditCallback = (Component, Wrapper) => (props) => {
const useWrapper = select(STORE_NAME).getConfigUseWrapper();
return (
useWrapper ?
<Wrapper props={props}>
<Component {...props} />
</Wrapper> :
<Component {...props} />
);
};
/**
* Set icon object with icon, background and foreground.
*
* @param {object} globalManifest - Global manifest.
* @param {object} blockManifest - Block manifest.
*
* @access private
*
* @returns {object}
*/
export const getIconOptions = (
globalManifest,
blockManifest
) => {
const {
background: backgroundGlobal,
foreground: foregroundGlobal,
} = globalManifest;
const {
icon,
} = blockManifest;
if (typeof icon === 'undefined') {
return {};
}
// Use built-in icons if 'src' is provided and the
// icon exists in the library
if (icon.src !== undefined && blockIcons[icon.src] !== undefined) {
return {
background: (typeof icon.background === 'undefined') ? backgroundGlobal : icon.background,
foreground: (typeof icon.foreground === 'undefined') ? foregroundGlobal : icon.foreground,
src: <span dangerouslySetInnerHTML={{ __html: blockIcons[icon.src] }} />,
};
}
return {
background: (typeof icon.background === 'undefined') ? backgroundGlobal : icon.background,
foreground: (typeof icon.foreground === 'undefined') ? foregroundGlobal : icon.foreground,
src: icon.src.includes('<svg') ? <span dangerouslySetInnerHTML={{ __html: icon.src }} /> : icon.src,
};
};
/**
* Iterate over attributes or example attributes object in block/component manifest and append the parent prefixes.
*
* @param {object} manifest - Object of component/block manifest to get data from.
* @param {string} newName - New renamed component name.
* @param {string} realName - Original real component name.
* @param {boolean} [isExample=false] - Type of items to iterate, if false example key will be use, if true attributes will be used.
* @param {string} [parent=''] - Parent component key with stacked parent component names for the final output.
* @param {boolean} [currentAttributes=false] - Check if current attribute is a part of the current component.
*
* @access private
*
* @returns {object}
*/
export const prepareComponentAttribute = (manifest, newName, realName, isExample = false, parent = '', currentAttributes = false) => {
const output = {};
// Define different data point for attributes or example.
const componentAttributes = isExample ? manifest?.example?.attributes : manifest?.attributes;
// It can occur that attributes or example key is missing in manifest so bailout.
if (typeof componentAttributes === 'undefined') {
return output;
}
// Prepare parent case.
const newParent = camelCase(parent);
// Iterate each attribute and attach parent prefixes.
for (const [componentAttribute] of Object.entries(componentAttributes)) {
let attribute = componentAttribute;
// If there is a attribute name switch use the new one.
if (newName !== realName) {
attribute = componentAttribute.replace(realName, newName);
}
// Check if current attribute is used strip component prefix from attribute and replace it with parent prefix.
if (currentAttributes) {
attribute = componentAttribute.replace(`${lowerFirst(camelCase(realName))}`, '');
}
// Wrapper attributes that should not be modified.
const isWrapperAttribute = attribute.startsWith('wrapper') || attribute.startsWith('showWrapper');
// Determine if parent is empty and if parent name is the same as component/block name and skip wrapper attributes.
let attributeName = isWrapperAttribute ? attribute : `${newParent}${upperFirst(attribute)}`;
// Output new attribute names.
output[attributeName] = componentAttributes[componentAttribute];
}
return output;
};
/**
* Iterate over component object in block manifest and check if the component exists in the project.
* If components contains more component this function will run recursively.
*
* @param {object} componentsManifest - Object of components manifest to iterate.
* @param {object} manifest - Object of component/block manifest to get the data from.
* @param {boolean} [isExample=false] - Type of items to iterate, if true example key will be used, if false attributes will be used.
* @param {string} [parent=''] - Parent component key with stacked parent component names for the final output.
*
* @access private
*
* @returns {object}
*/
export const prepareComponentAttributes = (
componentsManifest,
manifest,
isExample = false,
parent = ''
) => {
const output = {};
const {
components = {},
} = manifest;
// Determine if this is component or block and provide the name, not used for anything important but only to output the error msg.
const name = manifest?.blockName ? manifest.blockName : manifest.componentName;
const newParent = (parent === '') ? name : parent;
// Iterate over components key in manifest recursively and check component names.
for (let [newComponentName, realComponentName] of Object.entries(components)) {
// Filter components real name.
const [component] = componentsManifest.filter((item) => item.componentName === kebabCase(realComponentName));
// Bailout if component doesn't exist.
if (!component) {
throw Error(`Component specified in "${name}" manifest doesn't exist in your components list. Please check if you project has "${realComponentName}" component.`);
}
let outputAttributes = {};
// If component has more components do recursive loop.
if (component?.components) {
outputAttributes = prepareComponentAttributes(componentsManifest, component, isExample, `${newParent}${upperFirst(camelCase(newComponentName))}`);
} else {
// Output the component attributes if there is no nesting left, and append the parent prefixes.
outputAttributes = prepareComponentAttribute(component, newComponentName, realComponentName, isExample, newParent);
}
// Populate the output recursively.
Object.assign(
output,
{
...output,
...outputAttributes,
}
);
}
// Add the current block/component attributes to the output.
Object.assign(output, prepareComponentAttribute(manifest, '', name, isExample, newParent, true));
return output;
};
/**
* Get Block attributes combined in one: "shared, global, wrapper, components, block".
*
* @param {object} globalManifest - Global manifest.
* @param {object} wrapperManifest - `Wrapper` manifest.
* @param {object} componentsManifest - Component manifest to iterate through.
* @param {object} parentManifest - Block or component (parent) manifest.
*
* @returns {object} Object of all attributes registered for a specific block.
*
* @access private
*
* Usage:
* ```js
* getAttributes(globalManifest, wrapperManifest, componentManifests, manifest)
* ```
*/
export const getAttributes = (
globalManifest,
wrapperManifest,
componentsManifest,
parentManifest
) => {
const {
blockName,
} = parentManifest;
const {
attributes: attributesGlobal,
blockClassPrefix = 'block',
} = globalManifest;
const output = {
blockName: {
type: 'string',
default: blockName,
},
blockClientId: {
type: 'string'
},
blockTopLevelId: { // Used to pass reference to all components.
type: 'string',
default: getUnique(),
},
blockFullName: {
type: 'string',
default: getFullBlockName(globalManifest, parentManifest),
},
blockClass: {
type: 'string',
default: `${blockClassPrefix}-${blockName}`,
},
blockJsClass: {
type: 'string',
default: `js-${blockClassPrefix}-${blockName}`,
},
...((typeof attributesGlobal === 'undefined') ? {} : attributesGlobal),
...(wrapperManifest?.attributes ?? {}),
...prepareComponentAttributes(componentsManifest, parentManifest),
};
return output;
};
/**
* Get Block example attributes combined in one: "components and block".
*
* @param {object} manifest - Block/component manifest.
* @param {string} [parent=''] - Parent component key with stacked parent component names for the final output.
*
* @returns {object}
*
* @access private
*
* Manifest:
* ```js
* {
* attributes: {
* buttonUse: {
* type: "string",
* default: true
* },
* buttonSize: {
* type: "string",
* default: "big"
* },
* buttonContent: {
* type: "string"
* },
* }
* }
* ```
*
* Usage:
* ```js
* getExample('button', manifest);
* ```
*
* Output:
* ```js
* {
* "buttonUse": true,
* "buttonSize": "big",
* "buttonContent": "",
* }
* ```
*/
export const getExample = (
parent = '',
manifest = {}
) => {
return prepareComponentAttributes(select(STORE_NAME).getComponents(), manifest, true, parent);
};
/**
* Map and prepare all options from block manifest.json file for usage in registerBlockVariation method.
*
* @param {object} globalManifest - Global manifest.
* @param {object} variationManifest - Variation manifest.
* @param {Array} blocksManifest - Blocks manifests.
*
* @access private
*
* @returns {object}
*/
export const registerVariation = (
globalManifest = {},
variationManifest = {}
) => {
const parentBlockManifest = select(STORE_NAME).getBlock(variationManifest.parentName);
// Append globalManifest data in to output.
if (variationManifest['icon']) {
variationManifest['icon'] = getIconOptions(globalManifest, variationManifest);
} else {
// There is no icon passed to variation, use it's parent icon instead
variationManifest['icon'] = parentBlockManifest?.icon;
}
// Set full example list.
if (typeof variationManifest['example'] === 'undefined') {
variationManifest['example'] = {};
}
// Set full examples list.
variationManifest['example'].viewportWidth = 800;
variationManifest['example'].attributes = {
...parentBlockManifest?.example?.attributes,
...variationManifest['attributes'],
...variationManifest['example']?.attributes,
};
// This is a full block name used in Block Editor.
const fullBlockName = getFullBlockNameVariation(globalManifest, variationManifest);
return {
blockName: fullBlockName,
options: {
...variationManifest,
blockName: fullBlockName,
},
};
};
/**
* Map and prepare all options from block manifest.json file for usage in registerBlockType method.
*
* @param {object} globalManifest - Global manifest.
* @param {object} wrapperManifest - `Wrapper` manifest.
* @param {object} componentsManifest - Manifest of all components in a single object.
* @param {object} blockManifest - Block manifest.
* @param {function} wrapperComponent - Callback function that returns a `Wrapper`.
* @param {function} blockComponent - Edit callback function.
*
* @access private
*
* @returns {object}
*/
export const registerBlock = (
globalManifest = {},
wrapperManifest = {},
componentsManifest = {},
blockManifest = {},
wrapperComponent,
blockComponent
) => {
// Block Icon option.
blockManifest['icon'] = getIconOptions(globalManifest, blockManifest);
// This is a full block name used in Block Editor.
const fullBlockName = getFullBlockName(globalManifest, blockManifest);
// Set full attributes list.
const attributes = getAttributes(globalManifest, wrapperManifest, componentsManifest, blockManifest);
blockManifest['attributes'] = {
metadata: {
type: 'object'
},
...attributes,
};
// Set full example list.
if (typeof blockManifest['example'] === 'undefined') {
blockManifest['example'] = {};
}
// Find all attributes that have default value and output that to example.
const exampleAttributes = {};
for (const [key, value] of Object.entries(attributes)) {
if (value?.default) {
exampleAttributes[key] = value.default;
}
}
// Set full examples list.
blockManifest['example'].viewportWidth = 800;
blockManifest['example'].attributes = {
...exampleAttributes,
...getExample('', blockManifest),
};
// Block supports.
if (typeof blockManifest['supports'] === 'undefined') {
blockManifest['supports'] = {};
}
return {
blockName: fullBlockName,
options: {
...blockManifest,
blockName: fullBlockName,
edit: getEditCallback(blockComponent, wrapperComponent),
save: getSaveCallback(blockManifest),
merge: getMergeCallback(blockManifest),
// WP 6.4+ Block renaming support
__experimentalBlockRenaming: true,
__experimentalLabel: (attributes, { context }) => {
const customName = attributes?.metadata?.name ?? blockManifest?.title ?? fullBlockName;
if (context === 'visual' && customName !== blockManifest?.title) {
return `${customName} (${blockManifest?.title})`;
}
if (context === 'accessibility') {
const { content } = attributes;
return !content || content?.length === 0 ? __('Empty', 'eightshift-frontend-libs') : content;
}
return customName;
},
supports: {
...blockManifest['supports'],
__experimentalMetadata: true,
},
},
};
};