initial commit
This commit is contained in:
@@ -0,0 +1,515 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
AlignmentToolbar,
|
||||
BlockControls,
|
||||
InnerBlocks,
|
||||
InspectorControls,
|
||||
MediaReplaceFlow,
|
||||
PanelColorSettings,
|
||||
withColors,
|
||||
RichText,
|
||||
} from '@wordpress/block-editor';
|
||||
import { withSelect } from '@wordpress/data';
|
||||
import {
|
||||
Button,
|
||||
FocalPointPicker,
|
||||
PanelBody,
|
||||
Placeholder,
|
||||
RangeControl,
|
||||
ResizableBox,
|
||||
Spinner,
|
||||
ToggleControl,
|
||||
ToolbarGroup,
|
||||
withSpokenMessages,
|
||||
} from '@wordpress/components';
|
||||
import classnames from 'classnames';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { compose, createHigherOrderComponent } from '@wordpress/compose';
|
||||
import { isEmpty } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import ProductControl from '@woocommerce/editor-components/product-control';
|
||||
import ErrorPlaceholder from '@woocommerce/editor-components/error-placeholder';
|
||||
import { withProduct } from '@woocommerce/block-hocs';
|
||||
import { Icon, star } from '@woocommerce/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { dimRatioToClass, getBackgroundImageStyles } from './utils';
|
||||
import {
|
||||
getImageSrcFromProduct,
|
||||
getImageIdFromProduct,
|
||||
} from '../../utils/products';
|
||||
|
||||
/**
|
||||
* Component to handle edit mode of "Featured Product".
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {Object} props.attributes Incoming block attributes.
|
||||
* @param {function(any):any} props.debouncedSpeak Function for delayed speak.
|
||||
* @param {string} props.error Error message.
|
||||
* @param {function(any):any} props.getProduct Function for getting the product.
|
||||
* @param {boolean} props.isLoading Whether product is loading or not.
|
||||
* @param {boolean} props.isSelected Whether block is selected or not.
|
||||
* @param {Object} props.overlayColor Overlay color object.
|
||||
* @param {Object} props.product Product object.
|
||||
* @param {function(any):any} props.setAttributes Setter for attributes.
|
||||
* @param {function(any):any} props.setOverlayColor Setter for overlay color.
|
||||
* @param {function():any} props.triggerUrlUpdate Function for triggering a url update for product.
|
||||
*/
|
||||
const FeaturedProduct = ( {
|
||||
attributes,
|
||||
debouncedSpeak,
|
||||
error,
|
||||
getProduct,
|
||||
isLoading,
|
||||
isSelected,
|
||||
overlayColor,
|
||||
product,
|
||||
setAttributes,
|
||||
setOverlayColor,
|
||||
triggerUrlUpdate = () => void null,
|
||||
} ) => {
|
||||
const renderApiError = () => (
|
||||
<ErrorPlaceholder
|
||||
className="wc-block-featured-product-error"
|
||||
error={ error }
|
||||
isLoading={ isLoading }
|
||||
onRetry={ getProduct }
|
||||
/>
|
||||
);
|
||||
|
||||
const renderEditMode = () => {
|
||||
const onDone = () => {
|
||||
setAttributes( { editMode: false } );
|
||||
debouncedSpeak(
|
||||
__(
|
||||
'Showing Featured Product block preview.',
|
||||
'woocommerce'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{ getBlockControls() }
|
||||
<Placeholder
|
||||
icon={ <Icon srcElement={ star } /> }
|
||||
label={ __(
|
||||
'Featured Product',
|
||||
'woocommerce'
|
||||
) }
|
||||
className="wc-block-featured-product"
|
||||
>
|
||||
{ __(
|
||||
'Visually highlight a product or variation and encourage prompt action',
|
||||
'woocommerce'
|
||||
) }
|
||||
<div className="wc-block-featured-product__selection">
|
||||
<ProductControl
|
||||
selected={ attributes.productId || 0 }
|
||||
showVariations
|
||||
onChange={ ( value = [] ) => {
|
||||
const id = value[ 0 ] ? value[ 0 ].id : 0;
|
||||
setAttributes( {
|
||||
productId: id,
|
||||
mediaId: 0,
|
||||
mediaSrc: '',
|
||||
} );
|
||||
triggerUrlUpdate();
|
||||
} }
|
||||
/>
|
||||
<Button isPrimary onClick={ onDone }>
|
||||
{ __( 'Done', 'woocommerce' ) }
|
||||
</Button>
|
||||
</div>
|
||||
</Placeholder>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getBlockControls = () => {
|
||||
const { contentAlign, editMode, mediaSrc } = attributes;
|
||||
const mediaId = attributes.mediaId || getImageIdFromProduct( product );
|
||||
|
||||
return (
|
||||
<BlockControls>
|
||||
<AlignmentToolbar
|
||||
value={ contentAlign }
|
||||
onChange={ ( nextAlign ) => {
|
||||
setAttributes( { contentAlign: nextAlign } );
|
||||
} }
|
||||
/>
|
||||
<MediaReplaceFlow
|
||||
mediaId={ mediaId }
|
||||
mediaURL={ mediaSrc }
|
||||
accept="image/*"
|
||||
onSelect={ ( media ) => {
|
||||
setAttributes( {
|
||||
mediaId: media.id,
|
||||
mediaSrc: media.url,
|
||||
} );
|
||||
} }
|
||||
allowedTypes={ [ 'image' ] }
|
||||
/>
|
||||
|
||||
<ToolbarGroup
|
||||
controls={ [
|
||||
{
|
||||
icon: 'edit',
|
||||
title: __( 'Edit' ),
|
||||
onClick: () =>
|
||||
setAttributes( { editMode: ! editMode } ),
|
||||
isActive: editMode,
|
||||
},
|
||||
] }
|
||||
/>
|
||||
</BlockControls>
|
||||
);
|
||||
};
|
||||
|
||||
const getInspectorControls = () => {
|
||||
const url = attributes.mediaSrc || getImageSrcFromProduct( product );
|
||||
const { focalPoint = { x: 0.5, y: 0.5 } } = attributes;
|
||||
// FocalPointPicker was introduced in Gutenberg 5.0 (WordPress 5.2),
|
||||
// so we need to check if it exists before using it.
|
||||
const focalPointPickerExists = typeof FocalPointPicker === 'function';
|
||||
|
||||
return (
|
||||
<InspectorControls key="inspector">
|
||||
<PanelBody
|
||||
title={ __( 'Content', 'woocommerce' ) }
|
||||
>
|
||||
<ToggleControl
|
||||
label={ __(
|
||||
'Show description',
|
||||
'woocommerce'
|
||||
) }
|
||||
checked={ attributes.showDesc }
|
||||
onChange={
|
||||
// prettier-ignore
|
||||
() => setAttributes( { showDesc: ! attributes.showDesc } )
|
||||
}
|
||||
/>
|
||||
<ToggleControl
|
||||
label={ __(
|
||||
'Show price',
|
||||
'woocommerce'
|
||||
) }
|
||||
checked={ attributes.showPrice }
|
||||
onChange={
|
||||
// prettier-ignore
|
||||
() => setAttributes( { showPrice: ! attributes.showPrice } )
|
||||
}
|
||||
/>
|
||||
</PanelBody>
|
||||
<PanelColorSettings
|
||||
title={ __( 'Overlay', 'woocommerce' ) }
|
||||
colorSettings={ [
|
||||
{
|
||||
value: overlayColor.color,
|
||||
onChange: setOverlayColor,
|
||||
label: __(
|
||||
'Overlay Color',
|
||||
'woocommerce'
|
||||
),
|
||||
},
|
||||
] }
|
||||
>
|
||||
{ !! url && (
|
||||
<>
|
||||
<RangeControl
|
||||
label={ __(
|
||||
'Background Opacity',
|
||||
'woocommerce'
|
||||
) }
|
||||
value={ attributes.dimRatio }
|
||||
onChange={ ( ratio ) =>
|
||||
setAttributes( { dimRatio: ratio } )
|
||||
}
|
||||
min={ 0 }
|
||||
max={ 100 }
|
||||
step={ 10 }
|
||||
/>
|
||||
{ focalPointPickerExists && (
|
||||
<FocalPointPicker
|
||||
label={ __( 'Focal Point Picker' ) }
|
||||
url={ url }
|
||||
value={ focalPoint }
|
||||
onChange={ ( value ) =>
|
||||
setAttributes( { focalPoint: value } )
|
||||
}
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
) }
|
||||
</PanelColorSettings>
|
||||
</InspectorControls>
|
||||
);
|
||||
};
|
||||
|
||||
const renderProduct = () => {
|
||||
const {
|
||||
className,
|
||||
contentAlign,
|
||||
dimRatio,
|
||||
focalPoint,
|
||||
height,
|
||||
showDesc,
|
||||
showPrice,
|
||||
} = attributes;
|
||||
const classes = classnames(
|
||||
'wc-block-featured-product',
|
||||
{
|
||||
'is-selected': isSelected && attributes.productId !== 'preview',
|
||||
'is-loading': ! product && isLoading,
|
||||
'is-not-found': ! product && ! isLoading,
|
||||
'has-background-dim': dimRatio !== 0,
|
||||
},
|
||||
dimRatioToClass( dimRatio ),
|
||||
contentAlign !== 'center' && `has-${ contentAlign }-content`,
|
||||
className
|
||||
);
|
||||
|
||||
const style = getBackgroundImageStyles(
|
||||
attributes.mediaSrc || product
|
||||
);
|
||||
|
||||
if ( overlayColor.color ) {
|
||||
style.backgroundColor = overlayColor.color;
|
||||
}
|
||||
if ( focalPoint ) {
|
||||
const bgPosX = focalPoint.x * 100;
|
||||
const bgPosY = focalPoint.y * 100;
|
||||
style.backgroundPosition = `${ bgPosX }% ${ bgPosY }%`;
|
||||
}
|
||||
|
||||
const onResizeStop = ( event, direction, elt ) => {
|
||||
setAttributes( { height: parseInt( elt.style.height, 10 ) } );
|
||||
};
|
||||
|
||||
return (
|
||||
<ResizableBox
|
||||
className={ classes }
|
||||
size={ { height } }
|
||||
minHeight={ getSetting( 'min_height', 500 ) }
|
||||
enable={ { bottom: true } }
|
||||
onResizeStop={ onResizeStop }
|
||||
style={ style }
|
||||
>
|
||||
<div className="wc-block-featured-product__wrapper">
|
||||
<h2
|
||||
className="wc-block-featured-product__title"
|
||||
dangerouslySetInnerHTML={ {
|
||||
__html: product.name,
|
||||
} }
|
||||
/>
|
||||
{ ! isEmpty( product.variation ) && (
|
||||
<h3
|
||||
className="wc-block-featured-product__variation"
|
||||
dangerouslySetInnerHTML={ {
|
||||
__html: product.variation,
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
{ showDesc && (
|
||||
<div
|
||||
className="wc-block-featured-product__description"
|
||||
dangerouslySetInnerHTML={ {
|
||||
__html: product.short_description,
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
{ showPrice && (
|
||||
<div
|
||||
className="wc-block-featured-product__price"
|
||||
dangerouslySetInnerHTML={ {
|
||||
__html: product.price_html,
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
<div className="wc-block-featured-product__link">
|
||||
{ renderButton() }
|
||||
</div>
|
||||
</div>
|
||||
</ResizableBox>
|
||||
);
|
||||
};
|
||||
|
||||
const renderButton = () => {
|
||||
const buttonClasses = classnames(
|
||||
'wp-block-button__link',
|
||||
'is-style-fill'
|
||||
);
|
||||
const buttonStyle = {
|
||||
backgroundColor: 'vivid-green-cyan',
|
||||
borderRadius: '5px',
|
||||
};
|
||||
const wrapperStyle = {
|
||||
width: '100%',
|
||||
};
|
||||
return attributes.productId === 'preview' ? (
|
||||
<div className="wp-block-button aligncenter" style={ wrapperStyle }>
|
||||
<RichText.Content
|
||||
tagName="a"
|
||||
className={ buttonClasses }
|
||||
href={ product.permalink }
|
||||
title={ attributes.linkText }
|
||||
style={ buttonStyle }
|
||||
value={ attributes.linkText }
|
||||
target={ product.permalink }
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<InnerBlocks
|
||||
template={ [
|
||||
[
|
||||
'core/button',
|
||||
{
|
||||
text: __(
|
||||
'Shop now',
|
||||
'woocommerce'
|
||||
),
|
||||
url: product.permalink,
|
||||
align: 'center',
|
||||
},
|
||||
],
|
||||
] }
|
||||
templateLock="all"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderNoProduct = () => (
|
||||
<Placeholder
|
||||
className="wc-block-featured-product"
|
||||
icon={ <Icon srcElement={ star } /> }
|
||||
label={ __( 'Featured Product', 'woocommerce' ) }
|
||||
>
|
||||
{ isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
__( 'No product is selected.', 'woocommerce' )
|
||||
) }
|
||||
</Placeholder>
|
||||
);
|
||||
|
||||
const { editMode } = attributes;
|
||||
|
||||
if ( error ) {
|
||||
return renderApiError();
|
||||
}
|
||||
|
||||
if ( editMode ) {
|
||||
return renderEditMode();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ getBlockControls() }
|
||||
{ getInspectorControls() }
|
||||
{ product ? renderProduct() : renderNoProduct() }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
FeaturedProduct.propTypes = {
|
||||
/**
|
||||
* The attributes for this block.
|
||||
*/
|
||||
attributes: PropTypes.object.isRequired,
|
||||
/**
|
||||
* Whether this block is currently active.
|
||||
*/
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
/**
|
||||
* The register block name.
|
||||
*/
|
||||
name: PropTypes.string.isRequired,
|
||||
/**
|
||||
* A callback to update attributes.
|
||||
*/
|
||||
setAttributes: PropTypes.func.isRequired,
|
||||
// from withProduct
|
||||
error: PropTypes.object,
|
||||
getProduct: PropTypes.func,
|
||||
isLoading: PropTypes.bool,
|
||||
product: PropTypes.shape( {
|
||||
name: PropTypes.node,
|
||||
variation: PropTypes.node,
|
||||
description: PropTypes.node,
|
||||
price_html: PropTypes.node,
|
||||
permalink: PropTypes.string,
|
||||
} ),
|
||||
// from withColors
|
||||
overlayColor: PropTypes.object,
|
||||
setOverlayColor: PropTypes.func.isRequired,
|
||||
// from withSpokenMessages
|
||||
debouncedSpeak: PropTypes.func.isRequired,
|
||||
triggerUrlUpdate: PropTypes.func,
|
||||
};
|
||||
|
||||
export default compose( [
|
||||
withProduct,
|
||||
withColors( { overlayColor: 'background-color' } ),
|
||||
withSpokenMessages,
|
||||
withSelect( ( select, { clientId }, { dispatch } ) => {
|
||||
const Block = select( 'core/block-editor' ).getBlock( clientId );
|
||||
const buttonBlockId = Block?.innerBlocks[ 0 ]?.clientId || '';
|
||||
const currentButtonAttributes =
|
||||
Block?.innerBlocks[ 0 ]?.attributes || {};
|
||||
const updateBlockAttributes = ( attributes ) => {
|
||||
if ( buttonBlockId ) {
|
||||
dispatch( 'core/block-editor' ).updateBlockAttributes(
|
||||
buttonBlockId,
|
||||
attributes
|
||||
);
|
||||
}
|
||||
};
|
||||
return { updateBlockAttributes, currentButtonAttributes };
|
||||
} ),
|
||||
createHigherOrderComponent( ( ProductComponent ) => {
|
||||
class WrappedComponent extends Component {
|
||||
state = {
|
||||
doUrlUpdate: false,
|
||||
};
|
||||
componentDidUpdate() {
|
||||
const {
|
||||
attributes,
|
||||
updateBlockAttributes,
|
||||
currentButtonAttributes,
|
||||
product,
|
||||
} = this.props;
|
||||
if (
|
||||
this.state.doUrlUpdate &&
|
||||
! attributes.editMode &&
|
||||
product?.permalink &&
|
||||
currentButtonAttributes?.url &&
|
||||
product.permalink !== currentButtonAttributes.url
|
||||
) {
|
||||
updateBlockAttributes( {
|
||||
...currentButtonAttributes,
|
||||
url: product.permalink,
|
||||
} );
|
||||
this.setState( { doUrlUpdate: false } );
|
||||
}
|
||||
}
|
||||
triggerUrlUpdate = () => {
|
||||
this.setState( { doUrlUpdate: true } );
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<ProductComponent
|
||||
triggerUrlUpdate={ this.triggerUrlUpdate }
|
||||
{ ...this.props }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return WrappedComponent;
|
||||
}, 'withUpdateButtonAttributes' ),
|
||||
] )( FeaturedProduct );
|
||||
Reference in New Issue
Block a user