diff --git a/config/install/core.entity_form_display.node.content_page.default.yml b/config/install/core.entity_form_display.node.content_page.default.yml index 776a0d8..51a2c9c 100644 --- a/config/install/core.entity_form_display.node.content_page.default.yml +++ b/config/install/core.entity_form_display.node.content_page.default.yml @@ -1,15 +1,19 @@ -langcode: en +uuid: 37d8e6ba-7a46-4b21-b38f-b98a1aaddfed +langcode: pt-br status: true dependencies: config: - field.field.node.content_page.body - field.field.node.content_page.field_parent_page + - field.field.node.content_page.field_redirect_link - field.field.node.content_page.field_site_section - node.type.content_page module: - dynamic_entity_reference - - path + - link - text +_core: + default_config_hash: tmbpOtX26Afqxu9LxKtH1hZgFGb0MHl1vPy6gR2U8BQ id: node.content_page.default targetEntityType: node bundle: content_page @@ -17,7 +21,7 @@ mode: default content: body: type: text_textarea_with_summary - weight: 2 + weight: 4 region: content settings: rows: 9 @@ -27,13 +31,13 @@ content: third_party_settings: { } created: type: datetime_timestamp - weight: 10 + weight: 6 region: content settings: { } third_party_settings: { } field_parent_page: type: dynamic_entity_reference_default - weight: 0 + weight: 2 region: content settings: match_operator: CONTAINS @@ -41,42 +45,37 @@ content: size: 60 placeholder: '' third_party_settings: { } + field_redirect_link: + type: link_default + weight: 26 + region: content + settings: + placeholder_url: '' + placeholder_title: '' + third_party_settings: { } field_site_section: type: options_select weight: 1 region: content settings: { } third_party_settings: { } - path: - type: path - weight: 30 - region: content - settings: { } - third_party_settings: { } - promote: - type: boolean_checkbox - weight: 15 + langcode: + type: language_select + weight: 3 region: content settings: - display_label: true + include_locked: true third_party_settings: { } status: type: boolean_checkbox - weight: 120 - region: content - settings: - display_label: true - third_party_settings: { } - sticky: - type: boolean_checkbox - weight: 16 + weight: 7 region: content settings: display_label: true third_party_settings: { } title: type: string_textfield - weight: -5 + weight: 0 region: content settings: size: 60 @@ -92,4 +91,14 @@ content: size: 60 placeholder: '' third_party_settings: { } -hidden: { } +hidden: + gva_box_layout: true + gva_breadcrumb: true + gva_header: true + gva_node_class: true + gva_node_layout: true + gva_pagebuilder_content: true + gva_pagebuilder_enable: true + path: true + promote: true + sticky: true diff --git a/config/install/core.entity_view_display.node.content_page.default.yml b/config/install/core.entity_view_display.node.content_page.default.yml index ec00c3c..2dae4e1 100644 --- a/config/install/core.entity_view_display.node.content_page.default.yml +++ b/config/install/core.entity_view_display.node.content_page.default.yml @@ -1,13 +1,24 @@ +uuid: dd8e1e8f-7596-4196-a8ae-1bb7c8e5da0a langcode: en status: true dependencies: config: - field.field.node.content_page.body - field.field.node.content_page.field_parent_page + - field.field.node.content_page.field_redirect_link - field.field.node.content_page.field_site_section - node.type.content_page module: + - layout_builder + - link - text + - user +third_party_settings: + layout_builder: + enabled: false + allow_custom: false +_core: + default_config_hash: yOaQdWIbNqm6bN5B-vKu-rkQvPlfYxQkeHYNXmGlkio id: node.content_page.default targetEntityType: node bundle: content_page @@ -20,11 +31,20 @@ content: third_party_settings: { } weight: 0 region: content - links: - settings: { } + field_redirect_link: + type: link + label: above + settings: + trim_length: 80 + url_only: false + url_plain: false + rel: '' + target: '' third_party_settings: { } - weight: 100 + weight: 1 region: content hidden: field_parent_page: true field_site_section: true + langcode: true + links: true diff --git a/config/install/field.field.node.content_page.field_redirect_link.yml b/config/install/field.field.node.content_page.field_redirect_link.yml new file mode 100644 index 0000000..1ee7837 --- /dev/null +++ b/config/install/field.field.node.content_page.field_redirect_link.yml @@ -0,0 +1,23 @@ +uuid: 426b6b25-a2f2-4020-8b23-01dc9e82f429 +langcode: pt-br +status: true +dependencies: + config: + - field.storage.node.field_redirect_link + - node.type.content_page + module: + - link +id: node.content_page.field_redirect_link +field_name: field_redirect_link +entity_type: node +bundle: content_page +label: 'Redirect Link' +description: '' +required: false +translatable: null +default_value: { } +default_value_callback: '' +settings: + title: 0 + link_type: 17 +field_type: link diff --git a/config/install/field.field.node.content_page.field_site_section.yml b/config/install/field.field.node.content_page.field_site_section.yml index 9072e4f..1c93b5d 100644 --- a/config/install/field.field.node.content_page.field_site_section.yml +++ b/config/install/field.field.node.content_page.field_site_section.yml @@ -13,7 +13,7 @@ entity_type: node bundle: content_page label: 'Site Section' description: 'For root pages, select the section. For child pages, this field is filled automatically.' -required: false +required: true translatable: false default_value: { } default_value_callback: '' diff --git a/config/install/field.storage.node.field_redirect_link.yml b/config/install/field.storage.node.field_redirect_link.yml new file mode 100644 index 0000000..093aa9b --- /dev/null +++ b/config/install/field.storage.node.field_redirect_link.yml @@ -0,0 +1,19 @@ +uuid: 6212ce1e-3dfb-48b9-b15c-43f0e8c6fd24 +langcode: pt-br +status: true +dependencies: + module: + - link + - node +id: node.field_redirect_link +field_name: field_redirect_link +entity_type: node +type: link +settings: { } +module: link +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/config/install/node.type.content_page.yml b/config/install/node.type.content_page.yml index 2c29587..2bf8f7c 100644 --- a/config/install/node.type.content_page.yml +++ b/config/install/node.type.content_page.yml @@ -10,4 +10,4 @@ description: 'Pages with hierarchical parent-child structure for content organiz help: 'For root pages, fill in the Site Section manually. For child pages, select the parent page and the section will be inherited automatically.' new_revision: true preview_mode: 1 -display_submitted: true +display_submitted: false diff --git a/config/schema/structural_pages.schema.yml b/config/schema/structural_pages.schema.yml index 56c91e8..8dd2fc4 100644 --- a/config/schema/structural_pages.schema.yml +++ b/config/schema/structural_pages.schema.yml @@ -17,3 +17,17 @@ structural_pages.settings: bundle: type: string label: 'Bundle' + +block.settings.structural_pages_menu: + type: block_settings + label: 'Structural Pages Menu Block' + mapping: + max_depth: + type: integer + label: 'Maximum depth' + show_ancestor_title: + type: boolean + label: 'Show ancestor title' + expand_active_trail: + type: boolean + label: 'Expand active trail' diff --git a/css/structural-pages-menu.css b/css/structural-pages-menu.css index 670a99a..54ca832 100644 --- a/css/structural-pages-menu.css +++ b/css/structural-pages-menu.css @@ -1,63 +1,103 @@ /** - * @file - * Styles for the structural pages menu block. + * Structural Pages Menu - Custom Flyout Styles + * Ensures the nested menu items fly out to the right exactly like the Gavias theme reference menu. */ -.structural-pages-menu { - font-size: 0.9rem; +.block-structural-pages-menu { + padding: 0 !important; } -.structural-pages-menu__title { - font-size: 1.1rem; - margin-bottom: 0.75rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid #ddd; +/* Apply the gray background and padding directly to the nav so it remains styled even if detached by the sticky script */ +.sidebar .structural-pages-menu { + padding: 30px; + background-color: #f3f3f3; + border-radius: 10px; + width: 100%; } -.structural-pages-menu__title a { - text-decoration: none; - color: inherit; +/* Remove the padding and background from the outer Drupal block wrapper to avoid double padding */ +.sidebar [id^="block-structuralpagesmenublock"] { + padding: 0 !important; + background: transparent !important; } -.structural-pages-menu__title a:hover { - text-decoration: underline; +/* Position relative on parent items to anchor the absolute sub-menu */ +.structural-pages-menu .structural-pages-menu__item { + position: relative; } -.structural-pages-menu__list { +/* Hide sub-menus by default and position them to the right */ +.structural-pages-menu .sub-menu { + position: absolute; + top: 0; + left: 100%; + visibility: hidden; + opacity: 0; + min-width: 260px; + background-color: #ffffff; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08); + z-index: 999; + transition: all 0.25s ease-out; + transform: translateY(10px); + pointer-events: none; + border-left: 2px solid var(--notech-theme-color, #db162f); list-style: none; - margin: 0; padding: 0; + margin: 0; } -.structural-pages-menu__list--level-2, -.structural-pages-menu__list--level-3, -.structural-pages-menu__list--level-4 { - padding-left: 1rem; +/* Show submenu on hover or focus */ +.structural-pages-menu .structural-pages-menu__item:hover>.sub-menu, +.structural-pages-menu .structural-pages-menu__item:focus-within>.sub-menu { + visibility: visible; + opacity: 1; + transform: translateY(0); + pointer-events: auto; + padding: 5px 30px !important; } -.structural-pages-menu__item { - margin: 0.25rem 0; +/* Add an indicator icon (arrow) to items that have children */ +.structural-pages-menu .structural-pages-menu__item--has-children>a::after { + content: " \203A"; + float: right; + font-family: monospace; + font-size: 1.4em; + font-weight: bold; + line-height: 0.8; + opacity: 0.4; + transition: opacity 0.2s; } -.structural-pages-menu__link { +.structural-pages-menu .structural-pages-menu__item--has-children:hover>a::after { + opacity: 1; +} + +/* Styles for the links inside the flyout menus */ +.structural-pages-menu .sub-menu>li>a { display: block; - padding: 0.25rem 0.5rem; + padding: 12px 20px; + font-size: 14px; + color: #333333; text-decoration: none; - color: #333; - border-radius: 3px; - transition: background-color 0.15s ease; + border-bottom: 1px solid #f2f2f2; + transition: all 0.2s ease; + background-color: transparent; + font-weight: 500; } -.structural-pages-menu__link:hover { - background-color: #f5f5f5; - text-decoration: none; +.structural-pages-menu .sub-menu>li:last-child>a { + border-bottom: none; } -.structural-pages-menu__link--active-trail { - font-weight: 600; - color: #0073bd; +.structural-pages-menu .sub-menu>li>a:hover { + color: var(--notech-theme-color, #db162f); + background-color: #fcfcfc; + padding-left: 25px; + /* Subtle interactively indication */ } -.structural-pages-menu__item--active-trail > .structural-pages-menu__link { - background-color: #e8f4fc; -} +/* Ensure active trail items inside submenu remain highlighted */ +.structural-pages-menu .sub-menu .menu-item--active-trail>a, +.structural-pages-menu .sub-menu .is-active { + color: var(--notech-theme-color, #db162f); +} \ No newline at end of file diff --git a/css/structural_pages_select2.css b/css/structural_pages_select2.css new file mode 100644 index 0000000..2123ed7 --- /dev/null +++ b/css/structural_pages_select2.css @@ -0,0 +1,60 @@ +/* Custom styles to make Select2 match Drupal 10 Claro Theme's native select */ +.select2-container--default .select2-selection--single { + height: 3rem; /* 48px matches Claro's form elements */ + border: 1px solid #8e929c; /* Claro default border */ + border-radius: 2px; + background-color: #f3f4f9; /* Claro default input background */ + display: flex; + align-items: center; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.select2-container--default .select2-selection--single .select2-selection__rendered { + line-height: normal; + color: #222330; + padding-left: 1rem; + font-size: 1rem; +} + +.select2-container--default .select2-selection--single .select2-selection__arrow { + height: 100%; + right: 0.5rem; + display: flex; + align-items: center; +} + +/* Hover effect */ +.select2-container--default .select2-selection--single:hover { + border-color: #55565b; +} + +/* Focus and Open effect */ +.select2-container--default.select2-container--focus .select2-selection--single, +.select2-container--default.select2-container--open .select2-selection--single { + border-color: #003cc5; + box-shadow: 0 0 0 2px #26a769; /* Claro green-ish or blue-ish focus ring */ + outline: none; +} + +.select2-container--default.select2-container--open .select2-selection--single { + box-shadow: 0 0 0 2px #003cc5; /* Active blue focus */ +} + +/* Adjust dropdown list styling slightly */ +.select2-dropdown { + border: 1px solid #003cc5; + border-radius: 2px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); +} + +.select2-container--default .select2-search--dropdown .select2-search__field { + border: 1px solid #8e929c; + border-radius: 2px; + padding: 0.5rem; + height: 2.5rem; +} + +.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable { + background-color: #003cc5; + color: white; +} diff --git a/js/structural_pages_select2.js b/js/structural_pages_select2.js new file mode 100644 index 0000000..d6503f0 --- /dev/null +++ b/js/structural_pages_select2.js @@ -0,0 +1,34 @@ +(function ($, Drupal, once) { + Drupal.behaviors.structuralPagesSelect2 = { + attach: function (context, settings) { + function initSelect2() { + var elements = once('structural-pages-select2', 'select.select2-widget', context); + if (elements.length) { + var $select = $(elements); + // If the select2 jQuery plugin is loaded (by another module/theme), use it. + // Otherwise, try chosen if it's there. + if (typeof $.fn.select2 !== 'undefined') { + $select.select2({ + width: '100%', + dropdownAutoWidth: true + }); + } + else if (typeof $.fn.chosen !== 'undefined') { + $select.chosen({ + width: '100%', + search_contains: true + }); + } + } + } + + // Initialize on load + initSelect2(); + + // Some admin themes replace select elements entirely. This ensures we hook in. + $(document).on('ajaxComplete', function () { + initSelect2(); + }); + } + }; +})(jQuery, Drupal, once); diff --git a/src/EventSubscriber/StructuralPagesRedirectSubscriber.php b/src/EventSubscriber/StructuralPagesRedirectSubscriber.php new file mode 100644 index 0000000..082e8b8 --- /dev/null +++ b/src/EventSubscriber/StructuralPagesRedirectSubscriber.php @@ -0,0 +1,79 @@ +routeMatch = $route_match; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + $events[KernelEvents::REQUEST][] = ['onKernelRequest', 30]; + return $events; + } + + /** + * Redirects if the node has a redirect link. + * + * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event + * The event to process. + */ + public function onKernelRequest(RequestEvent $event) + { + if (!$event->isMainRequest()) { + return; + } + + $route_name = $this->routeMatch->getRouteName(); + if ($route_name === 'entity.node.canonical') { + $node = $this->routeMatch->getParameter('node'); + if ($node instanceof NodeInterface && $node->bundle() === 'content_page') { + if ($node->hasField('field_redirect_link') && !$node->get('field_redirect_link')->isEmpty()) { + try { + /** @var \Drupal\link\LinkItemInterface $link_item */ + $link_item = $node->get('field_redirect_link')->first(); + $url = $link_item->getUrl()->toString(); + + // Cacheable redirect response. + $response = new TrustedRedirectResponse($url, 301); + $response->addCacheableDependency($node); + $event->setResponse($response); + } catch (\Exception $e) { + // If the link is invalid, proceed normally. + } + } + } + } + } + +} diff --git a/src/Plugin/Block/StructuralPagesMenuBlock.php b/src/Plugin/Block/StructuralPagesMenuBlock.php index 646fbbe..6d2fb1c 100644 --- a/src/Plugin/Block/StructuralPagesMenuBlock.php +++ b/src/Plugin/Block/StructuralPagesMenuBlock.php @@ -27,12 +27,13 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * } * ) */ -class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPluginInterface { +class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPluginInterface +{ /** - * Maximum depth to prevent infinite loops. + * Maximum depth to prevent infinite loops. We set it very high (50) to allow unlimited sublevels. */ - protected const MAX_DEPTH = 10; + protected const MAX_DEPTH = 50; /** * The entity type manager. @@ -82,7 +83,8 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug /** * {@inheritdoc} */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static + { return new static( $configuration, $plugin_id, @@ -96,9 +98,10 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug /** * {@inheritdoc} */ - public function defaultConfiguration(): array { + public function defaultConfiguration(): array + { return [ - 'max_depth' => 3, + 'max_depth' => 50, 'show_ancestor_title' => TRUE, 'expand_active_trail' => TRUE, ] + parent::defaultConfiguration(); @@ -107,7 +110,8 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug /** * {@inheritdoc} */ - public function blockForm($form, FormStateInterface $form_state): array { + public function blockForm($form, FormStateInterface $form_state): array + { $form = parent::blockForm($form, $form_state); $form['max_depth'] = [ @@ -139,7 +143,8 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug /** * {@inheritdoc} */ - public function blockSubmit($form, FormStateInterface $form_state): void { + public function blockSubmit($form, FormStateInterface $form_state): void + { $this->configuration['max_depth'] = $form_state->getValue('max_depth'); $this->configuration['show_ancestor_title'] = $form_state->getValue('show_ancestor_title'); $this->configuration['expand_active_trail'] = $form_state->getValue('expand_active_trail'); @@ -148,7 +153,8 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug /** * {@inheritdoc} */ - public function build(): array { + public function build(): array + { $this->cacheTags = []; // Get current context. @@ -177,6 +183,7 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug $build = [ '#theme' => 'structural_pages_menu', '#ancestor' => $ancestor, + '#ancestor_url' => $ancestor->hasLinkTemplate('canonical') ? $ancestor->toUrl()->toString() : '', '#tree' => $tree, '#active_trail' => $active_trail, '#show_ancestor_title' => $this->configuration['show_ancestor_title'], @@ -195,15 +202,15 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug * @return \Drupal\node\NodeInterface|null * The current node or NULL. */ - protected function getCurrentNode(): ?NodeInterface { + protected function getCurrentNode(): ?NodeInterface + { // Try to get from block context first. try { $node = $this->getContextValue('node'); if ($node instanceof NodeInterface) { return $node; } - } - catch (\Exception $e) { + } catch (\Exception $e) { // Context not available. } @@ -221,7 +228,8 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug * @return \Drupal\Core\Entity\EntityInterface|null * The ancestor entity (term, user, or group) or NULL. */ - protected function findAncestor(?NodeInterface $node): ?EntityInterface { + protected function findAncestor(?NodeInterface $node): ?EntityInterface + { if (!$node) { // Check if we're on a taxonomy term, user, or group page. return $this->getAncestorFromRoute(); @@ -278,7 +286,8 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug * @return \Drupal\Core\Entity\EntityInterface|null * The ancestor entity or NULL. */ - protected function getAncestorFromRoute(): ?EntityInterface { + protected function getAncestorFromRoute(): ?EntityInterface + { return $this->handlerManager->getEntityFromRoute($this->routeMatch); } @@ -291,7 +300,8 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug * @return array * The menu tree structure. */ - protected function buildTree(EntityInterface $ancestor): array { + protected function buildTree(EntityInterface $ancestor): array + { $root_pages = $this->getChildPages($ancestor->getEntityTypeId(), (string) $ancestor->id()); if (empty($root_pages)) { @@ -321,14 +331,29 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug * @return array * The branch structure. */ - protected function buildBranch(NodeInterface $node, int $current_depth, int $max_depth): array { + protected function buildBranch(NodeInterface $node, int $current_depth, int $max_depth): array + { $this->cacheTags[] = 'node:' . $node->id(); + $url = $node->toUrl()->toString(); + $is_redirect = FALSE; + if ($node->hasField('field_redirect_link') && !$node->get('field_redirect_link')->isEmpty()) { + try { + /** @var \Drupal\link\LinkItemInterface $link_item */ + $link_item = $node->get('field_redirect_link')->first(); + $url = $link_item->getUrl()->toString(); + $is_redirect = TRUE; + } catch (\Exception $e) { + // Fallback to node url if the redirect link is invalid. + } + } + $branch = [ 'entity' => $node, 'id' => $node->id(), 'title' => $node->label(), - 'url' => $node->toUrl()->toString(), + 'url' => $url, + 'is_redirect' => $is_redirect, 'depth' => $current_depth, 'children' => [], ]; @@ -354,15 +379,24 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug * @return \Drupal\node\NodeInterface[] * Array of child nodes. */ - protected function getChildPages(string $parent_type, string $parent_id): array { + protected function getChildPages(string $parent_type, string $parent_id): array + { $query = $this->entityTypeManager->getStorage('node')->getQuery() ->accessCheck(TRUE) ->condition('type', 'content_page') ->condition('status', 1) - ->condition('field_parent_page.target_type', $parent_type) - ->condition('field_parent_page.target_id', $parent_id) ->sort('title', 'ASC'); + if ($parent_type === 'taxonomy_term') { + // Root pages of a section have no parent page, but belong to the section. + $query->notExists('field_parent_page'); + $query->condition('field_site_section.target_id', $parent_id); + } else { + // Child pages belong to a specific parent entity (node). + $query->condition('field_parent_page.target_type', $parent_type); + $query->condition('field_parent_page.target_id', $parent_id); + } + $nids = $query->execute(); if (empty($nids)) { @@ -386,7 +420,8 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug * @return array * Array of node IDs in the active trail. */ - protected function getActiveTrail(NodeInterface $node): array { + protected function getActiveTrail(NodeInterface $node): array + { $trail = [$node->id()]; $visited = [$node->id() => TRUE]; $current = $node; @@ -427,14 +462,16 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug /** * {@inheritdoc} */ - public function getCacheTags(): array { + public function getCacheTags(): array + { return Cache::mergeTags(parent::getCacheTags(), ['node_list:content_page']); } /** * {@inheritdoc} */ - public function getCacheContexts(): array { + public function getCacheContexts(): array + { return Cache::mergeContexts(parent::getCacheContexts(), ['route', 'url.path']); } diff --git a/structural_pages.libraries.yml b/structural_pages.libraries.yml index 07d4454..13266bb 100644 --- a/structural_pages.libraries.yml +++ b/structural_pages.libraries.yml @@ -3,3 +3,17 @@ menu: css: component: css/structural-pages-menu.css: {} + +select2: + version: 4.1.0-rc.0 + css: + component: + https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css: { type: external } + css/structural_pages_select2.css: {} + js: + https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js: { type: external } + js/structural_pages_select2.js: {} + dependencies: + - core/jquery + - core/drupal + - core/once diff --git a/structural_pages.module b/structural_pages.module index f594ef4..9772ea2 100644 --- a/structural_pages.module +++ b/structural_pages.module @@ -212,6 +212,7 @@ function structural_pages_theme(): array { 'structural_pages_menu' => [ 'variables' => [ 'ancestor' => NULL, + 'ancestor_url' => '', 'tree' => [], 'active_trail' => [], 'show_ancestor_title' => TRUE, @@ -332,3 +333,285 @@ function _structural_pages_get_term_hierarchy_path(TermInterface $term): string 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. + */ +function _structural_pages_alter_parent_page_form(&$form, \Drupal\Core\Form\FormStateInterface $form_state) { + // Hide the native Drupal menu settings tab, since this module generates + // the structural pages menu automatically. + if (isset($form['menu'])) { + $form['menu']['#access'] = FALSE; + } + + if (isset($form['field_site_section']) && isset($form['field_parent_page'])) { + + // 1. Add AJAX behavior to update parent page options when section changes. + if (isset($form['field_site_section']['widget'])) { + $form['field_site_section']['widget']['#ajax'] = [ + 'callback' => 'structural_pages_parent_page_ajax_callback', + 'wrapper' => 'parent-page-wrapper', + ]; + } + + // Wrap the container where our custom select will go. + $form['structural_pages_wrapper'] = [ + '#type' => 'container', + '#attributes' => ['id' => 'parent-page-wrapper'], + '#weight' => $form['field_parent_page']['#weight'] ?? 1, + '#attached' => [ + 'library' => [ + 'structural_pages/select2', + ], + ], + ]; + + // 3. Retrieve the currently selected site_section ID. + $site_section_id = NULL; + + // From AJAX state (user just changed it). + $site_section_value = $form_state->getValue('field_site_section'); + if (!empty($site_section_value[0]['target_id'])) { + $site_section_id = $site_section_value[0]['target_id']; + } + // From initial form load. + elseif (isset($form['field_site_section']['widget']['#default_value'])) { + $default = $form['field_site_section']['widget']['#default_value']; + if (is_array($default) && !empty($default[0])) { + $site_section_id = $default[0]; + } + elseif (is_scalar($default)) { + $site_section_id = $default; + } + } + + $current_entity = $form_state->getFormObject()->getEntity(); + $current_nid = $current_entity ? $current_entity->id() : NULL; + + if ($site_section_id) { + $options = _structural_pages_build_parent_tree_options($site_section_id, $current_nid); + + $default_parent = ''; + if ($current_entity && !$current_entity->isNew() && !$current_entity->get('field_parent_page')->isEmpty()) { + $default_parent = $current_entity->get('field_parent_page')->target_type . ':' . $current_entity->get('field_parent_page')->target_id; + } + + // Hide the original field. + $form['field_parent_page']['#access'] = FALSE; + + // Create a new visual field. + $form['structural_pages_wrapper']['custom_parent_page'] = [ + '#type' => 'select', + '#title' => t('Parent Page'), + '#options' => $options, + '#default_value' => $default_parent, + '#empty_option' => t('- Raiz da Seção -'), + '#description' => t('Select the parent page within this site section.'), + // Attach a select2/chosen class if available in standard themes + '#attributes' => [ + 'class' => ['select2-widget', 'chosen-enable'], + 'data-placeholder' => t('Search for a parent page...'), + ], + ]; + + // We must add a custom submit/validate handler to map our select back to field_parent_page. + array_unshift($form['#validate'], 'structural_pages_custom_parent_validate'); + } else { + // If no section chosen yet, hide parent page completely so they pick a section first. + $form['field_parent_page']['#access'] = FALSE; + $form['structural_pages_wrapper']['custom_parent_page'] = [ + '#type' => 'markup', + '#markup' => '
' . t('Por favor, selecione uma Site Section primeiro.') . '
', + ]; + } + } +} + +/** + * Validation callback to map the select list back into dynamic_entity_reference. + */ +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)) { + list($type, $id) = explode(':', $custom_val); + $form_state->setValue('field_parent_page', [ + ['target_id' => $id, 'target_type' => $type] + ]); + } else { + $form_state->setValue('field_parent_page', []); + } +} + +/** + * 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 section. + */ +function _structural_pages_build_parent_tree_options($site_section_id, $current_node_id = NULL) { + $options = []; + + // Add the Section's Taxonomy Term as a Root option + $term = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->load($site_section_id); + if ($term) { + $options['taxonomy_term:' . $site_section_id] = '< Raiz (' . $term->getName() . ') >'; + } + + $query = \Drupal::entityQuery('node') + ->condition('type', 'content_page') + ->condition('field_site_section', $site_section_id) + ->accessCheck(TRUE) + ->sort('title', 'ASC'); + $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; // Skip self + } + + $parent_id = NULL; + $parent_type = NULL; + if (!$node->get('field_parent_page')->isEmpty()) { + $parent_id = $node->get('field_parent_page')->target_id; + $parent_type = $node->get('field_parent_page')->target_type; + } + + // If the parent is another content_page in our list, it's a child. + if ($parent_type === 'node' && isset($nodes[$parent_id])) { + $children[$parent_id][] = $nid; + } else { + // It's a root (parent is a taxonomy term, a section_page, or empty) + $roots[] = $nid; + } + } + + // Try to sort roots alphabetically + usort($roots, function($a, $b) use ($nodes) { + return strcmp($nodes[$a]->getTitle(), $nodes[$b]->getTitle()); + }); + + $build_options = function($nids, $depth) use (&$build_options, &$options, $nodes, $children) { + // We use non-breaking spaces and hyphens for a tree view look + $prefix = str_repeat('— ', $depth); + foreach ($nids as $nid) { + $options['node:' . $nid] = $prefix . $nodes[$nid]->getTitle(); + if (isset($children[$nid])) { + // Sort children alphabetically too + $child_nids = $children[$nid]; + usort($child_nids, function($a, $b) use ($nodes) { + return strcmp($nodes[$a]->getTitle(), $nodes[$b]->getTitle()); + }); + $build_options($child_nids, $depth + 1); + } + } + }; + + $build_options($roots, 0); + + return $options; +} + +/** + * 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. + $query->condition('field_parent_page.target_id', $parent_id); + $query->condition('field_parent_page.target_type', 'node'); + } 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()) { + $query->condition('field_site_section.target_id', $current_node->get('field_site_section')->target_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 = []; + } + } + } + } +} diff --git a/structural_pages.services.yml b/structural_pages.services.yml index 24c3b3a..3d435d4 100644 --- a/structural_pages.services.yml +++ b/structural_pages.services.yml @@ -14,3 +14,10 @@ services: - '@plugin.manager.parent_entity_handler' tags: - { name: breadcrumb_builder, priority: 100 } + + structural_pages.redirect_subscriber: + class: Drupal\structural_pages\EventSubscriber\StructuralPagesRedirectSubscriber + arguments: + - '@current_route_match' + tags: + - { name: event_subscriber } diff --git a/templates/structural-pages-menu.html.twig b/templates/structural-pages-menu.html.twig index 4cf87b0..37383c6 100644 --- a/templates/structural-pages-menu.html.twig +++ b/templates/structural-pages-menu.html.twig @@ -18,22 +18,26 @@ #} {{ attach_library('structural_pages/menu') }} {% if tree is not empty %} -