diff --git a/config/schema/custom_field.schema.yml b/config/schema/custom_field.schema.yml index 4c865df179859aa43b6e5b54ca9a0d50c89fee5d..20c9abb4fad8352c629b903ee2c86a9bc95d62b1 100644 --- a/config/schema/custom_field.schema.yml +++ b/config/schema/custom_field.schema.yml @@ -158,6 +158,21 @@ custom_field.formatter.*: link: type: boolean label: 'Link label to the referenced entity' + hierarchy_display: + type: string + label: 'Terms to display' + hierarchy_link: + type: boolean + label: 'Link each term' + hierarchy_wrap: + type: string + label: 'Wrap each term' + hierarchy_separator: + type: string + label: 'Separator' + hierarchy_reverse: + type: boolean + label: 'Reverse order' field.formatter.settings.custom_formatter: type: custom_field_formatter_base diff --git a/custom_field.module b/custom_field.module index 78ca342f18875abfaff4ab645d0e3563db80b078..de6fcf9f71dde4df3f48011dc7e6fb23668f7db7 100755 --- a/custom_field.module +++ b/custom_field.module @@ -34,6 +34,15 @@ function custom_field_theme(): array { return [ 'custom_field' => $item, 'custom_field_item' => $item, + 'custom_field_hierarchical_formatter' => [ + 'variables' => [ + 'terms' => [], + 'wrapper' => '', + 'separator' => ' » ', + 'link' => FALSE, + ], + 'file' => 'custom_field_hierarchical_formatter.theme.inc', + ], ]; } diff --git a/custom_field_hierarchical_formatter.theme.inc b/custom_field_hierarchical_formatter.theme.inc new file mode 100644 index 0000000000000000000000000000000000000000..d16fb8ad42a98e8896c1b6b339491f135ca1d146 --- /dev/null +++ b/custom_field_hierarchical_formatter.theme.inc @@ -0,0 +1,70 @@ +<?php + +/** + * @file + * Theme preprocess used to prepare Twig variables. + */ + +use Drupal\Component\Utility\Html; +use Drupal\Core\Link; +use Drupal\Core\Render\Markup; + +/** + * Prepares term objects for Twig template. + * + * @param array $variables + * An associative array with preprocess variables for this theme. + * by theme_preprocess. + */ +function template_preprocess_custom_field_hierarchical_formatter(array &$variables) { + $terms = []; + $variables['terms_objects'] = $variables['terms']; + + /** @var \Drupal\taxonomy\TermInterface|[] $item */ + foreach ($variables['terms'] as $item) { + if (is_array($item)) { + $group = []; + foreach ($item as $value) { + if ($variables['link']) { + $link = Link::fromTextAndUrl($value->label(), $value->toUrl())->toRenderable(); + $group[] = \Drupal::service('renderer')->render($link); + } + else { + $group[] = $value->label(); + } + } + + $terms[] = Markup::create(implode('<span class="child-separator">, </span>', $group)); + } + else { + if ($variables['link']) { + $link = Link::fromTextAndUrl($item->label(), $item->toUrl())->toRenderable(); + $terms[] = \Drupal::service('renderer')->render($link); + } + else { + $terms[] = $item->label(); + } + } + } + + if ($variables['wrapper'] != 'none') { + $count = 0; + foreach ($terms as &$term) { + $count++; + $term = [ + '#type' => 'html_tag', + '#tag' => in_array($variables['wrapper'], ['ol', 'ul']) ? 'li' : $variables['wrapper'], + '#value' => $term, + '#attributes' => [ + 'class' => [ + Html::cleanCssIdentifier('taxonomy-term'), + Html::cleanCssIdentifier("count {$count}"), + ], + ], + ]; + } + } + + unset($variables['link']); + $variables['terms'] = $terms; +} diff --git a/src/Plugin/CustomField/FieldFormatter/HierarchicalFormatter.php b/src/Plugin/CustomField/FieldFormatter/HierarchicalFormatter.php new file mode 100644 index 0000000000000000000000000000000000000000..45ad11020d362cb73020671504635466064fc603 --- /dev/null +++ b/src/Plugin/CustomField/FieldFormatter/HierarchicalFormatter.php @@ -0,0 +1,220 @@ +<?php + +namespace Drupal\custom_field\Plugin\CustomField\FieldFormatter; + +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Field\FieldItemInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\custom_field\Plugin\CustomFieldTypeInterface; + +/** + * Plugin implementation of the 'hierarchical_term_formatter' formatter. + * + * @FieldFormatter( + * id = "hierarchical_term_formatter", + * label = @Translation("Hierarchical term"), + * description = @Translation("Display the term hierarchy."), + * field_types = { + * "entity_reference", + * } + * ) + */ +class HierarchicalFormatter extends EntityReferenceFormatterBase { + + /** + * Returns a list of supported display options. + * + * @return array + * An array whose keys are display machine names + * and whose values are their labels. + */ + private function displayOptions(): array { + return [ + 'all' => $this->t('The selected term and all of its parents'), + 'parents' => $this->t('Just the parent terms'), + 'root' => $this->t('Just the topmost/root term'), + 'nonroot' => $this->t('Any non-topmost/root terms'), + 'leaf' => $this->t('Just the selected term'), + ]; + } + + /** + * Returns a list of supported wrapping options. + * + * @return array + * An array whose keys are wrapper machine names + * and whose values are their labels. + */ + private function wrapOptions(): array { + return [ + 'none' => $this->t('None'), + 'span' => $this->t('@tag elements', ['@tag' => '<span>']), + 'div' => $this->t('@tag elements', ['@tag' => '<div>']), + 'ul' => $this->t('@tag elements surrounded by a @parent_tag', [ + '@tag' => '<li>', + '@parent_tag' => '<ul>', + ]), + 'ol' => $this->t('@tag elements surrounded by a @parent_tag', [ + '@tag' => '<li>', + '@parent_tag' => '<ol>', + ]), + ]; + } + + /** + * {@inheritdoc} + */ + public static function defaultSettings(): array { + return [ + 'hierarchy_display' => 'all', + 'hierarchy_link' => FALSE, + 'hierarchy_wrap' => 'none', + 'hierarchy_separator' => ' » ', + 'hierarchy_reverse' => FALSE, + ]; + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state, array $settings) { + $settings += static::defaultSettings(); + $element['hierarchy_display'] = [ + '#title' => $this->t('Terms to display'), + '#description' => $this->t('Choose what terms to display.'), + '#type' => 'select', + '#options' => $this->displayOptions(), + '#default_value' => $settings['hierarchy_display'], + ]; + $element['hierarchy_link'] = [ + '#title' => $this->t('Link each term'), + '#description' => $this->t('If checked, the terms will link to their corresponding term pages.'), + '#type' => 'checkbox', + '#default_value' => $settings['hierarchy_link'], + ]; + $element['hierarchy_reverse'] = [ + '#title' => $this->t('Reverse order'), + '#description' => $this->t('If checked, children display first, parents last.'), + '#type' => 'checkbox', + '#default_value' => $settings['hierarchy_reverse'], + ]; + $element['hierarchy_wrap'] = [ + '#title' => $this->t('Wrap each term'), + '#description' => $this->t('Choose what type of html elements you would like to wrap the terms in, if any.'), + '#type' => 'select', + '#options' => $this->wrapOptions(), + '#default_value' => $settings['hierarchy_wrap'], + ]; + $element['hierarchy_separator'] = [ + '#title' => $this->t('Separator'), + '#description' => $this->t('Enter some text or markup that will separate each term in the hierarchy. Leave blank for no separator. Example: <em>»</em>'), + '#type' => 'textfield', + '#size' => 20, + '#default_value' => $settings['hierarchy_separator'], + ]; + + return $element; + } + + /** + * {@inheritdoc} + */ + public function formatValue(FieldItemInterface $item, CustomFieldTypeInterface $field, array $settings) { + /** @var \Drupal\taxonomy\Entity\Term $entity */ + $entity = $settings['value']; + + if (!$entity instanceof EntityInterface) { + return NULL; + } + + /** @var \Drupal\taxonomy\TermStorageInterface $storage */ + $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); + $access = $this->checkAccess($entity); + if (!$access->isAllowed()) { + return NULL; + } + + $formatter_settings = $settings['formatter_settings'] + static::defaultSettings(); + $tid = $entity->id(); + $term_tree = []; + + switch ($formatter_settings['hierarchy_display']) { + case 'leaf': + $term_tree = [$entity]; + break; + + case 'root': + $parents = $storage->loadAllParents($tid); + if (!empty($parents)) { + $term_tree = [array_pop($parents)]; + } + break; + + case 'parents': + $term_tree = array_reverse($storage->loadAllParents($tid)); + array_pop($term_tree); + break; + + case 'nonroot': + $parents = $storage->loadAllParents($tid); + if (count($parents) > 1) { + $term_tree = array_reverse($parents); + // This gets rid of the first topmost term. + array_shift($term_tree); + // Terms can have multiple parents. Now remove any remaining topmost + // terms. + foreach ($term_tree as $key => $term) { + $has_parents = $storage->loadAllParents($term->id()); + // This has no parents and is topmost. + if (empty($has_parents)) { + unset($term_tree[$key]); + } + } + } + break; + + default: + $term_tree = array_reverse($storage->loadAllParents($tid)); + break; + } + + // Change output order if Reverse order is checked. + if ($formatter_settings['hierarchy_reverse'] && count($term_tree)) { + $term_tree = array_reverse($term_tree); + } + + // Remove empty elements caused by discarded items. + $term_tree = array_filter($term_tree); + + foreach ($term_tree as $index => $term) { + if ($term->hasTranslation($item->getLangcode())) { + $term_tree[$index] = $term->getTranslation($item->getLangcode()); + } + } + + $build = [ + '#theme' => 'custom_field_hierarchical_formatter', + '#terms' => $term_tree, + '#wrapper' => $formatter_settings['hierarchy_wrap'], + '#separator' => $formatter_settings['hierarchy_separator'], + '#link' => $formatter_settings['hierarchy_link'], + ]; + + return $build; + } + + /** + * {@inheritdoc} + */ + protected function checkAccess(EntityInterface $entity) { + return $entity->access('view label', NULL, TRUE); + } + + /** + * {@inheritdoc} + */ + public static function isApplicable(CustomFieldTypeInterface $custom_item): bool { + return $custom_item->getTargetType() === 'taxonomy_term'; + } + +} diff --git a/src/Plugin/CustomFieldFormatterBase.php b/src/Plugin/CustomFieldFormatterBase.php index dce9e6db4b25e383ff2befe1d9ff00d7f3d6e083..8a513f636fc81a8a15f9be1dcaa90b57916eb5d4 100755 --- a/src/Plugin/CustomFieldFormatterBase.php +++ b/src/Plugin/CustomFieldFormatterBase.php @@ -83,4 +83,12 @@ abstract class CustomFieldFormatterBase extends PluginSettingsBase implements Cu return []; } + /** + * {@inheritdoc} + */ + public static function isApplicable(CustomFieldTypeInterface $custom_item): bool { + // By default, formatters are available for all fields. + return TRUE; + } + } diff --git a/src/Plugin/CustomFieldFormatterInterface.php b/src/Plugin/CustomFieldFormatterInterface.php index 9af83dd6cba7480d4ae672597ea96b8f571c1431..428d60d3dff80b5e7929251db6193684d501cf27 100644 --- a/src/Plugin/CustomFieldFormatterInterface.php +++ b/src/Plugin/CustomFieldFormatterInterface.php @@ -86,4 +86,15 @@ interface CustomFieldFormatterInterface { */ public function onFormatterDependencyRemoval(array $dependencies, array $settings): array; + /** + * Returns if the formatter can be used for the provided field. + * + * @param \Drupal\custom_field\Plugin\CustomFieldTypeInterface $custom_item + * The custom field type. + * + * @return bool + * TRUE if the formatter can be used, FALSE otherwise. + */ + public static function isApplicable(CustomFieldTypeInterface $custom_item): bool; + } diff --git a/templates/custom-field-hierarchical-formatter.html.twig b/templates/custom-field-hierarchical-formatter.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..11e1e2c7fe1f0ff4f409a74c26027a59160322f3 --- /dev/null +++ b/templates/custom-field-hierarchical-formatter.html.twig @@ -0,0 +1,27 @@ +{# +/** + * @file + * Default twig file for Hierarchical Term Formatter. + * + * Available variables: + * - terms: The processed taxonomy terms to display. + * - wrapper: The HTML tag that should wrap each of the terms. + * - separator: The text used to separate each of the terms. + * + * @see template_preprocess_hierarchical_term_formatter() + * + * @ingroup themeable + */ +#} +{% if wrapper == 'ol' or wrapper == 'ul' %} + <{{ wrapper }} class="terms-hierarchy"> +{% endif %} +{% for term in terms -%} + {{ term }} + {% if loop.last == false %} + <span class="separator">{{ separator }}</span> + {% endif %} +{%- endfor %} +{% if wrapper == 'ol' or wrapper == 'ul' %} + </{{ wrapper }}> +{% endif %}