rebase from live enviornment
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
BlockConfiguration,
|
||||
registerBlockType,
|
||||
unregisterBlockType,
|
||||
registerBlockVariation,
|
||||
unregisterBlockVariation,
|
||||
BlockVariation,
|
||||
BlockAttributes,
|
||||
} from '@wordpress/blocks';
|
||||
|
||||
export interface BlockRegistrationStrategy {
|
||||
register(
|
||||
blockNameOrMetadata: string | Partial< BlockConfiguration >,
|
||||
blockSettings: Partial< BlockConfiguration >
|
||||
): boolean;
|
||||
unregister( blockName: string, variationName?: string ): boolean;
|
||||
}
|
||||
|
||||
export class BlockTypeStrategy implements BlockRegistrationStrategy {
|
||||
register(
|
||||
blockNameOrMetadata: string | Partial< BlockConfiguration >,
|
||||
blockSettings: Partial< BlockConfiguration >
|
||||
): boolean {
|
||||
return Boolean(
|
||||
// @ts-expect-error: `registerBlockType` is typed in WordPress core
|
||||
registerBlockType( blockNameOrMetadata, blockSettings )
|
||||
);
|
||||
}
|
||||
|
||||
unregister( blockName: string ): boolean {
|
||||
return Boolean( unregisterBlockType( blockName ) );
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy for BlockVariation
|
||||
export class BlockVariationStrategy implements BlockRegistrationStrategy {
|
||||
register(
|
||||
blockName: string,
|
||||
blockSettings: Partial< BlockConfiguration >
|
||||
): boolean {
|
||||
return Boolean(
|
||||
registerBlockVariation(
|
||||
blockName,
|
||||
blockSettings as BlockVariation< BlockAttributes >
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
unregister( blockName: string, variationName: string ): boolean {
|
||||
return Boolean( unregisterBlockVariation( blockName, variationName ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getBlockType } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
TemplateChangeDetector,
|
||||
TemplateChangeDetectorObserver,
|
||||
} from './template-change-detector';
|
||||
import {
|
||||
BlockRegistrationStrategy,
|
||||
BlockTypeStrategy,
|
||||
BlockVariationStrategy,
|
||||
} from './block-registration-strategy';
|
||||
import { BLOCKS_WITH_RESTRICTION } from './blocks-with-restriction';
|
||||
|
||||
/**
|
||||
* Manages the registration and unregistration of blocks based on template or page restrictions.
|
||||
*
|
||||
* This class implements the TemplateChangeDetectorObserver interface and is responsible for managing the registration and unregistration of blocks based on the restrictions defined in the BLOCKS_WITH_RESTRICTION constant.
|
||||
*
|
||||
* The class maintains a list of unregistered blocks and uses a block registration strategy to register and unregister blocks as needed. The strategy used depends on whether the block is a variation block or a regular block.
|
||||
*
|
||||
* The `run` method is the main entry point for the class. It is called with a TemplateChangeDetector object and registers and unregisters blocks based on the current template and whether the editor is in post or page mode.
|
||||
*/
|
||||
export class BlockRegistrationManager
|
||||
implements TemplateChangeDetectorObserver
|
||||
{
|
||||
private unregisteredBlocks: string[] = [];
|
||||
private blockRegistrationStrategy: BlockRegistrationStrategy;
|
||||
|
||||
constructor() {
|
||||
this.blockRegistrationStrategy = new BlockTypeStrategy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a block should be registered based on the current template or page.
|
||||
*
|
||||
* This method checks whether a block with restrictions should be registered based on the current template ID and
|
||||
* whether the editor is in post or page mode. It checks whether the current template ID starts with any of the
|
||||
* allowed templates or template parts for the block, and whether the block is available in the post or page editor.
|
||||
*
|
||||
* @param {Object} params - The parameters for the method.
|
||||
* @param {string} params.blockWithRestrictionName - The name of the block with restrictions.
|
||||
* @param {string} params.currentTemplateId - The ID of the current template.
|
||||
* @param {boolean} params.isPostOrPage - Whether the editor is in a post or page.
|
||||
* @return {boolean} True if the block should be registered, false otherwise.
|
||||
*/
|
||||
private shouldBlockBeRegistered( {
|
||||
blockWithRestrictionName,
|
||||
currentTemplateId,
|
||||
isPostOrPage,
|
||||
}: {
|
||||
blockWithRestrictionName: string;
|
||||
currentTemplateId: string;
|
||||
isPostOrPage: boolean;
|
||||
} ) {
|
||||
const {
|
||||
allowedTemplates,
|
||||
allowedTemplateParts,
|
||||
availableInPostOrPageEditor,
|
||||
} = BLOCKS_WITH_RESTRICTION[ blockWithRestrictionName ];
|
||||
const shouldBeAvailableOnTemplate = Object.keys(
|
||||
allowedTemplates
|
||||
).some( ( allowedTemplate ) =>
|
||||
currentTemplateId.startsWith( allowedTemplate )
|
||||
);
|
||||
const shouldBeAvailableOnTemplatePart = Object.keys(
|
||||
allowedTemplateParts
|
||||
).some( ( allowedTemplate ) =>
|
||||
currentTemplateId.startsWith( allowedTemplate )
|
||||
);
|
||||
const shouldBeAvailableOnPostOrPageEditor =
|
||||
isPostOrPage && availableInPostOrPageEditor;
|
||||
|
||||
return (
|
||||
shouldBeAvailableOnTemplate ||
|
||||
shouldBeAvailableOnTemplatePart ||
|
||||
shouldBeAvailableOnPostOrPageEditor
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters blocks before entering a restricted area based on the current template or page/post.
|
||||
*
|
||||
* This method iterates over all blocks with restrictions and unregisters them if they should not be registered
|
||||
* based on the current template ID and whether the editor is in a post or page. It uses a block registration
|
||||
* strategy to unregister the blocks, which depends on whether the block is a variation block or a regular block.
|
||||
*
|
||||
* @param {Object} params - The parameters for the method.
|
||||
* @param {string} params.currentTemplateId - The ID of the current template.
|
||||
* @param {boolean} params.isPostOrPage - Whether the editor is in post or page mode.
|
||||
*/
|
||||
unregisterBlocksBeforeEnteringRestrictedArea( {
|
||||
currentTemplateId,
|
||||
isPostOrPage,
|
||||
}: {
|
||||
currentTemplateId: string;
|
||||
isPostOrPage: boolean;
|
||||
} ) {
|
||||
for ( const blockWithRestrictionName of Object.keys(
|
||||
BLOCKS_WITH_RESTRICTION
|
||||
) ) {
|
||||
if (
|
||||
this.shouldBlockBeRegistered( {
|
||||
blockWithRestrictionName,
|
||||
currentTemplateId,
|
||||
isPostOrPage,
|
||||
} )
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! getBlockType( blockWithRestrictionName ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.blockRegistrationStrategy = BLOCKS_WITH_RESTRICTION[
|
||||
blockWithRestrictionName
|
||||
].isVariationBlock
|
||||
? new BlockVariationStrategy()
|
||||
: new BlockTypeStrategy();
|
||||
|
||||
this.blockRegistrationStrategy.unregister(
|
||||
blockWithRestrictionName
|
||||
);
|
||||
this.unregisteredBlocks.push( blockWithRestrictionName );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers blocks after leaving a restricted area.
|
||||
*
|
||||
* This method iterates over all unregistered blocks and registers them if they are not restricted in the current context.
|
||||
* It uses a block registration strategy to register the blocks, which depends on whether the block is a variation block or a regular block.
|
||||
* If the block is successfully registered, it is removed from the list of unregistered blocks.
|
||||
*/
|
||||
registerBlocksAfterLeavingRestrictedArea() {
|
||||
for ( const unregisteredBlockName of this.unregisteredBlocks ) {
|
||||
if ( ! getBlockType( unregisteredBlockName ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const restrictedBlockData =
|
||||
BLOCKS_WITH_RESTRICTION[ unregisteredBlockName ];
|
||||
this.blockRegistrationStrategy = BLOCKS_WITH_RESTRICTION[
|
||||
unregisteredBlockName
|
||||
].isVariationBlock
|
||||
? new BlockVariationStrategy()
|
||||
: new BlockTypeStrategy();
|
||||
const isBlockRegistered = this.blockRegistrationStrategy.register(
|
||||
restrictedBlockData.blockMetadata,
|
||||
restrictedBlockData.blockSettings
|
||||
);
|
||||
this.unregisteredBlocks = isBlockRegistered
|
||||
? this.unregisteredBlocks.filter(
|
||||
( blockName ) => blockName !== unregisteredBlockName
|
||||
)
|
||||
: this.unregisteredBlocks;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the block registration manager.
|
||||
*
|
||||
* This method is the main entry point for the block registration manager. It is called with a TemplateChangeDetector object,
|
||||
* and registers and unregisters blocks based on the current template and whether the editor is in a post or page.
|
||||
*
|
||||
* @param {TemplateChangeDetector} templateChangeDetector - The template change detector object.
|
||||
*/
|
||||
run( templateChangeDetector: TemplateChangeDetector ) {
|
||||
this.registerBlocksAfterLeavingRestrictedArea();
|
||||
this.unregisterBlocksBeforeEnteringRestrictedArea( {
|
||||
currentTemplateId:
|
||||
templateChangeDetector.getCurrentTemplateId() || '',
|
||||
isPostOrPage: templateChangeDetector.getIsPostOrPage(),
|
||||
} );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { BlockConfiguration } from '@wordpress/blocks';
|
||||
import { ProductGalleryBlockSettings } from '@woocommerce/blocks/product-gallery/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import productGalleryBlockMetadata from '../../../blocks/product-gallery/block.json';
|
||||
|
||||
export interface BlocksWithRestriction {
|
||||
[ key: string ]: {
|
||||
blockMetadata: Partial< BlockConfiguration >;
|
||||
blockSettings: Partial< BlockConfiguration >;
|
||||
allowedTemplates: {
|
||||
[ key: string ]: boolean;
|
||||
};
|
||||
allowedTemplateParts: {
|
||||
[ key: string ]: boolean;
|
||||
};
|
||||
availableInPostOrPageEditor: boolean;
|
||||
isVariationBlock: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error: `metadata` currently does not have a type definition in WordPress core
|
||||
export const BLOCKS_WITH_RESTRICTION: BlocksWithRestriction = {
|
||||
[ productGalleryBlockMetadata.name ]: {
|
||||
blockMetadata: productGalleryBlockMetadata,
|
||||
blockSettings: ProductGalleryBlockSettings,
|
||||
allowedTemplates: {
|
||||
'single-product': true,
|
||||
},
|
||||
allowedTemplateParts: {
|
||||
'product-gallery': true,
|
||||
},
|
||||
availableInPostOrPageEditor: false,
|
||||
isVariationBlock: false,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import domReady from '@wordpress/dom-ready';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { BlockRegistrationManager } from './blocks-registration-manager';
|
||||
import { TemplateChangeDetector } from './template-change-detector';
|
||||
|
||||
domReady( () => {
|
||||
const templateChangeDetector = new TemplateChangeDetector();
|
||||
const blockRegistrationManager = new BlockRegistrationManager();
|
||||
templateChangeDetector.add( blockRegistrationManager );
|
||||
} );
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { subscribe, select } from '@wordpress/data';
|
||||
import { isNumber } from '@woocommerce/types';
|
||||
|
||||
interface TemplateChangeDetectorSubject {
|
||||
add( observer: TemplateChangeDetectorObserver ): void;
|
||||
getPreviousTemplateId(): string | undefined;
|
||||
getCurrentTemplateId(): string | undefined;
|
||||
notify(): void;
|
||||
}
|
||||
|
||||
export interface TemplateChangeDetectorObserver {
|
||||
run( subject: TemplateChangeDetectorSubject ): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class implements the TemplateChangeDetectorSubject interface and is responsible for detecting changes in the
|
||||
* current template or page and notifying any observers of these changes. It maintains a list of observers and provides methods
|
||||
* to add observers and notify them of changes.
|
||||
*
|
||||
* The class also provides methods to get the previous and current template IDs and whether the editor is in a post or page.
|
||||
*
|
||||
* The `checkIfTemplateHasChangedAndNotifySubscribers` method is the main method of the class. It checks if the current
|
||||
* template has changed and, if so, notifies all observers.
|
||||
*/
|
||||
export class TemplateChangeDetector implements TemplateChangeDetectorSubject {
|
||||
private previousTemplateId: string | undefined;
|
||||
private currentTemplateId: string | undefined;
|
||||
private isPostOrPage: boolean;
|
||||
|
||||
private observers: TemplateChangeDetectorObserver[] = [];
|
||||
|
||||
constructor() {
|
||||
this.isPostOrPage = false;
|
||||
subscribe( () => {
|
||||
this.checkIfTemplateHasChangedAndNotifySubscribers();
|
||||
}, 'core/edit-site' );
|
||||
}
|
||||
|
||||
public add( observer: TemplateChangeDetectorObserver ): void {
|
||||
this.observers.push( observer );
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger an update in each subscriber.
|
||||
*/
|
||||
public notify(): void {
|
||||
for ( const observer of this.observers ) {
|
||||
observer.run( this );
|
||||
}
|
||||
}
|
||||
|
||||
public getPreviousTemplateId() {
|
||||
return this.previousTemplateId;
|
||||
}
|
||||
|
||||
public getCurrentTemplateId() {
|
||||
return this.currentTemplateId;
|
||||
}
|
||||
|
||||
public getIsPostOrPage() {
|
||||
return this.isPostOrPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the template ID.
|
||||
*
|
||||
* This method takes a template or a post ID and returns it parsed in the expected format.
|
||||
*
|
||||
* @param {string | number | undefined} templateId - The template ID to parse.
|
||||
* @return {string | undefined} The parsed template ID.
|
||||
*/
|
||||
private parseTemplateId(
|
||||
templateId: string | number | undefined
|
||||
): string | undefined {
|
||||
if ( isNumber( templateId ) ) {
|
||||
return String( templateId );
|
||||
}
|
||||
return templateId?.split( '//' )[ 1 ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current template or page has changed and notifies subscribers.
|
||||
*
|
||||
* If the current template ID has changed and is not undefined (which means that it is not a page, post or template), it notifies all subscribers.
|
||||
*/
|
||||
public checkIfTemplateHasChangedAndNotifySubscribers(): void {
|
||||
this.previousTemplateId = this.currentTemplateId;
|
||||
|
||||
const postOrPageId = select( 'core/editor' )?.getCurrentPostId<
|
||||
string | number | undefined
|
||||
>();
|
||||
|
||||
this.isPostOrPage = Boolean( postOrPageId );
|
||||
|
||||
const editedPostId =
|
||||
postOrPageId ||
|
||||
select( 'core/edit-site' )?.getEditedPostId<
|
||||
string | number | undefined
|
||||
>();
|
||||
this.currentTemplateId = this.parseTemplateId( editedPostId );
|
||||
|
||||
const hasChangedTemplate =
|
||||
this.previousTemplateId !== this.currentTemplateId;
|
||||
const hasTemplateId = Boolean( this.currentTemplateId );
|
||||
|
||||
if ( ! hasChangedTemplate || ! hasTemplateId ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createBlock } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Creates blocks for a given inner blocks Template.
|
||||
*
|
||||
* @param {Array} template Inner Blocks Template.
|
||||
*/
|
||||
export const createBlocksFromTemplate = ( template ) => {
|
||||
return template.map( ( [ name, atts = {}, innerBlocks = [] ] ) => {
|
||||
const children = innerBlocks
|
||||
? createBlocksFromTemplate( innerBlocks )
|
||||
: [];
|
||||
return createBlock( name, atts, children );
|
||||
} );
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getRegisteredBlockComponents } from '@woocommerce/blocks-registry';
|
||||
import type { RegisteredBlockComponent } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import '../blocks/component-init';
|
||||
|
||||
/**
|
||||
* Map named Blocks to defined React Components to render on the frontend.
|
||||
*
|
||||
* @param {string} blockName Name of the parent block.
|
||||
*/
|
||||
export const getBlockMap = (
|
||||
blockName: string
|
||||
): Record< string, RegisteredBlockComponent > =>
|
||||
getRegisteredBlockComponents( blockName );
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './get-block-map';
|
||||
export * from './create-blocks-from-template';
|
||||
export * from './render-parent-block';
|
||||
export * from './render-standalone-blocks';
|
||||
export * from './register-block-single-product-template';
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isNumber, isEmpty } from '@woocommerce/types';
|
||||
import {
|
||||
BlockAttributes,
|
||||
BlockConfiguration,
|
||||
BlockVariation,
|
||||
getBlockType,
|
||||
registerBlockType,
|
||||
registerBlockVariation,
|
||||
unregisterBlockType,
|
||||
unregisterBlockVariation,
|
||||
} from '@wordpress/blocks';
|
||||
import { subscribe, select } from '@wordpress/data';
|
||||
|
||||
// Creating a local cache to prevent multiple registration tries.
|
||||
const blocksRegistered = new Set();
|
||||
|
||||
function parseTemplateId( templateId: string | number | undefined ) {
|
||||
// With GB 16.3.0 the return type can be a number: https://github.com/WordPress/gutenberg/issues/53230
|
||||
const parsedTemplateId = isNumber( templateId ) ? undefined : templateId;
|
||||
return parsedTemplateId?.split( '//' )[ 1 ];
|
||||
}
|
||||
|
||||
export const registerBlockSingleProductTemplate = ( {
|
||||
blockName,
|
||||
blockMetadata,
|
||||
blockSettings,
|
||||
isVariationBlock = false,
|
||||
variationName,
|
||||
isAvailableOnPostEditor,
|
||||
}: {
|
||||
blockName: string;
|
||||
blockMetadata: Partial< BlockConfiguration >;
|
||||
blockSettings: Partial< BlockConfiguration >;
|
||||
isAvailableOnPostEditor: boolean;
|
||||
isVariationBlock?: boolean;
|
||||
variationName?: string;
|
||||
} ) => {
|
||||
let currentTemplateId: string | undefined = '';
|
||||
|
||||
subscribe( () => {
|
||||
const previousTemplateId = currentTemplateId;
|
||||
const store = select( 'core/edit-site' );
|
||||
|
||||
// With GB 16.3.0 the return type can be a number: https://github.com/WordPress/gutenberg/issues/53230
|
||||
currentTemplateId = parseTemplateId(
|
||||
store?.getEditedPostId< string | number | undefined >()
|
||||
);
|
||||
const hasChangedTemplate = previousTemplateId !== currentTemplateId;
|
||||
const hasTemplateId = Boolean( currentTemplateId );
|
||||
|
||||
if ( ! hasChangedTemplate || ! hasTemplateId || ! blockName ) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isBlockRegistered = Boolean( getBlockType( blockName ) );
|
||||
|
||||
/**
|
||||
* We need to unregister the block each time the user visits or leaves the Single Product template.
|
||||
*
|
||||
* The Single Product template is the only template where the `ancestor` property is not needed because it provides the context
|
||||
* for the product blocks. We need to unregister and re-register the block to remove or add the `ancestor` property depending on which
|
||||
* location (template, post, page, etc.) the user is in.
|
||||
*
|
||||
*/
|
||||
if (
|
||||
isBlockRegistered &&
|
||||
( currentTemplateId?.includes( 'single-product' ) ||
|
||||
previousTemplateId?.includes( 'single-product' ) )
|
||||
) {
|
||||
if ( isVariationBlock && variationName ) {
|
||||
unregisterBlockVariation( blockName, variationName );
|
||||
} else {
|
||||
unregisterBlockType( blockName );
|
||||
}
|
||||
isBlockRegistered = false;
|
||||
}
|
||||
|
||||
if ( ! isBlockRegistered ) {
|
||||
if ( isVariationBlock ) {
|
||||
// @ts-expect-error: `registerBlockType` is not typed in WordPress core
|
||||
registerBlockVariation( blockName, blockSettings );
|
||||
} else {
|
||||
const ancestor = isEmpty( blockSettings?.ancestor )
|
||||
? [ 'woocommerce/single-product' ]
|
||||
: blockSettings?.ancestor;
|
||||
// @ts-expect-error: `registerBlockType` is not typed in WordPress core
|
||||
registerBlockType( blockMetadata, {
|
||||
...blockSettings,
|
||||
ancestor: ! currentTemplateId?.includes( 'single-product' )
|
||||
? ancestor
|
||||
: undefined,
|
||||
} );
|
||||
}
|
||||
}
|
||||
}, 'core/edit-site' );
|
||||
|
||||
subscribe( () => {
|
||||
const isBlockRegistered = Boolean( variationName )
|
||||
? blocksRegistered.has( variationName )
|
||||
: blocksRegistered.has( blockName );
|
||||
// This subscribe callback could be invoked with the core/blocks store
|
||||
// which would cause infinite registration loops because of the `registerBlockType` call.
|
||||
// This local cache helps prevent that.
|
||||
if ( ! isBlockRegistered && isAvailableOnPostEditor ) {
|
||||
if ( isVariationBlock ) {
|
||||
blocksRegistered.add( variationName );
|
||||
registerBlockVariation(
|
||||
blockName,
|
||||
blockSettings as BlockVariation< BlockAttributes >
|
||||
);
|
||||
} else {
|
||||
blocksRegistered.add( blockName );
|
||||
// @ts-expect-error: `registerBlockType` is typed in WordPress core
|
||||
registerBlockType( blockMetadata, blockSettings );
|
||||
}
|
||||
}
|
||||
}, 'core/edit-post' );
|
||||
};
|
||||
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { renderFrontend } from '@woocommerce/base-utils';
|
||||
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
|
||||
import {
|
||||
Fragment,
|
||||
Suspense,
|
||||
isValidElement,
|
||||
cloneElement,
|
||||
} from '@wordpress/element';
|
||||
import parse from 'html-react-parser';
|
||||
import {
|
||||
getRegisteredBlocks,
|
||||
hasInnerBlocks,
|
||||
} from '@woocommerce/blocks-checkout';
|
||||
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
||||
|
||||
/**
|
||||
* This file contains logic used on the frontend to convert DOM elements (saved by the block editor) to React
|
||||
* Components. These components are registered using registerBlockComponent() and registerCheckoutBlock() and map 1:1
|
||||
* to a block by name.
|
||||
*
|
||||
* Blocks using this system will have their blockName stored as a data attribute, for example:
|
||||
* <div data-block-name="woocommerce/product-title"></div>
|
||||
*
|
||||
* This block name is then read, and using the map, dynamically converted to a real React Component.
|
||||
*
|
||||
* @see registerBlockComponent
|
||||
* @see registerCheckoutBlock
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gets a component from the block map for a given block name, or returns null if a component is not registered.
|
||||
*/
|
||||
const getBlockComponentFromMap = (
|
||||
block: string,
|
||||
blockMap: Record< string, React.ReactNode >
|
||||
): React.ElementType | null => {
|
||||
return block && blockMap[ block ]
|
||||
? ( blockMap[ block ] as React.ElementType )
|
||||
: null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Render forced blocks which are missing from the template.
|
||||
*
|
||||
* Forced blocks are registered in registerCheckoutBlock. If a block is forced, it will be inserted in the editor
|
||||
* automatically, however, until that happens they may be missing from the frontend. To fix this, we look up what blocks
|
||||
* are registered as forced, and then append them here if they are missing.
|
||||
*
|
||||
* @see registerCheckoutBlock
|
||||
*/
|
||||
const renderForcedBlocks = (
|
||||
block: string,
|
||||
blockMap: Record< string, React.ReactNode >,
|
||||
// Current children from the parent (siblings of the forced block)
|
||||
blockChildren: NodeListOf< ChildNode > | null,
|
||||
// Wrapper for inner components.
|
||||
blockWrapper?: React.ElementType
|
||||
) => {
|
||||
if ( ! hasInnerBlocks( block ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentBlocks = blockChildren
|
||||
? ( Array.from( blockChildren )
|
||||
.map( ( node: Node ) =>
|
||||
node instanceof HTMLElement
|
||||
? node?.dataset.blockName || null
|
||||
: null
|
||||
)
|
||||
.filter( Boolean ) as string[] )
|
||||
: [];
|
||||
|
||||
const forcedBlocks = getRegisteredBlocks( block ).filter(
|
||||
( { blockName, force } ) =>
|
||||
force === true && ! currentBlocks.includes( blockName )
|
||||
);
|
||||
|
||||
// This will wrap inner blocks with the provided wrapper. If no wrapper is provided, we default to Fragment.
|
||||
const InnerBlockComponentWrapper = blockWrapper ? blockWrapper : Fragment;
|
||||
|
||||
return (
|
||||
<>
|
||||
{ forcedBlocks.map(
|
||||
(
|
||||
{ blockName, component },
|
||||
index: number
|
||||
): JSX.Element | null => {
|
||||
const ForcedComponent = component
|
||||
? component
|
||||
: getBlockComponentFromMap( blockName, blockMap );
|
||||
return ForcedComponent ? (
|
||||
<BlockErrorBoundary
|
||||
key={ `${ blockName }_blockerror` }
|
||||
text={ `Unexpected error in: ${ blockName }` }
|
||||
showErrorBlock={ CURRENT_USER_IS_ADMIN as boolean }
|
||||
>
|
||||
<InnerBlockComponentWrapper>
|
||||
<ForcedComponent
|
||||
key={ `${ blockName }_forced_${ index }` }
|
||||
/>
|
||||
</InnerBlockComponentWrapper>
|
||||
</BlockErrorBoundary>
|
||||
) : null;
|
||||
}
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface renderInnerBlocksProps {
|
||||
// Block (parent) being rendered. Used for inner block component mapping.
|
||||
block: string;
|
||||
// Map of block names to block components for children.
|
||||
blockMap: Record< string, React.ReactNode >;
|
||||
// Wrapper for inner components.
|
||||
blockWrapper?: React.ElementType | undefined;
|
||||
// Elements from the DOM being converted to components.
|
||||
children: HTMLCollection | NodeList;
|
||||
// Depth within the DOM hierarchy.
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively replace block markup in the DOM with React Components.
|
||||
*/
|
||||
const renderInnerBlocks = ( {
|
||||
// This is the parent block we're working within (see renderParentBlock)
|
||||
block,
|
||||
// This is the map of blockNames->components
|
||||
blockMap,
|
||||
// Component which inner blocks are wrapped with.
|
||||
blockWrapper,
|
||||
// The children from the DOM we're currently iterating over.
|
||||
children,
|
||||
// Current depth of the children. Used to ensure keys are unique.
|
||||
depth = 1,
|
||||
}: renderInnerBlocksProps ): ( string | JSX.Element | null )[] | null => {
|
||||
if ( ! children || children.length === 0 ) {
|
||||
return null;
|
||||
}
|
||||
return Array.from( children ).map( ( node: Node, index: number ) => {
|
||||
/**
|
||||
* This will grab the blockName from the data- attributes stored in block markup. Without a blockName, we cannot
|
||||
* convert the HTMLElement to a React component.
|
||||
*/
|
||||
const { blockName = '', ...componentProps } = {
|
||||
...( node instanceof HTMLElement ? node.dataset : {} ),
|
||||
className: node instanceof Element ? node?.className : '',
|
||||
};
|
||||
const componentKey = `${ block }_${ depth }_${ index }`;
|
||||
const InnerBlockComponent = getBlockComponentFromMap(
|
||||
blockName,
|
||||
blockMap
|
||||
);
|
||||
|
||||
/**
|
||||
* If the component cannot be found, or blockName is missing, return the original element. This also ensures
|
||||
* that children within the element are processed also, since it may be an element containing block markup.
|
||||
*
|
||||
* Note we use childNodes rather than children so that text nodes are also rendered.
|
||||
*/
|
||||
if ( ! InnerBlockComponent ) {
|
||||
const parsedElement = parse(
|
||||
( node instanceof Element && node?.outerHTML ) ||
|
||||
node?.textContent ||
|
||||
''
|
||||
);
|
||||
|
||||
// Returns text nodes without manipulation.
|
||||
if ( typeof parsedElement === 'string' && !! parsedElement ) {
|
||||
return parsedElement;
|
||||
}
|
||||
|
||||
// Do not render invalid elements.
|
||||
if ( ! isValidElement( parsedElement ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderedChildren = node.childNodes.length
|
||||
? renderInnerBlocks( {
|
||||
block,
|
||||
blockMap,
|
||||
children: node.childNodes,
|
||||
depth: depth + 1,
|
||||
blockWrapper,
|
||||
} )
|
||||
: undefined;
|
||||
|
||||
// We pass props here rather than componentProps to avoid the data attributes being renamed.
|
||||
return renderedChildren
|
||||
? cloneElement(
|
||||
parsedElement,
|
||||
{
|
||||
key: componentKey,
|
||||
...( parsedElement?.props || {} ),
|
||||
},
|
||||
renderedChildren
|
||||
)
|
||||
: cloneElement( parsedElement, {
|
||||
key: componentKey,
|
||||
...( parsedElement?.props || {} ),
|
||||
} );
|
||||
}
|
||||
|
||||
// This will wrap inner blocks with the provided wrapper. If no wrapper is provided, we default to Fragment.
|
||||
const InnerBlockComponentWrapper = blockWrapper
|
||||
? blockWrapper
|
||||
: Fragment;
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
key={ `${ block }_${ depth }_${ index }_suspense` }
|
||||
fallback={ <div className="wc-block-placeholder" /> }
|
||||
>
|
||||
{ /* Prevent third party components from breaking the entire checkout */ }
|
||||
<BlockErrorBoundary
|
||||
text={ `Unexpected error in: ${ blockName }` }
|
||||
showErrorBlock={ CURRENT_USER_IS_ADMIN as boolean }
|
||||
>
|
||||
<InnerBlockComponentWrapper>
|
||||
<InnerBlockComponent
|
||||
key={ componentKey }
|
||||
{ ...componentProps }
|
||||
>
|
||||
{
|
||||
/**
|
||||
* Within this Inner Block Component we also need to recursively render it's children. This
|
||||
* is done here with a depth+1. The same block map and parent is used, but we pass new
|
||||
* children from this element.
|
||||
*/
|
||||
renderInnerBlocks( {
|
||||
block,
|
||||
blockMap,
|
||||
children: node.childNodes,
|
||||
depth: depth + 1,
|
||||
blockWrapper,
|
||||
} )
|
||||
}
|
||||
{
|
||||
/**
|
||||
* In addition to the inner blocks, we may also need to render FORCED blocks which have not
|
||||
* yet been added to the inner block template. We do this by comparing the current children
|
||||
* to the list of registered forced blocks.
|
||||
*
|
||||
* @see registerCheckoutBlock
|
||||
*/
|
||||
renderForcedBlocks(
|
||||
blockName,
|
||||
blockMap,
|
||||
node.childNodes,
|
||||
blockWrapper
|
||||
)
|
||||
}
|
||||
</InnerBlockComponent>
|
||||
</InnerBlockComponentWrapper>
|
||||
</BlockErrorBoundary>
|
||||
</Suspense>
|
||||
);
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Render a parent block on the frontend.
|
||||
*
|
||||
* This is the main entry point used on the frontend to convert Block Markup (with inner blocks) in the DOM to React
|
||||
* Components.
|
||||
*
|
||||
* This uses renderFrontend(). The difference is, renderFrontend renders a single block, but renderParentBlock() also
|
||||
* handles inner blocks by recursively running over children from the DOM.
|
||||
*
|
||||
* @see renderInnerBlocks
|
||||
* @see renderFrontend
|
||||
*/
|
||||
export const renderParentBlock = ( {
|
||||
Block,
|
||||
selector,
|
||||
blockName,
|
||||
getProps = () => ( {} ),
|
||||
blockMap,
|
||||
blockWrapper,
|
||||
}: {
|
||||
// Parent Block Name. Used for inner block component mapping.
|
||||
blockName: string;
|
||||
// Map of block names to block components for children.
|
||||
blockMap: Record< string, React.ReactNode >;
|
||||
// Wrapper for inner components.
|
||||
blockWrapper?: React.ElementType;
|
||||
// React component to use as a replacement.
|
||||
Block: React.FunctionComponent;
|
||||
// CSS selector to match the elements to replace.
|
||||
selector: string;
|
||||
// Function to generate the props object for the block.
|
||||
getProps: ( el: Element, i: number ) => Record< string, unknown >;
|
||||
} ): void => {
|
||||
/**
|
||||
* In addition to getProps, we need to render and return the children. This adds children to props.
|
||||
*/
|
||||
const getPropsWithChildren = ( element: Element, i: number ) => {
|
||||
const children = renderInnerBlocks( {
|
||||
block: blockName,
|
||||
blockMap,
|
||||
children: element.children || [],
|
||||
blockWrapper,
|
||||
} );
|
||||
return { ...getProps( element, i ), children };
|
||||
};
|
||||
/**
|
||||
* The only difference between using renderParentBlock and renderFrontend is that here we provide children.
|
||||
*/
|
||||
renderFrontend( {
|
||||
Block,
|
||||
selector,
|
||||
getProps: getPropsWithChildren,
|
||||
} );
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { renderFrontend } from '@woocommerce/base-utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getBlockMap } from './get-block-map';
|
||||
|
||||
export const renderStandaloneBlocks = () => {
|
||||
const blockMap = getBlockMap( '' );
|
||||
|
||||
Object.keys( blockMap ).forEach( ( blockName ) => {
|
||||
const selector = '.wp-block-' + blockName.replace( '/', '-' );
|
||||
|
||||
const getProps = ( el ) => {
|
||||
return el.dataset;
|
||||
};
|
||||
|
||||
renderFrontend( {
|
||||
Block: blockMap[ blockName ],
|
||||
selector,
|
||||
getProps,
|
||||
} );
|
||||
} );
|
||||
};
|
||||
Reference in New Issue
Block a user