* setup basic column blocks

* fix classnames

* fix broken block.json in cart items block

* move hacks back

* dubplciate columns
This commit is contained in:
Seghir Nadir 2021-09-21 11:18:27 +01:00 committed by GitHub
parent f9b9679d62
commit 56551347a6
19 changed files with 559 additions and 17 deletions

View File

@ -0,0 +1,15 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
export const Columns = ( {
children,
...props
}: {
children?: React.ReactNode;
} ): JSX.Element => {
const blockProps = useBlockProps( props );
return <div { ...blockProps }>{ children }</div>;
};

View File

@ -0,0 +1 @@
export * from './columns-block';

View File

@ -1,16 +1,12 @@
/* tslint:disable */
/**
* External dependencies
*/
import classnames from 'classnames';
import { __ } from '@wordpress/i18n';
import { CartCheckoutFeedbackPrompt } from '@woocommerce/editor-components/feedback-prompt';
import { InspectorControls } from '@wordpress/block-editor';
import {
Disabled,
PanelBody,
ToggleControl,
Notice,
} from '@wordpress/components';
import { InnerBlocks, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ToggleControl, Notice } from '@wordpress/components';
import PropTypes from 'prop-types';
import { CartCheckoutCompatibilityNotice } from '@woocommerce/editor-components/compatibility-notices';
import ViewSwitcher from '@woocommerce/editor-components/view-switcher';
@ -25,13 +21,13 @@ import {
import { createInterpolateElement, useRef } from '@wordpress/element';
import { getAdminLink, getSetting } from '@woocommerce/settings';
import { previewCart } from '@woocommerce/resource-previews';
import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout';
/**
* Internal dependencies
*/
import Block from './block.js';
import EmptyCartEdit from './empty-cart-edit';
import './editor.scss';
import { Columns } from './columns';
const BlockSettings = ( { attributes, setAttributes } ) => {
const {
@ -162,6 +158,10 @@ const BlockSettings = ( { attributes, setAttributes } ) => {
);
};
const ALLOWED_BLOCKS: string[] = [
'woocommerce/cart-items-block',
'woocommerce/cart-totals-block',
];
/**
* Component to handle edit mode of "Cart Block".
*
@ -176,6 +176,13 @@ const BlockSettings = ( { attributes, setAttributes } ) => {
* @param {function(any):any} props.setAttributes Setter for attributes.
*/
const CartEditor = ( { className, attributes, setAttributes } ) => {
const cartClassName = classnames( 'wc-block-cart', {
'has-dark-controls': attributes.hasDarkControls,
} );
const defaultInnerBlocksTemplate = [
[ 'woocommerce/cart-items-block', {}, [] ],
[ 'woocommerce/cart-totals-block', {}, [] ],
];
return (
<div
className={ classnames( className, 'wp-block-woocommerce-cart', {
@ -221,16 +228,26 @@ const CartEditor = ( { className, attributes, setAttributes } ) => {
attributes={ attributes }
setAttributes={ setAttributes }
/>
<Disabled>
<CartProvider>
<Block attributes={ attributes } />
</CartProvider>
</Disabled>
<CartProvider>
<Columns>
<SidebarLayout
className={ cartClassName }
>
<InnerBlocks
allowedBlocks={
ALLOWED_BLOCKS
}
template={
defaultInnerBlocksTemplate
}
templateLock="insert"
/>
</SidebarLayout>
</Columns>
</CartProvider>
</EditorProvider>
<EmptyCartEdit hidden={ true } />
</>
) }
{ currentView === 'empty' && <EmptyCartEdit /> }
</BlockErrorBoundary>
) }
/>

View File

@ -0,0 +1,19 @@
/**
* External dependencies
*/
import { getBlockTypes } from '@wordpress/blocks';
// List of core block types to allow in inner block areas.
const coreBlockTypes = [ 'core/paragraph', 'core/image', 'core/separator' ];
/**
* Gets a list of allowed blocks types under a specific parent block type.
*/
export const getAllowedBlocks = ( block: string ): string[] => [
...getBlockTypes()
.filter( ( blockType ) =>
( blockType?.parent || [] ).includes( block )
)
.map( ( { name } ) => name ),
...coreBlockTypes,
];

View File

@ -6,3 +6,50 @@
max-height: 1000px;
overflow: hidden;
}
.wp-block-woocommerce-cart-i2 {
.wc-block-components-sidebar-layout {
display: block;
}
.block-editor-block-list__layout {
display: flex;
flex-flow: row wrap;
align-items: flex-start;
.wc-block-cart__additional_fields {
padding: 0;
}
}
.wc-block-components-main,
.wc-block-components-sidebar,
.block-editor-block-list__layout {
> :first-child {
margin-top: 0;
}
}
.wp-block-woocommerce-cart-totals-block,
.wp-block-woocommerce-cart-items-block {
.block-editor-block-list__layout {
display: block;
}
}
}
body.wc-lock-selected-block--move {
.block-editor-block-mover__move-button-container,
.block-editor-block-mover {
display: none;
}
}
body.wc-lock-selected-block--remove {
.block-editor-block-settings-menu__popover {
.components-menu-group:last-child {
display: none;
}
.components-menu-group:nth-last-child(2) {
margin-bottom: -12px;
}
}
}

View File

@ -0,0 +1,151 @@
/**
* HACKS
*
* This file contains functionality to "lock" blocks i.e. to prevent blocks being moved or deleted. This needs to be
* kept in place until native support for locking is available in WordPress (estimated WordPress 5.9).
*/
/**
* @todo Remove custom block locking (requires native WordPress support)
*/
/**
* External dependencies
*/
import {
useBlockProps,
store as blockEditorStore,
} from '@wordpress/block-editor';
import { isTextField } from '@wordpress/dom';
import { useSelect, subscribe, select as _select } from '@wordpress/data';
import { useEffect, useRef } from '@wordpress/element';
import { MutableRefObject } from 'react';
import { BACKSPACE, DELETE } from '@wordpress/keycodes';
import { hasFilter } from '@wordpress/hooks';
/**
* Toggle class on body.
*
* @param {string} className CSS Class name.
* @param {boolean} add True to add, false to remove.
*/
const toggleBodyClass = ( className: string, add = true ) => {
if ( add ) {
window.document.body.classList.add( className );
} else {
window.document.body.classList.remove( className );
}
};
/**
* addClassToBody
*
* This components watches the current selected block and adds a class name to the body if that block is locked. If the
* current block is not locked, it removes the class name. The appended body class is used to hide UI elements to prevent
* the block from being deleted.
*
* We use a component so we can react to changes in the store.
*/
export const addClassToBody = (): void => {
if ( ! hasFilter( 'blocks.registerBlockType', 'core/lock/addAttribute' ) ) {
subscribe( () => {
const blockEditorSelect = _select( blockEditorStore );
if ( ! blockEditorSelect ) {
return;
}
const selectedBlock = blockEditorSelect.getSelectedBlock();
if ( ! selectedBlock ) {
return;
}
toggleBodyClass(
'wc-lock-selected-block--remove',
!! selectedBlock?.attributes?.lock?.remove
);
toggleBodyClass(
'wc-lock-selected-block--move',
!! selectedBlock?.attributes?.lock?.move
);
} );
}
};
/**
* This is a hook we use in conjunction with useBlockProps. Its goal is to check if a block is locked (move or remove)
* and will stop the keydown event from propagating to stop it from being deleted via the keyboard.
*
* @todo Disable custom locking support if native support is detected.
*/
const useLockBlock = ( {
clientId,
ref,
attributes,
}: {
clientId: string;
ref: MutableRefObject< Element | undefined >;
attributes: Record< string, unknown >;
} ): void => {
const lockInCore = hasFilter(
'blocks.registerBlockType',
'core/lock/addAttribute'
);
const { isSelected } = useSelect(
( select ) => {
return {
isSelected: select( blockEditorStore ).isBlockSelected(
clientId
),
};
},
[ clientId ]
);
const node = ref.current;
return useEffect( () => {
if ( ! isSelected || ! node || lockInCore ) {
return;
}
function onKeyDown( event: KeyboardEvent ) {
const { keyCode, target } = event;
if ( keyCode !== BACKSPACE && keyCode !== DELETE ) {
return;
}
if ( target !== node || isTextField( target ) ) {
return;
}
// Prevent the keyboard event from propogating if it supports locking.
if ( attributes?.lock?.remove ) {
event.preventDefault();
event.stopPropagation();
}
}
node.addEventListener( 'keydown', onKeyDown, true );
return () => {
node.removeEventListener( 'keydown', onKeyDown, true );
};
}, [ node, isSelected, lockInCore, attributes ] );
};
/**
* This hook is a light wrapper to useBlockProps, it wraps that hook plus useLockBlock to pass data between them.
*/
export const useBlockPropsWithLocking = (
props: Record< string, unknown > = {}
): Record< string, unknown > => {
const ref = useRef< Element >();
const { attributes } = props;
const blockProps = useBlockProps( { ref, ...props } );
useLockBlock( {
ref,
attributes,
clientId: blockProps[ 'data-block' ],
} );
return blockProps;
};

View File

@ -12,7 +12,7 @@ import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
import edit from './edit';
import './style.scss';
import blockAttributes from './attributes';
import './inner-blocks';
/**
* Register and run the Cart block.
*/

View File

@ -0,0 +1,25 @@
{
"name": "woocommerce/cart-items-block",
"version": "1.0.0",
"title": "Cart Items block",
"description": "Column containing cart items.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false
},
"attributes": {
"lock": {
"default": {
"remove": true,
"move": true
}
}
},
"parent": [ "woocommerce/cart-i2" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

@ -0,0 +1,41 @@
/**
* External dependencies
*/
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import { Main } from '@woocommerce/base-components/sidebar-layout';
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
/**
* Internal dependencies
*/
import { useForcedLayout } from '../../use-forced-layout';
import { getAllowedBlocks } from '../../editor-utils';
export const Edit = ( { clientId }: { clientId: string } ): JSX.Element => {
const blockProps = useBlockProps();
const allowedBlocks = getAllowedBlocks( innerBlockAreas.CART_ITEMS );
useForcedLayout( {
clientId,
template: allowedBlocks,
} );
return (
<Main className="wc-block-cart__main">
<div { ...blockProps }>
<InnerBlocks
allowedBlocks={ allowedBlocks }
templateLock={ false }
renderAppender={ InnerBlocks.ButtonBlockAppender }
/>
</div>
</Main>
);
};
export const Save = (): JSX.Element => {
return (
<div { ...useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
};

View File

@ -0,0 +1,14 @@
/**
* External dependencies
*/
import { Main } from '@woocommerce/base-components/sidebar-layout';
const FrontendBlock = ( {
children,
}: {
children: JSX.Element;
} ): JSX.Element => {
return <Main className="wc-block-cart__main">{ children }</Main>;
};
export default FrontendBlock;

View File

@ -0,0 +1,20 @@
/**
* External dependencies
*/
import { Icon, column } from '@wordpress/icons';
import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import metadata from './block.json';
registerFeaturePluginBlockType( metadata, {
icon: {
src: <Icon icon={ column } />,
foreground: '#874FB9',
},
edit: Edit,
save: Save,
} );

View File

@ -0,0 +1,27 @@
{
"name": "woocommerce/cart-totals-block",
"version": "1.0.0",
"title": "Cart Totals",
"description": "Column containing the cart totals.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false
},
"attributes": {
"checkbox": {
"type": "boolean",
"default": false
},
"text": {
"type": "string",
"required": false
}
},
"parent": [ "woocommerce/cart-i2" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

@ -0,0 +1,47 @@
/**
* External dependencies
*/
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import { Sidebar } from '@woocommerce/base-components/sidebar-layout';
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
import Title from '@woocommerce/base-components/title';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import './style.scss';
import { useForcedLayout } from '../../use-forced-layout';
import { getAllowedBlocks } from '../../editor-utils';
export const Edit = ( { clientId }: { clientId: string } ): JSX.Element => {
const blockProps = useBlockProps();
const allowedBlocks = getAllowedBlocks( innerBlockAreas.CART_TOTALS );
useForcedLayout( {
clientId,
template: allowedBlocks,
} );
return (
<Sidebar className="wc-block-cart__sidebar">
<div { ...blockProps }>
<Title headingLevel="2" className="wc-block-cart__totals-title">
{ __( 'Cart totals', 'woo-gutenberg-products-block' ) }
</Title>
<InnerBlocks
allowedBlocks={ allowedBlocks }
templateLock={ false }
/>
</div>
</Sidebar>
);
};
export const Save = (): JSX.Element => {
return (
<div { ...useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
};

View File

@ -0,0 +1,19 @@
/**
* External dependencies
*/
import { Sidebar } from '@woocommerce/base-components/sidebar-layout';
/**
* Internal dependencies
*/
import './style.scss';
const FrontendBlock = ( {
children,
}: {
children: JSX.Element;
} ): JSX.Element => {
return <Sidebar className="wc-block-cart__sidebar">{ children }</Sidebar>;
};
export default FrontendBlock;

View File

@ -0,0 +1,20 @@
/**
* External dependencies
*/
import { Icon, column } from '@wordpress/icons';
import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import metadata from './block.json';
registerFeaturePluginBlockType( metadata, {
icon: {
src: <Icon icon={ column } />,
foreground: '#874FB9',
},
edit: Edit,
save: Save,
} );

View File

@ -0,0 +1,8 @@
.is-mobile,
.is-small,
.is-medium {
.wc-block-cart__sidebar {
margin-bottom: $gap-large;
order: 0;
}
}

View File

@ -0,0 +1,5 @@
/**
* Internal dependencies
*/
import './cart-items-block';
import './cart-totals-block';

View File

@ -0,0 +1,63 @@
/**
* External dependencies
*/
import { useLayoutEffect, useRef } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import {
createBlock,
getBlockType,
Block,
AttributeSource,
} from '@wordpress/blocks';
const isBlockLocked = ( {
attributes,
}: {
attributes: Record< string, AttributeSource.Attribute >;
} ) => Boolean( attributes.lock?.remove || attributes.lock?.default?.remove );
export const useForcedLayout = ( {
clientId,
template,
}: {
clientId: string;
template: Array< string >;
} ): void => {
const currentTemplate = useRef( template );
const { insertBlock } = useDispatch( 'core/block-editor' );
const { innerBlocks, templateTypes } = useSelect(
( select ) => {
return {
innerBlocks: select( 'core/block-editor' ).getBlocks(
clientId
),
templateTypes: currentTemplate.current.map( ( blockName ) =>
getBlockType( blockName )
),
};
},
[ clientId, currentTemplate ]
);
/**
* If the current inner blocks differ from the registered blocks, push the differences.
*
*/
useLayoutEffect( () => {
if ( ! clientId ) {
return;
}
// Missing check to see if registered block is 'forced'
templateTypes.forEach( ( block: Block | undefined ) => {
if (
block &&
isBlockLocked( block ) &&
! innerBlocks.find(
( { name }: { name: string } ) => name === block.name
)
) {
const newBlock = createBlock( block.name );
insertBlock( newBlock, innerBlocks.length, clientId, false );
}
} );
}, [ clientId, innerBlocks, insertBlock, templateTypes ] );
};

View File

@ -13,6 +13,9 @@ export enum innerBlockAreas {
BILLING_ADDRESS = 'woocommerce/checkout-billing-address-block',
SHIPPING_METHODS = 'woocommerce/checkout-shipping-methods-block',
PAYMENT_METHODS = 'woocommerce/checkout-payment-methods-block',
CART = 'woocommerce/cart-i2',
CART_ITEMS = 'woocommerce/cart-items-block',
CART_TOTALS = 'woocommerce/cart-totals-block',
}
interface CheckoutBlockOptionsMetadata extends Partial< BlockConfiguration > {