clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\layout_builder\Form\LayoutBuilderEntityFormTrait;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a form for configuring navigation blocks.
*
* @internal
*/
final class LayoutForm extends FormBase {
use LayoutBuilderEntityFormTrait {
buildActions as buildActionsElement;
saveTasks as saveTasks;
}
/**
* {@inheritdoc}
*/
public function getBaseFormId(): string {
return 'navigation_layout';
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'navigation_layout';
}
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new LayoutForm.
*/
public function __construct(protected LayoutTempstoreRepositoryInterface $layoutTempstoreRepository) {
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository')
);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?SectionStorageInterface $section_storage = NULL) {
$form['#attributes']['class'][] = 'layout-builder-form';
$form['layout_builder'] = [
'#type' => 'layout_builder',
'#section_storage' => $section_storage,
];
$form['#attached']['library'][] = 'navigation/navigation.layoutBuilder';
$this->sectionStorage = $section_storage;
$form['actions'] = [
'submit' => [
'#type' => 'submit',
'#value' => $this->t('Save'),
],
] + $this->buildActionsElement([]);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$this->sectionStorage->save();
$this->saveTasks($form_state, new TranslatableMarkup('Saved navigation blocks'));
}
}

View File

@@ -0,0 +1,223 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation\Form;
use Drupal\Component\Utility\Environment;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\file\Entity\File;
use Drupal\file\FileUsage\FileUsageInterface;
use Drupal\navigation\NavigationRenderer;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure Navigation settings for this site.
*
* @internal
*/
final class SettingsForm extends ConfigFormBase {
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The file usage service.
*
* @var \Drupal\file\FileUsage\FileUsageInterface
*/
protected $fileUsage;
/**
* The file URL generator.
*
* @var \Drupal\Core\File\FileUrlGeneratorInterface
*/
protected $fileUrlGenerator;
/**
* Renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected RendererInterface $renderer;
/**
* Constructs a Navigation SettingsForm object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager
* The typed config manager.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* File system service.
* @param \Drupal\Core\File\FileUrlGeneratorInterface $fileUrlGenerator
* The file URL generator.
* @param \Drupal\file\FileUsage\FileUsageInterface $fileUsage
* The File Usage service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* Renderer service.
*/
public function __construct(
ConfigFactoryInterface $config_factory,
TypedConfigManagerInterface $typed_config_manager,
FileSystemInterface $file_system,
FileUrlGeneratorInterface $fileUrlGenerator,
FileUsageInterface $fileUsage,
RendererInterface $renderer,
) {
parent::__construct($config_factory, $typed_config_manager);
$this->fileSystem = $file_system;
$this->fileUrlGenerator = $fileUrlGenerator;
$this->fileUsage = $fileUsage;
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('config.typed'),
$container->get('file_system'),
$container->get('file_url_generator'),
$container->get('file.usage'),
$container->get('renderer')
);
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'navigation_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames(): array {
return ['navigation.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
$config = $this->config('navigation.settings');
$form['#attached']['library'][] = 'core/drupal.states';
$form['logo'] = [
'#type' => 'fieldset',
'#title' => $this->t('Logo options'),
];
$form['logo']['logo_provider'] = [
'#type' => 'radios',
'#title' => $this->t('Choose logo handling'),
'#title_display' => 'invisible',
'#options' => [
NavigationRenderer::LOGO_PROVIDER_DEFAULT => $this->t('Default logo'),
NavigationRenderer::LOGO_PROVIDER_HIDE => $this->t('Hide logo'),
NavigationRenderer::LOGO_PROVIDER_CUSTOM => $this->t('Custom logo'),
],
'#default_value' => $config->get('logo_provider'),
];
$form['logo']['image'] = [
'#type' => 'container',
'#states' => [
'visible' => [
':input[name="logo_provider"]' => ['value' => NavigationRenderer::LOGO_PROVIDER_CUSTOM],
],
],
];
$allowed = 'png jpg jpeg';
$current_logo_managed_fid = $config->get('logo_managed');
$max_navigation_allowed = $config->get('logo_max_filesize');
$max_system_allowed = Environment::getUploadMaxSize();
$max_allowed = $max_navigation_allowed < $max_system_allowed ? $max_navigation_allowed : $max_system_allowed;
$upload_validators = [
'FileExtension' => ['extensions' => $allowed],
'FileSizeLimit' => ['fileLimit' => $max_allowed],
];
$file_upload_help = [
'#theme' => 'file_upload_help',
'#description' => $this->t('Recommended image dimension 40 x 40 pixels.'),
'#upload_validators' => $upload_validators,
'#cardinality' => 1,
];
$form['logo']['image']['logo_managed'] = [
'#type' => 'managed_file',
'#title' => t('Choose custom logo'),
'#upload_validators' => $upload_validators,
'#upload_location' => 'public://navigation-logo',
'#description' => $this->renderer->renderInIsolation($file_upload_help),
'#default_value' => $current_logo_managed_fid,
'#multiple' => FALSE,
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
$logo_managed = $form_state->getValue('logo_managed');
if ($form_state->getValue('logo_provider') === NavigationRenderer::LOGO_PROVIDER_CUSTOM && empty($logo_managed) === TRUE) {
$form_state->setErrorByName('logo_managed', 'An image file is required with the current logo handling option.');
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$config = $this->config('navigation.settings');
// Get the previous config settings.
$previous_logo_provider = $config->get('logo_provider');
$logo_managed = $config->get('logo_managed');
$previous_logo_fid = $logo_managed ? reset($logo_managed) : NULL;
// Get new values from the form.
$new_logo_provider = $form_state->getValue('logo_provider');
$logo = $form_state->getValue('logo_managed');
$new_logo_fid = !empty($logo) ? reset($logo) : NULL;
// Pre-load files if any for FileUsageInterface.
$previous_logo_managed = $previous_logo_fid ? File::load($previous_logo_fid) : NULL;
$new_logo_managed = $new_logo_fid ? File::load($new_logo_fid) : NULL;
// Decrement if previous logo_provider was 'custom' and has changed to a
// different fid and there's a change in the logo fid.
if ($previous_logo_provider === NavigationRenderer::LOGO_PROVIDER_CUSTOM
&& ($new_logo_provider !== NavigationRenderer::LOGO_PROVIDER_CUSTOM || $previous_logo_fid !== $new_logo_fid)
&& $previous_logo_managed
) {
$this->fileUsage->delete($previous_logo_managed, 'navigation', 'logo', 1);
}
// Increment usage if different from the previous one.
if ($new_logo_managed && $new_logo_fid !== $previous_logo_fid) {
$new_logo_managed->setPermanent();
$new_logo_managed->save();
$this->fileUsage->add($new_logo_managed, 'navigation', 'logo', 1);
}
$config
->set('logo_provider', $form_state->getValue('logo_provider'))
->set('logo_managed', $form_state->getValue('logo_managed'))
->save();
parent::submitForm($form, $form_state);
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation\Menu;
use Drupal\Core\Menu\MenuLinkTree;
/**
* Extends MenuLinkTree to add specific theme suggestions for the navigation.
*
* @internal
*/
final class NavigationMenuLinkTree extends MenuLinkTree {
/**
* {@inheritdoc}
*/
public function build(array $tree): array {
if (!$tree) {
return [];
}
$build = parent::build($tree);
if (empty($build['#items'])) {
return [];
}
/** @var \Drupal\Core\Menu\MenuLinkInterface $link */
$first_link = reset($tree)->link;
// Get the menu name of the first link.
$menu_name = $first_link->getMenuName();
// Add a more specific theme suggestion to differentiate this rendered
// menu from others.
$build['#menu_name'] = $menu_name;
$build['#theme'] = 'navigation_menu__' . strtr($menu_name, '-', '_');
// Loop through menu items and add the plugin id as a class.
foreach ($tree as $item) {
if ($item->access->isAllowed()) {
$plugin_id = $item->link->getPluginId();
$plugin_class = str_replace('.', '_', $plugin_id);
$build['#items'][$plugin_id]['class'] = $plugin_class;
}
}
return $build;
}
}

View File

@@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
/**
* Build the menu links for the Content menu.
*
* The content menu contains a "Create" section, along with links to other
* overview pages for different entity types.
*
* @internal The navigation module is experimental.
*/
final class NavigationContentLinks implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* Construct a new NavigationContentLinks object.
*
* @param \Drupal\Core\Routing\RouteProviderInterface $routeProvider
* The route provider.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
*/
public function __construct(private RouteProviderInterface $routeProvider, private EntityTypeManagerInterface $entityTypeManager) {}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('router.route_provider'),
$container->get('entity_type.manager')
);
}
/**
* Add links to the Content menu, based on enabled modules.
*
* @param array $links
* The array of links being altered.
*/
public function addMenuLinks(array &$links): void {
// First, add the top-level menu items.
// @todo Consider turning this into a data object so we can avoid typos in
// array keys.
$content_links = [
'navigation.create' => [
'route_name' => 'node.add_page',
'title' => $this->t('Create'),
'weight' => -10,
],
'navigation.content' => [
'route_name' => 'view.content.page_1',
'title' => $this->t('Content'),
],
'navigation.files' => [
'route_name' => 'view.files.page_1',
'title' => $this->t('Files'),
],
'navigation.media' => [
'route_name' => 'view.media.media_page_list',
'title' => $this->t('Media'),
],
'navigation.blocks' => [
'route_name' => 'view.block_content.page_1',
'title' => $this->t('Blocks'),
],
];
foreach ($content_links as $link_name => $link) {
$this->addLink($link_name, $link, $links);
}
// Add supported add links under the Create button.
$this->addCreateEntityLinks('node_type', 'node.add', $links);
$this->addCreateEntityLinks('media_type', 'entity.media.add_form', $links, ['document', 'image']);
// Finally, add the bundleless User link and pin it to the bottom.
$this->addLink('navigation.create.user', [
'route_name' => 'user.admin_create',
'title' => $this->t('User'),
'parent' => 'navigation.create',
'weight' => 100,
], $links);
}
/**
* Remove the admin/content link, and any direct children.
*
* @param array $links
* The array of links being altered.
*/
public function removeAdminContentLink(array &$links): void {
unset($links['system.admin_content']);
// Also remove any links that have set admin/content as their parent link.
// They are unsupported by the Navigation module.
foreach ($links as $link_name => $link) {
if (isset($link['parent']) && $link['parent'] === 'system.admin_content') {
// @todo Do we need to make this recursive, and unset children of these
// links too?
unset($links[$link_name]);
}
}
}
/**
* Remove the help link as render it outside any menu.
*
* @param array $links
* The array of links being altered.
*/
public function removeHelpLink(array &$links): void {
unset($links['help.main']);
}
/**
* Add create links for an entity type.
*
* This function preserves the order of entity types as it is called.
*
* @param string $entity_type
* The entity type to add links for, such as node_type.
* @param string $add_route_id
* The ID of the route for the entity type add form.
* @param array $links
* The existing array of links to add to.
* @param array $bundle_allow_list
* A list of allowed bundles to include. Can be used to limit the list of
* bundles that are included for noisy entity types like media.
*/
private function addCreateEntityLinks(string $entity_type, string $add_route_id, array &$links, array $bundle_allow_list = []): void {
// Ensure subsequent calls always get added to the bottom, and not in
// alphabetical order.
static $weight = 0;
// The module providing the entity type is either not installed, or in the
// process of being uninstalled.
if (!$this->entityTypeManager->hasDefinition($entity_type)) {
return;
}
// Sort all types within an entity type alphabetically.
$definition = $this->entityTypeManager->getDefinition($entity_type);
$types = $this->entityTypeManager->getStorage($entity_type)->loadMultiple();
if (method_exists($definition->getClass(), 'sort')) {
uasort($types, [$definition->getClass(), 'sort']);
}
$add_content_links = [];
foreach ($types as $type) {
// Skip if the bundle is not in the allow list.
if (!empty($bundle_allow_list) && !in_array($type->id(), $bundle_allow_list)) {
continue;
}
$add_content_links['navigation.content.' . $type->getEntityTypeId() . '.' . $type->id()] = [
'title' => $type->label(),
'route_name' => $add_route_id,
'route_parameters' => [
$entity_type => $type->id(),
],
'parent' => 'navigation.create',
'weight' => $weight,
];
}
foreach ($add_content_links as $link_name => $link) {
$this->addLink($link_name, $link, $links);
}
$weight++;
}
/**
* Ensure a route exists and add the link.
*
* @param string $link_name
* The name of the link being added.
* @param array $link
* The link array, as defined in hook_menu_links_discovered_alter().
* @param array $links
* The existing array of links.
*/
private function addLink(string $link_name, array $link, array &$links): void {
try {
// Ensure the route exists (there is no separate "exists" method).
$this->routeProvider->getRouteByName($link['route_name']);
$links[$link_name] = $link + ['menu_name' => 'content', 'provider' => 'navigation'];
}
catch (RouteNotFoundException $e) {
// The module isn't installed, or the route (such as provided by a view)
// has been deleted.
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\Render\Element;
/**
* Defines a layout class for navigation.
*
* @internal
*/
final class NavigationLayout extends LayoutDefault {
/**
* {@inheritdoc}
*/
public function build(array $regions) {
foreach (Element::children($regions) as $region_id) {
foreach (Element::children($regions[$region_id]) as $component_uuid) {
if (!Element::isEmpty($regions[$region_id][$component_uuid])) {
$regions[$region_id][$component_uuid]['#theme'] = 'block__navigation';
}
}
}
return parent::build($regions);
}
}

View File

@@ -0,0 +1,358 @@
<?php
namespace Drupal\navigation;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Image\ImageFactory;
use Drupal\Core\Menu\LocalTaskManagerInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\file\Entity\File;
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Handle rendering for different pieces of the navigation.
*
* @internal The navigation module is experimental.
*/
final class NavigationRenderer {
/**
* Use the default Drupal logo in the navigation.
*/
const LOGO_PROVIDER_DEFAULT = 'default';
/**
* Hide the logo in the navigation.
*/
const LOGO_PROVIDER_HIDE = 'hide';
/**
* Use the custom provided logo in the navigation.
*/
const LOGO_PROVIDER_CUSTOM = 'custom';
/**
* A list of all the link paths of enabled content entities.
*
* @var array
*/
protected array $contentEntityPaths;
/**
* The navigation local tasks render array.
*
* @var array
*/
protected array $localTasks;
/**
* Construct a new NavigationRenderer object.
*/
public function __construct(
private ConfigFactoryInterface $configFactory,
private ModuleHandlerInterface $moduleHandler,
private RouteMatchInterface $routeMatch,
private LocalTaskManagerInterface $localTaskManager,
private EntityTypeManagerInterface $entityTypeManager,
private ImageFactory $imageFactory,
private FileUrlGeneratorInterface $fileUrlGenerator,
private SectionStorageManagerInterface $sectionStorageManager,
private RequestStack $requestStack,
private ModuleExtensionList $moduleExtensionList,
) {}
/**
* Remove the toolbar provided by Toolbar module.
*
* @param array $page_top
* A renderable array representing the top of the page.
*
* @see toolbar_page_top()
* @see hook_page_top()
*/
public function removeToolbar(array &$page_top): void {
if (isset($page_top['toolbar'])) {
unset($page_top['toolbar']);
}
}
/**
* Build out the navigation bar.
*
* @param array $page_top
* A renderable array representing the top of the page.
*
* @see toolbar_page_top()
* @see hook_page_top()
*/
public function buildNavigation(array &$page_top): void {
$logo_settings = $this->configFactory->get('navigation.settings');
$logo_provider = $logo_settings->get('logo_provider');
$cacheability = new CacheableMetadata();
$contexts = [
'navigation' => new Context(ContextDefinition::create('string'), 'navigation'),
];
$storage = $this->sectionStorageManager->findByContext($contexts, $cacheability);
$build = [];
if ($storage) {
foreach ($storage->getSections() as $delta => $section) {
$build[$delta] = $section->toRenderArray([]);
}
}
// The render array is built based on decisions made by SectionStorage
// plugins and therefore it needs to depend on the accumulated
// cacheability of those decisions.
$cacheability->addCacheableDependency($logo_settings)
->addCacheableDependency($this->configFactory->get('navigation.block_layout'));
$cacheability->applyTo($build);
$module_path = $this->requestStack->getCurrentRequest()->getBasePath() . '/' . $this->moduleExtensionList->getPath('navigation');
$asset_url = $module_path . '/assets/fonts/inter-var.woff2';
$defaults = [
'#hide_logo' => $logo_provider === self::LOGO_PROVIDER_HIDE,
'#attached' => [
'html_head_link' => [
[
[
'rel' => 'preload',
'href' => $asset_url,
'as' => 'font',
'crossorigin' => 'anonymous',
],
],
],
],
];
$build[0] = NestedArray::mergeDeepArray([$build[0], $defaults]);
$page_top['navigation'] = $build;
if ($logo_provider === self::LOGO_PROVIDER_CUSTOM) {
$logo_managed_fid = $logo_settings->get('logo_managed');
if (isset($logo_managed_fid[0]) && $logo_managed_fid[0] > 0) {
$logo_managed = File::load($logo_managed_fid[0]);
if ($logo_managed instanceof File) {
$logo_managed_uri = $logo_managed->getFileUri();
$logo_managed_url = $this->fileUrlGenerator->generateAbsoluteString($logo_managed_uri);
$page_top['navigation']['#logo_path'] = $logo_managed_url;
$image = $this->imageFactory->get($logo_managed_uri);
if ($image->isValid()) {
$page_top['navigation']['#logo_width'] = $image->getWidth();
$page_top['navigation']['#logo_height'] = $image->getHeight();
}
}
}
}
}
/**
* Build the top bar for content entity pages.
*
* @param array $page_top
* A renderable array representing the top of the page.
*
* @see navigation_page_top()
* @see hook_page_top()
*/
public function buildTopBar(array &$page_top): void {
if (!$this->moduleHandler->moduleExists('navigation_top_bar')) {
return;
}
$page_top['top_bar'] = [
'#theme' => 'top_bar',
'#attached' => [
'library' => [
'navigation/internal.navigation',
],
],
'#cache' => [
'contexts' => [
'url.path',
'user.permissions',
],
],
];
// Local tasks for content entities.
if ($this->hasLocalTasks()) {
$local_tasks = $this->getLocalTasks();
$page_top['top_bar']['#local_tasks'] = [
'#theme' => 'top_bar_local_tasks',
'#local_tasks' => $local_tasks['tasks'],
];
assert($local_tasks['cacheability'] instanceof CacheableMetadata);
CacheableMetadata::createFromRenderArray($page_top['top_bar'])
->addCacheableDependency($local_tasks['cacheability'])
->applyTo($page_top['top_bar']);
}
}
/**
* Alter the build of any local_tasks_block plugin block.
*
* If we are showing the local tasks in the top bar, hide the local tasks
* from display to avoid duplicating the links.
*
* @param array $build
* A renderable array representing the local_tasks_block plugin block to be
* rendered.
* @param \Drupal\Core\Block\BlockPluginInterface $block
* Block plugin object representing a local_tasks_block.
*
* @see navigation_block_build_local_tasks_block_alter()
*/
public function removeLocalTasks(array &$build, BlockPluginInterface $block): void {
if ($block->getPluginId() !== 'local_tasks_block') {
return;
}
if ($this->hasLocalTasks() && $this->moduleHandler->moduleExists('navigation_top_bar')) {
$build['#access'] = FALSE;
}
}
/**
* Local tasks list based on user access.
*
* @return array
* Local tasks keyed by route name.
*/
private function getLocalTasks(): array {
if (isset($this->localTasks)) {
return $this->localTasks;
}
$cacheability = new CacheableMetadata();
$cacheability->addCacheableDependency($this->localTaskManager);
$this->localTasks = [
'tasks' => [],
'cacheability' => $cacheability,
];
// For now, we're only interested in local tasks corresponding to a content
// entity.
if (!$this->meetsContentEntityRoutesCondition()) {
return $this->localTasks;
}
$entity_local_tasks = $this->localTaskManager->getLocalTasks($this->routeMatch->getRouteName());
foreach ($entity_local_tasks['tabs'] as $route_name => $local_task) {
// The $local_task array that we get here is tailor-made for use
// with the menu-local-tasks.html.twig, eg. the menu_local_task
// theme hook. It has all the information we need, but we're not
// rendering local tasks, or tabs, we're rendering a simple list of
// links. Here we're taking advantage of all the good stuff found in
// the render array, namely the #link, and #access properties, using
// them to render a simple link.
// @see \Drupal\Core\Menu\LocalTaskManager::getTasksBuild()
$link = $local_task['#link'];
$link['localized_options'] += [
'set_active_class' => TRUE,
];
$this->localTasks['tasks'][$route_name] = [
'#theme' => 'top_bar_local_task',
'#link' => [
'#type' => 'link',
'#title' => $link['title'],
'#url' => $link['url'],
'#options' => $link['localized_options'],
],
'#access' => $local_task['#access'],
];
}
$this->localTasks['cacheability'] = $cacheability->merge($entity_local_tasks['cacheability']);
return $this->localTasks;
}
/**
* Do we have local tasks that we want to show in the top bar?
*
* @return bool
* TRUE if there are local tasks available for the top bar, FALSE otherwise.
*/
private function hasLocalTasks(): bool {
$local_tasks = $this->getLocalTasks();
return !empty($local_tasks['tasks']);
}
/**
* Determines if content entity route condition is met.
*
* @return bool
* TRUE if the content entity route condition is met, FALSE otherwise.
*/
protected function meetsContentEntityRoutesCondition(): bool {
return array_key_exists($this->routeMatch->getRouteObject()->getPath(), $this->getContentEntityPaths());
}
/**
* Returns the paths for the link templates of all content entities.
*
* @return array
* An array of all content entity type IDs, keyed by the corresponding link
* template paths.
*/
protected function getContentEntityPaths(): array {
if (isset($this->contentEntityPaths)) {
return $this->contentEntityPaths;
}
$this->contentEntityPaths = [];
$entity_types = $this->entityTypeManager->getDefinitions();
foreach ($entity_types as $entity_type) {
if ($entity_type->entityClassImplements(ContentEntityInterface::class)) {
$entity_paths = $this->getContentEntityTypePaths($entity_type);
$this->contentEntityPaths = array_merge($this->contentEntityPaths, $entity_paths);
}
}
return $this->contentEntityPaths;
}
/**
* Returns the path for the link template for a given content entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
*
* @return array
* Array containing the paths for the given content entity type.
*/
protected function getContentEntityTypePaths(EntityTypeInterface $entity_type): array {
$paths = array_filter($entity_type->getLinkTemplates(), fn ($template) => $template !== 'collection', ARRAY_FILTER_USE_KEY);
if ($this->isLayoutBuilderEntityType($entity_type)) {
$paths[] = $entity_type->getLinkTemplate('canonical') . '/layout';
}
return array_fill_keys($paths, $entity_type->id());
}
/**
* Determines if a given entity type is layout builder relevant or not.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return bool
* Whether this entity type is a Layout builder candidate or not
*
* @see \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage::getEntityTypes()
*/
protected function isLayoutBuilderEntityType(EntityTypeInterface $entity_type): bool {
return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasHandlerClass('form', 'layout_builder') && $entity_type->hasViewBuilderClass() && $entity_type->hasLinkTemplate('canonical');
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
use Symfony\Component\DependencyInjection\Reference;
/**
* Defines a service provider for the Navigation module.
*
* @internal
*/
final class NavigationServiceProvider implements ServiceProviderInterface {
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container): void {
// If shortcuts module service is available, register our own service.
if ($container->has('shortcut.lazy_builders')) {
$container
->register('navigation.shortcut_lazy_builder', ShortcutLazyBuilder::class)
->addArgument(new Reference('shortcut.lazy_builders'));
}
}
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\navigation\Plugin\Derivative\SystemMenuNavigationBlock as SystemMenuNavigationBlockDeriver;
use Drupal\system\Plugin\Block\SystemMenuBlock;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a generic menu navigation block.
*
* @internal
*/
#[Block(
id: "navigation_menu",
admin_label: new TranslatableMarkup("Navigation menu"),
category: new TranslatableMarkup("Menus (Navigation)"),
deriver: SystemMenuNavigationBlockDeriver::class,
)]
final class NavigationMenuBlock extends SystemMenuBlock implements ContainerFactoryPluginInterface {
const NAVIGATION_MAX_DEPTH = 3;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('navigation.menu_tree'),
$container->get('menu.active_trail'),
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration(): array {
return [
'level' => 1,
'depth' => 0,
];
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state): array {
$form = parent::blockForm($form, $form_state);
unset($form['menu_levels']['expand_all_items']);
$form['menu_levels']['depth']['#options'] = range(1, static::NAVIGATION_MAX_DEPTH);
return $form;
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state): void {
$this->configuration['level'] = $form_state->getValue('level');
$this->configuration['depth'] = $form_state->getValue('depth');
}
/**
* {@inheritdoc}
*/
public function build(): array {
$menu_name = $this->getDerivativeId();
$level = $this->configuration['level'];
$depth = $this->configuration['depth'];
$parameters = new MenuTreeParameters();
$parameters
->setMinDepth($level)
->setMaxDepth(min($level + $depth, $this->menuTree->maxDepth()))
->onlyEnabledLinks();
$tree = $this->menuTree->load($menu_name, $parameters);
$manipulators = [
['callable' => 'menu.default_tree_manipulators:checkAccess'],
['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
];
$tree = $this->menuTree->transform($tree, $manipulators);
$build = $this->menuTree->build($tree);
if (!empty($build)) {
$build['#title'] = $this->configuration['label'];
}
return $build;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
return [
'module' => [
'system',
],
];
}
/**
* {@inheritdoc}
*/
public function getCacheContexts(): array {
// We don't use menu active trails here.
return array_filter(parent::getCacheContexts(), static fn (string $tag) => !str_starts_with($tag, 'route.menu_active_trails'));
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation\Plugin\Block;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a shortcuts navigation block class.
*
* @internal
*
* @todo Move to Shortcut module as part of the core MR process.
*/
#[Block(
id: 'navigation_shortcuts',
admin_label: new TranslatableMarkup('Navigation Shortcuts'),
)]
final class NavigationShortcutsBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* Constructs a new ShortcutsNavigationBlock.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module handler service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, protected ModuleHandlerInterface $moduleHandler) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('module_handler')
);
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account): AccessResultInterface {
return AccessResult::allowedIfHasPermission($account, 'access shortcuts');
}
/**
* {@inheritdoc}
*/
public function build(): array {
// This navigation block requires shortcut module. Once the plugin is moved
// to the module, this should not be necessary.
if (!$this->moduleHandler->moduleExists('shortcut')) {
return [];
}
return [
'shortcuts' => [
// @phpstan-ignore-next-line
'#lazy_builder' => ['navigation.shortcut_lazy_builder:lazyLinks', [$this->configuration['label']]],
'#create_placeholder' => TRUE,
'#cache' => [
'keys' => ['shortcut_set_navigation_links'],
'contexts' => ['user'],
],
'#lazy_builder_preview' => [
'#markup' => '<a href="#" class="toolbar-tray-lazy-placeholder-link">&nbsp;</a>',
],
],
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a user navigation block.
*
* @internal
*/
#[Block(
id: 'navigation_user',
admin_label: new TranslatableMarkup('User'),
)]
final class NavigationUserBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build(): array {
return [
'user' => [
'#lazy_builder' => [
'navigation.user_lazy_builder:renderNavigationLinks',
[],
],
'#create_placeholder' => TRUE,
'#cache' => [
'keys' => ['user_set_navigation_links'],
'contexts' => ['user'],
],
'#lazy_builder_preview' => [
'#markup' => '<a href="#" class="toolbar-tray-lazy-placeholder-link">&nbsp;</a>',
],
],
];
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides navigation block plugin definitions for custom menus.
*
* @internal
* @see \Drupal\navigation\Plugin\Block\NavigationMenuBlock
*/
final class SystemMenuNavigationBlock extends DeriverBase implements ContainerDeriverInterface {
/**
* Constructs new SystemMenuNavigationBlock.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $menuStorage
* The menu storage.
*/
public function __construct(protected EntityStorageInterface $menuStorage) {}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id): static {
return new static(
$container->get('entity_type.manager')->getStorage('menu')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition): array {
foreach ($this->menuStorage->loadMultiple() as $menu => $entity) {
$this->derivatives[$menu] = $base_plugin_definition;
$this->derivatives[$menu]['admin_label'] = $entity->label();
$this->derivatives[$menu]['config_dependencies']['config'] = [$entity->getConfigDependencyName()];
}
return $this->derivatives;
}
}

View File

@@ -0,0 +1,215 @@
<?php
namespace Drupal\navigation\Plugin\SectionStorage;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\ContextAwarePluginTrait;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\layout_builder\Attribute\SectionStorage;
use Drupal\layout_builder\Plugin\SectionStorage\SectionStorageLocalTaskProviderInterface;
use Drupal\layout_builder\Routing\LayoutBuilderRoutesTrait;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionListTrait;
use Drupal\layout_builder\SectionStorageInterface;
use Drupal\navigation\Form\LayoutForm;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\RouteCollection;
/**
* Provides navigation section storage.
*
* @internal The navigation module is experimental.
*/
#[SectionStorage(id: "navigation",
context_definitions: [
"navigation" => new ContextDefinition(
data_type: "string",
label: new TranslatableMarkup("Navigation flag"),
),
],
handles_permission_check: TRUE,
)]
final class NavigationSectionStorage extends PluginBase implements SectionStorageInterface, SectionStorageLocalTaskProviderInterface, ContainerFactoryPluginInterface, CacheableDependencyInterface {
const STORAGE_ID = 'navigation.block_layout';
use ContextAwarePluginTrait;
use LayoutBuilderRoutesTrait;
use SectionListTrait;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected ConfigFactoryInterface $configFactory;
/**
* An array of sections.
*
* @var \Drupal\layout_builder\Section[]|null
*/
protected $sections;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $config_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('config.factory')
);
}
/**
* {@inheritdoc}
*/
public function getStorageType(): string {
return $this->getPluginId();
}
/**
* {@inheritdoc}
*/
public function getStorageId(): string {
return self::STORAGE_ID;
}
/**
* {@inheritdoc}
*/
public function label(): string {
return 'Navigation layout';
}
/**
* Returns the name to be used to store in the config system.
*/
protected function getConfigName(): string {
return self::STORAGE_ID;
}
/**
* {@inheritdoc}
*/
public function getSections(): array {
if (is_null($this->sections)) {
$sections = $this->configFactory->get($this->getConfigName())->get('sections') ?: [];
$this->setSections(array_map([Section::class, 'fromArray'], $sections));
}
return $this->sections;
}
/**
* {@inheritdoc}
*/
protected function setSections(array $sections): static {
$this->sections = array_values($sections);
return $this;
}
/**
* {@inheritdoc}
*/
public function save(): int {
$sections = array_map(function (Section $section) {
return $section->toArray();
}, $this->getSections());
$config = $this->configFactory->getEditable($this->getConfigName());
$return = $config->get('sections') ? SAVED_UPDATED : SAVED_NEW;
$config->set('sections', $sections)->save();
return $return;
}
/**
* {@inheritdoc}
*/
public function buildRoutes(RouteCollection $collection): void {
$this->buildLayoutRoutes($collection, $this->getPluginDefinition(), '/admin/config/user-interface/navigation-block');
$default_route = 'layout_builder.' . $this->getPluginDefinition()->id() . '.view';
$route = $collection->get($default_route);
// Use a form for editing the layout instead of a controller.
$defaults = $route->getDefaults();
$defaults['_form'] = LayoutForm::class;
unset($defaults['_controller']);
$route->setDefaults($defaults);
}
/**
* {@inheritdoc}
*/
public function deriveContextsFromRoute($value, $definition, $name, array $defaults): array {
return ['navigation' => new Context(new ContextDefinition('string'), 'navigation')];
}
/**
* {@inheritdoc}
*/
public function buildLocalTasks($base_plugin_definition): array {
return [];
}
/**
* {@inheritdoc}
*/
public function getLayoutBuilderUrl($rel = 'view'): Url {
return Url::fromRoute("layout_builder.{$this->getStorageType()}.$rel", ['id' => $this->getStorageId()]);
}
/**
* {@inheritdoc}
*/
public function getRedirectUrl(): Url {
return $this->getLayoutBuilderUrl();
}
/**
* {@inheritdoc}
*/
public function access($operation, ?AccountInterface $account = NULL, $return_as_object = FALSE): AccessResultInterface | bool {
$result = AccessResult::allowedIfHasPermission($account, 'configure navigation layout');
return $return_as_object ? $result : $result->isAllowed();
}
/**
* {@inheritdoc}
*/
public function getContextsDuringPreview(): array {
return $this->getContexts();
}
/**
* {@inheritdoc}
*/
public function isApplicable(RefinableCacheableDependencyInterface $cacheability): bool {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getContextMapping(): array {
return ['navigation' => 'navigation'];
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation;
use Drupal\Core\Security\Attribute\TrustedCallback;
use Drupal\navigation\Plugin\SectionStorage\NavigationSectionStorage;
/**
* Defines a class for render element callbacks.
*
* @internal
*/
final class RenderCallbacks {
/**
* Pre-render callback for layout builder.
*/
#[TrustedCallback]
public static function alterLayoutBuilder(array $element): array {
if (($element['#section_storage'] ?? NULL) instanceof NavigationSectionStorage) {
// Remove add section links that exist before and after the existing
// section.
unset($element['layout_builder'][0], $element['layout_builder'][2]);
// Remove add block link from the footer section and the remove and
// configure buttons from the existing section.
unset(
$element['layout_builder'][1]['remove'],
$element['layout_builder'][1]['configure'],
$element['layout_builder'][1]['layout-builder__section']['footer']['layout_builder_add_block'],
);
}
return $element;
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\shortcut\ShortcutLazyBuilders;
/**
* Lazy Builders for Navigation shortcuts links.
*
* @internal The navigation module is experimental.
* @see \Drupal\shortcut\ShortcutLazyBuilders
*/
final class ShortcutLazyBuilder implements TrustedCallbackInterface {
/**
* Constructs a ShortcutLazyBuilders object.
*
* @param \Drupal\shortcut\ShortcutLazyBuilders $shortcutLazyBuilder
* The original shortcuts lazy builder service.
*/
public function __construct(
protected readonly ShortcutLazyBuilders $shortcutLazyBuilder,
) {}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['lazyLinks'];
}
/**
* The #lazy_builder callback; builds shortcut navigation links.
*
* @param string $label
* (Optional) The links label. Defaults to "Shortcuts".
*
* @return array
* A renderable array of shortcut links.
*/
public function lazyLinks(string $label = 'Shortcuts') {
$shortcut_links = $this->shortcutLazyBuilder->lazyLinks();
if (empty($shortcut_links['shortcuts']['#links'])) {
return [
'#cache' => $shortcut_links['#cache'],
];
}
$shortcuts_items = [
[
'title' => $label,
'class' => 'shortcuts',
'below' => $shortcut_links['shortcuts']['#links'],
],
];
return [
'#title' => $label,
'#theme' => 'navigation_menu',
'#menu_name' => 'shortcuts',
'#items' => $shortcuts_items,
'#cache' => $shortcut_links['#cache'],
];
}
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
/**
* User navigation block lazy builder.
*
* @internal The navigation module is experimental.
*/
final class UserLazyBuilder implements TrustedCallbackInterface {
use StringTranslationTrait;
/**
* Constructs an UserLazyBuilder object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module handler.
* @param \Drupal\Core\Session\AccountProxyInterface $account
* The current user.
*/
public function __construct(
protected readonly ModuleHandlerInterface $moduleHandler,
protected readonly AccountProxyInterface $account,
) {}
/**
* Lazy builder callback for rendering navigation links.
*
* @return array
* A renderable array as expected by the renderer service.
*/
public function renderNavigationLinks() {
return [
'#help' => $this->moduleHandler->moduleExists('help'),
'#theme' => 'menu_region__footer',
'#items' => $this->userOperationLinks(),
'#menu_name' => 'user',
'#title' => $this->account->getDisplayName(),
'#cache' => [
'contexts' => [
'user',
],
],
];
}
/**
* Returns the user operation links in navigation expected format.
*
* @param bool $include_edit
* (Optional) Whether to include the edit account link or not.
*
* @return array
* List of operation links for the current user.
*/
public function userOperationLinks(bool $include_edit = TRUE): array {
$links = [
'account' => [
'title' => $this->t('View profile'),
'url' => Url::fromRoute('user.page'),
'attributes' => [
'title' => $this->t('User account'),
],
],
'account_edit' => [
'title' => $this->t('Edit profile'),
'url' => Url::fromRoute('entity.user.edit_form', ['user' => $this->account->id()]),
'attributes' => [
'title' => $this->t('Edit user account'),
],
],
'logout' => [
'title' => $this->t('Log out'),
'url' => Url::fromRoute('user.logout'),
],
];
if (!$include_edit) {
unset($links['account_edit']);
}
return $links;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['renderNavigationLinks'];
}
}