diff --git a/plugins/woocommerce-blocks/assets/css/abstracts/_mixins.scss b/plugins/woocommerce-blocks/assets/css/abstracts/_mixins.scss index 2bf27517e6c..96e1735c2f8 100644 --- a/plugins/woocommerce-blocks/assets/css/abstracts/_mixins.scss +++ b/plugins/woocommerce-blocks/assets/css/abstracts/_mixins.scss @@ -176,6 +176,61 @@ $fontSizes: ( -ms-word-break: break-all; } +// Add support for content alignment classes +@mixin with-alignment { + // 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; + } +} + +// Shows an semi-transparent overlay +@mixin with-background-dim($opacity: 0.5) { + &.has-background-dim { + .background-dim__overlay::before { + content: ""; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background-color: inherit; + border-radius: inherit; + opacity: $opacity; + z-index: 1; + } + } + + @for $i from 1 through 10 { + &.has-background-dim-#{ $i * 10 } .background-dim__overlay::before { + opacity: $i * 0.1; + } + } +} + // Shows a border with the current color and a custom opacity. That can't be achieved // with normal border because `currentColor` doesn't allow tweaking the opacity, and // setting the opacity of the entire element would change the children's opacity too. diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/block.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-category/block.js deleted file mode 100644 index 5e9f167c86f..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/block.js +++ /dev/null @@ -1,700 +0,0 @@ -/* eslint-disable @wordpress/no-unsafe-wp-apis */ - -/** - * External dependencies - */ -import { useCallback, useEffect, useState } from 'react'; -import { __ } from '@wordpress/i18n'; -import { - AlignmentToolbar, - BlockControls, - InnerBlocks, - InspectorControls, - MediaReplaceFlow, - RichText, - __experimentalGetSpacingClassesAndStyles as getSpacingClassesAndStyles, - __experimentalImageEditingProvider as ImageEditingProvider, - __experimentalImageEditor as ImageEditor, - __experimentalPanelColorGradientSettings as PanelColorGradientSettings, - __experimentalUseGradient as useGradient, -} from '@wordpress/block-editor'; -import { - Button, - FocalPointPicker, - PanelBody, - Placeholder, - RangeControl, - Spinner, - ToggleControl, - ToolbarButton, - ToolbarGroup, - withSpokenMessages, - __experimentalToggleGroupControl as ToggleGroupControl, - __experimentalToggleGroupControlOption as ToggleGroupControlOption, - TextareaControl, - ExternalLink, -} from '@wordpress/components'; -import classnames from 'classnames'; -import { Component } from '@wordpress/element'; -import { withSelect } from '@wordpress/data'; -import { compose, createHigherOrderComponent } from '@wordpress/compose'; -import PropTypes from 'prop-types'; -import { folderStarred } from '@woocommerce/icons'; -import { crop, Icon } from '@wordpress/icons'; -import ProductCategoryControl from '@woocommerce/editor-components/product-category-control'; -import ErrorPlaceholder from '@woocommerce/editor-components/error-placeholder'; -import TextToolbarButton from '@woocommerce/editor-components/text-toolbar-button'; - -/** - * Internal dependencies - */ -import { - dimRatioToClass, - getCategoryImageId, - getCategoryImageSrc, - calculateBackgroundImagePosition, -} from './utils'; -import { withCategory } from '../../hocs'; -import { ConstrainedResizable } from '../featured-product/block'; - -const DEFAULT_EDITOR_SIZE = { - height: 500, - width: 500, -}; - -/** - * Component to handle edit mode of "Featured Category". - * - * @param {Object} props Incoming props for the component. - * @param {Object} props.attributes Incoming block attributes. - * @param {boolean} props.isSelected Whether block is selected or not. - * @param {function(any):any} props.setAttributes Function for setting new attributes. - * @param {string} props.error Error message - * @param {function(any):any} props.getCategory Function for getting category details. - * @param {boolean} props.isLoading Whether loading or not. - * @param {Object} props.category The product category object. - * @param {function(any):any} props.debouncedSpeak Function for delayed speak. - * @param {function():void} props.triggerUrlUpdate Function to update Shop now button Url. - */ -const FeaturedCategory = ( { - attributes, - isSelected, - setAttributes, - error, - getCategory, - isLoading, - category, - debouncedSpeak, - triggerUrlUpdate = () => void null, -} ) => { - const { mediaId, mediaSrc } = attributes; - - const [ isEditingImage, setIsEditingImage ] = useState( false ); - const [ backgroundImageSize, setBackgroundImageSize ] = useState( {} ); - const { setGradient } = useGradient( { - gradientAttribute: 'overlayGradient', - customGradientAttribute: 'overlayGradient', - } ); - - const backgroundImageSrc = mediaSrc || getCategoryImageSrc( category ); - const backgroundImageId = mediaId || getCategoryImageId( category ); - - const onResize = useCallback( - ( _event, _direction, elt ) => { - setAttributes( { minHeight: parseInt( elt.style.height, 10 ) } ); - }, - [ setAttributes ] - ); - - useEffect( () => { - setIsEditingImage( false ); - }, [ isSelected ] ); - - const renderApiError = () => ( - - ); - - const getBlockControls = () => { - const { contentAlign, editMode } = attributes; - - return ( - - { - setAttributes( { contentAlign: nextAlign } ); - } } - /> - - { backgroundImageSrc && ! isEditingImage && ( - setIsEditingImage( true ) } - icon={ crop } - label={ __( - 'Edit category image', - 'woo-gutenberg-products-block' - ) } - /> - ) } - { - setAttributes( { - mediaId: media.id, - mediaSrc: media.url, - } ); - } } - allowedTypes={ [ 'image' ] } - /> - { backgroundImageId && mediaSrc ? ( - - setAttributes( { mediaId: 0, mediaSrc: '' } ) - } - > - { __( 'Reset', 'woo-gutenberg-products-block' ) } - - ) : null } - - - setAttributes( { editMode: ! editMode } ), - isActive: editMode, - }, - ] } - /> - - ); - }; - - const getInspectorControls = () => { - 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 ( - - - - setAttributes( { showDesc: ! attributes.showDesc } ) - } - /> - - { !! backgroundImageSrc && ( - <> - { focalPointPickerExists && ( - - -

- { __( - 'Choose “Cover” if you want the image to scale automatically to always fit its container.', - 'woo-gutenberg-products-block' - ) } -

-

- { __( - 'Note: by choosing “Cover” you will lose the ability to freely move the focal point precisely.', - 'woo-gutenberg-products-block' - ) } -

- - } - label={ __( - 'Image fit', - 'woo-gutenberg-products-block' - ) } - value={ attributes.imageFit } - onChange={ ( value ) => - setAttributes( { - imageFit: value, - } ) - } - > - - -
- - setAttributes( { - focalPoint: value, - } ) - } - /> - { - setAttributes( { alt } ); - } } - help={ - <> - - { __( - 'Describe the purpose of the image', - 'woo-gutenberg-products-block' - ) } - - { __( - 'Leaving it empty will use the category name.', - 'woo-gutenberg-products-block' - ) } - - } - /> -
- ) } - - setAttributes( { overlayColor } ), - onGradientChange: ( overlayGradient ) => { - setGradient( overlayGradient ); - setAttributes( { - overlayGradient, - } ); - }, - label: __( - 'Color', - 'woo-gutenberg-products-block' - ), - }, - ] } - > - - setAttributes( { dimRatio } ) - } - min={ 0 } - max={ 100 } - step={ 10 } - required - /> - - - ) } -
- ); - }; - - const renderEditMode = () => { - const onDone = () => { - setAttributes( { editMode: false } ); - debouncedSpeak( - __( - 'Showing Featured Product block preview.', - 'woo-gutenberg-products-block' - ) - ); - }; - - return ( - } - label={ __( - 'Featured Category', - 'woo-gutenberg-products-block' - ) } - className="wc-block-featured-category" - > - { __( - 'Visually highlight a product category and encourage prompt action.', - 'woo-gutenberg-products-block' - ) } -
- { - const id = value[ 0 ] ? value[ 0 ].id : 0; - setAttributes( { - categoryId: id, - mediaId: 0, - mediaSrc: '', - } ); - triggerUrlUpdate(); - } } - isSingle - /> - -
-
- ); - }; - - 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.categoryId === 'preview' ? ( -
- -
- ) : ( - - ); - }; - - const renderCategory = () => { - const { - contentAlign, - dimRatio, - focalPoint, - imageFit, - minHeight, - overlayColor, - overlayGradient, - showDesc, - style, - } = attributes; - - const classes = classnames( - 'wc-block-featured-category', - { - 'is-selected': isSelected && attributes.productId !== 'preview', - 'is-loading': ! category && isLoading, - 'is-not-found': ! category && ! isLoading, - 'has-background-dim': dimRatio !== 0, - }, - dimRatioToClass( dimRatio ), - contentAlign !== 'center' && `has-${ contentAlign }-content` - ); - - const containerStyle = { - borderRadius: style?.border?.radius, - }; - - const wrapperStyle = { - ...getSpacingClassesAndStyles( attributes ).style, - minHeight, - }; - - const backgroundImageStyle = { - ...calculateBackgroundImagePosition( focalPoint ), - objectFit: imageFit, - }; - - const overlayStyle = { - background: overlayGradient, - backgroundColor: overlayColor, - }; - - return ( - <> - -
-
-
- { backgroundImageSrc && ( - { { - setBackgroundImageSize( { - height: e.target?.naturalHeight, - width: e.target?.naturalWidth, - } ); - } } - /> - ) } -

- { showDesc && ( -
- ) } -
- { renderButton() } -
-
-

- - ); - }; - - const renderNoCategory = () => ( - } - label={ __( 'Featured Category', 'woo-gutenberg-products-block' ) } - > - { isLoading ? ( - - ) : ( - __( - 'No product category is selected.', - 'woo-gutenberg-products-block' - ) - ) } - - ); - - const { editMode } = attributes; - - if ( error ) { - return renderApiError(); - } - - if ( editMode ) { - return renderEditMode(); - } - - if ( isEditingImage ) { - return ( - <> - { - setAttributes( { mediaId: id, mediaSrc: url } ); - } } - isEditing={ isEditingImage } - onFinishEditing={ () => setIsEditingImage( false ) } - > - - - - ); - } - - return ( - <> - { getBlockControls() } - { getInspectorControls() } - { category ? renderCategory() : renderNoCategory() } - - ); -}; - -FeaturedCategory.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 withCategory - error: PropTypes.object, - getCategory: PropTypes.func, - isLoading: PropTypes.bool, - category: PropTypes.shape( { - name: PropTypes.node, - description: PropTypes.node, - permalink: PropTypes.string, - } ), - // from withSpokenMessages - debouncedSpeak: PropTypes.func.isRequired, - triggerUrlUpdate: PropTypes.func, -}; - -export default compose( [ - withCategory, - 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, - category, - } = this.props; - if ( - this.state.doUrlUpdate && - ! attributes.editMode && - category?.permalink && - currentButtonAttributes?.url && - category.permalink !== currentButtonAttributes.url - ) { - updateBlockAttributes( { - ...currentButtonAttributes, - url: category.permalink, - } ); - this.setState( { doUrlUpdate: false } ); - } - } - triggerUrlUpdate = () => { - this.setState( { doUrlUpdate: true } ); - }; - render() { - return ( - - ); - } - } - return WrappedComponent; - }, 'withUpdateButtonAttributes' ), -] )( FeaturedCategory ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/featured-category/edit.tsx deleted file mode 100644 index 990154b4121..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/edit.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * External dependencies - */ -import { useBlockProps } from '@wordpress/block-editor'; - -/** - * Internal dependencies - */ -import Block from './block'; - -export const Edit = ( props: unknown ): JSX.Element => { - const blockProps = useBlockProps(); - - // The useBlockProps function returns the style with the `color`. - // We need to remove it to avoid the block to be styled with the color. - const { color, ...styles } = blockProps.style; - - return ( -
- -
- ); -}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/featured-category/editor.scss deleted file mode 100644 index 2a5b2726ccb..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/editor.scss +++ /dev/null @@ -1,15 +0,0 @@ -.wc-block-featured-category { - background-color: inherit; - - .components-resizable-box__handle { - z-index: 10; - } - - .components-placeholder__label svg { - fill: currentColor; - margin-right: 1ch; - } -} -.wc-block-featured-category__selection { - width: 100%; -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/featured-category/index.tsx deleted file mode 100644 index fef4efb2668..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/index.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/** - * External dependencies - */ -import { InnerBlocks } from '@wordpress/block-editor'; -import { registerBlockType } from '@wordpress/blocks'; -import { getSetting } from '@woocommerce/settings'; -import { folderStarred } from '@woocommerce/icons'; -import { Icon } from '@wordpress/icons'; -import { isFeaturePluginBuild } from '@woocommerce/block-settings'; - -/** - * Internal dependencies - */ -import './style.scss'; -import './editor.scss'; -import { example } from './example'; -import { Edit } from './edit'; -import metadata from './block.json'; - -/** - * Register and run the "Featured Category" block. - */ -registerBlockType( metadata, { - icon: { - src: ( - - ), - }, - attributes: { - ...metadata.attributes, - /** - * A minimum height for the block. - * - * Note: if padding is increased, this way the inner content will never - * overflow, but instead will resize the container. - * - * It was decided to change this to make this block more in line with - * the “Cover” block. - */ - minHeight: { - type: 'number', - default: getSetting( 'default_height', 500 ), - }, - }, - supports: { - ...metadata.supports, - color: { - background: true, - text: true, - ...( isFeaturePluginBuild() && { - __experimentalDuotone: - '.wc-block-featured-category__background-image', - } ), - }, - spacing: { - padding: true, - ...( isFeaturePluginBuild() && { - __experimentalDefaultControls: { - padding: true, - }, - __experimentalSkipSerialization: true, - } ), - }, - ...( isFeaturePluginBuild() && { - __experimentalBorder: { - color: true, - radius: true, - width: true, - __experimentalSkipSerialization: true, - }, - } ), - }, - example, - /** - * Renders and manages the block. - * - * @param {Object} props Props to pass to block. - */ - edit: Edit, - - /** - * Block content is rendered in PHP, not via save function. - */ - save: () => { - return ; - }, -} ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/featured-category/style.scss deleted file mode 100644 index 0cab1f8b697..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/style.scss +++ /dev/null @@ -1,186 +0,0 @@ -.wp-block-woocommerce-featured-category { - background-color: transparent; - border-color: transparent; - color: #fff; - box-sizing: border-box; - - .components-resizable-box__container { - position: absolute !important; - top: 0; - left: 0; - right: 0; - bottom: 0; - min-height: 50px; - - &:not(.is-resizing) { - height: auto !important; - } - } - - // Applying image edits - .is-applying { - .components-spinner { - position: absolute; - top: 50%; - left: 50%; - margin-top: -9px; - margin-left: -9px; - } - - img { - opacity: 0.3; - } - } -} - -.wc-block-featured-category { - align-content: center; - align-items: center; - background-size: cover; - background-position: center center; - display: flex; - flex-wrap: wrap; - justify-content: center; - margin: 0; - overflow: hidden; - position: relative; - width: 100%; - - .wc-block-featured-category__wrapper { - align-content: center; - align-items: center; - box-sizing: border-box; - display: flex; - flex-wrap: wrap; - height: 100%; - justify-content: center; - overflow: hidden; - } - - &.has-left-content { - justify-content: flex-start; - - .wc-block-featured-category__title, - .wc-block-featured-category__description, - .wc-block-featured-category__price { - margin-left: 0; - text-align: left; - } - } - - &.has-right-content { - justify-content: flex-end; - - .wc-block-featured-category__title, - .wc-block-featured-category__description, - .wc-block-featured-category__price { - margin-right: 0; - text-align: right; - } - } - - .wc-block-featured-category__title, - .wc-block-featured-category__description, - .wc-block-featured-category__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-category__title, - .wc-block-featured-category__description, - .wc-block-featured-category__price, - .wc-block-featured-category__link { - color: inherit; - width: 100%; - padding: 0 48px 16px 48px; - z-index: 1; - } - - .wc-block-featured-category__title { - margin-top: 0; - - div { - color: inherit; - } - - &::before { - display: none; - } - } - - .wc-block-featured-category__description { - color: inherit; - p { - margin: 0; - } - } - - .wc-block-featured-category__background-image { - @include absolute-stretch(); - object-fit: none; - } - - .wp-block-button.aligncenter { - text-align: center; - } - - &.has-background-dim { - .wc-block-featured-category__overlay::before { - background: inherit; - border-radius: inherit; - content: ""; - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - opacity: 0.5; - z-index: 1; - } - } - - @for $i from 1 through 10 { - &.has-background-dim.has-background-dim-#{ $i * 10 } { - .wc-block-featured-category__overlay::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; - } -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/utils.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-category/utils.js deleted file mode 100644 index 831f0756760..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/utils.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * External dependencies - */ -import { isObject } from 'lodash'; - -/** - * Get the src from a category object, unless null (no image). - * - * @param {Object|null} category A product category object from the API. - * @return {string} The src of the category image. - */ -function getCategoryImageSrc( category ) { - if ( category && isObject( category.image ) ) { - return category.image.src; - } - return ''; -} - -/** - * Get the attachment ID from a category object, unless null (no image). - * - * @param {Object|null} category A product category object from the API. - * @return {number} The id of the category image. - */ -function getCategoryImageId( category ) { - if ( category && isObject( category.image ) ) { - return category.image.id; - } - return 0; -} - -/** - * Generate a style object given either a product category image from the API or URL to an image. - * - * @param {string} url An image URL. - * @return {Object} A style object with a backgroundImage set (if a valid image is provided). - */ -function getBackgroundImageStyles( 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 { - getCategoryImageSrc, - getCategoryImageId, - getBackgroundImageStyles, - dimRatioToClass, -}; - -export function calculateBackgroundImagePosition( coords ) { - if ( ! coords ) return {}; - - const x = Math.round( coords.x * 100 ); - const y = Math.round( coords.y * 100 ); - - return { - objectPosition: `${ x }% ${ y }%`, - }; -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/block-controls.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/block-controls.js new file mode 100644 index 00000000000..5f4c7b8c219 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/block-controls.js @@ -0,0 +1,116 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + AlignmentToolbar, + BlockControls as BlockControlsWrapper, + MediaReplaceFlow, +} from '@wordpress/block-editor'; +import { ToolbarButton, ToolbarGroup } from '@wordpress/components'; +import { crop } from '@wordpress/icons'; +import TextToolbarButton from '@woocommerce/editor-components/text-toolbar-button'; + +/** + * Internal dependencies + */ +import { useBackgroundImage } from './use-background-image'; + +export const BlockControls = ( { + backgroundImageId, + backgroundImageSrc, + contentAlign, + cropLabel, + editLabel, + editMode, + isEditingImage, + mediaSrc, + setAttributes, + setIsEditingImage, +} ) => { + return ( + + { + setAttributes( { contentAlign: nextAlign } ); + } } + /> + + { backgroundImageSrc && ! isEditingImage && ( + setIsEditingImage( true ) } + icon={ crop } + label={ cropLabel } + /> + ) } + { + setAttributes( { + mediaId: media.id, + mediaSrc: media.url, + } ); + } } + allowedTypes={ [ 'image' ] } + /> + { backgroundImageId && mediaSrc ? ( + + setAttributes( { mediaId: 0, mediaSrc: '' } ) + } + > + { __( 'Reset', 'woo-gutenberg-products-block' ) } + + ) : null } + + + setAttributes( { editMode: ! editMode } ), + isActive: editMode, + }, + ] } + /> + + ); +}; + +export const withBlockControls = ( { cropLabel, editLabel } ) => ( + Component +) => ( props ) => { + const [ isEditingImage, setIsEditingImage ] = props.useEditingImage; + const { attributes, category, name, product, setAttributes } = props; + const { contentAlign, editMode, mediaId, mediaSrc } = attributes; + const item = category || product; + + const { backgroundImageId, backgroundImageSrc } = useBackgroundImage( { + item, + mediaId, + mediaSrc, + blockName: name, + } ); + + return ( + <> + + + + ); +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/call-to-action.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/call-to-action.js new file mode 100644 index 00000000000..bcab9aec57e --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/call-to-action.js @@ -0,0 +1,57 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { RichText, InnerBlocks } from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; + +export const CallToAction = ( { itemId, linkText, permalink } ) => { + const buttonClasses = classnames( + 'wp-block-button__link', + 'is-style-fill' + ); + const buttonStyle = { + backgroundColor: 'vivid-green-cyan', + borderRadius: '5px', + }; + const wrapperStyle = { + width: '100%', + }; + return itemId === 'preview' ? ( +
+ +
+ ) : ( + + ); +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/constants.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/constants.js new file mode 100644 index 00000000000..d96ceb50315 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/constants.js @@ -0,0 +1,9 @@ +export const DEFAULT_EDITOR_SIZE = { + height: 500, + width: 500, +}; + +export const BLOCK_NAMES = { + featuredCategory: 'woocommerce/featured-category', + featuredProduct: 'woocommerce/featured-product', +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/constrained-resizable.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/constrained-resizable.js new file mode 100644 index 00000000000..a63d63e57ee --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/constrained-resizable.js @@ -0,0 +1,44 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { useState } from 'react'; +import { ResizableBox } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { useThrottle } from '../../utils/useThrottle'; + +export const ConstrainedResizable = ( { + className = '', + onResize, + ...props +} ) => { + const [ isResizing, setIsResizing ] = useState( false ); + + const classNames = classnames( className, { + 'is-resizing': isResizing, + } ); + const throttledResize = useThrottle( + ( event, direction, elt ) => { + if ( ! isResizing ) setIsResizing( true ); + onResize( event, direction, elt ); + }, + 50, + { leading: true } + ); + + return ( + { + onResize( ...args ); + setIsResizing( false ); + } } + { ...props } + /> + ); +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/edit.tsx new file mode 100644 index 00000000000..9ee26016b5c --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/edit.tsx @@ -0,0 +1,21 @@ +/** + * External dependencies + */ +import { FunctionComponent } from 'react'; +import { useBlockProps } from '@wordpress/block-editor'; + +export function Edit< T >( Block: FunctionComponent< T > ) { + return function WithBlock( props: T ): JSX.Element { + const blockProps = useBlockProps(); + + // The useBlockProps function returns the style with the `color`. + // We need to remove it to avoid the block to be styled with the color. + const { color, ...styles } = blockProps.style; + + return ( +
+ +
+ ); + }; +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/block.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/block.js new file mode 100644 index 00000000000..b3436b5c572 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/block.js @@ -0,0 +1,118 @@ +/** + * External dependencies + */ +import { withCategory } from '@woocommerce/block-hocs'; +import { withSpokenMessages } from '@wordpress/components'; +import { compose, createHigherOrderComponent } from '@wordpress/compose'; +import { withSelect } from '@wordpress/data'; +import { Component } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { folderStarred } from '@woocommerce/icons'; + +/** + * Internal dependencies + */ +import { withBlockControls } from '../block-controls'; +import { withImageEditor } from '../image-editor'; +import { withInspectorControls } from '../inspector-controls'; +import { withApiError } from '../with-api-error'; +import { withEditMode } from '../with-edit-mode'; +import { withEditingImage } from '../with-editing-image'; +import { withFeaturedItem } from '../with-featured-item'; + +const GENERIC_CONFIG = { + icon: folderStarred, + label: __( 'Featured Category', 'woo-gutenberg-products-block' ), +}; + +const BLOCK_CONTROL_CONFIG = { + cropLabel: __( 'Edit category image', 'woo-gutenberg-products-block' ), + editLabel: __( 'Edit selected category', 'woo-gutenberg-products-block' ), +}; + +const CONTENT_CONFIG = { + ...GENERIC_CONFIG, + emptyMessage: __( + 'No product category is selected.', + 'woo-gutenberg-products-block' + ), +}; + +const EDIT_MODE_CONFIG = { + ...GENERIC_CONFIG, + description: __( + 'Visually highlight a product category and encourage prompt action.', + 'woo-gutenberg-products-block' + ), + editLabel: __( + 'Showing Featured Product block preview.', + 'woo-gutenberg-products-block' + ), +}; + +export default compose( [ + withCategory, + 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, + category, + } = this.props; + if ( + this.state.doUrlUpdate && + ! attributes.editMode && + category?.permalink && + currentButtonAttributes?.url && + category.permalink !== currentButtonAttributes.url + ) { + updateBlockAttributes( { + ...currentButtonAttributes, + url: category.permalink, + } ); + this.setState( { doUrlUpdate: false } ); + } + } + triggerUrlUpdate = () => { + this.setState( { doUrlUpdate: true } ); + }; + render() { + return ( + + ); + } + } + return WrappedComponent; + }, 'withUpdateButtonAttributes' ), + withEditingImage, + withEditMode( EDIT_MODE_CONFIG ), + withFeaturedItem( CONTENT_CONFIG ), + withApiError, + withImageEditor, + withInspectorControls, + withBlockControls( BLOCK_CONTROL_CONFIG ), +] )( () => <> ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/block.json b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/block.json similarity index 100% rename from plugins/woocommerce-blocks/assets/js/blocks/featured-category/block.json rename to plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/block.json diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/editor.scss new file mode 100644 index 00000000000..9b0eb3b1329 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/editor.scss @@ -0,0 +1,6 @@ +@import "../style"; + +.wp-block-woocommerce-featured-category { + @extend %with-media-controls; + @extend %with-resizable-box; +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/example.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/example.js similarity index 51% rename from plugins/woocommerce-blocks/assets/js/blocks/featured-category/example.js rename to plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/example.js index 341176cd0eb..92f45ba2925 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/example.js +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/example.js @@ -1,18 +1,10 @@ /** * External dependencies */ -import { getSetting } from '@woocommerce/settings'; import { previewCategories } from '@woocommerce/resource-previews'; export const example = { attributes: { - alt: '', - contentAlign: 'center', - dimRatio: 50, - editMode: false, - height: getSetting( 'default_height', 500 ), - mediaSrc: '', - showDesc: true, categoryId: 'preview', previewCategory: previewCategories[ 0 ], }, diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/index.tsx new file mode 100644 index 00000000000..75bbcd4c96f --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/index.tsx @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { folderStarred } from '@woocommerce/icons'; +import { Icon } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import './style.scss'; +import './editor.scss'; +import Block from './block'; +import metadata from './block.json'; +import { register } from '../register'; +import { example } from './example'; + +register( Block, example, metadata, { + icon: { + src: ( + + ), + }, +} ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/style.scss new file mode 100644 index 00000000000..a6688dfa188 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/style.scss @@ -0,0 +1,9 @@ +@import "../style"; + +.wp-block-woocommerce-featured-category { + @extend %wp-block-featured-item; +} + +.wc-block-featured-category { + @include wc-block-featured-item(); +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/utils.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/utils.js new file mode 100644 index 00000000000..f5abff54969 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/utils.js @@ -0,0 +1,32 @@ +/** + * External dependencies + */ +import { isObject } from 'lodash'; + +/** + * Get the src from a category object, unless null (no image). + * + * @param {Object|null} category A product category object from the API. + * @return {string} The src of the category image. + */ +function getCategoryImageSrc( category ) { + if ( category && isObject( category.image ) ) { + return category.image.src; + } + return ''; +} + +/** + * Get the attachment ID from a category object, unless null (no image). + * + * @param {Object|null} category A product category object from the API. + * @return {number} The id of the category image. + */ +function getCategoryImageId( category ) { + if ( category && isObject( category.image ) ) { + return category.image.id; + } + return 0; +} + +export { getCategoryImageSrc, getCategoryImageId }; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/block.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/block.js new file mode 100644 index 00000000000..2b98b4df9b8 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/block.js @@ -0,0 +1,118 @@ +/** + * External dependencies + */ +import { withProduct } from '@woocommerce/block-hocs'; +import { withSpokenMessages } from '@wordpress/components'; +import { compose, createHigherOrderComponent } from '@wordpress/compose'; +import { withSelect } from '@wordpress/data'; +import { Component } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { starEmpty } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { withBlockControls } from '../block-controls'; +import { withImageEditor } from '../image-editor'; +import { withInspectorControls } from '../inspector-controls'; +import { withApiError } from '../with-api-error'; +import { withEditMode } from '../with-edit-mode'; +import { withEditingImage } from '../with-editing-image'; +import { withFeaturedItem } from '../with-featured-item'; + +const GENERIC_CONFIG = { + icon: starEmpty, + label: __( 'Featured Product', 'woo-gutenberg-products-block' ), +}; + +const BLOCK_CONTROL_CONFIG = { + cropLabel: __( 'Edit product image', 'woo-gutenberg-products-block' ), + editLabel: __( 'Edit selected product', 'woo-gutenberg-products-block' ), +}; + +const CONTENT_CONFIG = { + ...GENERIC_CONFIG, + emptyMessage: __( + 'No product is selected.', + 'woo-gutenberg-products-block' + ), +}; + +const EDIT_MODE_CONFIG = { + ...GENERIC_CONFIG, + description: __( + 'Visually highlight a product or variation and encourage prompt action', + 'woo-gutenberg-products-block' + ), + editLabel: __( + 'Showing Featured Product block preview.', + 'woo-gutenberg-products-block' + ), +}; + +export default compose( [ + withProduct, + 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 ( + + ); + } + } + return WrappedComponent; + }, 'withUpdateButtonAttributes' ), + withEditingImage, + withEditMode( EDIT_MODE_CONFIG ), + withFeaturedItem( CONTENT_CONFIG ), + withApiError, + withImageEditor, + withInspectorControls, + withBlockControls( BLOCK_CONTROL_CONFIG ), +] )( () => <> ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/block.json b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/block.json similarity index 100% rename from plugins/woocommerce-blocks/assets/js/blocks/featured-product/block.json rename to plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/block.json diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/editor.scss new file mode 100644 index 00000000000..39553be112c --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/editor.scss @@ -0,0 +1,10 @@ +@import "../style"; + +.wp-block-woocommerce-featured-product { + @extend %with-media-controls; + @extend %with-resizable-box; + + &__message { + margin-bottom: 16px; + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/example.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/example.js new file mode 100644 index 00000000000..71fbe0c1da0 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/example.js @@ -0,0 +1,11 @@ +/** + * External dependencies + */ +import { previewProducts } from '@woocommerce/resource-previews'; + +export const example = { + attributes: { + productId: 'preview', + previewProduct: previewProducts[ 0 ], + }, +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/index.tsx new file mode 100644 index 00000000000..0bf5e083f7f --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/index.tsx @@ -0,0 +1,25 @@ +/** + * External dependencies + */ +import { Icon, starEmpty } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import './style.scss'; +import './editor.scss'; +import Block from './block'; +import { register } from '../register'; +import { example } from './example'; +import metadata from './block.json'; + +register( Block, example, metadata, { + icon: { + src: ( + + ), + }, +} ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/style.scss new file mode 100644 index 00000000000..5e2a726dba7 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/style.scss @@ -0,0 +1,32 @@ +@import "../style"; + +.wp-block-woocommerce-featured-product { + @extend %wp-block-featured-item; + background-color: transparent; +} + +.wc-block-featured-product { + @include wc-block-featured-item(); + + .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.5; + } + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/image-editor.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/image-editor.js new file mode 100644 index 00000000000..8de0d51ca56 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/image-editor.js @@ -0,0 +1,85 @@ +/* eslint-disable @wordpress/no-unsafe-wp-apis */ + +/** + * External dependencies + */ +import { + __experimentalImageEditingProvider as ImageEditingProvider, + __experimentalImageEditor as GutenbergImageEditor, +} from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import { BLOCK_NAMES, DEFAULT_EDITOR_SIZE } from './constants'; +import { useBackgroundImage } from './use-background-image'; + +export const ImageEditor = ( { + backgroundImageId, + backgroundImageSize, + backgroundImageSrc, + isEditingImage, + setAttributes, + setIsEditingImage, +} ) => { + return ( + <> + { + setAttributes( { mediaId: id, mediaSrc: url } ); + } } + isEditing={ isEditingImage } + onFinishEditing={ () => setIsEditingImage( false ) } + > + + + + ); +}; + +export const withImageEditor = ( Component ) => ( props ) => { + const [ isEditingImage, setIsEditingImage ] = props.useEditingImage; + + const { attributes, backgroundImageSize, name, setAttributes } = props; + const { mediaId, mediaSrc } = attributes; + const item = + name === BLOCK_NAMES.featuredProduct ? props.product : props.category; + + const { backgroundImageId, backgroundImageSrc } = useBackgroundImage( { + item, + mediaId, + mediaSrc, + blockName: name, + } ); + + if ( isEditingImage ) { + return ( + + ); + } + + return ; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/inspector-controls.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/inspector-controls.js new file mode 100644 index 00000000000..6faafc1b9c6 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/inspector-controls.js @@ -0,0 +1,292 @@ +/* eslint-disable @wordpress/no-unsafe-wp-apis */ + +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + InspectorControls as GutenbergInspectorControls, + __experimentalPanelColorGradientSettings as PanelColorGradientSettings, + __experimentalUseGradient as useGradient, +} from '@wordpress/block-editor'; +import { + FocalPointPicker, + PanelBody, + RangeControl, + ToggleControl, + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOption as ToggleGroupControlOption, + TextareaControl, + ExternalLink, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { useBackgroundImage } from './use-background-image'; +import { BLOCK_NAMES } from './constants'; + +export const InspectorControls = ( { + alt, + backgroundImageSrc, + contentPanel, + dimRatio, + focalPoint = { x: 0.5, y: 0.5 }, + hasParallax, + isRepeated, + imageFit, + overlayColor, + overlayGradient, + setAttributes, + setGradient, + showDesc, +} ) => { + // 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'; + + const isImgElement = ! isRepeated && ! hasParallax; + + return ( + + + setAttributes( { showDesc: ! showDesc } ) } + /> + { contentPanel } + + { !! backgroundImageSrc && ( + <> + { focalPointPickerExists && ( + + { + setAttributes( { + hasParallax: ! hasParallax, + } ); + } } + /> + { + setAttributes( { + isRepeated: ! isRepeated, + } ); + } } + /> + { ! isRepeated && ( + +

+ { __( + 'Choose “Cover” if you want the image to scale automatically to always fit its container.', + 'woo-gutenberg-products-block' + ) } +

+

+ { __( + 'Note: by choosing “Cover” you will lose the ability to freely move the focal point precisely.', + 'woo-gutenberg-products-block' + ) } +

+ + } + label={ __( + 'Image fit', + 'woo-gutenberg-products-block' + ) } + value={ imageFit } + onChange={ ( value ) => + setAttributes( { + imageFit: value, + } ) + } + > + + +
+ ) } + + setAttributes( { + focalPoint: value, + } ) + } + /> + { isImgElement && ( + { + setAttributes( { alt: value } ); + } } + help={ + <> + + { __( + 'Describe the purpose of the image', + 'woo-gutenberg-products-block' + ) } + + + } + /> + ) } +
+ ) } + + setAttributes( { overlayColor: value } ), + onGradientChange: ( value ) => { + setGradient( value ); + setAttributes( { + overlayGradient: value, + } ); + }, + label: __( + 'Color', + 'woo-gutenberg-products-block' + ), + }, + ] } + > + + setAttributes( { dimRatio: value } ) + } + min={ 0 } + max={ 100 } + step={ 10 } + required + /> + + + ) } +
+ ); +}; + +export const withInspectorControls = ( Component ) => ( props ) => { + const { attributes, name, setAttributes } = props; + const { + alt, + dimRatio, + focalPoint, + hasParallax, + isRepeated, + imageFit, + mediaId, + mediaSrc, + overlayColor, + overlayGradient, + showDesc, + showPrice, + } = attributes; + + const item = + name === BLOCK_NAMES.featuredProduct ? props.product : props.category; + + const { setGradient } = useGradient( { + gradientAttribute: 'overlayGradient', + customGradientAttribute: 'overlayGradient', + } ); + const { backgroundImageSrc } = useBackgroundImage( { + item, + mediaId, + mediaSrc, + blockName: name, + } ); + + const contentPanel = name === BLOCK_NAMES.featuredProduct && ( + + setAttributes( { + showPrice: ! showPrice, + } ) + } + /> + ); + + return ( + <> + + + + ); +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/register.tsx b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/register.tsx new file mode 100644 index 00000000000..067b22e30f2 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/register.tsx @@ -0,0 +1,100 @@ +/** + * External dependencies + */ +import { FunctionComponent } from 'react'; +import { InnerBlocks } from '@wordpress/block-editor'; +import { BlockConfiguration, registerBlockType } from '@wordpress/blocks'; +import { getSetting } from '@woocommerce/settings'; +import { isFeaturePluginBuild } from '@woocommerce/block-settings'; + +/** + * Internal dependencies + */ +import { Edit } from './edit'; + +export function register( + Block: FunctionComponent, + example: { attributes: Record< string, unknown > }, + metadata: BlockConfiguration, + settings: Partial< BlockConfiguration > +): void { + const DEFAULT_SETTINGS = { + attributes: { + ...metadata.attributes, + /** + * A minimum height for the block. + * + * Note: if padding is increased, this way the inner content will never + * overflow, but instead will resize the container. + * + * It was decided to change this to make this block more in line with + * the “Cover” block. + */ + minHeight: { + type: 'number', + default: getSetting( 'default_height', 500 ), + }, + }, + supports: { + ...metadata.supports, + color: { + background: metadata.supports?.color?.background, + text: metadata.supports?.color?.text, + ...( isFeaturePluginBuild() && { + __experimentalDuotone: + metadata.supports?.color?.__experimentalDuotone, + } ), + }, + spacing: { + padding: metadata.supports?.spacing?.padding, + ...( isFeaturePluginBuild() && { + __experimentalDefaultControls: { + padding: + metadata.supports?.spacing + ?.__experimentalDefaultControls, + }, + __experimentalSkipSerialization: + metadata.supports?.spacing + ?.__experimentalSkipSerialization, + } ), + }, + ...( isFeaturePluginBuild() && { + __experimentalBorder: metadata?.supports?.__experimentalBorder, + } ), + }, + }; + + const DEFAULT_EXAMPLE = { + attributes: { + alt: '', + contentAlign: 'center', + dimRatio: 50, + editMode: false, + hasParallax: false, + isRepeated: false, + height: getSetting( 'default_height', 500 ), + mediaSrc: '', + overlayColor: '#000000', + showDesc: true, + }, + }; + + registerBlockType( metadata, { + ...DEFAULT_SETTINGS, + example: { + ...DEFAULT_EXAMPLE, + example, + }, + /** + * Renders and manages the block. + * + * @param {Object} props Props to pass to block. + */ + edit: Edit( Block ), + /** + * Block content is rendered in PHP, not via save function. + */ + save: () => , + ...settings, + } ); +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/style.scss similarity index 50% rename from plugins/woocommerce-blocks/assets/js/blocks/featured-product/style.scss rename to plugins/woocommerce-blocks/assets/js/blocks/featured-items/style.scss index caf6872aef9..562d62fb4ed 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/style.scss +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/style.scss @@ -1,27 +1,12 @@ -.wp-block-woocommerce-featured-product { - background-color: transparent; - border-color: transparent; - color: #fff; - box-sizing: border-box; +@mixin with-content-selection { + background-color: inherit; - .components-resizable-box__container { - position: absolute !important; - top: 0; - left: 0; - right: 0; - bottom: 0; - min-height: 50px; - - &:not(.is-resizing) { - height: auto !important; - } - } - - &.is-repeated { - background-repeat: repeat; - background-size: auto; + &__selection { + width: 100%; } +} +%with-media-controls { // Applying image edits .is-applying { .components-spinner { @@ -38,11 +23,42 @@ } } -.wc-block-featured-product { +%with-resizable-box { + .components-resizable-box__container { + position: absolute !important; + top: 0; + left: 0; + right: 0; + bottom: 0; + min-height: 50px; + + &:not(.is-resizing) { + height: auto !important; + } + } + + .components-resizable-box__handle { + z-index: 10; + } +} + +%wp-block-featured-item { + background-color: transparent; + border-color: transparent; + color: #fff; + box-sizing: border-box; +} + +@mixin wc-block-featured-item { + $block: &; + + @include with-background-dim(); + @include with-content-selection(); + align-content: center; align-items: center; - background-size: cover; background-position: center center; + background-size: cover; display: flex; flex-wrap: wrap; justify-content: center; @@ -51,25 +67,13 @@ position: relative; width: 100%; - .wc-block-featured-product__wrapper { - align-content: center; - align-items: center; - box-sizing: border-box; - display: flex; - flex-wrap: wrap; - height: 100%; - width: 100%; - justify-content: center; - overflow: hidden; - } - &.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 { + #{$block}__description, + #{$block}__price, + #{$block}__title, + #{$block}__variation { margin-left: 0; text-align: left; } @@ -78,23 +82,27 @@ &.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 { + #{$block}__description, + #{$block}__price, + #{$block}__title, + #{$block}__variation { 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 { + &.is-repeated { + background-repeat: repeat; + background-size: auto; + } + + &__description, + &__price, + &__title, + &__variation { line-height: 1.25; margin-bottom: 0; text-align: center; - color: inherit; a, a:hover, @@ -104,39 +112,18 @@ } } - .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 { + &__description, + &__link, + &__price, + &__title, + &__variation { + color: inherit; width: 100%; - padding: 16px 48px 0 48px; + padding: 0 48px 16px 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.5; - } - } - - .wc-block-featured-product__background-image { + & &__background-image { @include absolute-stretch(); object-fit: none; @@ -157,57 +144,39 @@ } } + &__description { + color: inherit; + + p { + margin: 0; + } + } + + &__title { + margin-top: 0; + + div { + color: inherit; + } + + &::before { + display: none; + } + } + + &__wrapper { + align-content: center; + align-items: center; + box-sizing: border-box; + display: flex; + flex-wrap: wrap; + justify-content: center; + overflow: hidden; + width: 100%; + height: 100%; + } + .wp-block-button.aligncenter { text-align: center; } - - &.has-background-dim { - .wc-block-featured-product__overlay::before { - background: inherit; - content: ""; - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - opacity: 0.5; - z-index: 1; - } - } - - @for $i from 1 through 10 { - &.has-background-dim.has-background-dim-#{ $i * 10 } { - .wc-block-featured-product__overlay::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; - } } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/use-background-image.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/use-background-image.js new file mode 100644 index 00000000000..77d62f6ceb4 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/use-background-image.js @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import { + getImageSrcFromProduct, + getImageIdFromProduct, +} from '@woocommerce/utils'; +import { useEffect, useState } from 'react'; + +/** + * Internal dependencies + */ +import { BLOCK_NAMES } from './constants'; +import { + getCategoryImageSrc, + getCategoryImageId, +} from './featured-category/utils'; + +export function useBackgroundImage( { blockName, item, mediaId, mediaSrc } ) { + const [ backgroundImageId, setBackgroundImageId ] = useState( 0 ); + const [ backgroundImageSrc, setBackgroundImageSrc ] = useState( '' ); + + useEffect( () => { + if ( mediaId ) { + setBackgroundImageId( mediaId ); + } else { + setBackgroundImageId( + blockName === BLOCK_NAMES.featuredProduct + ? getImageIdFromProduct( item ) + : getCategoryImageId( item ) + ); + } + }, [ blockName, item, mediaId ] ); + + useEffect( () => { + if ( mediaSrc ) { + setBackgroundImageSrc( mediaSrc ); + } else { + setBackgroundImageSrc( + blockName === BLOCK_NAMES.featuredProduct + ? getImageSrcFromProduct( item ) + : getCategoryImageSrc( item ) + ); + } + }, [ blockName, item, mediaSrc ] ); + + return { backgroundImageId, backgroundImageSrc }; +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/utils.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/utils.js new file mode 100644 index 00000000000..53be26f3a62 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/utils.js @@ -0,0 +1,87 @@ +export function calculateBackgroundImagePosition( coords ) { + if ( ! coords ) return {}; + + return { + objectPosition: calculatePercentPositionFromCoordinates( coords ), + }; +} + +export function calculatePercentPositionFromCoordinates( coords ) { + if ( ! coords ) return ''; + + const x = Math.round( coords.x * 100 ); + const y = Math.round( coords.y * 100 ); + + return `${ x }% ${ y }%`; +} + +/** + * Generate the style object of the background image of the block. + * + * It outputs styles for either an `img` element or a `div` with a background, + * depending on what is needed. + * + * @param {Object} opts Options for the element. + * @param {Object} opts.focalPoint X and Y coordinates of the image. + * @param {'cover' | 'none'} opts.imageFit How to fit the image in the wrapper. + * @param {boolean} opts.isImgElement Whether the rendered background is an `img` element. + * @param {boolean} opts.isRepeated Whether the background is repeated (no effect if `isImgElement` is `true`). + * @param {string} opts.url The url of the image. + * + * @return {Object} A style object with a backgroundImage set (if a valid image is provided). + */ +export function getBackgroundImageStyles( { + focalPoint, + imageFit, + isImgElement, + isRepeated, + url, +} ) { + let styles = {}; + + if ( isImgElement ) { + styles = { + ...styles, + ...calculateBackgroundImagePosition( focalPoint ), + objectFit: imageFit, + }; + } else { + styles = { + ...styles, + ...( url && { + backgroundImage: `url(${ url })`, + } ), + backgroundPosition: calculatePercentPositionFromCoordinates( + focalPoint + ), + ...( ! isRepeated && { + backgroundRepeat: 'no-repeat', + backgroundSize: imageFit === 'cover' ? imageFit : 'auto', + } ), + }; + } + + return styles; +} + +/** + * Generates the CSS class prefix for scoping elements to a block. + * + * @param {string} blockName The name of the block. + * @return {string} The prefix for the HTML elements belonging to that block. + */ +export function getClassPrefixFromName( blockName ) { + return `wc-block-${ blockName.split( '/' )[ 1 ] }`; +} + +/** + * 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). + */ +export function dimRatioToClass( ratio ) { + return ratio === 0 || ratio === 50 + ? null + : `has-background-dim-${ 10 * Math.round( ratio / 10 ) }`; +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-api-error.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-api-error.js new file mode 100644 index 00000000000..7e920db80c0 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-api-error.js @@ -0,0 +1,33 @@ +/** + * External dependencies + */ +import ErrorPlaceholder from '@woocommerce/editor-components/error-placeholder'; + +/** + * Internal dependencies + */ +import { BLOCK_NAMES } from './constants'; +import { getClassPrefixFromName } from './utils'; + +export const withApiError = ( Component ) => ( props ) => { + const { error, isLoading, name } = props; + + const className = getClassPrefixFromName( name ); + const onRetry = + name === BLOCK_NAMES.featuredCategory + ? props.getCategory + : props.getProduct; + + if ( error ) { + return ( + + ); + } + + return ; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-edit-mode.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-edit-mode.js new file mode 100644 index 00000000000..52eff715c2f --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-edit-mode.js @@ -0,0 +1,81 @@ +/** + * External dependencies + */ +import { Placeholder, Icon, Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import ProductCategoryControl from '@woocommerce/editor-components/product-category-control'; +import ProductControl from '@woocommerce/editor-components/product-control'; + +/** + * Internal dependencies + */ +import { getClassPrefixFromName } from './utils'; +import { BLOCK_NAMES } from './constants'; + +export const withEditMode = ( { description, editLabel, icon, label } ) => ( + Component +) => ( props ) => { + const { + attributes, + debouncedSpeak, + name, + setAttributes, + triggerUrlUpdate = () => void null, + } = props; + + const className = getClassPrefixFromName( name ); + + const onDone = () => { + setAttributes( { editMode: false } ); + debouncedSpeak( editLabel ); + }; + + if ( attributes.editMode ) { + return ( + } + label={ label } + className={ className } + > + { description } +
+ { name === BLOCK_NAMES.featuredCategory && ( + { + const id = value[ 0 ] ? value[ 0 ].id : 0; + setAttributes( { + categoryId: id, + mediaId: 0, + mediaSrc: '', + } ); + triggerUrlUpdate(); + } } + isSingle + /> + ) } + { name === BLOCK_NAMES.featuredProduct && ( + { + const id = value[ 0 ] ? value[ 0 ].id : 0; + setAttributes( { + productId: id, + mediaId: 0, + mediaSrc: '', + } ); + triggerUrlUpdate(); + } } + /> + ) } + +
+
+ ); + } + + return ; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-editing-image.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-editing-image.js new file mode 100644 index 00000000000..e3653d43545 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-editing-image.js @@ -0,0 +1,20 @@ +/** + * External dependencies + */ +import { useEffect, useState } from 'react'; + +export const withEditingImage = ( Component ) => ( props ) => { + const [ isEditingImage, setIsEditingImage ] = useState( false ); + const { isSelected } = props; + + useEffect( () => { + setIsEditingImage( false ); + }, [ isSelected ] ); + + return ( + + ); +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-featured-item.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-featured-item.js new file mode 100644 index 00000000000..9b683060c49 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-featured-item.js @@ -0,0 +1,237 @@ +/* eslint-disable @wordpress/no-unsafe-wp-apis */ + +/** + * External dependencies + */ +import { __experimentalGetSpacingClassesAndStyles as getSpacingClassesAndStyles } from '@wordpress/block-editor'; +import { Icon, Placeholder, Spinner } from '@wordpress/components'; +import classnames from 'classnames'; +import { isEmpty } from 'lodash'; +import { useCallback, useState } from 'react'; + +/** + * Internal dependencies + */ +import { CallToAction } from './call-to-action'; +import { ConstrainedResizable } from './constrained-resizable'; +import { useBackgroundImage } from './use-background-image'; +import { + dimRatioToClass, + getBackgroundImageStyles, + getClassPrefixFromName, +} from './utils'; + +export const withFeaturedItem = ( { emptyMessage, icon, label } ) => ( + Component +) => ( props ) => { + const [ isEditingImage ] = props.useEditingImage; + + const { + attributes, + category, + isLoading, + isSelected, + name, + product, + setAttributes, + } = props; + const { mediaId, mediaSrc } = attributes; + const item = category || product; + const [ backgroundImageSize, setBackgroundImageSize ] = useState( {} ); + + const { backgroundImageSrc } = useBackgroundImage( { + item, + mediaId, + mediaSrc, + blockName: name, + } ); + + const className = getClassPrefixFromName( name ); + + const onResize = useCallback( + ( _event, _direction, elt ) => { + setAttributes( { minHeight: parseInt( elt.style.height, 10 ) } ); + }, + [ setAttributes ] + ); + + const renderButton = () => { + const { categoryId, linkText, productId } = attributes; + + return ( + + ); + }; + + const renderNoItem = () => ( + } + label={ label } + > + { isLoading ? : emptyMessage } + + ); + + const renderItem = () => { + const { + contentAlign, + dimRatio, + focalPoint, + hasParallax, + isRepeated, + imageFit, + minHeight, + overlayColor, + overlayGradient, + showDesc, + showPrice, + style, + } = attributes; + + const classes = classnames( + className, + { + 'is-selected': + isSelected && + attributes.categoryId !== 'preview' && + attributes.productId !== 'preview', + 'is-loading': ! item && isLoading, + 'is-not-found': ! item && ! isLoading, + 'has-background-dim': dimRatio !== 0, + 'is-repeated': isRepeated, + }, + dimRatioToClass( dimRatio ), + contentAlign !== 'center' && `has-${ contentAlign }-content` + ); + + const containerStyle = { + borderRadius: style?.border?.radius, + }; + + const wrapperStyle = { + ...getSpacingClassesAndStyles( attributes ).style, + minHeight, + }; + + const isImgElement = ! isRepeated && ! hasParallax; + + const backgroundImageStyle = getBackgroundImageStyles( { + focalPoint, + imageFit, + isImgElement, + isRepeated, + url: backgroundImageSrc, + } ); + + const overlayStyle = { + background: overlayGradient, + backgroundColor: overlayColor, + }; + + return ( + <> + +
+
+
+ { backgroundImageSrc && + ( isImgElement ? ( + { { + setBackgroundImageSize( { + height: e.target?.naturalHeight, + width: e.target?.naturalWidth, + } ); + } } + /> + ) : ( +
+ ) ) } +

+ { ! isEmpty( product?.variation ) && ( +

+ ) } + { showDesc && ( +
+ ) } + { showPrice && ( +
+ ) } +
+ { renderButton() } +
+
+
+ + ); + }; + + if ( isEditingImage ) { + return ( + + ); + } + + return ( + <> + + { item ? renderItem() : renderNoItem() } + + ); +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/block.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-product/block.js deleted file mode 100644 index 263b309343d..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/block.js +++ /dev/null @@ -1,851 +0,0 @@ -/* eslint-disable @wordpress/no-unsafe-wp-apis */ - -/** - * External dependencies - */ -import { useCallback, useEffect, useState } from 'react'; -import { __ } from '@wordpress/i18n'; -import { - AlignmentToolbar, - BlockControls, - InnerBlocks, - InspectorControls, - MediaReplaceFlow, - RichText, - __experimentalGetSpacingClassesAndStyles as getSpacingClassesAndStyles, - __experimentalImageEditingProvider as ImageEditingProvider, - __experimentalImageEditor as ImageEditor, - __experimentalPanelColorGradientSettings as PanelColorGradientSettings, - __experimentalUseGradient as useGradient, -} from '@wordpress/block-editor'; -import { withSelect } from '@wordpress/data'; -import { - Button, - ExternalLink, - FocalPointPicker, - PanelBody, - Placeholder, - RangeControl, - ResizableBox, - Spinner, - TextareaControl, - ToggleControl, - ToolbarButton, - ToolbarGroup, - withSpokenMessages, - __experimentalToggleGroupControl as ToggleGroupControl, - __experimentalToggleGroupControlOption as ToggleGroupControlOption, -} 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 ProductControl from '@woocommerce/editor-components/product-control'; -import ErrorPlaceholder from '@woocommerce/editor-components/error-placeholder'; -import TextToolbarButton from '@woocommerce/editor-components/text-toolbar-button'; -import { withProduct } from '@woocommerce/block-hocs'; -import { crop, Icon, starEmpty } from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import { - backgroundImageStyles, - calculateImagePosition, - dimRatioToClass, -} from './utils'; -import { - getImageSrcFromProduct, - getImageIdFromProduct, -} from '../../utils/products'; -import { useThrottle } from '../../utils/useThrottle'; - -const DEFAULT_EDITOR_SIZE = { - height: 500, - width: 500, -}; - -export const ConstrainedResizable = ( { - className = '', - onResize, - ...props -} ) => { - const [ isResizing, setIsResizing ] = useState( false ); - - const classNames = classnames( className, { - 'is-resizing': isResizing, - } ); - const throttledResize = useThrottle( - ( event, direction, elt ) => { - if ( ! isResizing ) setIsResizing( true ); - onResize( event, direction, elt ); - }, - 50, - { leading: true } - ); - - return ( - { - onResize( ...args ); - setIsResizing( false ); - } } - { ...props } - /> - ); -}; - -/** - * 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.product Product object. - * @param {function(any):any} props.setAttributes Setter for attributes. - * @param {function():any} props.triggerUrlUpdate Function for triggering a url update for product. - */ -const FeaturedProduct = ( { - attributes, - debouncedSpeak, - error, - getProduct, - isLoading, - isSelected, - product, - setAttributes, - triggerUrlUpdate = () => void null, -} ) => { - const { mediaId, mediaSrc } = attributes; - - const [ isEditingImage, setIsEditingImage ] = useState( false ); - const [ backgroundImageSize, setBackgroundImageSize ] = useState( {} ); - const { setGradient } = useGradient( { - gradientAttribute: 'overlayGradient', - customGradientAttribute: 'overlayGradient', - } ); - - const backgroundImageSrc = mediaSrc || getImageSrcFromProduct( product ); - const backgroundImageId = mediaId || getImageIdFromProduct( product ); - - const onResize = useCallback( - ( _event, _direction, elt ) => { - setAttributes( { minHeight: parseInt( elt.style.height, 10 ) } ); - }, - [ setAttributes ] - ); - - const renderApiError = () => ( - - ); - - useEffect( () => { - setIsEditingImage( false ); - }, [ isSelected ] ); - - const renderEditMode = () => { - const onDone = () => { - setAttributes( { editMode: false } ); - debouncedSpeak( - __( - 'Showing Featured Product block preview.', - 'woo-gutenberg-products-block' - ) - ); - }; - - return ( - <> - { getBlockControls() } - } - label={ __( - 'Featured Product', - 'woo-gutenberg-products-block' - ) } - className="wc-block-featured-product" - > - { __( - 'Visually highlight a product or variation and encourage prompt action', - 'woo-gutenberg-products-block' - ) } -
- { - const id = value[ 0 ] ? value[ 0 ].id : 0; - setAttributes( { - productId: id, - mediaId: 0, - mediaSrc: '', - } ); - triggerUrlUpdate(); - } } - /> - -
-
- - ); - }; - - const getBlockControls = () => { - const { contentAlign, editMode } = attributes; - - return ( - - { - setAttributes( { contentAlign: nextAlign } ); - } } - /> - - { backgroundImageSrc && ! isEditingImage && ( - setIsEditingImage( true ) } - icon={ crop } - label={ __( - 'Edit product image', - 'woo-gutenberg-products-block' - ) } - /> - ) } - { - setAttributes( { - mediaId: media.id, - mediaSrc: media.url, - } ); - } } - allowedTypes={ [ 'image' ] } - /> - { backgroundImageId && mediaSrc ? ( - - setAttributes( { mediaId: 0, mediaSrc: '' } ) - } - > - { __( 'Reset', 'woo-gutenberg-products-block' ) } - - ) : null } - - - setAttributes( { editMode: ! editMode } ), - isActive: editMode, - }, - ] } - /> - - ); - }; - - const getInspectorControls = () => { - const url = attributes.mediaSrc || getImageSrcFromProduct( product ); - const { - focalPoint = { x: 0.5, y: 0.5 }, - hasParallax, - isRepeated, - } = 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'; - - const isImgElement = ! isRepeated && ! hasParallax; - - return ( - <> - - - - setAttributes( { - showDesc: ! attributes.showDesc, - } ) - } - /> - - setAttributes( { - showPrice: ! attributes.showPrice, - } ) - } - /> - - { !! url && ( - <> - { focalPointPickerExists && ( - - { - setAttributes( { - hasParallax: ! hasParallax, - } ); - } } - /> - { - setAttributes( { - isRepeated: ! isRepeated, - } ); - } } - /> - { ! isRepeated && ( - -

- { __( - 'Choose “Cover” if you want the image to scale automatically to always fit its container.', - 'woo-gutenberg-products-block' - ) } -

-

- { __( - 'Note: by choosing “Cover” you will lose the ability to freely move the focal point precisely.', - 'woo-gutenberg-products-block' - ) } -

- - } - label={ __( - 'Image fit', - 'woo-gutenberg-products-block' - ) } - value={ attributes.imageFit } - onChange={ ( value ) => - setAttributes( { - imageFit: value, - } ) - } - > - - -
- ) } - - setAttributes( { - focalPoint: value, - } ) - } - /> - { isImgElement && ( - { - setAttributes( { alt } ); - } } - help={ - <> - - { __( - 'Describe the purpose of the image', - 'woo-gutenberg-products-block' - ) } - - { __( - 'Leaving it empty will use the product name.', - 'woo-gutenberg-products-block' - ) } - - } - /> - ) } -
- ) } - - setAttributes( { overlayColor } ), - onGradientChange: ( - overlayGradient - ) => { - setGradient( overlayGradient ); - setAttributes( { - overlayGradient, - } ); - }, - label: __( - 'Color', - 'woo-gutenberg-products-block' - ), - }, - ] } - > - - setAttributes( { dimRatio } ) - } - min={ 0 } - max={ 100 } - step={ 10 } - required - /> - - - ) } -
- - - ); - }; - - const renderProduct = () => { - const { - contentAlign, - dimRatio, - focalPoint, - hasParallax, - isRepeated, - imageFit, - minHeight, - overlayColor, - overlayGradient, - showDesc, - showPrice, - style, - } = attributes; - - const classes = classnames( - 'wc-block-featured-product', - dimRatioToClass( dimRatio ), - { - 'is-selected': isSelected && attributes.productId !== 'preview', - 'is-loading': ! product && isLoading, - 'is-not-found': ! product && ! isLoading, - 'has-background-dim': dimRatio !== 0, - 'is-repeated': isRepeated, - }, - contentAlign !== 'center' && `has-${ contentAlign }-content` - ); - - const containerStyle = { - borderRadius: style?.border?.radius, - }; - - const backgroundImageStyle = { - objectPosition: calculateImagePosition( focalPoint ), - objectFit: imageFit, - }; - - const isImgElement = ! isRepeated && ! hasParallax; - - const wrapperStyle = { - ...getSpacingClassesAndStyles( attributes ).style, - minHeight, - }; - - const backgroundDivStyle = { - ...( ! isImgElement - ? { - ...backgroundImageStyles( backgroundImageSrc ), - backgroundPosition: calculateImagePosition( - focalPoint - ), - } - : undefined ), - ...( ! isRepeated && { - backgroundRepeat: 'no-repeat', - backgroundSize: imageFit === 'cover' ? imageFit : 'auto', - } ), - }; - - const overlayStyle = { - background: overlayGradient, - backgroundColor: overlayColor, - }; - - return ( - <> - -
-
-
- { isImgElement && ( - { { - setBackgroundImageSize( { - height: e.target?.naturalHeight, - width: e.target?.naturalWidth, - } ); - } } - /> - ) } - { ! isImgElement && ( -
- ) } -

- { ! isEmpty( product.variation ) && ( -

- ) } - { showDesc && ( -
- ) } - { showPrice && ( -
- ) } -
- { renderButton() } -
-
-
- - ); - }; - - 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' ? ( -
- -
- ) : ( - - ); - }; - - const renderNoProduct = () => ( - } - label={ __( 'Featured Product', 'woo-gutenberg-products-block' ) } - > - { isLoading ? ( - - ) : ( - __( 'No product is selected.', 'woo-gutenberg-products-block' ) - ) } - - ); - - const { editMode } = attributes; - - if ( error ) { - return renderApiError(); - } - - if ( editMode ) { - return renderEditMode(); - } - - if ( isEditingImage ) { - return ( - <> - { - setAttributes( { mediaId: id, mediaSrc: url } ); - } } - isEditing={ isEditingImage } - onFinishEditing={ () => setIsEditingImage( false ) } - > - - - - ); - } - - 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 withSpokenMessages - debouncedSpeak: PropTypes.func.isRequired, - triggerUrlUpdate: PropTypes.func, -}; - -export default compose( [ - withProduct, - 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 ( - - ); - } - } - - return WrappedComponent; - }, 'withUpdateButtonAttributes' ), -] )( FeaturedProduct ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/featured-product/edit.tsx deleted file mode 100644 index d8fe566c42f..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/edit.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/** - * External dependencies - */ -import { useBlockProps } from '@wordpress/block-editor'; - -/** - * Internal dependencies - */ -import Block from './block'; -import './editor.scss'; - -export const Edit = ( props: unknown ): JSX.Element => { - const blockProps = useBlockProps(); - - return ( -
- -
- ); -}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/featured-product/editor.scss deleted file mode 100644 index acc516bef47..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/editor.scss +++ /dev/null @@ -1,14 +0,0 @@ -.wc-block-featured-product { - background-color: inherit; - - .components-resizable-box__handle { - z-index: 10; - } -} -.wc-block-featured-product__message { - margin-bottom: 16px; -} - -.wc-block-featured-product__selection { - width: 100%; -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/example.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-product/example.js deleted file mode 100644 index f57fbc999b2..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/example.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * External dependencies - */ -import { getSetting } from '@woocommerce/settings'; -import { previewProducts } from '@woocommerce/resource-previews'; - -export const example = { - attributes: { - alt: '', - contentAlign: 'center', - dimRatio: 50, - editMode: false, - hasParallax: false, - isRepeated: false, - height: getSetting( 'default_height', 500 ), - mediaSrc: '', - overlayColor: '#000000', - showDesc: true, - productId: 'preview', - previewProduct: previewProducts[ 0 ], - }, -}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/featured-product/index.tsx deleted file mode 100644 index 1b5c3b3b287..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/index.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/** - * External dependencies - */ -import { InnerBlocks } from '@wordpress/block-editor'; -import { registerBlockType } from '@wordpress/blocks'; -import { getSetting } from '@woocommerce/settings'; -import { isFeaturePluginBuild } from '@woocommerce/block-settings'; -import { Icon, starEmpty } from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import './style.scss'; -import { example } from './example'; -import { Edit } from './edit'; -import metadata from './block.json'; - -/** - * Register and run the "Featured Product" block. - */ -registerBlockType( metadata, { - icon: { - src: ( - - ), - }, - attributes: { - ...metadata.attributes, - /** - * A minimum height for the block. - * - * Note: if padding is increased, this way the inner content will never - * overflow, but instead will resize the container. - * - * It was decided to change this to make this block more in line with - * the “Cover” block. - */ - minHeight: { - type: 'number', - default: getSetting( 'default_height', 500 ), - }, - }, - supports: { - ...metadata.supports, - color: { - background: true, - text: true, - ...( isFeaturePluginBuild() && { - __experimentalDuotone: - '.wc-block-featured-product__background-image', - } ), - }, - spacing: { - padding: true, - ...( isFeaturePluginBuild() && { - __experimentalDefaultControls: { - padding: true, - }, - __experimentalSkipSerialization: true, - } ), - }, - ...( isFeaturePluginBuild() && { - __experimentalBorder: { - color: true, - radius: true, - width: true, - __experimentalSkipSerialization: true, - }, - } ), - }, - example, - - /** - * Renders and manages the block. - * - * @param {Object} props Props to pass to block. - */ - edit: Edit, - - /** - * Block content is rendered in PHP, not via save function. - */ - save: () => { - return ; - }, -} ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/utils.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-product/utils.js deleted file mode 100644 index 82fa814a7b9..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/utils.js +++ /dev/null @@ -1,24 +0,0 @@ -export function calculateImagePosition( coords ) { - if ( ! coords ) return {}; - - const x = Math.round( coords.x * 100 ); - const y = Math.round( coords.y * 100 ); - - return `${ x }% ${ y }%`; -} - -/** - * 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). - */ -export function dimRatioToClass( ratio ) { - return ratio === 0 || ratio === 50 - ? null - : `has-background-dim-${ 10 * Math.round( ratio / 10 ) }`; -} - -export function backgroundImageStyles( url ) { - return { backgroundImage: `url(${ url })` }; -} diff --git a/plugins/woocommerce-blocks/bin/webpack-configs.js b/plugins/woocommerce-blocks/bin/webpack-configs.js index 0c824517ad7..90978da1d20 100644 --- a/plugins/woocommerce-blocks/bin/webpack-configs.js +++ b/plugins/woocommerce-blocks/bin/webpack-configs.js @@ -264,11 +264,13 @@ const getMainConfig = ( options = {} ) => { to: './checkout/block.json', }, { - from: './assets/js/blocks/featured-category/block.json', + from: + './assets/js/blocks/featured-items/featured-category/block.json', to: './featured-category/block.json', }, { - from: './assets/js/blocks/featured-product/block.json', + from: + './assets/js/blocks/featured-items/featured-product/block.json', to: './featured-product/block.json', }, { diff --git a/plugins/woocommerce-blocks/bin/webpack-entries.js b/plugins/woocommerce-blocks/bin/webpack-entries.js index 903189b85fb..b6e9d239f15 100644 --- a/plugins/woocommerce-blocks/bin/webpack-entries.js +++ b/plugins/woocommerce-blocks/bin/webpack-entries.js @@ -20,7 +20,9 @@ const blocks = { 'product-on-sale': {}, 'product-top-rated': {}, 'products-by-attribute': {}, - 'featured-product': {}, + 'featured-product': { + customDir: 'featured-items/featured-product', + }, 'all-reviews': { customDir: 'reviews/all-reviews', }, @@ -32,7 +34,9 @@ const blocks = { }, 'product-search': {}, 'product-tag': {}, - 'featured-category': {}, + 'featured-category': { + customDir: 'featured-items/featured-category', + }, 'all-products': { customDir: 'products/all-products', }, diff --git a/plugins/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php b/plugins/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php index 12e00b999a7..00e7d7b0393 100644 --- a/plugins/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php +++ b/plugins/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php @@ -179,7 +179,7 @@ class FeaturedCategory extends AbstractDynamicBlock { $overlay_styles = 'background-color: #000000'; } - return sprintf( '', esc_attr( $overlay_styles ) ); + return sprintf( '
', esc_attr( $overlay_styles ) ); } /** diff --git a/plugins/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php b/plugins/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php index cd10147eec8..1405ab69d50 100644 --- a/plugins/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php +++ b/plugins/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php @@ -230,7 +230,7 @@ class FeaturedProduct extends AbstractDynamicBlock { $overlay_styles = 'background-color: #000000'; } - return sprintf( '', esc_attr( $overlay_styles ) ); + return sprintf( '
', esc_attr( $overlay_styles ) ); } /**