Skip to content

Instantly share code, notes, and snippets.

@gzalinski
Created September 14, 2021 18:12
Show Gist options
  • Save gzalinski/7279a38fd611e9e41b2abf4a59e923a7 to your computer and use it in GitHub Desktop.
Save gzalinski/7279a38fd611e9e41b2abf4a59e923a7 to your computer and use it in GitHub Desktop.
Get product categories with setting and display in list/menu/dropdown
<?php
/**
* Class XC_ProductCategories
* @usage $cat_list = new XC_ProductCategories();
* @usage $cat_list->render($attributes);
*/
class XC_ProductCategories {
public $defaults
= array(
'hasCount' => true,
'hasImage' => false,
'hasEmpty' => false,
'template' => 'list', //list/dropdown/menu
'isHierarchical' => true,
'depth' => 999,
'parentID' => 0
);
function render( $attributes = '' ) {
$attributes = ! $attributes ? $this->defaults : array_replace( $this->defaults, $attributes );
$uid = uniqid( 'product-categories-' );
$categories = $this->get_categories( $attributes );
if ( empty( $categories ) ) {
return '';
}
$output = '<div class="xc-product-categories">';
if ( $attributes['template'] == 'list' ) {
$output .= $this->renderList( $categories, $attributes, $uid );
} elseif ( $attributes['template'] == 'menu' ) {
$output .= $this->renderMenu( $categories, $attributes, $uid );
} elseif ( $attributes['template'] == 'dropdown' ) {
$output .= $this->renderDropdown( $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 ) {
$args = [
'hide_empty' => ! $attributes['hasEmpty'],
'pad_counts' => true,
'hierarchical' => $attributes['isHierarchical'],
'child_of' => $attributes['parentID'],
];
$categories = get_terms( 'product_cat', $args );
if ( ! is_array( $categories ) || empty( $categories ) ) {
return [];
}
// This ensures that no categories with a product count of 0 is rendered.
if ( ! $attributes['hasEmpty'] ) {
$categories = array_filter(
$categories,
function ( $category ) {
return 0 !== $category->count;
}
);
}
return $attributes['isHierarchical'] ? $this->build_category_tree( $categories, $attributes ) : $categories;
}
/**
* Build hierarchical tree of categories.
*
* @param array $categories List of terms.
*
* @return array
*/
protected function build_category_tree( $categories, $attributes ) {
$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-' . $attributes['parentID'] ];
unset( $categories_by_parent[ 'cat-' . $attributes['parentID'] ] );
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 ) {
$aria_label = empty( $attributes['hasCount'] )
?
__( 'List of categories', 'woocommerce' )
:
__( 'List of categories with their product counts', 'woocommerce' );
$output = '
<div class="xc-product-categories__dropdown">
<label
class="screen-reader-text"
for="' . esc_attr( $uid ) . '-select"
>
' . esc_html__( 'Select a category', 'woocommerce' ) . '
</label>
<select aria-label="' . esc_attr( $aria_label ) . '" id="' . esc_attr( $uid ) . '-select">
<option value="false" hidden>
' . esc_html__( 'Select a category', 'woocommerce' ) . '
</option>
' . $this->renderDropdownOptions( $categories, $attributes, $uid ) . '
</select>
</div>
<button
type="button"
class="xc-product-categories__button"
aria-label="' . esc_html__( 'Go to category', 'woocommerce' ) . '"
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 = '';
$max_depth = $attributes['depth'];
if ( $depth > $max_depth ) {
return;
}
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 renderMenu( $categories, $attributes, $uid, $depth = 0 ) {
$max_depth = $attributes['depth'];
if ( $depth > $max_depth ) {
return;
}
$classes = [
'xc-product-categories-menu',
'xc-product-categories-menu--depth-' . absint( $depth ),
];
if ( ! empty( $attributes['hasImage'] ) ) {
$classes[] = 'xc-product-categories-menu--has-images';
}
$output = '<ul class="' . esc_attr( implode( ' ', $classes ) ) . '">' . $this->renderMenuItems( $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 renderMenuItems( $categories, $attributes, $uid, $depth = 0 ) {
$output = '';
foreach ( $categories as $category ) {
$has_children = ! empty( $category->children );
$class = 'xc-product-categories-menu-item';
$class .= $has_children ? ' has-children' : '';
if ( $has_children && $depth == 0 ) {
$item_action = '<button class="btn-toggle-category" aria-expanded="false"><i class="icon-down"></i></button>';
} else {
$item_action = $this->getCount( $category, $attributes );
}
$output .= '
<li class="' . $class . '">
<div class="xc-product-categories-menu-item-wrap">
<a href="' . esc_attr( get_term_link( $category->term_id, 'product_cat' ) ) . '">
' . $this->get_image_html( $category, $attributes ) . esc_html( $category->name ) . '
</a>
' . $item_action . '
</div>
' . ( $has_children ? $this->renderMenu( $category->children, $attributes, $uid,
$depth + 1 ) : '' ) . '
</li>
';
}
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 ) {
$max_depth = $attributes['depth'];
if ( $depth > $max_depth ) {
return;
}
$classes = [
'xc-product-categories-list',
'xc-product-categories-list--depth-' . absint( $depth ),
];
if ( ! empty( $attributes['hasImage'] ) ) {
$classes[] = 'xc-product-categories-list--has-images';
}
$output = '<ul class="' . esc_attr( implode( ' ', $classes ) ) . '">' . $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 ) {
$has_children = ! empty( $category->children );
$class = 'xc-product-categories-list-item';
$class .= $has_children ? ' has-children' : '';
$output .= '
<li class="' . $class . '">
<a href="' . esc_attr( get_term_link( $category->term_id, 'product_cat' ) ) . '">
' . $this->get_image_html( $category, $attributes ) . esc_html( $category->name ) . '
</a>
' . $this->getCount( $category, $attributes ) . '
' . ( $has_children ? $this->renderList( $category->children, $attributes, $uid,
$depth + 1 ) : '' ) . '
</li>
';
}
return $output;
}
/**
* Returns the category image html
*
* @param \WP_Term $category Term object.
* @param array $attributes Block attributes. Default empty array.
* @param string $size Image size, defaults to 'woocommerce_thumbnail'.
*
* @return string
*/
public function get_image_html( $category, $attributes, $size = 'woocommerce_thumbnail' ) {
if ( empty( $attributes['hasImage'] ) ) {
return '';
}
$image_id = get_term_meta( $category->term_id, 'thumbnail_id', true );
if ( ! $image_id ) {
return '<span class="xc-product-categories-list-item__image xc-product-categories-list-item__image--placeholder">'
. wc_placeholder_img( 'woocommerce_thumbnail' ) . '</span>';
}
return '<span class="xc-product-categories-list-item__image">' . wp_get_attachment_image( $image_id, 'woocommerce_thumbnail' )
. '</span>';
}
/**
* 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 '';
}
if ( $attributes['template'] == 'dropdown' ) {
return '(' . absint( $category->count ) . ')';
}
$screen_reader_text = sprintf(
/* translators: %s number of products in cart. */
_n( '%d product', '%d products', absint( $category->count ), 'woocommerce' ),
absint( $category->count )
);
return '<span class="xc-item-count">'
. '<span aria-hidden="true">' . absint( $category->count ) . '</span>'
. '<span class="screen-reader-text">' . esc_html( $screen_reader_text ) . '</span>'
. '</span>';
}
}
// $XC_ProductCategories = new XC_ProductCategories();
// $ProductCategories = $XC_ProductCategories->render( [ 'template' => 'menu', 'parentID' => 2934, 'depth' => 2 ] );
// echo $ProductCategories;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment