Adiciona termos de nível 3 ao vocabulário MSC 2020

Expande o CSV de ~597 para ~6100 termos incorporando o terceiro nível
hierárquico (códigos de 5 caracteres, ex.: 03B05). Inclui as traduções
pt-br dos termos de nível 3 já no CSV.

Atualiza MscTermListFormatter e MscTermSelectWidget para suportar a
hierarquia de três níveis, adiciona biblioteca CSS dedicada ao formatter
e adiciona o schema de configuração do formatter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-15 09:19:55 -03:00
parent 706c2bd737
commit d33378c2f0
7 changed files with 6493 additions and 727 deletions

View File

@@ -0,0 +1,7 @@
field.widget.settings.msc_term_select:
type: mapping
label: 'Configurações do widget MSC 2020 — Seleção em cascata'
mapping:
max_depth:
type: integer
label: 'Nível máximo'

View File

@@ -0,0 +1,12 @@
.msc-terms-list {
list-style: none;
padding: 0;
margin: 0;
}
.msc-terms-list__l1-children,
.msc-terms-list__l2-children {
list-style: none;
padding-left: 1.5em;
margin: 0;
}

View File

@@ -1,2 +1,8 @@
msc_term_select_widget: msc_term_select_widget:
version: VERSION version: VERSION
msc_term_list:
version: VERSION
css:
component:
css/msc-term-list.css: {}

View File

@@ -14,9 +14,9 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/** /**
* Formatter que exibe termos MSC 2020 como lista hierárquica aninhada. * Formatter que exibe termos MSC 2020 como lista hierárquica aninhada.
* *
* Os termos selecionados são agrupados pela categoria pai. O pai aparece * Os termos selecionados são agrupados pela sua hierarquia de ancestrais.
* como item de primeiro nível e os filhos como sub-lista dentro dele. * Suporta até 3 níveis (área → subcampo → tópico). Ancestrais ausentes na
* Opcionalmente, os rótulos são linkados para a página do termo. * seleção são carregados automaticamente para garantir o agrupamento correto.
* *
* @FieldFormatter( * @FieldFormatter(
* id = "msc_term_list", * id = "msc_term_list",
@@ -101,79 +101,142 @@ class MscTermListFormatter extends FormatterBase {
$storage = $this->entityTypeManager->getStorage('taxonomy_term'); $storage = $this->entityTypeManager->getStorage('taxonomy_term');
$terms = $storage->loadMultiple($tids); $terms = $storage->loadMultiple($tids);
// Separa pais e filhos; coleta códigos de pais ausentes. // Classifica os termos selecionados pelos seus níveis na hierarquia.
$parents_by_code = []; // $l1_terms[l1_code] = term (ou NULL se não selecionado)
$children_by_code = []; // $l2_terms[l1_code][l2_code] = term
$missing_codes = []; // $l3_terms[l2_code][l3_code] = term
$l1_terms = [];
$l2_terms = [];
$l3_terms = [];
// Códigos de ancestrais que precisam ser carregados para agrupamento.
$missing_l2_codes = [];
$missing_l1_codes = [];
foreach ($terms as $term) { foreach ($terms as $term) {
$code = $term->get('field_msc_code')->value; $code = $term->get('field_msc_code')->value;
if (strlen($code) === 2) { $len = strlen($code);
$parents_by_code[$code] = $term;
if ($len === 2) {
$l1_terms[$code] = $term;
} }
else { elseif ($len === 3) {
$parent_code = substr($code, 0, 2); $l1_code = substr($code, 0, 2);
$children_by_code[$parent_code][] = $term; $l2_terms[$l1_code][$code] = $term;
if (!isset($parents_by_code[$parent_code])) { if (!isset($l1_terms[$l1_code])) {
$missing_codes[$parent_code] = $parent_code; $missing_l1_codes[$l1_code] = $l1_code;
}
}
elseif ($len === 5) {
$l2_code = substr($code, 0, 3);
$l1_code = substr($code, 0, 2);
$l3_terms[$l2_code][$code] = $term;
if (!array_key_exists($l2_code, $l2_terms[$l1_code] ?? [])) {
$missing_l2_codes[$l2_code] = $l2_code;
}
if (!isset($l1_terms[$l1_code])) {
$missing_l1_codes[$l1_code] = $l1_code;
} }
} }
} }
// Carrega pais que não foram selecionados mas são necessários para agrupar. // Carrega L2 ausentes (necessários para agrupar L3).
if (!empty($missing_codes)) { if (!empty($missing_l2_codes)) {
$parent_tids = $storage->getQuery() $found_tids = $storage->getQuery()
->condition('vid', 'msc_2020') ->condition('vid', 'msc_2020')
->condition('field_msc_code', array_values($missing_codes), 'IN') ->condition('field_msc_code', array_values($missing_l2_codes), 'IN')
->accessCheck(FALSE) ->accessCheck(FALSE)
->execute(); ->execute();
foreach ($storage->loadMultiple($parent_tids) as $term) { foreach ($storage->loadMultiple($found_tids) as $term) {
$code = $term->get('field_msc_code')->value; $code = $term->get('field_msc_code')->value;
$parents_by_code[$code] = $term; $l1_code = substr($code, 0, 2);
$l2_terms[$l1_code][$code] = $term;
if (!isset($l1_terms[$l1_code])) {
$missing_l1_codes[$l1_code] = $l1_code;
}
} }
} }
// Ordena grupos por código. // Carrega L1 ausentes (necessários para agrupar L2 ou L3).
$all_codes = array_unique( if (!empty($missing_l1_codes)) {
array_merge(array_keys($parents_by_code), array_keys($children_by_code)) $found_tids = $storage->getQuery()
); ->condition('vid', 'msc_2020')
sort($all_codes); ->condition('field_msc_code', array_values($missing_l1_codes), 'IN')
->accessCheck(FALSE)
->execute();
foreach ($storage->loadMultiple($found_tids) as $term) {
$code = $term->get('field_msc_code')->value;
$l1_terms[$code] = $term;
}
}
// Monta o render array como lista aninhada. // Coleta todos os códigos L1 que aparecem (diretamente ou como ancestral).
$all_l1_codes = array_unique(array_merge(
array_keys($l1_terms),
array_keys($l2_terms),
array_map(fn($c) => substr($c, 0, 2), array_keys($l3_terms)),
));
sort($all_l1_codes);
// Monta o render array como lista aninhada (até 3 níveis).
$build = [ $build = [
'#prefix' => '<ul class="msc-terms-list">', '#prefix' => '<ul class="msc-terms-list">',
'#suffix' => '</ul>', '#suffix' => '</ul>',
]; ];
foreach ($all_codes as $i => $code) { foreach ($all_l1_codes as $i => $l1_code) {
$parent = $parents_by_code[$code] ?? NULL; $l1_term = $l1_terms[$l1_code] ?? NULL;
$children = $children_by_code[$code] ?? []; $l2_group = $l2_terms[$l1_code] ?? [];
usort($children, fn($a, $b) => strcmp( // L2 codes relevantes para este L1: os selecionados + os que agrupam L3.
$a->get('field_msc_code')->value, $l3_l2_codes = array_filter(
$b->get('field_msc_code')->value, array_keys($l3_terms),
)); fn($l2_code) => substr($l2_code, 0, 2) === $l1_code,
);
$all_l2_codes = array_unique(array_merge(array_keys($l2_group), $l3_l2_codes));
sort($all_l2_codes);
$build[$i] = [ $build[$i] = [
'#prefix' => '<li class="msc-terms-list__parent">', '#prefix' => '<li class="msc-terms-list__l1 msc-terms-list__parent">',
'#suffix' => '</li>', '#suffix' => '</li>',
'label' => $this->termLabel($parent, $code, $link), 'label' => $this->termLabel($l1_term, $l1_code, $link),
]; ];
if (!empty($children)) { if (!empty($all_l2_codes)) {
$build[$i]['children'] = [ $build[$i]['children'] = [
'#prefix' => '<ul class="msc-terms-list__children">', '#prefix' => '<ul class="msc-terms-list__l1-children msc-terms-list__children">',
'#suffix' => '</ul>', '#suffix' => '</ul>',
]; ];
foreach ($children as $j => $child) {
$child_code = $child->get('field_msc_code')->value; foreach ($all_l2_codes as $j => $l2_code) {
$l2_term = $l2_group[$l2_code] ?? NULL;
$l3_group = $l3_terms[$l2_code] ?? [];
ksort($l3_group);
$build[$i]['children'][$j] = [ $build[$i]['children'][$j] = [
'#prefix' => '<li class="msc-terms-list__child">', '#prefix' => '<li class="msc-terms-list__l2 msc-terms-list__child">',
'#suffix' => '</li>', '#suffix' => '</li>',
] + $this->termLabel($child, $child_code, $link); 'label' => $this->termLabel($l2_term, $l2_code, $link),
];
if (!empty($l3_group)) {
$build[$i]['children'][$j]['l3_children'] = [
'#prefix' => '<ul class="msc-terms-list__l2-children">',
'#suffix' => '</ul>',
];
foreach ($l3_group as $k => $l3_term) {
$l3_code = $l3_term->get('field_msc_code')->value;
$build[$i]['children'][$j]['l3_children'][$k] = [
'#prefix' => '<li class="msc-terms-list__l3">',
'#suffix' => '</li>',
] + $this->termLabel($l3_term, $l3_code, $link);
} }
} }
} }
}
}
$build['#attached']['library'][] = 'site_tools_msc_2020/msc_term_list';
return [$build]; return [$build];
} }
@@ -181,13 +244,12 @@ class MscTermListFormatter extends FormatterBase {
/** /**
* Retorna um render array para o rótulo do termo, com ou sem link. * 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 * Quando $term é NULL (ancestral não selecionado, apenas usado para
* sem link mesmo que a opção esteja ativa, pois o pai não foi selecionado. * agrupamento) o código é exibido sem link, independentemente da opção.
* *
* @param \Drupal\taxonomy\TermInterface|null $term * @param \Drupal\taxonomy\TermInterface|null $term
* Termo a exibir, ou NULL se o pai não foi carregado.
* @param string $code * @param string $code
* Código MSC usado como fallback quando $term é NULL. * Código MSC, usado como fallback quando $term é NULL.
* @param bool $link * @param bool $link
* Se TRUE e $term não é NULL, envolve o rótulo num link. * Se TRUE e $term não é NULL, envolve o rótulo num link.
*/ */

View File

@@ -14,10 +14,16 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/** /**
* Widget de seleção em cascata para o vocabulário MSC 2020. * Widget de seleção em cascata para o vocabulário MSC 2020.
* *
* O primeiro select (categorias pai) dispara um AJAX que reconstrói o segundo * O mero de níveis exibidos é configurável por instância do widget
* select (subcampos) via servidor. O valor armazenado no campo é o TID * (opção "Nível máximo" na tela Gerenciar exibição de formulário):
* selecionado no segundo select: o TID do pai para "área geral", ou o TID do * 1 — apenas áreas principais (código de 2 dígitos, ex: "03")
* filho para um subcampo específico. * 2 — áreas + subcampos (padrão; código de 3 caracteres, ex: "03B")
* 3 — áreas + subcampos + tópicos (código de 5 caracteres, ex: "03B05")
*
* O valor armazenado no campo é sempre o TID do nível mais profundo
* selecionado. A opção "— área geral —" em cada nível armazena o TID
* do nível imediatamente superior (o usuário classificou na área mais
* ampla, sem especificar um sub-nível).
* *
* @FieldWidget( * @FieldWidget(
* id = "msc_term_select", * id = "msc_term_select",
@@ -51,6 +57,36 @@ class MscTermSelectWidget extends WidgetBase {
); );
} }
public static function defaultSettings(): array {
return ['max_depth' => 2] + parent::defaultSettings();
}
public function settingsForm(array $form, FormStateInterface $form_state): array {
$elements = parent::settingsForm($form, $form_state);
$elements['max_depth'] = [
'#type' => 'select',
'#title' => $this->t('Nível máximo'),
'#options' => [
1 => $this->t('Nível 1 — apenas áreas principais'),
2 => $this->t('Nível 2 — áreas + subcampos'),
3 => $this->t('Nível 3 — áreas + subcampos + tópicos'),
],
'#default_value' => $this->getSetting('max_depth'),
];
return $elements;
}
public function settingsSummary(): array {
$labels = [
1 => $this->t('Nível 1 — apenas áreas principais'),
2 => $this->t('Nível 2 — áreas + subcampos'),
3 => $this->t('Nível 3 — áreas + subcampos + tópicos'),
];
$summary = parent::settingsSummary();
$summary[] = $labels[(int) $this->getSetting('max_depth')] ?? $labels[2];
return $summary;
}
public static function isApplicable(FieldDefinitionInterface $field_definition): bool { public static function isApplicable(FieldDefinitionInterface $field_definition): bool {
$storage = $field_definition->getFieldStorageDefinition(); $storage = $field_definition->getFieldStorageDefinition();
if ($storage->getSetting('target_type') !== 'taxonomy_term') { if ($storage->getSetting('target_type') !== 'taxonomy_term') {
@@ -61,15 +97,15 @@ class MscTermSelectWidget extends WidgetBase {
} }
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array { public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array {
$data = $this->buildTermData(); $max_depth = (int) $this->getSetting('max_depth');
$data = $this->buildTermData($max_depth);
// getUserInput() contém os dados crus submetidos e NÃO é afetado pelo
// #limit_validation_errors do botão "Add another item" (que descarta
// valores fora do caminho do campo em $form_state->getValues()).
$field_name = $this->fieldDefinition->getName(); $field_name = $this->fieldDefinition->getName();
$field_parents = $element['#field_parents'] ?? $form['#parents'] ?? []; $field_parents = $element['#field_parents'] ?? $form['#parents'] ?? [];
$base = array_merge($field_parents, [$field_name, $delta]); $base = array_merge($field_parents, [$field_name, $delta]);
// getUserInput() contém os dados crus submetidos, não afetado pelo
// #limit_validation_errors de botões auxiliares.
$raw_input = $form_state->getUserInput(); $raw_input = $form_state->getUserInput();
$parent_exists = FALSE; $parent_exists = FALSE;
$submitted_parent = NestedArray::getValue( $submitted_parent = NestedArray::getValue(
@@ -79,143 +115,280 @@ class MscTermSelectWidget extends WidgetBase {
); );
if ($parent_exists) { if ($parent_exists) {
// Rebuild pós-submissão (AJAX de parent_select, "Add another item", etc.) // Rebuild pós-submissão (AJAX, "Add another item", etc.)
$initial_parent = $submitted_parent !== '' ? (int) $submitted_parent : NULL; $l1 = $submitted_parent !== '' ? (int) $submitted_parent : NULL;
$child_exists = FALSE; $child_exists = FALSE;
$submitted_child = NestedArray::getValue( $submitted_child = NestedArray::getValue(
$raw_input, $raw_input,
array_merge($base, ['child_select']), array_merge($base, ['child_wrapper', 'child_select']),
$child_exists, $child_exists,
); );
$initial_child = ($child_exists && $submitted_child !== '' && $submitted_child !== NULL) $l2 = ($child_exists && $submitted_child !== '' && $submitted_child !== NULL)
? (int) $submitted_child ? (int) $submitted_child : $l1;
: $initial_parent;
$grand_exists = FALSE;
$submitted_grand = NestedArray::getValue(
$raw_input,
array_merge($base, ['child_wrapper', 'grandchild_wrapper', 'grandchild_select']),
$grand_exists,
);
// Quando l2 === l1 (área geral de L2), grandchild não é aplicável;
// o default é usar l2 como "área geral" do L3.
$l3 = ($grand_exists && $submitted_grand !== '' && $submitted_grand !== NULL)
? (int) $submitted_grand : $l2;
} }
else { else {
// Carga inicial: deriva do valor persistido na entidade. // Carga inicial: deriva do valor persistido na entidade.
$current_value = $items[$delta]->target_id ? (int) $items[$delta]->target_id : NULL; $current_value = $items[$delta]->target_id ? (int) $items[$delta]->target_id : NULL;
[$initial_parent, $initial_child] = $this->resolveInitialValues($current_value, $data); [$l1, $l2, $l3] = $this->resolveInitialValues($current_value, $data);
} }
// Opções do segundo select: pré-populadas para a categoria pai atual. // L1 select.
$child_options = ['' => $this->t('— selecione uma categoria primeiro —')];
if ($initial_parent !== NULL) {
$child_options = [];
$child_options[$initial_parent] = $this->t('— área geral (sem subcampo) —');
$child_options += $data['children'][$initial_parent] ?? [];
}
$field_id = $field_name . '--' . $delta;
$wrapper_id = Html::getId('msc-child-' . $field_id);
$element['parent_select'] = [ $element['parent_select'] = [
'#type' => 'select', '#type' => 'select',
'#title' => $element['#title'] ?? $this->t('Área MSC 2020'), '#title' => $element['#title'] ?? $this->t('Área MSC 2020'),
'#title_display' => $element['#title_display'] ?? 'before', '#title_display' => $element['#title_display'] ?? 'before',
'#options' => ['' => $this->t('— selecione uma categoria —')] + $data['parents'], '#options' => ['' => $this->t('— selecione uma categoria —')] + $data['l1'],
'#default_value' => $initial_parent ?? '', '#default_value' => $l1 ?? '',
'#required' => !empty($element['#required']), '#required' => !empty($element['#required']),
'#attributes' => ['class' => ['msc-parent-select']], '#attributes' => ['class' => ['msc-parent-select']],
'#ajax' => [
'callback' => [static::class, 'rebuildChildSelect'],
'wrapper' => $wrapper_id,
'event' => 'change',
],
]; ];
// O child_select é o elemento cujo valor é salvo no campo. if ($max_depth === 1) {
// #validated => TRUE: aceita o valor submetido sem verificar #options, // Apenas L1: parent_select é o valor final, sem cascata.
// necessário pois as opções podem ter mudado entre submissões. return $element;
$element['child_select'] = [ }
$field_id = $field_name . '--' . $delta;
$child_wrapper_id = Html::getId('msc-child-' . $field_id);
// AJAX em L1: reconstrói child_wrapper (inclui L2 e, se max_depth=3, L3).
$element['parent_select']['#ajax'] = [
'callback' => [static::class, 'rebuildChildWrapper'],
'wrapper' => $child_wrapper_id,
'event' => 'change',
];
// Opções do L2 select.
$child_options = ['' => $this->t('— selecione uma categoria primeiro —')];
if ($l1 !== NULL) {
$child_options = [$l1 => $this->t('— área geral (sem subcampo) —')];
$child_options += $data['l2'][$l1] ?? [];
}
// child_wrapper: container com id para o AJAX de L1 substituir.
$element['child_wrapper'] = [
'#type' => 'container',
'#attributes' => ['id' => $child_wrapper_id],
];
// L2 select. #validated => TRUE: aceita o valor submetido sem verificar
// #options, necessário pois as opções mudam entre submissões.
$element['child_wrapper']['child_select'] = [
'#type' => 'select', '#type' => 'select',
'#title' => $this->t('Subcampo'), '#title' => $this->t('Subcampo'),
'#prefix' => '<div id="' . $wrapper_id . '">',
'#suffix' => '</div>',
'#options' => $child_options, '#options' => $child_options,
'#default_value' => $initial_child ?? '', '#default_value' => $l2 ?? '',
'#validated' => TRUE, '#validated' => TRUE,
'#attributes' => ['class' => ['msc-child-select']], '#attributes' => ['class' => ['msc-child-select']],
]; ];
if ($max_depth === 2) {
return $element;
}
// max_depth === 3: AJAX em L2 + L3 select.
$grandchild_wrapper_id = Html::getId('msc-grand-' . $field_id);
$element['child_wrapper']['child_select']['#ajax'] = [
'callback' => [static::class, 'rebuildGrandchildWrapper'],
'wrapper' => $grandchild_wrapper_id,
'event' => 'change',
];
// Opções do L3 select.
// Se l2 === l1 (área geral de L2), não há L3 para mostrar.
$effective_l2 = ($l2 !== NULL && $l2 !== $l1) ? $l2 : NULL;
$grandchild_options = ['' => $this->t('— selecione um subcampo primeiro —')];
if ($effective_l2 !== NULL) {
$grandchild_options = [$effective_l2 => $this->t('— área geral (sem tópico) —')];
$grandchild_options += $data['l3'][$effective_l2] ?? [];
}
// grandchild_wrapper: container com id para o AJAX de L2 substituir.
$element['child_wrapper']['grandchild_wrapper'] = [
'#type' => 'container',
'#attributes' => ['id' => $grandchild_wrapper_id],
];
$element['child_wrapper']['grandchild_wrapper']['grandchild_select'] = [
'#type' => 'select',
'#title' => $this->t('Tópico'),
'#options' => $grandchild_options,
'#default_value' => $l3 ?? '',
'#validated' => TRUE,
'#attributes' => ['class' => ['msc-grandchild-select']],
];
return $element; return $element;
} }
/** /**
* Callback AJAX: reconstrói o child_select quando o parent_select muda. * AJAX: reconstrói child_wrapper quando L1 (parent_select) muda.
* *
* Drupal faz o rebuild completo do formulário antes de chamar este método, * Retorna o container inteiro (L2 select + L3 select se max_depth=3),
* então formElement() já calculou as opções corretas para o novo pai. * resetando ambos ao mesmo tempo.
*/ */
public static function rebuildChildSelect(array &$form, FormStateInterface $form_state): array { public static function rebuildChildWrapper(array &$form, FormStateInterface $form_state): array {
$parents = $form_state->getTriggeringElement()['#array_parents']; $parents = $form_state->getTriggeringElement()['#array_parents'];
array_pop($parents); array_pop($parents); // remove 'parent_select'
$parents[] = 'child_select'; $parents[] = 'child_wrapper';
return NestedArray::getValue($form, $parents); return NestedArray::getValue($form, $parents);
} }
/** /**
* Carrega todos os termos msc_2020 e retorna arrays de pais e filhos. * AJAX: reconstrói grandchild_wrapper quando L2 (child_select) muda.
*
* @return array{parents: array<int,string>, children: array<int,array<int,string>>}
*/ */
protected function buildTermData(): array { public static function rebuildGrandchildWrapper(array &$form, FormStateInterface $form_state): array {
$parents = $form_state->getTriggeringElement()['#array_parents'];
array_pop($parents); // remove 'child_select'
$parents[] = 'grandchild_wrapper';
return NestedArray::getValue($form, $parents);
}
/**
* Carrega todos os termos msc_2020 e retorna arrays por nível.
*
* @return array{
* l1: array<int,string>,
* l2: array<int,array<int,string>>,
* l3: array<int,array<int,string>>,
* }
*/
protected function buildTermData(int $max_depth): array {
$tree = $this->entityTypeManager $tree = $this->entityTypeManager
->getStorage('taxonomy_term') ->getStorage('taxonomy_term')
->loadTree('msc_2020', 0, NULL, TRUE); ->loadTree('msc_2020', 0, NULL, TRUE);
$code_to_tid = []; $code_to_tid = [];
$parents = []; $l1 = [];
$children = []; $l2 = [];
$l3 = [];
// Primeira passagem: monta mapa código→TID e coleta L1.
foreach ($tree as $term) { foreach ($tree as $term) {
$code = $term->get('field_msc_code')->value; $code = $term->get('field_msc_code')->value;
$tid = (int) $term->id(); $tid = (int) $term->id();
$code_to_tid[$code] = $tid; $code_to_tid[$code] = $tid;
if (strlen($code) === 2) { if (strlen($code) === 2) {
$parents[$tid] = $code . ' — ' . $term->label(); $l1[$tid] = $code . ' — ' . $term->label();
} }
} }
if ($max_depth >= 2) {
foreach ($tree as $term) { foreach ($tree as $term) {
$code = $term->get('field_msc_code')->value; $code = $term->get('field_msc_code')->value;
if (strlen($code) > 2) { if (strlen($code) === 3) {
$parent_code = substr($code, 0, 2); $parent_code = substr($code, 0, 2);
$parent_tid = $code_to_tid[$parent_code] ?? NULL; $parent_tid = $code_to_tid[$parent_code] ?? NULL;
if ($parent_tid) { if ($parent_tid !== NULL) {
$tid = (int) $term->id(); $tid = (int) $term->id();
$children[$parent_tid][$tid] = $code . ' — ' . $term->label(); $l2[$parent_tid][$tid] = $code . ' — ' . $term->label();
}
} }
} }
} }
return ['parents' => $parents, 'children' => $children]; if ($max_depth >= 3) {
foreach ($tree as $term) {
$code = $term->get('field_msc_code')->value;
if (strlen($code) === 5) {
$parent_code = substr($code, 0, 3);
$parent_tid = $code_to_tid[$parent_code] ?? NULL;
if ($parent_tid !== NULL) {
$tid = (int) $term->id();
$l3[$parent_tid][$tid] = $code . ' — ' . $term->label();
}
}
}
}
return ['l1' => $l1, 'l2' => $l2, 'l3' => $l3];
} }
/** /**
* Resolve o par (parent_tid, child_tid) a partir do TID persistido. * Resolve o trio (l1_tid, l2_tid, l3_tid) a partir do TID persistido.
* *
* @return array{int|null, int|null} * Os valores retornados são os defaults a preencher em cada select:
* l2_initial = l1_tid → L2 mostrará "área geral"
* l3_initial = l2_tid → L3 mostrará "área geral"
*
* @return array{int|null, int|null, int|null}
*/ */
protected function resolveInitialValues(?int $current_value, array $data): array { protected function resolveInitialValues(?int $current_value, array $data): array {
if ($current_value === NULL) { if ($current_value === NULL) {
return [NULL, NULL]; return [NULL, NULL, NULL];
} }
if (isset($data['parents'][$current_value])) {
return [$current_value, $current_value]; // TID de L1 armazenado.
if (isset($data['l1'][$current_value])) {
return [$current_value, $current_value, $current_value];
} }
foreach ($data['children'] as $parent_tid => $kids) {
if (isset($kids[$current_value])) { // TID de L2 armazenado.
return [$parent_tid, $current_value]; foreach ($data['l2'] as $l1_tid => $l2_terms) {
if (isset($l2_terms[$current_value])) {
// l3_initial = l2_tid → L3 mostrará "área geral" se existir.
return [$l1_tid, $current_value, $current_value];
} }
} }
return [NULL, NULL];
// TID de L3 armazenado.
foreach ($data['l3'] as $l2_tid => $l3_terms) {
if (isset($l3_terms[$current_value])) {
foreach ($data['l2'] as $l1_tid => $l2_terms) {
if (isset($l2_terms[$l2_tid])) {
return [$l1_tid, $l2_tid, $current_value];
}
}
}
}
return [NULL, NULL, NULL];
} }
public function massageFormValues(array $values, array $form, FormStateInterface $form_state): array { public function massageFormValues(array $values, array $form, FormStateInterface $form_state): array {
$max_depth = (int) $this->getSetting('max_depth');
foreach ($values as &$value) { foreach ($values as &$value) {
$child = $value['child_select'] ?? ''; if ($max_depth === 1) {
$value = ['target_id' => $child !== '' ? (int) $child : NULL]; $stored = ($value['parent_select'] ?? '') !== ''
? (int) $value['parent_select']
: NULL;
} }
elseif ($max_depth === 2) {
$child = $value['child_wrapper']['child_select'] ?? '';
$stored = $child !== '' ? (int) $child : NULL;
}
else {
// max_depth === 3: usa o valor mais profundo disponível.
$grand = $value['child_wrapper']['grandchild_wrapper']['grandchild_select'] ?? '';
$child = $value['child_wrapper']['child_select'] ?? '';
if ($grand !== '' && $grand !== NULL) {
$stored = (int) $grand;
}
elseif ($child !== '' && $child !== NULL) {
$stored = (int) $child;
}
else {
$stored = NULL;
}
}
$value = ['target_id' => $stored];
}
return $values; return $values;
} }

View File

@@ -21,7 +21,10 @@ process:
langcode: langcode:
plugin: default_value plugin: default_value
default_value: pt-br default_value: pt-br
name: name_pt_br name:
- plugin: skip_on_empty
method: row
source: name_pt_br
content_translation_source: content_translation_source:
plugin: default_value plugin: default_value
default_value: en default_value: en