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:
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.
*
* Os termos selecionados são agrupados pela categoria pai. O pai aparece
* como item de primeiro nível e os filhos como sub-lista dentro dele.
* Opcionalmente, os rótulos são linkados para a página do termo.
* Os termos selecionados são agrupados pela sua hierarquia de ancestrais.
* Suporta até 3 níveis (área → subcampo → tópico). Ancestrais ausentes na
* seleção são carregados automaticamente para garantir o agrupamento correto.
*
* @FieldFormatter(
* id = "msc_term_list",
@@ -70,8 +70,8 @@ class MscTermListFormatter extends FormatterBase {
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'),
'#type' => 'checkbox',
'#title' => $this->t('Link para a entidade referenciada'),
'#default_value' => $this->getSetting('link_to_entity'),
];
return $elements;
@@ -97,97 +97,159 @@ class MscTermListFormatter extends FormatterBase {
return [];
}
$link = (bool) $this->getSetting('link_to_entity');
$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 = [];
// Classifica os termos selecionados pelos seus níveis na hierarquia.
// $l1_terms[l1_code] = term (ou NULL se não selecionado)
// $l2_terms[l1_code][l2_code] = term
// $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) {
$code = $term->get('field_msc_code')->value;
if (strlen($code) === 2) {
$parents_by_code[$code] = $term;
$len = strlen($code);
if ($len === 2) {
$l1_terms[$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;
elseif ($len === 3) {
$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;
}
}
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.
if (!empty($missing_codes)) {
$parent_tids = $storage->getQuery()
// Carrega L2 ausentes (necessários para agrupar L3).
if (!empty($missing_l2_codes)) {
$found_tids = $storage->getQuery()
->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)
->execute();
foreach ($storage->loadMultiple($parent_tids) as $term) {
$code = $term->get('field_msc_code')->value;
$parents_by_code[$code] = $term;
foreach ($storage->loadMultiple($found_tids) as $term) {
$code = $term->get('field_msc_code')->value;
$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.
$all_codes = array_unique(
array_merge(array_keys($parents_by_code), array_keys($children_by_code))
);
sort($all_codes);
// Carrega L1 ausentes (necessários para agrupar L2 ou L3).
if (!empty($missing_l1_codes)) {
$found_tids = $storage->getQuery()
->condition('vid', 'msc_2020')
->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 = [
'#prefix' => '<ul class="msc-terms-list">',
'#suffix' => '</ul>',
];
foreach ($all_codes as $i => $code) {
$parent = $parents_by_code[$code] ?? NULL;
$children = $children_by_code[$code] ?? [];
foreach ($all_l1_codes as $i => $l1_code) {
$l1_term = $l1_terms[$l1_code] ?? NULL;
$l2_group = $l2_terms[$l1_code] ?? [];
usort($children, fn($a, $b) => strcmp(
$a->get('field_msc_code')->value,
$b->get('field_msc_code')->value,
));
// L2 codes relevantes para este L1: os selecionados + os que agrupam L3.
$l3_l2_codes = array_filter(
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] = [
'#prefix' => '<li class="msc-terms-list__parent">',
'#prefix' => '<li class="msc-terms-list__l1 msc-terms-list__parent">',
'#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'] = [
'#prefix' => '<ul class="msc-terms-list__children">',
'#prefix' => '<ul class="msc-terms-list__l1-children msc-terms-list__children">',
'#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] = [
'#prefix' => '<li class="msc-terms-list__child">',
'#prefix' => '<li class="msc-terms-list__l2 msc-terms-list__child">',
'#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];
}
/**
* 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.
* Quando $term é NULL (ancestral não selecionado, apenas usado para
* agrupamento) o código é exibido sem link, independentemente da opção.
*
* @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.
* 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.
*/

View File

@@ -14,10 +14,16 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* 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
* select (subcampos) via servidor. O valor armazenado no campo é o TID
* selecionado no segundo select: o TID do pai para "área geral", ou o TID do
* filho para um subcampo específico.
* O mero de níveis exibidos é configurável por instância do widget
* (opção "Nível máximo" na tela Gerenciar exibição de formulário):
* 1 — apenas áreas principais (código de 2 dígitos, ex: "03")
* 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(
* 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 {
$storage = $field_definition->getFieldStorageDefinition();
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 {
$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'] ?? [];
$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();
$parent_exists = FALSE;
$submitted_parent = NestedArray::getValue(
@@ -79,143 +115,280 @@ class MscTermSelectWidget extends WidgetBase {
);
if ($parent_exists) {
// Rebuild pós-submissão (AJAX de parent_select, "Add another item", etc.)
$initial_parent = $submitted_parent !== '' ? (int) $submitted_parent : NULL;
// Rebuild pós-submissão (AJAX, "Add another item", etc.)
$l1 = $submitted_parent !== '' ? (int) $submitted_parent : NULL;
$child_exists = FALSE;
$submitted_child = NestedArray::getValue(
$raw_input,
array_merge($base, ['child_select']),
array_merge($base, ['child_wrapper', 'child_select']),
$child_exists,
);
$initial_child = ($child_exists && $submitted_child !== '' && $submitted_child !== NULL)
? (int) $submitted_child
: $initial_parent;
$l2 = ($child_exists && $submitted_child !== '' && $submitted_child !== NULL)
? (int) $submitted_child : $l1;
$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 {
// Carga inicial: deriva do valor persistido na entidade.
$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.
$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);
// L1 select.
$element['parent_select'] = [
'#type' => 'select',
'#title' => $element['#title'] ?? $this->t('Área MSC 2020'),
'#title_display' => $element['#title_display'] ?? 'before',
'#options' => ['' => $this->t('— selecione uma categoria —')] + $data['parents'],
'#default_value' => $initial_parent ?? '',
'#options' => ['' => $this->t('— selecione uma categoria —')] + $data['l1'],
'#default_value' => $l1 ?? '',
'#required' => !empty($element['#required']),
'#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.
// #validated => TRUE: aceita o valor submetido sem verificar #options,
// necessário pois as opções podem ter mudado entre submissões.
$element['child_select'] = [
'#type' => 'select',
'#title' => $this->t('Subcampo'),
'#prefix' => '<div id="' . $wrapper_id . '">',
'#suffix' => '</div>',
'#options' => $child_options,
'#default_value' => $initial_child ?? '',
'#validated' => TRUE,
if ($max_depth === 1) {
// Apenas L1: parent_select é o valor final, sem cascata.
return $element;
}
$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',
'#title' => $this->t('Subcampo'),
'#options' => $child_options,
'#default_value' => $l2 ?? '',
'#validated' => TRUE,
'#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;
}
/**
* 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,
* então formElement() já calculou as opções corretas para o novo pai.
* Retorna o container inteiro (L2 select + L3 select se max_depth=3),
* 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'];
array_pop($parents);
$parents[] = 'child_select';
array_pop($parents); // remove 'parent_select'
$parents[] = 'child_wrapper';
return NestedArray::getValue($form, $parents);
}
/**
* Carrega todos os termos msc_2020 e retorna arrays de pais e filhos.
*
* @return array{parents: array<int,string>, children: array<int,array<int,string>>}
* AJAX: reconstrói grandchild_wrapper quando L2 (child_select) muda.
*/
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
->getStorage('taxonomy_term')
->loadTree('msc_2020', 0, NULL, TRUE);
$code_to_tid = [];
$parents = [];
$children = [];
$l1 = [];
$l2 = [];
$l3 = [];
// Primeira passagem: monta mapa código→TID e coleta L1.
foreach ($tree as $term) {
$code = $term->get('field_msc_code')->value;
$tid = (int) $term->id();
$tid = (int) $term->id();
$code_to_tid[$code] = $tid;
if (strlen($code) === 2) {
$parents[$tid] = $code . ' — ' . $term->label();
$l1[$tid] = $code . ' — ' . $term->label();
}
}
foreach ($tree as $term) {
$code = $term->get('field_msc_code')->value;
if (strlen($code) > 2) {
$parent_code = substr($code, 0, 2);
$parent_tid = $code_to_tid[$parent_code] ?? NULL;
if ($parent_tid) {
$tid = (int) $term->id();
$children[$parent_tid][$tid] = $code . ' — ' . $term->label();
if ($max_depth >= 2) {
foreach ($tree as $term) {
$code = $term->get('field_msc_code')->value;
if (strlen($code) === 3) {
$parent_code = substr($code, 0, 2);
$parent_tid = $code_to_tid[$parent_code] ?? NULL;
if ($parent_tid !== NULL) {
$tid = (int) $term->id();
$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 {
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])) {
return [$parent_tid, $current_value];
// TID de L2 armazenado.
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 {
$max_depth = (int) $this->getSetting('max_depth');
foreach ($values as &$value) {
$child = $value['child_select'] ?? '';
$value = ['target_id' => $child !== '' ? (int) $child : NULL];
if ($max_depth === 1) {
$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;
}

View File

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