Use Server Side Rendering for Product Category List block (https://github.com/woocommerce/woocommerce-blocks/pull/1024)

* Convert product list to SSR block

* Remove PRODUCT_CATEGORIES from block data

* onclick event handling

* Empty placeholder

* code style

* depth css class

* hasCount handling

* Deprecation handling of old saved markup

* Code style

* Legacy attribute mapping
This commit is contained in:
Mike Jolley 2019-10-16 13:02:43 +01:00 committed by GitHub
parent 7906937c9a
commit e227dff18e
10 changed files with 502 additions and 457 deletions

View File

@ -2,157 +2,175 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component, createRef, Fragment } from 'react';
import classnames from 'classnames';
import { HOME_URL } from '@woocommerce/block-settings';
import { Fragment } from 'react';
import { InspectorControls, ServerSideRender } from '@wordpress/editor';
import PropTypes from 'prop-types';
import { PanelBody, ToggleControl, Placeholder } from '@wordpress/components';
import { IconFolder } from '@woocommerce/block-components/icons';
/**
* Internal dependencies
*/
import withComponentId from '@woocommerce/base-hocs/with-component-id';
import ToggleButtonControl from '@woocommerce/block-components/toggle-button-control';
/**
* Component displaying the categories as dropdown or list.
*/
class ProductCategoriesBlock extends Component {
constructor() {
super( ...arguments );
this.select = createRef();
this.onNavigate = this.onNavigate.bind( this );
this.renderList = this.renderList.bind( this );
this.renderOptions = this.renderOptions.bind( this );
}
onNavigate() {
const { isPreview = false } = this.props;
const url = this.select.current.value;
if ( 'false' === url ) {
return;
}
const home = HOME_URL;
if ( ! isPreview && 0 === url.indexOf( home ) ) {
document.location.href = url;
}
}
renderList( items, depth = 0 ) {
const { isPreview = false } = this.props;
const { hasCount } = this.props.attributes;
const parentKey = 'parent-' + items[ 0 ].term_id;
const ProductCategoriesBlock = ( { attributes, setAttributes, name } ) => {
const getInspectorControls = () => {
const { hasCount, hasEmpty, isDropdown, isHierarchical } = attributes;
return (
<ul key={ parentKey } className="wc-block-product-categories-list">
{ items.map( ( cat ) => {
const count = hasCount ? (
<span className="wc-block-product-categories-list-item-count">
{ cat.count }
</span>
) : null;
return [
<li
key={ cat.term_id }
className="wc-block-product-categories-list-item"
>
{ /* eslint-disable-next-line jsx-a11y/anchor-is-valid */ }
<a href={ isPreview ? null : cat.link }>
{ cat.name }
</a>
{ count }
</li>,
!! cat.children &&
!! cat.children.length &&
this.renderList( cat.children, depth + 1 ),
];
} ) }
</ul>
);
}
renderOptions( items, depth = 0 ) {
const { hasCount } = this.props.attributes;
return items.map( ( cat ) => {
const count = hasCount ? `(${ cat.count })` : null;
return [
<option key={ cat.term_id } value={ cat.link }>
{ ''.repeat( depth ) } { cat.name } { count }
</option>,
!! cat.children &&
!! cat.children.length &&
this.renderOptions( cat.children, depth + 1 ),
];
} );
}
render() {
const { attributes, categories, componentId } = this.props;
const { className, isDropdown } = attributes;
const classes = classnames( 'wc-block-product-categories', className, {
'is-dropdown': isDropdown,
'is-list': ! isDropdown,
} );
const selectId = `prod-categories-${ componentId }`;
return (
<Fragment>
{ categories.length > 0 && (
<div className={ classes }>
{ isDropdown ? (
<Fragment>
<div className="wc-block-product-categories__dropdown">
<label
className="screen-reader-text"
htmlFor={ selectId }
>
{ __(
'Select a category',
'woo-gutenberg-products-block'
) }
</label>
<select id={ selectId } ref={ this.select }>
<option value="false" hidden>
{ __(
'Select a category',
'woo-gutenberg-products-block'
) }
</option>
{ this.renderOptions( categories ) }
</select>
</div>
<button
type="button"
className="wc-block-product-categories__button"
aria-label={ __(
'Go to category',
'woo-gutenberg-products-block'
) }
icon="arrow-right-alt2"
onClick={ this.onNavigate }
>
<svg
aria-hidden="true"
role="img"
focusable="false"
className="dashicon dashicons-arrow-right-alt2"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
>
<path d="M6 15l5-5-5-5 1-2 7 7-7 7z" />
</svg>
</button>
</Fragment>
) : (
this.renderList( categories )
<InspectorControls key="inspector">
<PanelBody
title={ __( 'Content', 'woo-gutenberg-products-block' ) }
initialOpen
>
<ToggleControl
label={ __(
'Show product count',
'woo-gutenberg-products-block'
) }
</div>
) }
</Fragment>
help={
hasCount
? __(
'Product count is visible.',
'woo-gutenberg-products-block'
)
: __(
'Product count is hidden.',
'woo-gutenberg-products-block'
)
}
checked={ hasCount }
onChange={ () =>
setAttributes( { hasCount: ! hasCount } )
}
/>
<ToggleControl
label={ __(
'Show hierarchy',
'woo-gutenberg-products-block'
) }
help={
isHierarchical
? __(
'Hierarchy is visible.',
'woo-gutenberg-products-block'
)
: __(
'Hierarchy is hidden.',
'woo-gutenberg-products-block'
)
}
checked={ isHierarchical }
onChange={ () =>
setAttributes( {
isHierarchical: ! isHierarchical,
} )
}
/>
<ToggleControl
label={ __(
'Show empty categories',
'woo-gutenberg-products-block'
) }
help={
hasEmpty
? __(
'Empty categories are visible.',
'woo-gutenberg-products-block'
)
: __(
'Empty categories are hidden.',
'woo-gutenberg-products-block'
)
}
checked={ hasEmpty }
onChange={ () =>
setAttributes( { hasEmpty: ! hasEmpty } )
}
/>
</PanelBody>
<PanelBody
title={ __(
'List Settings',
'woo-gutenberg-products-block'
) }
initialOpen
>
<ToggleButtonControl
label={ __(
'Display style',
'woo-gutenberg-products-block'
) }
value={ isDropdown ? 'dropdown' : 'list' }
options={ [
{
label: __(
'List',
'woo-gutenberg-products-block'
),
value: 'list',
},
{
label: __(
'Dropdown',
'woo-gutenberg-products-block'
),
value: 'dropdown',
},
] }
onChange={ ( value ) =>
setAttributes( {
isDropdown: 'dropdown' === value,
} )
}
/>
</PanelBody>
</InspectorControls>
);
}
}
};
export default withComponentId( ProductCategoriesBlock );
return (
<Fragment>
{ getInspectorControls() }
<ServerSideRender
block={ name }
attributes={ attributes }
EmptyResponsePlaceholder={ () => (
<Placeholder
icon={ <IconFolder /> }
label={ __(
'Product Categories List',
'woo-gutenberg-products-block'
) }
className="wc-block-product-categories"
>
{ __(
"This block shows product categories for your store. To use it, you'll first need to create a product and assign it to a category.",
'woo-gutenberg-products-block'
) }
</Placeholder>
) }
/>
</Fragment>
);
};
ProductCategoriesBlock.propTypes = {
/**
* The attributes for this block
*/
attributes: PropTypes.object.isRequired,
/**
* The register block name.
*/
name: PropTypes.string.isRequired,
/**
* A callback to update attributes
*/
setAttributes: PropTypes.func.isRequired,
};
export default ProductCategoriesBlock;

View File

@ -1,155 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Fragment } from '@wordpress/element';
import { InspectorControls } from '@wordpress/editor';
import { PanelBody, ToggleControl, Placeholder } from '@wordpress/components';
/**
* Internal dependencies
*/
import './editor.scss';
import Block from './block.js';
import ToggleButtonControl from '@woocommerce/block-components/toggle-button-control';
import getCategories from './get-categories';
import { IconFolder } from '@woocommerce/block-components/icons';
export default function( { attributes, setAttributes } ) {
const { hasCount, hasEmpty, isDropdown, isHierarchical } = attributes;
const categories = getCategories( attributes );
return (
<Fragment>
<InspectorControls key="inspector">
<PanelBody
title={ __( 'Content', 'woo-gutenberg-products-block' ) }
initialOpen
>
<ToggleControl
label={ __(
'Show product count',
'woo-gutenberg-products-block'
) }
help={
hasCount
? __(
'Product count is visible.',
'woo-gutenberg-products-block'
)
: __(
'Product count is hidden.',
'woo-gutenberg-products-block'
)
}
checked={ hasCount }
onChange={ () =>
setAttributes( { hasCount: ! hasCount } )
}
/>
<ToggleControl
label={ __(
'Show hierarchy',
'woo-gutenberg-products-block'
) }
help={
isHierarchical
? __(
'Hierarchy is visible.',
'woo-gutenberg-products-block'
)
: __(
'Hierarchy is hidden.',
'woo-gutenberg-products-block'
)
}
checked={ isHierarchical }
onChange={ () =>
setAttributes( {
isHierarchical: ! isHierarchical,
} )
}
/>
<ToggleControl
label={ __(
'Show empty categories',
'woo-gutenberg-products-block'
) }
help={
hasEmpty
? __(
'Empty categories are visible.',
'woo-gutenberg-products-block'
)
: __(
'Empty categories are hidden.',
'woo-gutenberg-products-block'
)
}
checked={ hasEmpty }
onChange={ () =>
setAttributes( { hasEmpty: ! hasEmpty } )
}
/>
</PanelBody>
<PanelBody
title={ __(
'List Settings',
'woo-gutenberg-products-block'
) }
initialOpen
>
<ToggleButtonControl
label={ __(
'Display style',
'woo-gutenberg-products-block'
) }
value={ isDropdown ? 'dropdown' : 'list' }
options={ [
{
label: __(
'List',
'woo-gutenberg-products-block'
),
value: 'list',
},
{
label: __(
'Dropdown',
'woo-gutenberg-products-block'
),
value: 'dropdown',
},
] }
onChange={ ( value ) =>
setAttributes( {
isDropdown: 'dropdown' === value,
} )
}
/>
</PanelBody>
</InspectorControls>
{ categories.length > 0 ? (
<Block
attributes={ attributes }
categories={ categories }
isPreview
/>
) : (
<Placeholder
className="wc-block-product-categories"
icon={ <IconFolder /> }
label={ __(
'Product Categories List',
'woo-gutenberg-products-block'
) }
>
{ __(
"This block shows product categories for your store. In order to preview this you'll first need to create a product and assign it to a category.",
'woo-gutenberg-products-block'
) }
</Placeholder>
) }
</Fragment>
);
}

View File

@ -1,22 +0,0 @@
/**
* Internal dependencies
*/
import Block from './block.js';
import getCategories from './get-categories';
import renderFrontend from '../../utils/render-frontend.js';
const getProps = ( el ) => {
const attributes = {
hasCount: el.dataset.hasCount === 'true',
hasEmpty: el.dataset.hasEmpty === 'true',
isDropdown: el.dataset.isDropdown === 'true',
isHierarchical: el.dataset.isHierarchical === 'true',
};
return {
attributes,
categories: getCategories( attributes ),
};
};
renderFrontend( '.wp-block-woocommerce-product-categories', Block, getProps );

View File

@ -1,19 +0,0 @@
/**
* External dependencies
*/
import { PRODUCT_CATEGORIES } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import { buildTermsTree } from './hierarchy';
/**
* Returns categories in tree form.
*/
export default function( { hasEmpty, isHierarchical } ) {
const categories = PRODUCT_CATEGORIES.filter(
( cat ) => hasEmpty || !! cat.count
);
return isHierarchical ? buildTermsTree( categories ) : categories;
}

View File

@ -1,39 +0,0 @@
/**
* Returns terms in a tree form.
*
* @param {Array} list Array of terms in flat format.
*
* @return {Array} Array of terms in tree format.
*/
export function buildTermsTree( list = [] ) {
// Group terms by the parent ID.
const termsByParent = list.reduce(
( r, v, i, a, k = v.parent ) => (
( r[ k ] || ( r[ k ] = [] ) ).push( v ), r
),
{}
);
const fillWithChildren = ( terms ) => {
return terms.map( ( term ) => {
const children = termsByParent[ term.term_id ];
delete termsByParent[ term.term_id ];
return {
...term,
children:
children && children.length
? fillWithChildren( children )
: [],
};
} );
};
const tree = fillWithChildren( termsByParent[ '0' ] || [] );
delete termsByParent[ '0' ];
Object.keys( termsByParent ).forEach( function( terms ) {
tree.push( ...fillWithChildren( terms || [] ) );
} );
return tree;
}

View File

@ -9,7 +9,7 @@ import { registerBlockType } from '@wordpress/blocks';
*/
import './editor.scss';
import './style.scss';
import edit from './edit.js';
import Block from './block.js';
import { IconFolder } from '@woocommerce/block-components/icons';
registerBlockType( 'woocommerce/product-categories', {
@ -35,9 +35,6 @@ registerBlockType( 'woocommerce/product-categories', {
hasCount: {
type: 'boolean',
default: true,
source: 'attribute',
selector: 'div',
attribute: 'data-has-count',
},
/**
@ -46,9 +43,6 @@ registerBlockType( 'woocommerce/product-categories', {
hasEmpty: {
type: 'boolean',
default: false,
source: 'attribute',
selector: 'div',
attribute: 'data-has-empty',
},
/**
@ -57,9 +51,6 @@ registerBlockType( 'woocommerce/product-categories', {
isDropdown: {
type: 'boolean',
default: false,
source: 'attribute',
selector: 'div',
attribute: 'data-is-dropdown',
},
/**
@ -68,53 +59,102 @@ registerBlockType( 'woocommerce/product-categories', {
isHierarchical: {
type: 'boolean',
default: true,
source: 'attribute',
selector: 'div',
attribute: 'data-is-hierarchical',
},
},
edit,
deprecated: [
{
// Deprecate HTML save method in favor of dynamic rendering.
attributes: {
hasCount: {
type: 'boolean',
default: true,
source: 'attribute',
selector: 'div',
attribute: 'data-has-count',
},
hasEmpty: {
type: 'boolean',
default: false,
source: 'attribute',
selector: 'div',
attribute: 'data-has-empty',
},
isDropdown: {
type: 'boolean',
default: false,
source: 'attribute',
selector: 'div',
attribute: 'data-is-dropdown',
},
isHierarchical: {
type: 'boolean',
default: true,
source: 'attribute',
selector: 'div',
attribute: 'data-is-hierarchical',
},
},
migrate( attributes ) {
return attributes;
},
save( props ) {
const {
hasCount,
hasEmpty,
isDropdown,
isHierarchical,
} = props.attributes;
const data = {};
if ( hasCount ) {
data[ 'data-has-count' ] = true;
}
if ( hasEmpty ) {
data[ 'data-has-empty' ] = true;
}
if ( isDropdown ) {
data[ 'data-is-dropdown' ] = true;
}
if ( isHierarchical ) {
data[ 'data-is-hierarchical' ] = true;
}
return (
<div className="is-loading" { ...data }>
{ isDropdown ? (
<span
aria-hidden
className="wc-block-product-categories__placeholder"
/>
) : (
<ul aria-hidden>
<li>
<span className="wc-block-product-categories__placeholder" />
</li>
<li>
<span className="wc-block-product-categories__placeholder" />
</li>
<li>
<span className="wc-block-product-categories__placeholder" />
</li>
</ul>
) }
</div>
);
},
},
],
/**
* Save the props to post content.
* Renders and manages the block.
*/
save( { attributes } ) {
const { hasCount, hasEmpty, isDropdown, isHierarchical } = attributes;
const data = {};
if ( hasCount ) {
data[ 'data-has-count' ] = true;
}
if ( hasEmpty ) {
data[ 'data-has-empty' ] = true;
}
if ( isDropdown ) {
data[ 'data-is-dropdown' ] = true;
}
if ( isHierarchical ) {
data[ 'data-is-hierarchical' ] = true;
}
return (
<div className="is-loading" { ...data }>
{ isDropdown ? (
<span
aria-hidden
className="wc-block-product-categories__placeholder"
/>
) : (
<ul aria-hidden>
<li>
<span className="wc-block-product-categories__placeholder" />
</li>
<li>
<span className="wc-block-product-categories__placeholder" />
</li>
<li>
<span className="wc-block-product-categories__placeholder" />
</li>
</ul>
) }
</div>
);
edit( props ) {
return <Block { ...props } />;
},
/**
* Save nothing; rendered by server.
*/
save() {
return null;
},
} );

View File

@ -16,4 +16,3 @@ export const IS_LARGE_CATALOG = getSetting( 'isLargeCatalog' );
export const LIMIT_TAGS = getSetting( 'limitTags' );
export const HAS_TAGS = getSetting( 'hasTags', true );
export const HOME_URL = getSetting( 'homeUrl ', '' );
export const PRODUCT_CATEGORIES = getSetting( 'productCategories', [] );

View File

@ -237,8 +237,6 @@ const getFrontConfig = ( options = {} ) => {
};
return {
entry: {
'product-categories':
'./assets/js/blocks/product-categories/frontend.js',
reviews: './assets/js/blocks/reviews/frontend.js',
},
output: {

View File

@ -84,18 +84,8 @@ class Assets {
* @since $VID:$ returned merged data along with incoming $settings
*/
public static function get_wc_block_data( $settings ) {
$tag_count = wp_count_terms( 'product_tag' );
$product_counts = wp_count_posts( 'product' );
$product_categories = get_terms(
'product_cat',
array(
'hide_empty' => false,
'pad_counts' => true,
)
);
foreach ( $product_categories as &$category ) {
$category->link = get_term_link( $category->term_id, 'product_cat' );
}
$tag_count = wp_count_terms( 'product_tag' );
$product_counts = wp_count_posts( 'product' );
// Global settings used in each block.
return array_merge(
@ -114,7 +104,6 @@ class Assets {
'isLargeCatalog' => $product_counts->publish > 200,
'limitTags' => $tag_count > 100,
'hasTags' => $tag_count > 0,
'productCategories' => $product_categories,
'homeUrl' => esc_url( home_url( '/' ) ),
'showAvatars' => '1' === get_option( 'show_avatars' ),
'enableReviewRating' => 'yes' === get_option( 'woocommerce_enable_review_rating' ),

View File

@ -12,7 +12,7 @@ defined( 'ABSPATH' ) || exit;
/**
* ProductCategories class.
*/
class ProductCategories extends AbstractBlock {
class ProductCategories extends AbstractDynamicBlock {
/**
* Block name.
@ -22,31 +22,267 @@ class ProductCategories extends AbstractBlock {
protected $block_name = 'product-categories';
/**
* Registers the block type with WordPress.
* Default attribute values, should match what's set in JS `registerBlockType`.
*
* @var array
*/
public function register_block_type() {
register_block_type(
$this->namespace . '/' . $this->block_name,
protected $defaults = array(
'hasCount' => true,
'hasEmpty' => false,
'isDropdown' => false,
'isHierarchical' => true,
);
/**
* Get block attributes.
*
* @return array
*/
protected function get_attributes() {
return array_merge(
parent::get_attributes(),
array(
'render_callback' => array( $this, 'render' ),
'editor_script' => 'wc-' . $this->block_name,
'editor_style' => 'wc-block-editor',
'style' => 'wc-block-style',
'script' => 'wc-' . $this->block_name . '-frontend',
'className' => $this->get_schema_string(),
'hasCount' => $this->get_schema_boolean( true ),
'hasEmpty' => $this->get_schema_boolean( false ),
'isDropdown' => $this->get_schema_boolean( false ),
'isHierarchical' => $this->get_schema_boolean( true ),
)
);
}
/**
* Append frontend scripts when rendering the Product Categories List block.
* Render the Product Categories List block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @return string Rendered block type output.
*/
public function render( $attributes = array(), $content = '' ) {
\Automattic\WooCommerce\Blocks\Assets::register_block_script( $this->block_name . '-frontend' );
$uid = uniqid( 'product-categories-' );
$categories = $this->get_categories( $attributes );
return $content;
if ( ! $categories ) {
return '';
}
if ( ! empty( $content ) ) {
// Deal with legacy attributes (before this was an SSR block) that differ from defaults.
if ( strstr( $content, 'data-has-count="false"' ) ) {
$attributes['hasCount'] = false;
}
if ( strstr( $content, 'data-is-dropdown="true"' ) ) {
$attributes['isDropdown'] = true;
}
if ( strstr( $content, 'data-is-hierarchical="false"' ) ) {
$attributes['isHierarchical'] = false;
}
if ( strstr( $content, 'data-has-empty="true"' ) ) {
$attributes['hasEmpty'] = true;
}
}
$output = '<div class="wc-block-product-categories ' . esc_attr( $attributes['className'] ) . ' ' . ( $attributes['isDropdown'] ? 'is-dropdown' : 'is-list' ) . '">';
$output .= ! empty( $attributes['isDropdown'] ) ? $this->renderDropdown( $categories, $attributes, $uid ) : $this->renderList( $categories, $attributes, $uid );
$output .= '</div>';
return $output;
}
/**
* Get categories (terms) from the db.
*
* @param array $attributes Block attributes. Default empty array.
* @return array
*/
protected function get_categories( $attributes ) {
$hierarchical = wc_string_to_bool( $attributes['isHierarchical'] );
$categories = get_terms(
'product_cat',
[
'hide_empty' => ! $attributes['hasEmpty'],
'pad_counts' => true,
'hierarchical' => true,
]
);
if ( ! $categories ) {
return [];
}
return $hierarchical ? $this->build_category_tree( $categories ) : $categories;
}
/**
* Build hierarchical tree of categories.
*
* @param array $categories List of terms.
* @return array
*/
protected function build_category_tree( $categories ) {
$categories_by_parent = [];
foreach ( $categories as $category ) {
if ( ! isset( $categories_by_parent[ 'cat-' . $category->parent ] ) ) {
$categories_by_parent[ 'cat-' . $category->parent ] = [];
}
$categories_by_parent[ 'cat-' . $category->parent ][] = $category;
}
$tree = $categories_by_parent['cat-0'];
unset( $categories_by_parent['cat-0'] );
foreach ( $tree as $category ) {
if ( ! empty( $categories_by_parent[ 'cat-' . $category->term_id ] ) ) {
$category->children = $this->fill_category_children( $categories_by_parent[ 'cat-' . $category->term_id ], $categories_by_parent );
}
}
return $tree;
}
/**
* Build hierarchical tree of categories by appending children in the tree.
*
* @param array $categories List of terms.
* @param array $categories_by_parent List of terms grouped by parent.
* @return array
*/
protected function fill_category_children( $categories, $categories_by_parent ) {
foreach ( $categories as $category ) {
if ( ! empty( $categories_by_parent[ 'cat-' . $category->term_id ] ) ) {
$category->children = $this->fill_category_children( $categories_by_parent[ 'cat-' . $category->term_id ], $categories_by_parent );
}
}
return $categories;
}
/**
* Render the category list as a dropdown.
*
* @param array $categories List of terms.
* @param array $attributes Block attributes. Default empty array.
* @param int $uid Unique ID for the rendered block, used for HTML IDs.
* @return string Rendered output.
*/
protected function renderDropdown( $categories, $attributes, $uid ) {
$output = '
<div class="wc-block-product-categories__dropdown">
<label
class="screen-reader-text"
for="' . esc_attr( $uid ) . '-select"
>
' . esc_html__( 'Select a category', 'woo-gutenberg-products-block' ) . '
</label>
<select id="' . esc_attr( $uid ) . '-select">
<option value="false" hidden>
' . esc_html__( 'Select a category', 'woo-gutenberg-products-block' ) . '
</option>
' . $this->renderDropdownOptions( $categories, $attributes, $uid ) . '
</select>
</div>
<button
type="button"
class="wc-block-product-categories__button"
aria-label="' . esc_html__( 'Go to category', 'woo-gutenberg-products-block' ) . '"
onclick="const url = document.getElementById( \'' . esc_attr( $uid ) . '-select\' ).value; if ( \'false\' !== url ) document.location.href = url;"
>
<svg
aria-hidden="true"
role="img"
focusable="false"
class="dashicon dashicons-arrow-right-alt2"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
>
<path d="M6 15l5-5-5-5 1-2 7 7-7 7z" />
</svg>
</button>
';
return $output;
}
/**
* Render dropdown options list.
*
* @param array $categories List of terms.
* @param array $attributes Block attributes. Default empty array.
* @param int $uid Unique ID for the rendered block, used for HTML IDs.
* @param int $depth Current depth.
* @return string Rendered output.
*/
protected function renderDropdownOptions( $categories, $attributes, $uid, $depth = 0 ) {
$output = '';
foreach ( $categories as $category ) {
$output .= '
<option value="' . esc_attr( get_term_link( $category->term_id, 'product_cat' ) ) . '">
' . str_repeat( '-', $depth ) . '
' . esc_html( $category->name ) . '
' . $this->getCount( $category, $attributes ) . '
</option>
' . ( ! empty( $category->children ) ? $this->renderDropdownOptions( $category->children, $attributes, $uid, $depth + 1 ) : '' ) . '
';
}
return $output;
}
/**
* Render the category list as a list.
*
* @param array $categories List of terms.
* @param array $attributes Block attributes. Default empty array.
* @param int $uid Unique ID for the rendered block, used for HTML IDs.
* @param int $depth Current depth.
* @return string Rendered output.
*/
protected function renderList( $categories, $attributes, $uid, $depth = 0 ) {
$output = '<ul class="wc-block-product-categories-list wc-block-product-categories-list--depth-' . absint( $depth ) . '">' . $this->renderListItems( $categories, $attributes, $uid, $depth ) . '</ul>';
return $output;
}
/**
* Render a list of terms.
*
* @param array $categories List of terms.
* @param array $attributes Block attributes. Default empty array.
* @param int $uid Unique ID for the rendered block, used for HTML IDs.
* @param int $depth Current depth.
* @return string Rendered output.
*/
protected function renderListItems( $categories, $attributes, $uid, $depth = 0 ) {
$output = '';
foreach ( $categories as $category ) {
$output .= '
<li class="wc-block-product-categories-list-item">
<a href="' . esc_attr( get_term_link( $category->term_id, 'product_cat' ) ) . '">
' . esc_html( $category->name ) . '
</a>
' . $this->getCount( $category, $attributes ) . '
' . ( ! empty( $category->children ) ? $this->renderList( $category->children, $attributes, $uid, $depth + 1 ) : '' ) . '
</li>
';
}
return $output;
}
/**
* Get the count, if displaying.
*
* @param object $category Term object.
* @param array $attributes Block attributes. Default empty array.
* @return string
*/
protected function getCount( $category, $attributes ) {
if ( empty( $attributes['hasCount'] ) ) {
return '';
}
return $attributes['isDropdown'] ? '(' . absint( $category->count ) . ')' : '<span class="wc-block-product-categories-list-item-count">' . absint( $category->count ) . '</span>';
}
}