mirror of
https://gitlab.unicamp.br/infimecc_drupal11_modules/site_tools.git
synced 2026-05-03 17:00:40 -03:00
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:
@@ -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'
|
||||
12
modules/site_tools_msc_2020/css/msc-term-list.css
Normal file
12
modules/site_tools_msc_2020/css/msc-term-list.css
Normal 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;
|
||||
}
|
||||
@@ -1,2 +1,8 @@
|
||||
msc_term_select_widget:
|
||||
version: VERSION
|
||||
|
||||
msc_term_list:
|
||||
version: VERSION
|
||||
css:
|
||||
component:
|
||||
css/msc-term-list.css: {}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 nú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;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user