mirror of
https://gitlab.unicamp.br/infimecc_drupal11_modules/structural_pages.git
synced 2026-05-04 23:00:40 -03:00
Campo booleano (padrão: ativo) que controla se a página aparece no menu de navegação. Quando desmarcado, oculta field_menu_title no formulário via #states e exclui a página da query em getChildPages(). O campo field_weight permanece sempre visível, pois a ordenação se aplica independentemente da exibição no menu. Hook update_10015 cria storage + instância, atualiza o form display e retroativamente define o valor como 1 para páginas existentes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
809 lines
28 KiB
Plaintext
809 lines
28 KiB
Plaintext
<?php
|
|
|
|
/**
|
|
* @file
|
|
* Primary module hooks for Structural Pages module.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Drupal\Core\Entity\EntityInterface;
|
|
use Drupal\node\NodeInterface;
|
|
use Drupal\taxonomy\TermInterface;
|
|
|
|
/**
|
|
* Implements hook_entity_presave().
|
|
*
|
|
* For content_page nodes with a node parent: inherits field_site_section from
|
|
* the parent content_page. For root pages (no parent) field_site_section is
|
|
* set directly by the user in the form and is left untouched here.
|
|
*/
|
|
function structural_pages_entity_presave(EntityInterface $entity): void {
|
|
if (!$entity instanceof NodeInterface || $entity->bundle() !== 'content_page') {
|
|
return;
|
|
}
|
|
|
|
// Root page: field_site_section is managed by the editor, nothing to inherit.
|
|
if (!$entity->hasField('field_parent_page') || $entity->get('field_parent_page')->isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
$parent_field = $entity->get('field_parent_page')->first();
|
|
if (!$parent_field) {
|
|
return;
|
|
}
|
|
|
|
$parent_id = $parent_field->target_id ?? NULL;
|
|
if (!$parent_id) {
|
|
return;
|
|
}
|
|
|
|
// Circular reference validation.
|
|
if (!$entity->isNew()) {
|
|
if (_structural_pages_creates_circular_reference($entity->id(), $parent_id)) {
|
|
\Drupal::messenger()->addError(t('Circular reference detected. A page cannot be a parent of itself or its ancestors.'));
|
|
$entity->set('field_parent_page', NULL);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Inherit field_site_section (DER value: target_type + target_id) from the
|
|
// parent content_page so sibling pages always share the same domain root.
|
|
$parent_node = \Drupal::entityTypeManager()->getStorage('node')->load($parent_id);
|
|
if (!$parent_node instanceof NodeInterface) {
|
|
return;
|
|
}
|
|
if (!$parent_node->hasField('field_site_section') || $parent_node->get('field_site_section')->isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
$parent_fss = $parent_node->get('field_site_section')->first();
|
|
$entity->set('field_site_section', [
|
|
'target_type' => $parent_fss->target_type ?? 'taxonomy_term',
|
|
'target_id' => $parent_fss->target_id,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Returns the configured site section vocabulary machine name.
|
|
*
|
|
* @return string
|
|
* The vocabulary ID (e.g., 'site_sections').
|
|
*/
|
|
function _structural_pages_get_vocabulary(): string {
|
|
return \Drupal::config('structural_pages.settings')
|
|
->get('site_section_vocabulary') ?? 'site_sections';
|
|
}
|
|
|
|
/**
|
|
* Checks if setting parent_id as parent of node_id would create circular reference.
|
|
*
|
|
* @param int|string $node_id
|
|
* The ID of the node being edited.
|
|
* @param int|string $parent_id
|
|
* The ID of the potential parent.
|
|
*
|
|
* @return bool
|
|
* TRUE if it would create circular reference, FALSE otherwise.
|
|
*/
|
|
function _structural_pages_creates_circular_reference(int|string $node_id, int|string $parent_id): bool {
|
|
$visited = [];
|
|
$current_id = $parent_id;
|
|
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
|
|
|
|
while ($current_id) {
|
|
// If we find the original node in the parent chain, it's circular.
|
|
if ($current_id == $node_id) {
|
|
return TRUE;
|
|
}
|
|
|
|
// Avoid infinite loops in case of corrupted data.
|
|
if (isset($visited[$current_id])) {
|
|
return TRUE;
|
|
}
|
|
$visited[$current_id] = TRUE;
|
|
|
|
// Load the current node and get its parent.
|
|
$current_node = $node_storage->load($current_id);
|
|
if (!$current_node instanceof NodeInterface ||
|
|
!$current_node->hasField('field_parent_page') ||
|
|
$current_node->get('field_parent_page')->isEmpty()) {
|
|
break;
|
|
}
|
|
|
|
$parent_field = $current_node->get('field_parent_page')->first();
|
|
// Only continue checking if parent is also a node.
|
|
if (!$parent_field || ($parent_field->target_type ?? NULL) !== 'node') {
|
|
break;
|
|
}
|
|
|
|
$current_id = $parent_field->target_id;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* Implements hook_theme().
|
|
*/
|
|
function structural_pages_theme(): array {
|
|
return [
|
|
'structural_pages_menu' => [
|
|
'variables' => [
|
|
'ancestor' => NULL,
|
|
'ancestor_url' => '',
|
|
'tree' => [],
|
|
'active_trail' => [],
|
|
'show_ancestor_title' => TRUE,
|
|
],
|
|
],
|
|
'structural_pages_menu_tree' => [
|
|
'variables' => [
|
|
'items' => [],
|
|
'active_trail' => [],
|
|
'depth' => 0,
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Implements hook_token_info().
|
|
*/
|
|
function structural_pages_token_info(): array {
|
|
$info = [];
|
|
|
|
$info['tokens']['node']['site-section-path'] = [
|
|
'name' => t('Site Section Path'),
|
|
'description' => t('The hierarchical path of the site section taxonomy (e.g., undergraduate/courses).'),
|
|
];
|
|
|
|
$info['tokens']['node']['content-page-ancestors'] = [
|
|
'name' => t('Content Page Ancestors Path'),
|
|
'description' => t('Array of ancestor path segments for a content_page node (domain + parent pages). Use [node:content-page-ancestors:join-path] in Pathauto patterns to get a slash-separated path (e.g., user/joao/pesquisa).'),
|
|
'type' => 'array',
|
|
];
|
|
|
|
$info['tokens']['term']['hierarchy-path'] = [
|
|
'name' => t('Hierarchy Path'),
|
|
'description' => t('The hierarchical path of the term including ancestors (e.g., institutional/news).'),
|
|
];
|
|
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* Implements hook_tokens().
|
|
*/
|
|
function structural_pages_tokens(string $type, array $tokens, array $data, array $options, $bubbleable_metadata): array {
|
|
$replacements = [];
|
|
|
|
if ($type === 'node' && !empty($data['node'])) {
|
|
$node = $data['node'];
|
|
foreach ($tokens as $name => $original) {
|
|
if ($name === 'site-section-path') {
|
|
$replacements[$original] = _structural_pages_get_section_path($node);
|
|
}
|
|
if ($name === 'content-page-ancestors') {
|
|
$path = _structural_pages_get_content_page_ancestors($node);
|
|
$replacements[$original] = !empty($path)
|
|
? array_values(array_filter(explode('/', $path)))
|
|
: '';
|
|
}
|
|
}
|
|
|
|
// Handle [node:content-page-ancestors:join-path] and other chained tokens.
|
|
// Pathauto's [array:join-path] cleans each segment individually and joins
|
|
// with '/', preserving slashes. The token ends in ':join-path]' which is
|
|
// in Pathauto's safe_tokens list, so the result is NOT re-cleaned.
|
|
if ($ancestors_tokens = \Drupal::token()->findWithPrefix($tokens, 'content-page-ancestors')) {
|
|
$path = _structural_pages_get_content_page_ancestors($node);
|
|
if (!empty($path)) {
|
|
$segments = array_values(array_filter(explode('/', $path)));
|
|
$replacements += \Drupal::token()->generate(
|
|
'array',
|
|
$ancestors_tokens,
|
|
['array' => $segments],
|
|
$options,
|
|
$bubbleable_metadata
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($type === 'term' && !empty($data['term'])) {
|
|
$term = $data['term'];
|
|
foreach ($tokens as $name => $original) {
|
|
if ($name === 'hierarchy-path') {
|
|
$replacements[$original] = _structural_pages_get_term_hierarchy_path($term);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $replacements;
|
|
}
|
|
|
|
/**
|
|
* Gets the hierarchical path of the site section for a node.
|
|
*
|
|
* @param \Drupal\node\NodeInterface $node
|
|
* The node.
|
|
*
|
|
* @return string
|
|
* The section path (e.g., "undergraduate/courses") or empty string.
|
|
*/
|
|
function _structural_pages_get_section_path(NodeInterface $node): string {
|
|
if (!$node->hasField('field_site_section') || $node->get('field_site_section')->isEmpty()) {
|
|
return '';
|
|
}
|
|
|
|
$fss = $node->get('field_site_section')->first();
|
|
// Only taxonomy_term domains have a hierarchical term path.
|
|
if (($fss->target_type ?? 'taxonomy_term') !== 'taxonomy_term') {
|
|
return '';
|
|
}
|
|
|
|
$term_id = $fss->target_id;
|
|
$term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
|
|
$term = $term_storage->load($term_id);
|
|
|
|
if (!$term) {
|
|
return '';
|
|
}
|
|
|
|
// Get all ancestors of the term.
|
|
$ancestors = $term_storage->loadAllParents($term_id);
|
|
$ancestors = array_reverse($ancestors);
|
|
|
|
// Build the path using the term names converted to URL.
|
|
$path_parts = [];
|
|
foreach ($ancestors as $ancestor) {
|
|
$path_parts[] = \Drupal::service('pathauto.alias_cleaner')->cleanString($ancestor->getName());
|
|
}
|
|
|
|
return implode('/', $path_parts);
|
|
}
|
|
|
|
/**
|
|
* Gets the full ancestor path for a content_page node.
|
|
*
|
|
* Traverses the field_parent_page chain to the root, then prepends the domain
|
|
* entity's URL alias (from field_site_section). Intended as a Pathauto token
|
|
* prefix: configure the pattern as "[node:content-page-ancestors]/[node:title]".
|
|
*
|
|
* Examples:
|
|
* - Root page under user "joao" → "user/joao"
|
|
* - Child of "Pesquisa" under user "joao" → "user/joao/pesquisa"
|
|
* - Root page under term "graduacao/disciplinas" → "graduacao/disciplinas"
|
|
*
|
|
* @param \Drupal\node\NodeInterface $node
|
|
* The content_page node.
|
|
*
|
|
* @return string
|
|
* The ancestor path without leading/trailing slashes, or empty string.
|
|
*/
|
|
function _structural_pages_get_content_page_ancestors(NodeInterface $node): string {
|
|
if ($node->bundle() !== 'content_page') {
|
|
return '';
|
|
}
|
|
|
|
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
|
|
$alias_cleaner = \Drupal::service('pathauto.alias_cleaner');
|
|
$path_alias_manager = \Drupal::service('path_alias.manager');
|
|
|
|
// Walk up the field_parent_page chain collecting parent titles.
|
|
$parent_slugs = [];
|
|
$visited = [];
|
|
$current = $node;
|
|
$max = 50;
|
|
|
|
while ($max-- > 0) {
|
|
if (isset($visited[$current->id()])) {
|
|
break;
|
|
}
|
|
$visited[$current->id()] = TRUE;
|
|
|
|
if (!$current->hasField('field_parent_page') || $current->get('field_parent_page')->isEmpty()) {
|
|
// Reached the root content_page: resolve domain from field_site_section.
|
|
if (!$current->hasField('field_site_section') || $current->get('field_site_section')->isEmpty()) {
|
|
break;
|
|
}
|
|
|
|
$fss = $current->get('field_site_section')->first();
|
|
$domain_type = $fss->target_type ?? 'taxonomy_term';
|
|
$domain_id = $fss->target_id;
|
|
$domain = \Drupal::entityTypeManager()->getStorage($domain_type)->load($domain_id);
|
|
|
|
if (!$domain || !$domain->hasLinkTemplate('canonical')) {
|
|
break;
|
|
}
|
|
|
|
// Build the system path for the domain entity.
|
|
// Constructed directly for known entity types to avoid URL generator
|
|
// context issues that can occur inside hook_tokens().
|
|
$domain_system_path = match ($domain_type) {
|
|
'user' => '/user/' . $domain_id,
|
|
'taxonomy_term' => '/taxonomy/term/' . $domain_id,
|
|
default => (static function () use ($domain): string {
|
|
try {
|
|
return '/' . $domain->toUrl('canonical')->getInternalPath();
|
|
}
|
|
catch (\Exception $e) {
|
|
return '';
|
|
}
|
|
})(),
|
|
};
|
|
|
|
if (empty($domain_system_path)) {
|
|
break;
|
|
}
|
|
|
|
// Resolve to alias if one exists.
|
|
$domain_alias = $path_alias_manager->getAliasByPath($domain_system_path);
|
|
$domain_path = ltrim($domain_alias, '/');
|
|
|
|
// Build: domain/parent2/parent1 (parents were collected innermost-first).
|
|
$parts = array_merge([$domain_path], array_reverse($parent_slugs));
|
|
return implode('/', array_filter($parts));
|
|
}
|
|
|
|
// Collect this node's parent's title (we traverse before adding current).
|
|
$parent_field = $current->get('field_parent_page')->first();
|
|
$parent_id = $parent_field->target_id ?? NULL;
|
|
if (!$parent_id) {
|
|
break;
|
|
}
|
|
|
|
$parent = $node_storage->load($parent_id);
|
|
if (!$parent) {
|
|
break;
|
|
}
|
|
|
|
// Add the parent's slug to the list (innermost first, reversed later).
|
|
$parent_slugs[] = $alias_cleaner->cleanString($parent->getTitle());
|
|
$current = $parent;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Gets the hierarchical path of a taxonomy term.
|
|
*
|
|
* @param \Drupal\taxonomy\TermInterface $term
|
|
* The taxonomy term.
|
|
*
|
|
* @return string
|
|
* The hierarchical path (e.g., "institutional/news") or empty string.
|
|
*/
|
|
function _structural_pages_get_term_hierarchy_path(TermInterface $term): string {
|
|
$term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
|
|
|
|
// Get all ancestors of the term (including itself).
|
|
$ancestors = $term_storage->loadAllParents($term->id());
|
|
$ancestors = array_reverse($ancestors);
|
|
|
|
// Build the path using the term names converted to URL.
|
|
$path_parts = [];
|
|
foreach ($ancestors as $ancestor) {
|
|
$path_parts[] = \Drupal::service('pathauto.alias_cleaner')->cleanString($ancestor->getName());
|
|
}
|
|
|
|
return implode('/', $path_parts);
|
|
}
|
|
|
|
/**
|
|
* Implements hook_form_FORM_ID_alter() for node_content_page_form.
|
|
*/
|
|
function structural_pages_form_node_content_page_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
|
|
_structural_pages_alter_parent_page_form($form, $form_state);
|
|
}
|
|
|
|
/**
|
|
* Implements hook_form_FORM_ID_alter() for node_content_page_edit_form.
|
|
*/
|
|
function structural_pages_form_node_content_page_edit_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
|
|
_structural_pages_alter_parent_page_form($form, $form_state);
|
|
}
|
|
|
|
/**
|
|
* Helper to alter the content_page forms for parent page filtering.
|
|
*
|
|
* Architecture:
|
|
* - field_site_section (DER): domain root — taxonomy_term, user, or event.
|
|
* - field_parent_page (DER, node-only): parent content_page within the domain,
|
|
* or empty for a root page.
|
|
*
|
|
* The native field_parent_page widget is replaced by a custom hierarchical
|
|
* select that lists only content_pages sharing the same field_site_section
|
|
* value. A sentinel option '__root__' represents an empty parent (root page).
|
|
* Query params ?domain_type=<type>&domain_id=<id> (or the aliases
|
|
* ?parent_section_type=<type>&parent_id=<id>) pre-fill field_site_section
|
|
* when creating a new page from an external domain context (e.g., user).
|
|
*/
|
|
function _structural_pages_alter_parent_page_form(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
|
|
// Hide the native Drupal menu settings tab.
|
|
if (isset($form['menu'])) {
|
|
$form['menu']['#access'] = FALSE;
|
|
}
|
|
|
|
// Conditional visibility: field_menu_title and field_weight are only
|
|
// relevant when field_show_in_menu is checked.
|
|
$show_in_menu_selector = 'input[name="field_show_in_menu[value]"]';
|
|
if (isset($form['field_menu_title'])) {
|
|
$form['field_menu_title']['#states'] = [
|
|
'visible' => [$show_in_menu_selector => ['checked' => TRUE]],
|
|
];
|
|
}
|
|
|
|
if (!isset($form['field_site_section']) || !isset($form['field_parent_page'])) {
|
|
return;
|
|
}
|
|
|
|
$current_entity = $form_state->getFormObject()->getEntity();
|
|
|
|
// Pre-fill field_site_section for new nodes created from a domain context.
|
|
if ($current_entity->isNew()) {
|
|
$request = \Drupal::request();
|
|
$qp_type = $request->query->get('domain_type') ?? $request->query->get('parent_section_type');
|
|
$qp_id = $request->query->get('domain_id') ?? $request->query->get('parent_id');
|
|
if ($qp_type && $qp_id && $qp_type !== 'node') {
|
|
$domain_entity = \Drupal::entityTypeManager()->getStorage($qp_type)->load($qp_id);
|
|
if ($domain_entity) {
|
|
// Set the entity value (used on AJAX rebuild and on save).
|
|
$current_entity->set('field_site_section', [
|
|
'target_type' => $qp_type,
|
|
'target_id' => $qp_id,
|
|
]);
|
|
// Also update the widget elements directly: hook_form_alter runs after
|
|
// the DER widget has already been built from the original (empty) entity,
|
|
// so #default_value in the rendered elements needs to be set explicitly.
|
|
if (isset($form['field_site_section']['widget'][0]['target_type'])) {
|
|
$form['field_site_section']['widget'][0]['target_type']['#default_value'] = $qp_type;
|
|
}
|
|
if (isset($form['field_site_section']['widget'][0]['target_id'])) {
|
|
$form['field_site_section']['widget'][0]['target_id']['#default_value'] = $domain_entity;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add AJAX so changing field_site_section rebuilds the parent page options.
|
|
$ajax = [
|
|
'callback' => 'structural_pages_parent_page_ajax_callback',
|
|
'wrapper' => 'parent-page-wrapper',
|
|
'event' => 'change',
|
|
];
|
|
if (isset($form['field_site_section']['widget'][0]['target_type'])) {
|
|
$form['field_site_section']['widget'][0]['target_type']['#ajax'] = $ajax;
|
|
}
|
|
if (isset($form['field_site_section']['widget'][0]['target_id'])) {
|
|
$form['field_site_section']['widget'][0]['target_id']['#ajax'] = $ajax;
|
|
}
|
|
// Fallback for single-type widgets (entity_reference-style DER or pre-DER).
|
|
if (!isset($form['field_site_section']['widget'][0]['target_type']) &&
|
|
!isset($form['field_site_section']['widget'][0]['target_id'])) {
|
|
$form['field_site_section']['widget']['#ajax'] = $ajax;
|
|
}
|
|
|
|
// Wrapper container replaced by AJAX.
|
|
$form['structural_pages_wrapper'] = [
|
|
'#type' => 'container',
|
|
'#attributes' => ['id' => 'parent-page-wrapper'],
|
|
'#weight' => $form['field_parent_page']['#weight'] ?? 1,
|
|
'#attached' => ['library' => ['structural_pages/select2']],
|
|
];
|
|
|
|
// Hide the native DER widget for field_parent_page; we replace it below.
|
|
$form['field_parent_page']['#access'] = FALSE;
|
|
|
|
// Determine the active domain (type + id) from field_site_section.
|
|
[$domain_type, $domain_id] = _structural_pages_get_form_domain($form, $form_state, $current_entity);
|
|
|
|
if ($domain_type && $domain_id) {
|
|
$domain_entity = \Drupal::entityTypeManager()->getStorage($domain_type)->load($domain_id);
|
|
$domain_label = $domain_entity ? $domain_entity->label() : t('Domínio');
|
|
|
|
$current_nid = (!$current_entity->isNew()) ? (int) $current_entity->id() : NULL;
|
|
$options = _structural_pages_build_parent_tree_options($domain_type, $domain_id, $current_nid);
|
|
$options = ['__root__' => '< ' . t('Raiz (@label)', ['@label' => $domain_label]) . ' >'] + $options;
|
|
|
|
// Determine default selection.
|
|
$default_parent = '__root__';
|
|
if (!$current_entity->isNew() && !$current_entity->get('field_parent_page')->isEmpty()) {
|
|
$pf = $current_entity->get('field_parent_page')->first();
|
|
if ($pf && $pf->target_id) {
|
|
$key = 'node:' . $pf->target_id;
|
|
if (isset($options[$key])) {
|
|
$default_parent = $key;
|
|
}
|
|
}
|
|
}
|
|
|
|
$form['structural_pages_wrapper']['custom_parent_page'] = [
|
|
'#type' => 'select',
|
|
'#title' => t('Parent Page'),
|
|
'#options' => $options,
|
|
'#default_value' => $default_parent,
|
|
'#description' => t('Select the parent page within this domain. Select Root to create a top-level page.'),
|
|
'#attributes' => [
|
|
'class' => ['select2-widget', 'chosen-enable'],
|
|
'data-placeholder' => t('Search for a parent page...'),
|
|
],
|
|
];
|
|
|
|
array_unshift($form['#validate'], 'structural_pages_custom_parent_validate');
|
|
}
|
|
else {
|
|
$form['structural_pages_wrapper']['custom_parent_page'] = [
|
|
'#type' => 'markup',
|
|
'#markup' => '<p><em>' . t('Por favor, selecione a Seção do Site primeiro.') . '</em></p>',
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the domain [type, id] from the form state or the entity being edited.
|
|
*
|
|
* Priority:
|
|
* 1. AJAX rebuild state (user just changed field_site_section).
|
|
* 2. Saved field_site_section value on the entity being edited.
|
|
* 3. Legacy widget #default_value (pre-DER fallback).
|
|
*
|
|
* @return array{0: string|null, 1: int|string|null}
|
|
*/
|
|
function _structural_pages_get_form_domain(array &$form, \Drupal\Core\Form\FormStateInterface $form_state, \Drupal\Core\Entity\EntityInterface $entity): array {
|
|
// 1. AJAX state.
|
|
$fss_value = $form_state->getValue('field_site_section');
|
|
if (!empty($fss_value[0]['target_id'])) {
|
|
return [
|
|
$fss_value[0]['target_type'] ?? 'taxonomy_term',
|
|
$fss_value[0]['target_id'],
|
|
];
|
|
}
|
|
|
|
// 2. Saved entity value.
|
|
if ($entity->hasField('field_site_section') && !$entity->get('field_site_section')->isEmpty()) {
|
|
$fss = $entity->get('field_site_section')->first();
|
|
return [
|
|
$fss->target_type ?? 'taxonomy_term',
|
|
$fss->target_id,
|
|
];
|
|
}
|
|
|
|
// 3. Legacy widget default value (entity_reference before DER migration).
|
|
if (isset($form['field_site_section']['widget']['#default_value'])) {
|
|
$default = $form['field_site_section']['widget']['#default_value'];
|
|
if (is_array($default) && !empty($default[0])) {
|
|
return ['taxonomy_term', $default[0]];
|
|
}
|
|
if (is_scalar($default) && $default) {
|
|
return ['taxonomy_term', $default];
|
|
}
|
|
}
|
|
|
|
return [NULL, NULL];
|
|
}
|
|
|
|
/**
|
|
* Validation callback: maps the custom select back to field_parent_page.
|
|
*
|
|
* '__root__' or empty → field_parent_page = [] (root page, no parent).
|
|
* 'node:<id>' → field_parent_page = [{target_id: id, target_type: node}].
|
|
*/
|
|
function structural_pages_custom_parent_validate(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
|
|
$custom_val = $form_state->getValue('custom_parent_page');
|
|
if (empty($custom_val) || $custom_val === '__root__') {
|
|
$form_state->setValue('field_parent_page', []);
|
|
}
|
|
else {
|
|
[, $id] = explode(':', $custom_val, 2);
|
|
$form_state->setValue('field_parent_page', [
|
|
['target_id' => $id, 'target_type' => 'node'],
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* AJAX callback to replace the parent page wrapper.
|
|
*/
|
|
function structural_pages_parent_page_ajax_callback(array &$form, \Drupal\Core\Form\FormStateInterface $form_state) {
|
|
return $form['structural_pages_wrapper'];
|
|
}
|
|
|
|
/**
|
|
* Builds a hierarchical tree array of content_pages for a given domain.
|
|
*
|
|
* Queries all content_pages with field_site_section matching [$domain_type,
|
|
* $domain_id], then arranges them in a tree keyed as 'node:<nid>'.
|
|
*
|
|
* @param string $domain_type
|
|
* The domain entity type (e.g. 'taxonomy_term', 'user').
|
|
* @param int|string $domain_id
|
|
* The domain entity ID.
|
|
* @param int|string|null $current_node_id
|
|
* The node being edited (excluded from options to prevent self-reference).
|
|
*
|
|
* @return array
|
|
* Associative array 'node:<nid>' => indented title.
|
|
*/
|
|
function _structural_pages_build_parent_tree_options(string $domain_type, int|string $domain_id, $current_node_id = NULL): array {
|
|
$options = [];
|
|
|
|
$query = \Drupal::entityQuery('node')
|
|
->condition('type', 'content_page')
|
|
->accessCheck(TRUE)
|
|
->sort('title', 'ASC');
|
|
|
|
_structural_pages_apply_domain_condition($query, $domain_type, $domain_id);
|
|
|
|
$nids = $query->execute();
|
|
|
|
if (empty($nids)) {
|
|
return $options;
|
|
}
|
|
|
|
$nodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($nids);
|
|
|
|
$children = [];
|
|
$roots = [];
|
|
|
|
foreach ($nodes as $nid => $node) {
|
|
if ($nid == $current_node_id) {
|
|
continue;
|
|
}
|
|
|
|
$parent_id = NULL;
|
|
if (!$node->get('field_parent_page')->isEmpty()) {
|
|
$parent_id = $node->get('field_parent_page')->target_id;
|
|
}
|
|
|
|
if ($parent_id && isset($nodes[$parent_id])) {
|
|
$children[$parent_id][] = $nid;
|
|
}
|
|
else {
|
|
$roots[] = $nid;
|
|
}
|
|
}
|
|
|
|
usort($roots, fn($a, $b) => strcmp($nodes[$a]->getTitle(), $nodes[$b]->getTitle()));
|
|
|
|
$build_options = function ($nids, $depth) use (&$build_options, &$options, $nodes, $children) {
|
|
$prefix = str_repeat('— ', $depth);
|
|
foreach ($nids as $nid) {
|
|
$options['node:' . $nid] = $prefix . $nodes[$nid]->getTitle();
|
|
if (isset($children[$nid])) {
|
|
$child_nids = $children[$nid];
|
|
usort($child_nids, fn($a, $b) => strcmp($nodes[$a]->getTitle(), $nodes[$b]->getTitle()));
|
|
$build_options($child_nids, $depth + 1);
|
|
}
|
|
}
|
|
};
|
|
|
|
$build_options($roots, 0);
|
|
|
|
return $options;
|
|
}
|
|
|
|
/**
|
|
* Applies domain conditions to a content_page entity query.
|
|
*
|
|
* Uses a direct database subquery on node__field_site_section to filter by
|
|
* both target_type and target_id. This avoids relying on DER's entity query
|
|
* integration (which may throw "Invalid specifier 'target_type'" depending on
|
|
* the installed DER version and Drupal's internal caching).
|
|
*
|
|
* @param \Drupal\Core\Entity\Query\QueryInterface $query
|
|
* The entity query to modify.
|
|
* @param string $domain_type
|
|
* The domain entity type (e.g. 'taxonomy_term', 'user').
|
|
* @param int|string $domain_id
|
|
* The domain entity ID.
|
|
*/
|
|
function _structural_pages_apply_domain_condition(\Drupal\Core\Entity\Query\QueryInterface $query, string $domain_type, int|string $domain_id): void {
|
|
$database = \Drupal::database();
|
|
$schema = $database->schema();
|
|
|
|
if ($schema->fieldExists('node__field_site_section', 'field_site_section_target_type')) {
|
|
// DER storage: query the field table directly to filter by both columns.
|
|
$nids = $database->select('node__field_site_section', 'fss')
|
|
->fields('fss', ['entity_id'])
|
|
->condition('fss.field_site_section_target_type', $domain_type)
|
|
->condition('fss.field_site_section_target_id', $domain_id)
|
|
->condition('fss.deleted', 0)
|
|
->execute()
|
|
->fetchCol();
|
|
|
|
// Add a condition that will never match if no results found.
|
|
$query->condition('nid', empty($nids) ? [0] : $nids, 'IN');
|
|
}
|
|
else {
|
|
// Legacy entity_reference storage (before update hook 10011 runs):
|
|
// all references are taxonomy_terms, target_id alone is sufficient.
|
|
$query->condition('field_site_section', $domain_id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements hook_preprocess_node().
|
|
*/
|
|
function structural_pages_preprocess_node(array &$variables): void {
|
|
// Forcefully remove the "submitted by" author and date information
|
|
// for content pages, regardless of theme settings.
|
|
if (isset($variables['node']) && $variables['node']->bundle() === 'content_page') {
|
|
$variables['display_submitted'] = FALSE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements hook_views_post_execute().
|
|
*/
|
|
function structural_pages_views_post_execute(\Drupal\views\ViewExecutable $view) {
|
|
// Fallback for child_pages view: if empty, show sibling pages instead.
|
|
if ($view->id() === 'child_pages' && empty($view->result)) {
|
|
$args = $view->args;
|
|
if (!empty($args[0]) && is_numeric($args[0])) {
|
|
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
|
|
$current_node = $node_storage->load($args[0]);
|
|
|
|
if ($current_node instanceof \Drupal\node\NodeInterface && $current_node->bundle() === 'content_page') {
|
|
$query = \Drupal::entityQuery('node')
|
|
->condition('type', 'content_page')
|
|
->condition('status', 1)
|
|
->accessCheck(TRUE)
|
|
->sort('title', 'ASC');
|
|
|
|
$parent_id = NULL;
|
|
if (!$current_node->get('field_parent_page')->isEmpty()) {
|
|
$parent_field = $current_node->get('field_parent_page')->first();
|
|
if (($parent_field->target_type ?? '') === 'node') {
|
|
$parent_id = $parent_field->target_id;
|
|
}
|
|
}
|
|
|
|
if ($parent_id) {
|
|
// Has a node parent. Siblings are nodes with the same parent.
|
|
// field_parent_page only points to nodes (post-migration), so
|
|
// filtering by target_id alone is sufficient.
|
|
$query->condition('field_parent_page.target_id', $parent_id);
|
|
} else {
|
|
// Root page. Siblings are other root pages in the same site section.
|
|
$query->notExists('field_parent_page');
|
|
|
|
if (!$current_node->get('field_site_section')->isEmpty()) {
|
|
$fss = $current_node->get('field_site_section')->first();
|
|
$domain_type = $fss->target_type ?? 'taxonomy_term';
|
|
$domain_id = $fss->target_id;
|
|
_structural_pages_apply_domain_condition($query, $domain_type, $domain_id);
|
|
}
|
|
}
|
|
|
|
$nids = $query->execute();
|
|
|
|
if (!empty($nids)) {
|
|
// Load the sibling nodes
|
|
$siblings = $node_storage->loadMultiple($nids);
|
|
$index = 0;
|
|
$view->result = [];
|
|
|
|
foreach ($siblings as $nid => $sibling) {
|
|
$row = new \Drupal\views\ResultRow();
|
|
$row->_entity = $sibling;
|
|
$row->nid = $nid;
|
|
$row->index = $index++;
|
|
$view->result[] = $row;
|
|
}
|
|
|
|
$view->total_rows = count($view->result);
|
|
|
|
// Optionally alter the title to indicate these are siblings
|
|
$view->setTitle(t('Páginas do mesmo nível'));
|
|
|
|
// Remove empty text since we now have results
|
|
$view->empty = [];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|