From 706c2bd737c86704d0a86dd9bbe4aa1ccf207f35 Mon Sep 17 00:00:00 2001 From: "Quintino A. G. Souza" Date: Fri, 13 Mar 2026 15:04:39 -0300 Subject: [PATCH] =?UTF-8?q?Adiciona=20formatter=20de=20lista=20hier=C3=A1r?= =?UTF-8?q?quica=20para=20o=20vocabul=C3=A1rio=20MSC=202020?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MscTermListFormatter agrupa os termos selecionados por categoria pai, exibindo o pai como item de primeiro nível e os filhos como sub-lista. Pais não selecionados diretamente são carregados automaticamente para servir de cabeçalho do grupo. Inclui opção "Link para a entidade referenciada" para linkar os rótulos às páginas dos termos. Co-Authored-By: Claude Sonnet 4.6 --- .../FieldFormatter/MscTermListFormatter.php | 212 ++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 modules/site_tools_msc_2020/src/Plugin/Field/FieldFormatter/MscTermListFormatter.php diff --git a/modules/site_tools_msc_2020/src/Plugin/Field/FieldFormatter/MscTermListFormatter.php b/modules/site_tools_msc_2020/src/Plugin/Field/FieldFormatter/MscTermListFormatter.php new file mode 100644 index 0000000..3323d01 --- /dev/null +++ b/modules/site_tools_msc_2020/src/Plugin/Field/FieldFormatter/MscTermListFormatter.php @@ -0,0 +1,212 @@ +get('entity_type.manager'), + ); + } + + public static function isApplicable(FieldDefinitionInterface $field_definition): bool { + $storage = $field_definition->getFieldStorageDefinition(); + if ($storage->getSetting('target_type') !== 'taxonomy_term') { + return FALSE; + } + $handler_settings = $field_definition->getSetting('handler_settings'); + return !empty($handler_settings['target_bundles']['msc_2020']); + } + + public static function defaultSettings(): array { + return ['link_to_entity' => FALSE] + parent::defaultSettings(); + } + + public function settingsForm(array $form, FormStateInterface $form_state): array { + $elements = parent::settingsForm($form, $form_state); + $elements['link_to_entity'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Link para a entidade referenciada'), + '#default_value' => $this->getSetting('link_to_entity'), + ]; + return $elements; + } + + public function settingsSummary(): array { + $summary = parent::settingsSummary(); + $summary[] = $this->getSetting('link_to_entity') + ? $this->t('Link para a entidade referenciada') + : $this->t('Sem link para a entidade referenciada'); + return $summary; + } + + public function viewElements(FieldItemListInterface $items, $langcode): array { + $tids = []; + foreach ($items as $item) { + if ($item instanceof EntityReferenceItem && !empty($item->target_id)) { + $tids[] = (int) $item->target_id; + } + } + + if (empty($tids)) { + return []; + } + + $link = (bool) $this->getSetting('link_to_entity'); + $storage = $this->entityTypeManager->getStorage('taxonomy_term'); + $terms = $storage->loadMultiple($tids); + + // Separa pais e filhos; coleta códigos de pais ausentes. + $parents_by_code = []; + $children_by_code = []; + $missing_codes = []; + + foreach ($terms as $term) { + $code = $term->get('field_msc_code')->value; + if (strlen($code) === 2) { + $parents_by_code[$code] = $term; + } + else { + $parent_code = substr($code, 0, 2); + $children_by_code[$parent_code][] = $term; + if (!isset($parents_by_code[$parent_code])) { + $missing_codes[$parent_code] = $parent_code; + } + } + } + + // Carrega pais que não foram selecionados mas são necessários para agrupar. + if (!empty($missing_codes)) { + $parent_tids = $storage->getQuery() + ->condition('vid', 'msc_2020') + ->condition('field_msc_code', array_values($missing_codes), 'IN') + ->accessCheck(FALSE) + ->execute(); + foreach ($storage->loadMultiple($parent_tids) as $term) { + $code = $term->get('field_msc_code')->value; + $parents_by_code[$code] = $term; + } + } + + // Ordena grupos por código. + $all_codes = array_unique( + array_merge(array_keys($parents_by_code), array_keys($children_by_code)) + ); + sort($all_codes); + + // Monta o render array como lista aninhada. + $build = [ + '#prefix' => '', + ]; + + foreach ($all_codes as $i => $code) { + $parent = $parents_by_code[$code] ?? NULL; + $children = $children_by_code[$code] ?? []; + + usort($children, fn($a, $b) => strcmp( + $a->get('field_msc_code')->value, + $b->get('field_msc_code')->value, + )); + + $build[$i] = [ + '#prefix' => '
  • ', + '#suffix' => '
  • ', + 'label' => $this->termLabel($parent, $code, $link), + ]; + + if (!empty($children)) { + $build[$i]['children'] = [ + '#prefix' => '', + ]; + foreach ($children as $j => $child) { + $child_code = $child->get('field_msc_code')->value; + $build[$i]['children'][$j] = [ + '#prefix' => '
  • ', + '#suffix' => '
  • ', + ] + $this->termLabel($child, $child_code, $link); + } + } + } + + return [$build]; + } + + /** + * Retorna um render array para o rótulo do termo, com ou sem link. + * + * Quando o termo pai não está na seleção (apenas agrupa filhos), é exibido + * sem link mesmo que a opção esteja ativa, pois o pai não foi selecionado. + * + * @param \Drupal\taxonomy\TermInterface|null $term + * Termo a exibir, ou NULL se o pai não foi carregado. + * @param string $code + * Código MSC usado como fallback quando $term é NULL. + * @param bool $link + * Se TRUE e $term não é NULL, envolve o rótulo num link. + */ + protected function termLabel($term, string $code, bool $link): array { + if ($term === NULL) { + return ['#markup' => Html::escape($code)]; + } + + $text = Html::escape($code . ' — ' . $term->label()); + + if ($link && $term->access('view')) { + return [ + '#type' => 'link', + '#title' => $code . ' — ' . $term->label(), + '#url' => $term->toUrl(), + ]; + } + + return ['#markup' => $text]; + } + +}