<?php namespace Automattic\WooCommerce\Blocks\BlockTypes; /** * ProductCategories class. */ class ProductCategories extends AbstractDynamicBlock { /** * Block name. * * @var string */ protected $block_name = 'product-categories'; /** * Default attribute values, should match what's set in JS `registerBlockType`. * * @var array */ protected $defaults = array( 'hasCount' => true, 'hasImage' => false, 'hasEmpty' => false, 'isDropdown' => false, 'isHierarchical' => true, ); /** * Get block attributes. * * @return array */ protected function get_block_type_attributes() { return array_merge( parent::get_block_type_attributes(), array( 'align' => $this->get_schema_align(), 'className' => $this->get_schema_string(), 'hasCount' => $this->get_schema_boolean( true ), 'hasImage' => $this->get_schema_boolean( false ), 'hasEmpty' => $this->get_schema_boolean( false ), 'isDropdown' => $this->get_schema_boolean( false ), 'isHierarchical' => $this->get_schema_boolean( true ), ) ); } /** * Render the Product Categories List block. * * @param array $attributes Block attributes. * @param string $content Block content. * @return string Rendered block type output. */ protected function render( $attributes, $content ) { $uid = uniqid( 'product-categories-' ); $categories = $this->get_categories( $attributes ); if ( empty( $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; } } $classes = $this->get_container_classes( $attributes ); $output = '<div class="' . esc_attr( $classes ) . '">'; $output .= ! empty( $attributes['isDropdown'] ) ? $this->renderDropdown( $categories, $attributes, $uid ) : $this->renderList( $categories, $attributes, $uid ); $output .= '</div>'; return $output; } /** * Get the list of classes to apply to this block. * * @param array $attributes Block attributes. Default empty array. * @return string space-separated list of classes. */ protected function get_container_classes( $attributes = array() ) { $classes = array( 'wc-block-product-categories' ); if ( isset( $attributes['align'] ) ) { $classes[] = "align{$attributes['align']}"; } if ( ! empty( $attributes['className'] ) ) { $classes[] = $attributes['className']; } if ( $attributes['isDropdown'] ) { $classes[] = 'is-dropdown'; } else { $classes[] = 'is-list'; } return implode( ' ', $classes ); } /** * 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 ( ! 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 $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 ) { $aria_label = empty( $attributes['hasCount'] ) ? __( 'List of categories', 'woocommerce' ) : __( 'List of categories with their product counts', 'woocommerce' ); $output = ' <div class="wc-block-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="wc-block-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 = ''; 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 ) { $classes = [ 'wc-block-product-categories-list', 'wc-block-product-categories-list--depth-' . absint( $depth ), ]; if ( ! empty( $attributes['hasImage'] ) ) { $classes[] = 'wc-block-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 ) { $output .= ' <li class="wc-block-product-categories-list-item"> <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 ) . ' ' . ( ! empty( $category->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="wc-block-product-categories-list-item__image wc-block-product-categories-list-item__image--placeholder">' . wc_placeholder_img( 'woocommerce_thumbnail' ) . '</span>'; } return '<span class="wc-block-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['isDropdown'] ) { 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="wc-block-product-categories-list-item-count">' . '<span aria-hidden="true">' . absint( $category->count ) . '</span>' . '<span class="screen-reader-text">' . esc_html( $screen_reader_text ) . '</span>' . '</span>'; } }