mirror of
https://gitlab.unicamp.br/infimecc_drupal11_modules/structural_pages.git
synced 2026-03-08 01:27:42 -03:00
Altera lógica de Site Section e adiciona redirect em content_page
- Adiciona campo field_redirect_link (link) no bundle content_page; EventSubscriber emite redirect 301 quando o campo está preenchido - field_site_section passa a ser obrigatório - Formulário de content_page: AJAX no site section, select hierárquico de página pai filtrado por seção, validação customizada - StructuralPagesMenuBlock: MAX_DEPTH 10→50, nova lógica de raiz via field_site_section, variável ancestor_url no render array - Template do menu: novas classes BEM/Gva, suporte a is_redirect, usa ancestor_url em vez de chamada Twig direta - CSS do menu reescrito com estilos flyout/sidebar; Select2 adicionado para o campo de página pai no formulário admin - display_submitted desabilitado no tipo content_page Co-Authored-By: Bauer <henrique@webcontent.com.br> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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: ''
|
||||
|
||||
19
config/install/field.storage.node.field_redirect_link.yml
Normal file
19
config/install/field.storage.node.field_redirect_link.yml
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
60
css/structural_pages_select2.css
Normal file
60
css/structural_pages_select2.css
Normal file
@@ -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;
|
||||
}
|
||||
34
js/structural_pages_select2.js
Normal file
34
js/structural_pages_select2.js
Normal file
@@ -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);
|
||||
79
src/EventSubscriber/StructuralPagesRedirectSubscriber.php
Normal file
79
src/EventSubscriber/StructuralPagesRedirectSubscriber.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\structural_pages\EventSubscriber;
|
||||
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Routing\TrustedRedirectResponse;
|
||||
use Drupal\node\NodeInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
/**
|
||||
* Redirects content pages to their configured link if set.
|
||||
*/
|
||||
class StructuralPagesRedirectSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* The route match.
|
||||
*
|
||||
* @var \Drupal\Core\Routing\RouteMatchInterface
|
||||
*/
|
||||
protected $routeMatch;
|
||||
|
||||
/**
|
||||
* Constructs a new StructuralPagesRedirectSubscriber object.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The route match.
|
||||
*/
|
||||
public function __construct(RouteMatchInterface $route_match)
|
||||
{
|
||||
$this->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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' => '<p><em>' . t('Por favor, selecione uma Site Section primeiro.') . '</em></p>',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -18,22 +18,26 @@
|
||||
#}
|
||||
{{ attach_library('structural_pages/menu') }}
|
||||
{% if tree is not empty %}
|
||||
<nav class="structural-pages-menu" aria-label="{{ 'Site structure navigation'|t }}">
|
||||
<nav class="block-menu navigation structural-pages-menu" aria-label="{{ 'Site structure navigation'|t }}">
|
||||
{% if show_ancestor_title and ancestor %}
|
||||
<h2 class="structural-pages-menu__title">
|
||||
{% if ancestor.toUrl is defined %}
|
||||
<a href="{{ ancestor.toUrl }}">{{ ancestor.label }}</a>
|
||||
<h2 class="structural-pages-menu__title block-title">
|
||||
<span>
|
||||
{% if ancestor_url %}
|
||||
<a href="{{ ancestor_url }}">{{ ancestor.label }}</a>
|
||||
{% else %}
|
||||
{{ ancestor.label }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</h2>
|
||||
{% endif %}
|
||||
|
||||
<ul class="structural-pages-menu__list structural-pages-menu__list--level-1">
|
||||
{% for item in tree %}
|
||||
{{ _self.menu_item(item, active_trail, 1) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="block-content">
|
||||
<ul class="gva_menu structural-pages-menu__list structural-pages-menu__list--level-1">
|
||||
{% for item in tree %}
|
||||
{{ _self.menu_item(item, active_trail, 1) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
@@ -41,19 +45,20 @@
|
||||
{% set is_active = item.id in active_trail %}
|
||||
{% set has_children = item.children is not empty %}
|
||||
{% set classes = [
|
||||
'menu-item',
|
||||
'structural-pages-menu__item',
|
||||
'structural-pages-menu__item--level-' ~ depth,
|
||||
is_active ? 'structural-pages-menu__item--active-trail',
|
||||
has_children ? 'structural-pages-menu__item--has-children',
|
||||
is_active ? 'menu-item--active-trail structural-pages-menu__item--active-trail',
|
||||
has_children ? 'menu-item--expanded structural-pages-menu__item--has-children',
|
||||
] %}
|
||||
|
||||
<li{{ create_attribute().addClass(classes) }}>
|
||||
<a href="{{ item.url }}" class="structural-pages-menu__link{% if is_active %} structural-pages-menu__link--active-trail{% endif %}">
|
||||
<a href="{{ item.url }}"{% if item.is_redirect %} target="_blank"{% endif %} class="structural-pages-menu__link{% if is_active %} is-active structural-pages-menu__link--active-trail{% endif %}">
|
||||
{{- item.title -}}
|
||||
</a>
|
||||
|
||||
{% if has_children %}
|
||||
<ul class="structural-pages-menu__list structural-pages-menu__list--level-{{ depth + 1 }}">
|
||||
<ul class="menu sub-menu structural-pages-menu__list structural-pages-menu__list--level-{{ depth + 1 }}">
|
||||
{% for child in item.children %}
|
||||
{{ _self.menu_item(child, active_trail, depth + 1) }}
|
||||
{% endfor %}
|
||||
|
||||
Reference in New Issue
Block a user