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 );
|
@ -0,0 +1,12 @@
|
||||
.wc-block-featured-product {
|
||||
.components-resizable-box__handle {
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
.wc-block-featured-product__message {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.wc-block-featured-product__selection {
|
||||
width: 100%;
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { previewProducts } from '@woocommerce/resource-previews';
|
||||
|
||||
export const example = {
|
||||
attributes: {
|
||||
contentAlign: 'center',
|
||||
dimRatio: 50,
|
||||
editMode: false,
|
||||
height: getSetting( 'default_height', 500 ),
|
||||
mediaSrc: '',
|
||||
showDesc: true,
|
||||
productId: 'preview',
|
||||
previewProduct: previewProducts[ 0 ],
|
||||
},
|
||||
};
|
@ -0,0 +1,163 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { InnerBlocks } from '@wordpress/block-editor';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { Icon, star } from '@woocommerce/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import './editor.scss';
|
||||
import { example } from './example';
|
||||
import Block from './block';
|
||||
|
||||
/**
|
||||
* Register and run the "Featured Product" block.
|
||||
*/
|
||||
registerBlockType( 'woocommerce/featured-product', {
|
||||
title: __( 'Featured Product', 'woocommerce' ),
|
||||
icon: {
|
||||
src: <Icon srcElement={ star } />,
|
||||
foreground: '#96588a',
|
||||
},
|
||||
category: 'woocommerce',
|
||||
keywords: [ __( 'WooCommerce', 'woocommerce' ) ],
|
||||
description: __(
|
||||
'Visually highlight a product or variation and encourage prompt action.',
|
||||
'woocommerce'
|
||||
),
|
||||
supports: {
|
||||
align: [ 'wide', 'full' ],
|
||||
html: false,
|
||||
},
|
||||
example,
|
||||
attributes: {
|
||||
/**
|
||||
* Alignment of content inside block.
|
||||
*/
|
||||
contentAlign: {
|
||||
type: 'string',
|
||||
default: 'center',
|
||||
},
|
||||
|
||||
/**
|
||||
* Percentage opacity of overlay.
|
||||
*/
|
||||
dimRatio: {
|
||||
type: 'number',
|
||||
default: 50,
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle for edit mode in the block preview.
|
||||
*/
|
||||
editMode: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Focus point for the background image
|
||||
*/
|
||||
focalPoint: {
|
||||
type: 'object',
|
||||
},
|
||||
|
||||
/**
|
||||
* A fixed height for the block.
|
||||
*/
|
||||
height: {
|
||||
type: 'number',
|
||||
default: getSetting( 'default_height', 500 ),
|
||||
},
|
||||
|
||||
/**
|
||||
* ID for a custom image, overriding the product's featured image.
|
||||
*/
|
||||
mediaId: {
|
||||
type: 'number',
|
||||
default: 0,
|
||||
},
|
||||
|
||||
/**
|
||||
* URL for a custom image, overriding the product's featured image.
|
||||
*/
|
||||
mediaSrc: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
|
||||
/**
|
||||
* The overlay color, from the color list.
|
||||
*/
|
||||
overlayColor: {
|
||||
type: 'string',
|
||||
},
|
||||
|
||||
/**
|
||||
* The overlay color, if a custom color value.
|
||||
*/
|
||||
customOverlayColor: {
|
||||
type: 'string',
|
||||
},
|
||||
|
||||
/**
|
||||
* Text for the product link.
|
||||
*/
|
||||
linkText: {
|
||||
type: 'string',
|
||||
default: __( 'Shop now', 'woocommerce' ),
|
||||
},
|
||||
|
||||
/**
|
||||
* The product ID to display.
|
||||
*/
|
||||
productId: {
|
||||
type: 'number',
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the product description.
|
||||
*/
|
||||
showDesc: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the product price.
|
||||
*/
|
||||
showPrice: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Product preview.
|
||||
*/
|
||||
previewProduct: {
|
||||
type: 'object',
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders and manages the block.
|
||||
*
|
||||
* @param {Object} props Props to pass to block.
|
||||
*/
|
||||
edit( props ) {
|
||||
return <Block { ...props } />;
|
||||
},
|
||||
|
||||
/**
|
||||
* Block content is rendered in PHP, not via save function.
|
||||
*/
|
||||
save() {
|
||||
return <InnerBlocks.Content />;
|
||||
},
|
||||
} );
|
@ -0,0 +1,146 @@
|
||||
.wc-block-featured-product {
|
||||
position: relative;
|
||||
background-color: $gray-900;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
width: 100%;
|
||||
margin: 0 0 1.5em 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
|
||||
.wc-block-featured-product__wrapper {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
&.has-left-content {
|
||||
justify-content: flex-start;
|
||||
|
||||
.wc-block-featured-product__title,
|
||||
.wc-block-featured-product__variation,
|
||||
.wc-block-featured-product__description,
|
||||
.wc-block-featured-product__price {
|
||||
margin-left: 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-right-content {
|
||||
justify-content: flex-end;
|
||||
|
||||
.wc-block-featured-product__title,
|
||||
.wc-block-featured-product__variation,
|
||||
.wc-block-featured-product__description,
|
||||
.wc-block-featured-product__price {
|
||||
margin-right: 0;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-featured-product__title,
|
||||
.wc-block-featured-product__variation,
|
||||
.wc-block-featured-product__description,
|
||||
.wc-block-featured-product__price {
|
||||
color: $white;
|
||||
line-height: 1.25;
|
||||
margin-bottom: 0;
|
||||
text-align: center;
|
||||
|
||||
a,
|
||||
a:hover,
|
||||
a:focus,
|
||||
a:active {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-featured-product__title,
|
||||
.wc-block-featured-product__variation,
|
||||
.wc-block-featured-product__description,
|
||||
.wc-block-featured-product__price,
|
||||
.wc-block-featured-product__link {
|
||||
width: 100%;
|
||||
padding: 16px 48px 0 48px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.wc-block-featured-product__title,
|
||||
.wc-block-featured-product__variation {
|
||||
margin-top: 0;
|
||||
border: 0;
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-featured-product__variation {
|
||||
font-style: italic;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.wc-block-featured-product__description {
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.wp-block-button.aligncenter {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.has-background-dim::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: inherit;
|
||||
opacity: 0.5;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@for $i from 1 through 10 {
|
||||
&.has-background-dim.has-background-dim-#{ $i * 10 }::before {
|
||||
opacity: $i * 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply max-width to floated items that have no intrinsic width
|
||||
&.alignleft,
|
||||
&.alignright {
|
||||
max-width: $content-width * 0.5;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Using flexbox without an assigned height property breaks vertical center alignment in IE11.
|
||||
// Appending an empty ::after element tricks IE11 into giving the cover image an implicit height, which sidesteps this issue.
|
||||
&::after {
|
||||
display: block;
|
||||
content: "";
|
||||
font-size: 0;
|
||||
min-height: inherit;
|
||||
|
||||
// IE doesn't support flex so omit that.
|
||||
@supports (position: sticky) {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Aligned cover blocks should not use our global alignment rules
|
||||
&.aligncenter,
|
||||
&.alignleft,
|
||||
&.alignright {
|
||||
display: flex;
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isObject } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getImageSrcFromProduct } from '../../utils/products';
|
||||
|
||||
/**
|
||||
* Generate a style object given either a product object or URL to an image.
|
||||
*
|
||||
* @param {Object|string} url A product object as returned from the API, or an image URL.
|
||||
* @return {Object} A style object with a backgroundImage set (if a valid image is provided).
|
||||
*/
|
||||
function getBackgroundImageStyles( url ) {
|
||||
// If `url` is an object, it's actually a product.
|
||||
if ( isObject( url ) ) {
|
||||
url = getImageSrcFromProduct( url );
|
||||
}
|
||||
if ( url ) {
|
||||
return { backgroundImage: `url(${ url })` };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the selected ratio to the correct background class.
|
||||
*
|
||||
* @param {number} ratio Selected opacity from 0 to 100.
|
||||
* @return {string} The class name, if applicable (not used for ratio 0 or 50).
|
||||
*/
|
||||
function dimRatioToClass( ratio ) {
|
||||
return ratio === 0 || ratio === 50
|
||||
? null
|
||||
: `has-background-dim-${ 10 * Math.round( ratio / 10 ) }`;
|
||||
}
|
||||
|
||||
export { getBackgroundImageStyles, dimRatioToClass };
|
Reference in New Issue
Block a user