/**
* External dependencies
*/
import classnames from 'classnames';
/**
* Internal dependencies
*/
import isCoreBlockWithExt from '../../utils/is-core-block-with-ext';
import getParentBlock from '../../utils/get-parent-block';
const {
canvasBreakpoints,
} = window;
/**
* WordPress dependencies
*/
const {
addFilter,
} = wp.hooks;
const {
Component,
} = wp.element;
const {
createHigherOrderComponent,
} = wp.compose;
const {
hasBlockSupport,
} = wp.blocks;
const { select, subscribe } = wp.data;
/**
* Extend block attributes with unique class name.
*
* @param {Object} blockSettings Original block settings.
* @param {String} name Original block name.
*
* @return {Object} Filtered block settings.
*/
function addAttribute( blockSettings, name ) {
let supports = (
hasBlockSupport( blockSettings, 'canvasUniqueClass', false ) ||
hasBlockSupport( blockSettings, 'canvasBackgroundImage', false ) ||
hasBlockSupport( blockSettings, 'canvasSpacings', false ) ||
hasBlockSupport( blockSettings, 'canvasBorder', false ) ||
hasBlockSupport( blockSettings, 'canvasResponsive', false )
);
// add support to core blocks
if ( isCoreBlockWithExt( name ) ) {
blockSettings.supports = {
...blockSettings.supports,
canvasUniqueClass: true,
};
supports = true;
}
if ( supports ) {
if ( blockSettings.attributes && ! blockSettings.attributes.canvasClassName ) {
blockSettings.attributes.canvasClassName = {
type: 'string',
};
}
}
return blockSettings;
}
/**
* Override the default edit UI to include a new block inspector control for
* assigning the custom spacings if needed.
*
* @param {function|Component} BlockEdit Original component.
*
* @return {string} Wrapped component.
*/
const withInspectorControl = createHigherOrderComponent( ( OriginalComponent ) => {
class CanvasUniqueClassNameWrapper extends Component {
constructor() {
super( ...arguments );
this.getAllBlocks = this.getAllBlocks.bind( this );
this.maybeCreateUniqueClassName = this.maybeCreateUniqueClassName.bind( this );
}
componentDidMount() {
this.maybeCreateUniqueClassName( true );
}
componentDidUpdate() {
this.maybeCreateUniqueClassName();
}
/**
* Get recursive all blocks of the current page
*/
getAllBlocks( blocks = false ) {
let result = [];
if ( ! blocks ) {
blocks = wp.data.select( 'core/block-editor' ).getBlocks();
}
if ( ! blocks ) {
return result;
}
blocks.forEach( ( data ) => {
result.push( data );
if ( data.innerBlocks && data.innerBlocks.length ) {
result = [
...result,
...this.getAllBlocks( data.innerBlocks ),
];
}
} );
return result;
}
/**
* Generate unique block class name
*/
maybeCreateUniqueClassName( checkDuplicates ) {
const {
name,
attributes,
setAttributes,
clientId,
} = this.props;
const {
getBlockHierarchyRootClientId,
getBlock,
} = select('core/block-editor');
const rootBlock = getBlock(getBlockHierarchyRootClientId(clientId));
if ( rootBlock && rootBlock.hasOwnProperty( 'name' ) && 'core/gallery' === rootBlock.name ) {
return;
}
if (
! hasBlockSupport( name, 'canvasUniqueClass', false ) &&
! hasBlockSupport( name, 'canvasBackgroundImage', false ) &&
! hasBlockSupport( name, 'canvasSpacings', false ) &&
! hasBlockSupport( name, 'canvasBorder', false ) &&
! hasBlockSupport( name, 'canvasResponsive', false )
) {
return;
}
let {
canvasClassName,
} = attributes;
// prevent unique ID duplication after block duplicated.
if ( checkDuplicates ) {
const allBlocks = this.getAllBlocks();
allBlocks.forEach( ( data ) => {
if (
data.clientId !== clientId &&
data.attributes &&
data.attributes.canvasClassName &&
data.attributes.canvasClassName === canvasClassName
) {
canvasClassName = '';
}
} );
}
if ( ! canvasClassName ) {
const newId = new Date().getTime();
// Generated HTML classes for blocks follow the `cnvs-block-{name}` nomenclature.
// Blocks provided by Canvas drop the prefixes 'canvas/'.
const className = 'cnvs-block-' + name.replace( /\//, '-' ).replace( /^canvas-/, '' );
setAttributes( {
canvasClassName: className + '-' + newId,
} );
}
}
render() {
return ;
}
}
return CanvasUniqueClassNameWrapper;
}, 'withInspectorControl' );
/**
* Override props assigned to save component to inject custom styles.
* This is only applied if the block's save result is an
* element and not a markup string.
*
* @param {Object} extraProps Additional props applied to save element.
* @param {Object} blockType Block type.
* @param {Object} attributes Current block attributes.
*
* @return {Object} Filtered props applied to save element.
*/
function addSaveProps( extraProps, blockType, attributes ) {
// add custom classname to non-canvas blocks.
// we need this class only when Spacings or Responsive controls added on the block.
if ( blockType.name && ! /^canvas/.test( name ) && attributes.canvasClassName ) {
const extAttrs = [
'backgroundImage',
'backgroundPosition',
'backgroundPositionXUnit',
'backgroundPositionXVal',
'backgroundPositionYUnit',
'backgroundPositionYVal',
'backgroundAttachment',
'backgroundRepeat',
'backgroundSize',
'backgroundSizeUnit',
'backgroundSizeVal',
'marginTop',
'marginRight',
'marginBottom',
'marginLeft',
'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft',
'borderRadiusTopLeft',
'borderRadiusTopRight',
'borderRadiusBottomLeft',
'borderRadiusBottomRight',
'borderStyle',
'canvasResponsiveHide',
];
let isCustomClassRequired = false;
// responsive attributes.
extAttrs.forEach( ( attr ) => {
if ( typeof attributes[ attr ] !== 'undefined' ) {
isCustomClassRequired = true;
}
Object.keys( canvasBreakpoints ).forEach( ( breakpoint ) => {
if ( ! isCustomClassRequired && typeof attributes[ `${ attr }_${ breakpoint }` ] !== 'undefined' ) {
isCustomClassRequired = true;
}
} );
} );
if ( isCustomClassRequired ) {
extraProps.className = classnames(
extraProps.className,
attributes.canvasClassName
);
}
}
return extraProps;
}
// Init filters.
addFilter( 'blocks.registerBlockType', 'canvas/unique-classname/additional-attributes', addAttribute );
addFilter( 'editor.BlockEdit', 'canvas/unique-classname/additional-attributes', withInspectorControl );
addFilter( 'blocks.getSaveContent.extraProps', 'canvas/unique-classname/save-props', addSaveProps );
/**
* Used to modify the block’s wrapper component containing the block’s edit
* component and all toolbars. It receives the original BlockListBlock
* component and returns a new wrapped component.
*/
const withClientIdClassName = createHigherOrderComponent( ( BlockListBlock ) => {
return ( props ) => {
if ( props.name && ! /^canvas/.test( props.name ) && props.attributes.canvasClassName ) {
return ;
} else {
return ;
}
};
}, 'withClientIdClassName' );
addFilter( 'editor.BlockListBlock', 'canvas/with-client-id-class-name', withClientIdClassName );