View source
<?php
namespace Drupal\hierarchical_taxonomy_menu\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\ResettableStackedRouteMatchInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
class HierarchicalTaxonomyMenuBlock extends BlockBase implements ContainerFactoryPluginInterface {
const SHOW_COUNT_NONE = '0';
const SHOW_COUNT_NODE = '1';
const SHOW_COUNT_COMMERCE_PRODUCT = '2';
protected $entitiesMap = [
self::SHOW_COUNT_NONE => '0',
self::SHOW_COUNT_NODE => 'node',
self::SHOW_COUNT_COMMERCE_PRODUCT => 'commerce_product',
];
protected $entityFieldManager;
protected $entityTypeManager;
protected $languageManager;
protected $currentRouteMatch;
protected $database;
protected $entityTypeBundleInfo;
protected static $terms = [];
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityFieldManagerInterface $entity_field_manager, EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager, ResettableStackedRouteMatchInterface $current_route_match, Connection $database, EntityTypeBundleInfoInterface $entity_type_bundle_info) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityFieldManager = $entity_field_manager;
$this->entityTypeManager = $entity_type_manager;
$this->languageManager = $language_manager;
$this->currentRouteMatch = $current_route_match;
$this->database = $database;
$this->entityTypeBundleInfo = $entity_type_bundle_info;
}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition, $container
->get('entity_field.manager'), $container
->get('entity_type.manager'), $container
->get('language_manager'), $container
->get('current_route_match'), $container
->get('database'), $container
->get('entity_type.bundle.info'));
}
public function defaultConfiguration() {
return [
'vocabulary' => '',
'max_depth' => 100,
'dynamic_block_title' => FALSE,
'collapsible' => FALSE,
'stay_open' => FALSE,
'interactive_parent' => FALSE,
'hide_block' => FALSE,
'use_image_style' => FALSE,
'image_height' => 16,
'image_width' => 16,
'image_style' => '',
'max_age' => 0,
'base_term' => '',
'dynamic_base_term' => FALSE,
'show_count' => '0',
'referencing_field' => '_none',
'calculate_count_recursively' => FALSE,
];
}
public function blockForm($form, FormStateInterface $form_state) {
$form['basic'] = [
'#type' => 'details',
'#title' => $this
->t('Basic settings'),
];
$form['basic']['vocabulary'] = [
'#title' => $this
->t('Use taxonomy terms from this vocabulary to create a menu'),
'#type' => 'select',
'#options' => $this
->getVocabularyOptions(),
'#required' => TRUE,
'#default_value' => $this->configuration['vocabulary'],
'#description' => $this
->t('You can display an image next to a menu item if your vocabulary has an image field.'),
];
$form['basic']['max_depth'] = [
'#title' => $this
->t('Number of sublevels to display'),
'#type' => 'select',
'#options' => [
'0' => '0',
'1' => '1',
'2' => '2',
'3' => '3',
'4' => '4',
'5' => '5',
'6' => '6',
'7' => '7',
'8' => '8',
'9' => '9',
'10' => '10',
'100' => $this
->t('Unlimited'),
],
'#default_value' => $this->configuration['max_depth'],
];
$form['basic']['dynamic_block_title'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Make the block title match the current taxonomy term name'),
'#default_value' => $this->configuration['dynamic_block_title'],
];
$form['basic']['collapsible'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Make the menu collapsed by default'),
'#default_value' => $this->configuration['collapsible'],
];
$form['basic']['stay_open'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Stay open at the current taxonomy term'),
'#default_value' => $this->configuration['stay_open'],
'#states' => [
'visible' => [
[
':input[name="settings[basic][collapsible]"]' => [
'checked' => TRUE,
],
],
],
],
];
$form['basic']['interactive_parent'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Allow parent items to be collapsible and selectable'),
'#default_value' => $this->configuration['interactive_parent'],
'#states' => [
'visible' => [
[
':input[name="settings[basic][collapsible]"]' => [
'checked' => TRUE,
],
],
],
],
];
$form['basic']['hide_block'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Hide block if the output is empty'),
'#default_value' => $this->configuration['hide_block'],
];
$form['image'] = [
'#type' => 'details',
'#title' => $this
->t('Image settings'),
];
$form['image']['notice'] = [
'#type' => 'markup',
'#markup' => $this
->t('If you are displaying an image next to menu items you can choose the size for that image. You can do that by providing the image size in pixels or by using an image style.'),
];
$form['image']['use_image_style'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Use image style'),
'#default_value' => $this->configuration['use_image_style'],
];
$form['image']['image_height'] = [
'#type' => 'number',
'#title' => $this
->t('Image height (px)'),
'#default_value' => $this->configuration['image_height'],
'#states' => [
'visible' => [
[
':input[name="settings[image][use_image_style]"]' => [
'checked' => FALSE,
],
],
],
],
];
$form['image']['image_width'] = [
'#type' => 'number',
'#title' => $this
->t('Image width (px)'),
'#default_value' => $this->configuration['image_width'],
'#states' => [
'visible' => [
[
':input[name="settings[image][use_image_style]"]' => [
'checked' => FALSE,
],
],
],
],
];
$form['image']['image_style'] = [
'#title' => $this
->t('Image style'),
'#type' => 'select',
'#options' => $this
->getImageStyleOptions(),
'#default_value' => $this->configuration['image_style'],
'#states' => [
'visible' => [
[
':input[name="settings[image][use_image_style]"]' => [
'checked' => TRUE,
],
],
],
],
];
$form['advanced'] = [
'#type' => 'details',
'#title' => $this
->t('Advanced settings'),
];
$form['advanced']['max_age'] = [
'#title' => $this
->t('Cache'),
'#type' => 'select',
'#options' => [
'0' => $this
->t('No Caching'),
'1800' => $this
->t('30 Minutes'),
'3600' => $this
->t('1 Hour'),
'21600' => $this
->t('6 Hours'),
'43200' => $this
->t('12 Hours'),
'86400' => $this
->t('1 Day'),
'604800' => $this
->t('1 Week'),
'PERMANENT' => $this
->t('Permanent'),
],
'#default_value' => $this->configuration['max_age'],
'#description' => $this
->t('Set the max age the menu is allowed to be cached for.'),
];
$form['advanced']['base_term'] = [
'#type' => 'textfield',
'#title' => $this
->t('Base term'),
'#size' => 20,
'#default_value' => $this->configuration['base_term'],
'#description' => $this
->t('Enter a base term and menu items will only be generated for its children. You can enter term ID or term name. Leave empty to generate menu for the entire vocabulary.'),
'#states' => [
'visible' => [
':input[name="settings[advanced][dynamic_base_term]"]' => [
'checked' => FALSE,
],
],
],
];
$form['advanced']['dynamic_base_term'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Dynamic Base term'),
'#default_value' => $this->configuration['dynamic_base_term'],
'#description' => $this
->t('Automatically set the base term from taxonomy page. The base term is then set to the current term and menu items will only be generated for its children.'),
];
$form['advanced']['show_count'] = [
'#type' => 'radios',
'#title' => $this
->t('Show count of referencing entities'),
'#options' => [
0 => $this
->t('No'),
1 => $this
->t('Show count of referencing nodes'),
2 => $this
->t('Show count of referencing commerce products'),
],
'#default_value' => $this->configuration['show_count'],
];
$form['advanced']['referencing_field'] = [
'#type' => 'select',
'#title' => $this
->t('Referencing field'),
'#options' => $this
->getReferencingFields(),
'#default_value' => $this->configuration['referencing_field'],
'#states' => [
'visible' => [
':input[name="settings[advanced][show_count]"]' => [
'value' => '2',
],
],
],
];
$form['advanced']['calculate_count_recursively'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Calculate count recursively'),
'#default_value' => $this->configuration['calculate_count_recursively'],
];
return $form;
}
private function getVocabularyOptions() {
$options = [];
$vocabularies = taxonomy_vocabulary_get_names();
foreach ($vocabularies as $vocabulary) {
$fields = $this->entityFieldManager
->getFieldDefinitions('taxonomy_term', $vocabulary);
$options[$vocabulary . '|'] = $this
->t('@vocabulary (no image)', [
'@vocabulary' => ucfirst($vocabulary),
]);
foreach ($fields as $field) {
if ($field
->getType() == 'image' || $this
->isMediaImage($field)) {
$field_name = $field
->getName();
$options[$vocabulary . '|' . $field_name] = $this
->t('@vocabulary (with image: @image_field)', [
'@vocabulary' => ucfirst($vocabulary),
'@image_field' => $field_name,
]);
}
}
}
return $options;
}
protected function isMediaImage(FieldDefinitionInterface $field) {
if ($field
->getType() == 'entity_reference' && $field
->getSetting('target_type') == 'media') {
if (isset($field
->getSetting('handler_settings')['target_bundles']['image'])) {
return TRUE;
}
}
return FALSE;
}
public function blockValidate($form, FormStateInterface $form_state) {
if ($form_state
->getValue([
'advanced',
'show_count',
]) == self::SHOW_COUNT_COMMERCE_PRODUCT && $form_state
->getValue([
'advanced',
'referencing_field',
]) == '_none') {
$form_state
->setErrorByName('advanced][referencing_field', $this
->t('Please select referencing field'));
}
}
public function blockSubmit($form, FormStateInterface $form_state) {
$this->configuration['vocabulary'] = $form_state
->getValue([
'basic',
'vocabulary',
]);
$this->configuration['max_depth'] = $form_state
->getValue([
'basic',
'max_depth',
]);
$this->configuration['dynamic_block_title'] = $form_state
->getValue([
'basic',
'dynamic_block_title',
]);
$this->configuration['collapsible'] = $form_state
->getValue([
'basic',
'collapsible',
]);
$this->configuration['stay_open'] = $form_state
->getValue([
'basic',
'stay_open',
]);
$this->configuration['interactive_parent'] = $form_state
->getValue([
'basic',
'interactive_parent',
]);
$this->configuration['hide_block'] = $form_state
->getValue([
'basic',
'hide_block',
]);
$this->configuration['use_image_style'] = $form_state
->getValue([
'image',
'use_image_style',
]);
$this->configuration['image_height'] = $form_state
->getValue([
'image',
'image_height',
]);
$this->configuration['image_width'] = $form_state
->getValue([
'image',
'image_width',
]);
$this->configuration['image_style'] = $form_state
->getValue([
'image',
'image_style',
]);
$this->configuration['max_age'] = $form_state
->getValue([
'advanced',
'max_age',
]);
$this->configuration['base_term'] = $form_state
->getValue([
'advanced',
'base_term',
]);
$this->configuration['dynamic_base_term'] = $form_state
->getValue([
'advanced',
'dynamic_base_term',
]);
$this->configuration['show_count'] = $form_state
->getValue([
'advanced',
'show_count',
]);
$this->configuration['referencing_field'] = $form_state
->getValue([
'advanced',
'referencing_field',
]);
$this->configuration['calculate_count_recursively'] = $form_state
->getValue([
'advanced',
'calculate_count_recursively',
]);
}
public function build() {
$vocabulary_config = $this->configuration['vocabulary'];
$vocabulary_config = explode('|', $vocabulary_config);
$vocabulary = isset($vocabulary_config[0]) ? $vocabulary_config[0] : NULL;
$base_term = $this
->getVocabularyBaseTerm($this->configuration['base_term'], $this->configuration['dynamic_base_term']);
$max_depth = $this->configuration['max_depth'];
$vocabulary_tree = $this->entityTypeManager
->getStorage('taxonomy_term')
->loadTree($vocabulary, $base_term, $max_depth + 1);
if ($this->configuration['hide_block'] && !$vocabulary_tree) {
return [];
}
$image_field = isset($vocabulary_config[1]) ? $vocabulary_config[1] : NULL;
$use_image_style = $this->configuration['use_image_style'];
$image_height = $this->configuration['image_height'];
$image_width = $this->configuration['image_width'];
$image_style = $use_image_style == TRUE ? $this->configuration['image_style'] : NULL;
$route_tid = $this
->getCurrentRoute();
$max_age = $this
->getMaxAge($this->configuration['max_age']);
$interactive_parent = $this->configuration['collapsible'] ? $this->configuration['interactive_parent'] : 0;
$show_count = $this->configuration['show_count'];
$referencing_field = $this->configuration['referencing_field'];
$vocabulary_tree_array = [];
foreach ($vocabulary_tree as $item) {
$vocabulary_tree_array[] = [
'tid' => $item->tid,
'status' => $this
->getStatusFromTid($item->tid),
'name' => $this
->getNameFromTid($item->tid),
'url' => $this
->getLinkFromTid($item->tid),
'parents' => $item->parents,
'use_image_style' => $use_image_style,
'image' => $this
->getImageFromTid($item->tid, $image_field, $image_style),
'height' => $image_height != '' ? $image_height : 16,
'width' => $image_width != '' ? $image_width : 16,
'interactive_parent' => $interactive_parent,
'show_count' => $show_count,
'entities' => !empty($show_count) ? $this
->getEntityIds($this->entitiesMap[$show_count], $referencing_field, $item->tid, $vocabulary, $this->configuration['calculate_count_recursively']) : [],
];
}
$tree = $this
->generateTree($vocabulary_tree_array, $base_term);
return [
'#theme' => 'hierarchical_taxonomy_menu',
'#menu_tree' => $tree,
'#route_tid' => $route_tid,
'#cache' => [
'max-age' => $max_age,
'tags' => [
'taxonomy_term_list',
],
],
'#current_depth' => 0,
'#vocabulary' => $vocabulary,
'#max_depth' => $max_depth,
'#collapsible' => $this->configuration['collapsible'],
'#attached' => [
'library' => [
'hierarchical_taxonomy_menu/hierarchical_taxonomy_menu',
],
'drupalSettings' => [
'stayOpen' => $this->configuration['stay_open'],
'interactiveParentMenu' => $this->configuration['interactive_parent'],
],
],
];
}
private function generateTree($array, $parent = 0) {
$tree = [];
foreach ($array as $item) {
if (reset($item['parents']) == $parent && $item['status'] == 1) {
$item['subitem'] = isset($item['subitem']) ? $item['subitem'] : $this
->generateTree($array, $item['tid']);
$tree[] = $item;
}
}
return $tree;
}
private function getNameFromTid($tid) {
$language = $this->languageManager
->getCurrentLanguage()
->getId();
if (isset(self::$terms[$tid])) {
$term = self::$terms[$tid];
}
else {
$term = $this->entityTypeManager
->getStorage('taxonomy_term')
->load($tid);
self::$terms[$tid] = $term;
}
$translation_languages = $term
->getTranslationLanguages();
if (isset($translation_languages[$language])) {
$term_translated = $term
->getTranslation($language);
return $term_translated
->getName();
}
return $term
->getName();
}
private function getStatusFromTid($tid) {
$language = $this->languageManager
->getCurrentLanguage()
->getId();
if (isset(self::$terms[$tid])) {
$term = self::$terms[$tid];
}
else {
$term = $this->entityTypeManager
->getStorage('taxonomy_term')
->load($tid);
self::$terms[$tid] = $term;
}
$translation_languages = $term
->getTranslationLanguages();
if (isset($translation_languages[$language])) {
$term_translated = $term
->getTranslation($language);
return $term_translated->status->value;
}
return $term->status->value;
}
private function getLinkFromTid($tid) {
$language = $this->languageManager
->getCurrentLanguage()
->getId();
if (isset(self::$terms[$tid])) {
$term = self::$terms[$tid];
}
else {
$term = $this->entityTypeManager
->getStorage('taxonomy_term')
->load($tid);
self::$terms[$tid] = $term;
}
$translation_languages = $term
->getTranslationLanguages();
if (isset($translation_languages[$language])) {
$term_translated = $term
->getTranslation($language);
return $term_translated
->toUrl();
}
return $term
->toUrl();
}
private function getCurrentRoute() {
if ($term_id = $this->currentRouteMatch
->getRawParameter('taxonomy_term')) {
return $term_id;
}
return NULL;
}
private function getImageFromTid($tid, $image_field, $image_style) {
if (!is_numeric($tid) || $image_field == '') {
return '';
}
if (isset(self::$terms[$tid])) {
$term = self::$terms[$tid];
}
else {
$term = $this->entityTypeManager
->getStorage('taxonomy_term')
->load($tid);
self::$terms[$tid] = $term;
}
$image_field_name = $term
->get($image_field)
->getValue();
$image_field_type = $term
->get($image_field)
->getFieldDefinition()
->getType();
if (!isset($image_field_name[0]['target_id'])) {
return '';
}
if ($image_field_type == 'image') {
$fid = $image_field_name[0]['target_id'];
}
else {
$fid = FALSE;
foreach ($image_field_name as $value) {
$media = $value['target_id'];
$media = $this->entityTypeManager
->getStorage('media')
->load($media);
if ($media && $media
->bundle() == 'image') {
foreach ($media
->referencedEntities() as $item) {
if ($item
->getEntityTypeId() == 'file') {
$fid = $item
->id();
break;
}
}
}
if ($fid) {
break;
}
}
}
if ($fid) {
$file = $this->entityTypeManager
->getStorage('file')
->load($fid);
if ($image_style) {
$style = $this->entityTypeManager
->getStorage('image_style')
->load($image_style);
if ($style) {
$path = $style
->buildUrl($file
->getFileUri());
}
else {
$path = Url::fromUri(file_create_url($file
->getFileUri()));
}
}
else {
$path = Url::fromUri(file_create_url($file
->getFileUri()));
}
return $path;
}
return '';
}
private function getImageStyleOptions() {
$options = [];
$styles = $this->entityTypeManager
->getStorage('image_style')
->loadMultiple();
foreach ($styles as $style) {
$style_name = $style
->getName();
$options[$style_name] = $style
->label();
}
return $options;
}
private function getVocabularyBaseTerm($base_term, $dynamic_base_term) {
if ($dynamic_base_term) {
if ($term_id = $this->currentRouteMatch
->getRawParameter('taxonomy_term')) {
return $term_id;
}
else {
return 0;
}
}
else {
if (!$base_term) {
return 0;
}
if (is_numeric($base_term)) {
return $base_term;
}
else {
$term = $this->entityTypeManager
->getStorage('taxonomy_term')
->loadByProperties([
'name' => $base_term,
]);
return $term ? reset($term)
->id() : 0;
}
}
}
public function getMaxAge($max_age) {
if (!$max_age) {
$max_age = 0;
return $max_age;
}
if ($max_age == 'PERMANENT') {
$max_age = Cache::PERMANENT;
return $max_age;
}
else {
return $max_age;
}
}
private function getEntityIds($entity_type_id, $field_name, $tid, $vocabulary, $calculate_count_recursively) {
if (!$calculate_count_recursively) {
return $this
->getEntityIdsForTerm($entity_type_id, $field_name, $tid);
}
else {
$entity_ids = $this
->getEntityIdsForTerm($entity_type_id, $field_name, $tid);
$child_tids = $this->entityTypeManager
->getStorage('taxonomy_term')
->loadTree($vocabulary, $tid);
foreach ($child_tids as $child_tid) {
$entity_ids = array_merge($entity_ids, $this
->getEntityIdsForTerm($entity_type_id, $field_name, $child_tid->tid));
}
return $entity_ids;
}
}
private function getEntityIdsForTerm($entity_type_id, $field_name, $tid) {
if (empty($field_name)) {
return [];
}
if ($entity_type_id == 'node') {
return $this->database
->select('taxonomy_index', 'ta')
->fields('ta', [
'nid',
])
->distinct(TRUE)
->condition('tid', $tid)
->execute()
->fetchCol();
}
else {
return $this->database
->select('commerce_product__' . $field_name, 'cp')
->fields('cp', [
'entity_id',
])
->distinct(TRUE)
->condition($field_name . '_target_id', $tid)
->execute()
->fetchCol();
}
}
private function getReferencingFields() {
$referencing_fields = [];
$referencing_fields['_none'] = $this
->t('- None -');
$bundles = $this->entityTypeBundleInfo
->getBundleInfo('commerce_product');
foreach ($bundles as $bundle => $data) {
$fields = $this->entityFieldManager
->getFieldDefinitions('commerce_product', $bundle);
foreach ($fields as $field) {
if ($field
->getType() == 'entity_reference' && $field
->getSetting('target_type') == 'taxonomy_term') {
$referencing_fields[$field
->getName()] = $field
->getLabel();
}
}
}
return $referencing_fields;
}
public function getCacheContexts() {
$dynamic_base_term = $this->configuration['dynamic_base_term'];
if ($dynamic_base_term) {
$base_term = $this
->getVocabularyBaseTerm($this->configuration['base_term'], $dynamic_base_term);
if (!$base_term) {
return parent::getCacheContexts();
}
}
return Cache::mergeContexts(parent::getCacheContexts(), [
'url.path',
]);
}
}