mirror of
https://gitlab.unicamp.br/infimecc_drupal11_modules/site_tools.git
synced 2026-05-03 19:00:41 -03:00
Adiciona widget de seleção em cascata para o vocabulário MSC 2020
Implementa MscTermSelectWidget, um FieldWidget para campos entity_reference apontando para o vocabulário msc_2020. Dois selects encadeados: o primeiro lista as 63 categorias pai; o segundo é reconstruído via Drupal AJAX (#ajax) ao mudar o pai, listando os subcampos da categoria selecionada mais a opção "área geral (sem subcampo)". O valor salvo no campo é o TID do segundo select. O estado dos selects é preservado em rebuilds AJAX (ex.: "Add another item") lendo getUserInput(), que não é afetado pelo #limit_validation_errors do botão. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
5
modules/site_tools_msc_2020/js/msc-select.js
Normal file
5
modules/site_tools_msc_2020/js/msc-select.js
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* @file
|
||||
* Reservado para melhorias futuras do widget MSC 2020.
|
||||
* A lógica de cascata entre os selects é gerida via Drupal AJAX (#ajax).
|
||||
*/
|
||||
@@ -0,0 +1,2 @@
|
||||
msc_term_select_widget:
|
||||
version: VERSION
|
||||
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\site_tools_msc_2020\Plugin\Field\FieldWidget;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\Field\WidgetBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
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.
|
||||
*
|
||||
* @FieldWidget(
|
||||
* id = "msc_term_select",
|
||||
* label = @Translation("MSC 2020 — Seleção em cascata"),
|
||||
* field_types = {
|
||||
* "entity_reference"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class MscTermSelectWidget extends WidgetBase {
|
||||
|
||||
public function __construct(
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
FieldDefinitionInterface $field_definition,
|
||||
array $settings,
|
||||
array $third_party_settings,
|
||||
protected EntityTypeManagerInterface $entityTypeManager,
|
||||
) {
|
||||
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
|
||||
}
|
||||
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
|
||||
return new static(
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$configuration['field_definition'],
|
||||
$configuration['settings'],
|
||||
$configuration['third_party_settings'],
|
||||
$container->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 function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array {
|
||||
$data = $this->buildTermData();
|
||||
|
||||
// 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_parents = $element['#field_parents'] ?? $form['#parents'] ?? [];
|
||||
$base = array_merge($field_parents, [$field_name, $delta]);
|
||||
|
||||
$raw_input = $form_state->getUserInput();
|
||||
$parent_exists = FALSE;
|
||||
$submitted_parent = NestedArray::getValue(
|
||||
$raw_input,
|
||||
array_merge($base, ['parent_select']),
|
||||
$parent_exists,
|
||||
);
|
||||
|
||||
if ($parent_exists) {
|
||||
// Rebuild pós-submissão (AJAX de parent_select, "Add another item", etc.)
|
||||
$initial_parent = $submitted_parent !== '' ? (int) $submitted_parent : NULL;
|
||||
$child_exists = FALSE;
|
||||
$submitted_child = NestedArray::getValue(
|
||||
$raw_input,
|
||||
array_merge($base, ['child_select']),
|
||||
$child_exists,
|
||||
);
|
||||
$initial_child = ($child_exists && $submitted_child !== '' && $submitted_child !== NULL)
|
||||
? (int) $submitted_child
|
||||
: $initial_parent;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
$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 ?? '',
|
||||
'#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,
|
||||
'#attributes' => ['class' => ['msc-child-select']],
|
||||
];
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback AJAX: reconstrói o child_select quando o 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.
|
||||
*/
|
||||
public static function rebuildChildSelect(array &$form, FormStateInterface $form_state): array {
|
||||
$parents = $form_state->getTriggeringElement()['#array_parents'];
|
||||
array_pop($parents);
|
||||
$parents[] = 'child_select';
|
||||
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>>}
|
||||
*/
|
||||
protected function buildTermData(): array {
|
||||
$tree = $this->entityTypeManager
|
||||
->getStorage('taxonomy_term')
|
||||
->loadTree('msc_2020', 0, NULL, TRUE);
|
||||
|
||||
$code_to_tid = [];
|
||||
$parents = [];
|
||||
$children = [];
|
||||
|
||||
foreach ($tree as $term) {
|
||||
$code = $term->get('field_msc_code')->value;
|
||||
$tid = (int) $term->id();
|
||||
$code_to_tid[$code] = $tid;
|
||||
if (strlen($code) === 2) {
|
||||
$parents[$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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['parents' => $parents, 'children' => $children];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve o par (parent_tid, child_tid) a partir do TID persistido.
|
||||
*
|
||||
* @return array{int|null, int|null}
|
||||
*/
|
||||
protected function resolveInitialValues(?int $current_value, array $data): array {
|
||||
if ($current_value === NULL) {
|
||||
return [NULL, NULL];
|
||||
}
|
||||
if (isset($data['parents'][$current_value])) {
|
||||
return [$current_value, $current_value];
|
||||
}
|
||||
foreach ($data['children'] as $parent_tid => $kids) {
|
||||
if (isset($kids[$current_value])) {
|
||||
return [$parent_tid, $current_value];
|
||||
}
|
||||
}
|
||||
return [NULL, NULL];
|
||||
}
|
||||
|
||||
public function massageFormValues(array $values, array $form, FormStateInterface $form_state): array {
|
||||
foreach ($values as &$value) {
|
||||
$child = $value['child_select'] ?? '';
|
||||
$value = ['target_id' => $child !== '' ? (int) $child : NULL];
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user