) : (
);
};
PK E[TQL L edit.tsxnu [ /**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import type { FunctionComponent } from 'react';
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 (
);
};
}
PK E[l utils.tsnu [ /**
* Internal dependencies
*/
import { Coordinates, ImageFit } from './types';
/**
* Given x and y coordinates between 0 and 1 returns a rounded percentage string.
*
* Useful for converting to a CSS-compatible position string.
*/
export function calculatePercentPositionFromCoordinates( coords: Coordinates ) {
if ( ! coords ) return '';
const x = Math.round( coords.x * 100 );
const y = Math.round( coords.y * 100 );
return `${ x }% ${ y }%`;
}
/**
* Given x and y coordinates between 0 and 1 returns a CSS `objectPosition`.
*/
export function calculateBackgroundImagePosition( coords: Coordinates ) {
if ( ! coords ) return {};
return {
objectPosition: calculatePercentPositionFromCoordinates( coords ),
};
}
/**
* 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.
*/
export function getBackgroundImageStyles( {
focalPoint,
imageFit,
isImgElement,
isRepeated,
url,
}: {
focalPoint: Coordinates;
imageFit: ImageFit;
isImgElement: boolean;
isRepeated: boolean;
url: string;
} ) {
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.
*/
export function getClassPrefixFromName( blockName: string ) {
return `wc-block-${ blockName.split( '/' )[ 1 ] }`;
}
/**
* Convert the selected ratio to the correct background class.
*
* @param ratio Selected opacity from 0 to 100.
* @return The class name, if applicable (not used for ratio 0 or 50).
*/
export function dimRatioToClass( ratio: number ) {
return ratio === 0 || ratio === 50
? null
: `has-background-dim-${ 10 * Math.round( ratio / 10 ) }`;
}
PK E[J3 3 featured-category/block.jsonnu [ {
"name": "woocommerce/featured-category",
"version": "1.0.0",
"title": "Featured Category",
"category": "woocommerce",
"keywords": [
"WooCommerce"
],
"description": "Visually highlight a product category and encourage prompt action.",
"supports": {
"align": [
"wide",
"full"
],
"html": false,
"color": {
"background": true,
"text": true
},
"spacing": {
"padding": true,
"__experimentalDefaultControls": {
"padding": true
},
"__experimentalSkipSerialization": true
},
"__experimentalBorder": {
"color": true,
"radius": true,
"width": true,
"__experimentalSkipSerialization": true
}
},
"attributes": {
"alt": {
"type": "string",
"default": ""
},
"contentAlign": {
"type": "string",
"default": "center"
},
"dimRatio": {
"type": "number",
"default": 50
},
"editMode": {
"type": "boolean",
"default": true
},
"focalPoint": {
"type": "object",
"default": {
"x": 0.5,
"y": 0.5
}
},
"imageFit": {
"type": "string",
"default": "none"
},
"hasParallax": {
"type": "boolean",
"default": false
},
"isRepeated": {
"type": "boolean",
"default": false
},
"mediaId": {
"type": "number",
"default": 0
},
"mediaSrc": {
"type": "string",
"default": ""
},
"minHeight": {
"type": "number",
"default": 500
},
"linkText": {
"default": "Shop now",
"type": "string"
},
"categoryId": {
"type": "number"
},
"overlayColor": {
"type": "string",
"default": "#000000"
},
"overlayGradient": {
"type": "string"
},
"previewCategory": {
"type": "object",
"default": null
},
"showDesc": {
"type": "boolean",
"default": true
}
},
"textdomain": "woocommerce",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}
PK E[(>ߍ featured-category/utils.tsnu [ /**
* External dependencies
*/
import { WP_REST_API_Category } from 'wp-types';
/**
* Internal dependencies
*/
import { isImageObject } from '../types';
/**
* Get the src from a category object, unless null (no image).
*/
export function getCategoryImageSrc( category: WP_REST_API_Category ) {
if ( category && isImageObject( category.image ) ) {
return category.image.src;
}
return '';
}
/**
* Get the attachment ID from a category object, unless null (no image).
*/
export function getCategoryImageId( category: WP_REST_API_Category ) {
if ( category && isImageObject( category.image ) ) {
return category.image.id;
}
return 0;
}
PK E[pP featured-category/example.tsnu [ /**
* External dependencies
*/
import { previewCategories } from '@woocommerce/resource-previews';
import type { Block } from '@wordpress/blocks';
type ExampleBlock = Block[ 'example' ] & {
attributes: {
categoryId: 'preview' | number;
previewCategory: typeof previewCategories[ number ];
editMode: false;
};
};
export const example: ExampleBlock = {
attributes: {
categoryId: 'preview',
previewCategory: previewCategories[ 0 ],
editMode: false,
},
} as const;
PK E[K
p featured-category/style.scssnu [ @import "../style";
.wp-block-woocommerce-featured-category {
@extend %wp-block-featured-item;
}
.wc-block-featured-category {
@include wc-block-featured-item();
}
PK E[ featured-category/index.tsxnu [ /**
* 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: (
),
},
} );
PK E[=uM} } featured-category/block.tsxnu [ /**
* External dependencies
*/
import { withCategory } from '@woocommerce/block-hocs';
import { withSpokenMessages } from '@wordpress/components';
import { compose } from '@wordpress/compose';
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';
import { withUpdateButtonAttributes } from '../with-update-button-attributes';
const GENERIC_CONFIG = {
icon: folderStarred,
label: __( 'Featured Category', 'woo-gutenberg-products-block' ),
};
const BLOCK_CONTROL_CONFIG = {
...GENERIC_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'
),
noSelectionButtonLabel: __(
'Select a category',
'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,
withUpdateButtonAttributes,
withEditingImage,
withEditMode( EDIT_MODE_CONFIG ),
withFeaturedItem( CONTENT_CONFIG ),
withApiError,
withImageEditor,
withInspectorControls,
withBlockControls( BLOCK_CONTROL_CONFIG ),
] )( () => <>> );
PK E[w,מ~ ~ featured-category/editor.scssnu [ @import "../style";
.wp-block-woocommerce-featured-category {
@extend %with-media-controls;
@extend %with-resizable-box;
}
PK E[dt use-background-image.tsnu [ /**
* External dependencies
*/
import { WP_REST_API_Category } from 'wp-types';
import { ProductResponseItem } from '@woocommerce/types';
import {
getImageSrcFromProduct,
getImageIdFromProduct,
} from '@woocommerce/utils';
import { useEffect, useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { BLOCK_NAMES } from './constants';
import {
getCategoryImageSrc,
getCategoryImageId,
} from './featured-category/utils';
interface BackgroundProps {
blockName: string;
item: ProductResponseItem | WP_REST_API_Category;
mediaId: number | undefined;
mediaSrc: string | undefined;
}
interface BackgroundImage {
backgroundImageId: number;
backgroundImageSrc: string;
}
export function useBackgroundImage( {
blockName,
item,
mediaId,
mediaSrc,
}: BackgroundProps ): BackgroundImage {
const [ backgroundImageId, setBackgroundImageId ] = useState( 0 );
const [ backgroundImageSrc, setBackgroundImageSrc ] = useState( '' );
useEffect( () => {
if ( mediaId ) {
setBackgroundImageId( mediaId );
} else {
setBackgroundImageId(
blockName === BLOCK_NAMES.featuredProduct
? getImageIdFromProduct( item as ProductResponseItem )
: getCategoryImageId( item as WP_REST_API_Category )
);
}
}, [ blockName, item, mediaId ] );
useEffect( () => {
if ( mediaSrc ) {
setBackgroundImageSrc( mediaSrc );
} else {
setBackgroundImageSrc(
blockName === BLOCK_NAMES.featuredProduct
? getImageSrcFromProduct( item as ProductResponseItem )
: getCategoryImageSrc( item as WP_REST_API_Category )
);
}
}, [ blockName, item, mediaSrc ] );
return { backgroundImageId, backgroundImageSrc };
}
PK E[А types.tsnu [ /**
* External dependencies
*/
import type { Block, BlockEditProps } from '@wordpress/blocks';
import { isNumber } from '@woocommerce/types';
export type EditorBlock< T > = Block< T > & BlockEditProps< T >;
export interface Coordinates {
x: number;
y: number;
}
export interface GenericBlockUIConfig {
icon: JSX.Element;
label: string;
}
export type ImageFit = 'cover' | 'none';
export interface ImageObject {
id: number;
src: string;
}
export function isImageObject( obj: unknown ): obj is ImageObject {
if ( ! obj ) return false;
return (
isNumber( ( obj as ImageObject ).id ) &&
typeof ( obj as ImageObject ).src === 'string'
);
}
PK E[vF# F# inspector-controls.tsxnu [ /* eslint-disable @wordpress/no-unsafe-wp-apis */
/**
* External dependencies
*/
import { WP_REST_API_Category } from 'wp-types';
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';
import { LooselyMustHave, ProductResponseItem } from '@woocommerce/types';
import type { ComponentType } from 'react';
/**
* Internal dependencies
*/
import { useBackgroundImage } from './use-background-image';
import { BLOCK_NAMES } from './constants';
import { FeaturedItemRequiredAttributes } from './with-featured-item';
import { EditorBlock, ImageFit } from './types';
type InspectorControlRequiredKeys =
| 'dimRatio'
| 'focalPoint'
| 'hasParallax'
| 'imageFit'
| 'isRepeated'
| 'overlayColor'
| 'overlayGradient'
| 'showDesc';
interface InspectorControlsRequiredAttributes
extends LooselyMustHave<
FeaturedItemRequiredAttributes,
InspectorControlRequiredKeys
> {
alt: string;
backgroundImageSrc: string;
contentPanel: JSX.Element | undefined;
}
interface InspectorControlsProps extends InspectorControlsRequiredAttributes {
setAttributes: (
attrs: Partial< InspectorControlsRequiredAttributes >
) => void;
// Gutenberg doesn't provide some types, so we have to hard-code them here
setGradient: ( newGradientValue: string ) => void;
}
interface WithInspectorControlsRequiredProps< T > {
attributes: InspectorControlsRequiredAttributes &
EditorBlock< T >[ 'attributes' ];
setAttributes: InspectorControlsProps[ 'setAttributes' ];
}
interface WithInspectorControlsCategoryProps< T >
extends WithInspectorControlsRequiredProps< T > {
category: WP_REST_API_Category;
product: never;
}
interface WithInspectorControlsProductProps< T >
extends WithInspectorControlsRequiredProps< T > {
category: never;
product: ProductResponseItem;
showPrice: boolean;
}
type WithInspectorControlsProps< T extends EditorBlock< T > > =
| ( T & WithInspectorControlsCategoryProps< T > )
| ( T & WithInspectorControlsProductProps< T > );
export const InspectorControls = ( {
alt,
backgroundImageSrc,
contentPanel,
dimRatio,
focalPoint,
hasParallax,
imageFit,
isRepeated,
overlayColor,
overlayGradient,
setAttributes,
setGradient,
showDesc,
}: InspectorControlsProps ) => {
// 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 && (
{ __(
'Select “Cover” to have the image automatically fit its container.',
'woo-gutenberg-products-block'
) }
{ __(
'This may affect your ability to freely move the focal point of the image.',
'woo-gutenberg-products-block'
) }
>
}
label={ __(
'Image fit',
'woo-gutenberg-products-block'
) }
value={ imageFit }
onChange={ ( value: ImageFit ) =>
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: string ) => {
setGradient( value );
setAttributes( {
overlayGradient: value,
} );
},
label: __(
'Color',
'woo-gutenberg-products-block'
),
},
] }
>
setAttributes( { dimRatio: value as number } )
}
min={ 0 }
max={ 100 }
step={ 10 }
required
/>
>
) }
);
};
export const withInspectorControls =
< T extends EditorBlock< T > >( Component: ComponentType< T > ) =>
( props: WithInspectorControlsProps< T > ) => {
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,
} )
}
/>
) : undefined;
return (
<>
>
);
};
PK E[ with-edit-mode.tsxnu [ /**
* External dependencies
*/
import { WP_REST_API_Category } from 'wp-types';
import { ProductResponseItem } from '@woocommerce/types';
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';
import type { ComponentType } from 'react';
/**
* Internal dependencies
*/
import { BLOCK_NAMES } from './constants';
import { EditorBlock, GenericBlockUIConfig } from './types';
import { getClassPrefixFromName } from './utils';
interface EditModeConfiguration extends GenericBlockUIConfig {
description: string;
editLabel: string;
}
type EditModeRequiredAttributes = {
categoryId?: number;
editMode: boolean;
mediaId: number;
mediaSrc: string;
productId?: number;
};
interface EditModeRequiredProps< T > {
attributes: EditModeRequiredAttributes & EditorBlock< T >[ 'attributes' ];
debouncedSpeak: ( label: string ) => void;
setAttributes: ( attrs: Partial< EditModeRequiredAttributes > ) => void;
triggerUrlUpdate: () => void;
}
type EditModeProps< T extends EditorBlock< T > > = T &
EditModeRequiredProps< T >;
export const withEditMode =
( { description, editLabel, icon, label }: EditModeConfiguration ) =>
< T extends EditorBlock< T > >( Component: ComponentType< T > ) =>
( props: EditModeProps< T > ) => {
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 && (
// Ignoring this TS error for now as it seems that `ProductCategoryControl`
// types might be too strict.
// @todo Convert `ProductCategoryControl` to TypeScript
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
{
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();
} }
/>
) }