diff --git a/core/core.services.yml b/core/core.services.yml index 56142b9..9fafd95 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -421,6 +421,8 @@ services: class: Drupal\system\Plugin\ImageToolkitInterface factory_method: getDefaultToolkit factory_service: image.toolkit.manager + breadcrumb: + class: Drupal\Core\Breadcrumb\BreadcrumbManager token: class: Drupal\Core\Utility\Token arguments: ['@module_handler'] diff --git a/core/includes/common.inc b/core/includes/common.inc index 4f72021..2c2bb87 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -283,6 +283,11 @@ function drupal_get_profile() { * @param $breadcrumb * Array of links, starting with "home" and proceeding up to but not including * the current page. + * + * @deprecated This will be removed in 8.0. Instead, register a new breadcrumb + * builder service. + * + * @see Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface */ function drupal_set_breadcrumb($breadcrumb = NULL) { $stored_breadcrumb = &drupal_static(__FUNCTION__); @@ -295,6 +300,11 @@ function drupal_set_breadcrumb($breadcrumb = NULL) { /** * Gets the breadcrumb trail for the current page. + * + * @deprecated This will be removed in 8.0. Instead, register a new breadcrumb + * builder service. + * + * @see Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface */ function drupal_get_breadcrumb() { $breadcrumb = drupal_set_breadcrumb(); diff --git a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php new file mode 100644 index 0000000..8dbd415 --- /dev/null +++ b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php @@ -0,0 +1,28 @@ +builders[$priority][] = $builder; + } + + /** + * {@inheritdoc} + */ + public function build(array $attributes) { + // Call the build method of registered breadcrumb builders, + // until one of them returns something other than NULL. + foreach ($this->getSortedBuilders() as $builder) { + $breadcrumb = $builder->build($attributes); + if (!isset($breadcrumb)) { + // The builder returned NULL, so we continue with the other builders. + continue; + } + elseif (is_array($breadcrumb)) { + // The builder returned an array of breadcrumb links. + return $breadcrumb; + } + elseif (FALSE === $breadcrumb) { + // A value of FALSE indicates that no breadcrumb should be displayed. + // BreadcrumbBuilderInterface dictates to return array() in this case. + return array(); + } + else { + $class = get_class($builder); + // TODO: Dedicated exception class. + throw new \Exception("Invalid breadcrumb returned by $class::build()."); + } + } + + // Fall back to an empty breadcrumb. + return array(); + } + + /** + * Returns the sorted array of breadcrumb builders. + * + * @return array + * An array of breadcrumb builder objects. + */ + protected function getSortedBuilders() { + if (!isset($this->sortedBuilders)) { + // Sort the builders according to priority. + krsort($this->builders); + // Merge the nested $this->builders array into $this->sortedBuilders. + $this->sortedBuilders = array(); + foreach ($this->builders as $builders) { + $this->sortedBuilders = array_merge($this->sortedBuilders, $builders); + } + } + return $this->sortedBuilders; + } + +} diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index d29da83..5d5ccb2 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -16,6 +16,7 @@ use Drupal\Core\DependencyInjection\Compiler\RegisterRouteEnhancersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterParamConvertersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterServicesForDestructionPass; +use Drupal\Core\DependencyInjection\Compiler\RegisterBreadcrumbBuilderPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; @@ -60,6 +61,9 @@ public function build(ContainerBuilder $container) { // Add the compiler pass that will process the tagged services. $container->addCompilerPass(new RegisterPathProcessorsPass()); $container->addCompilerPass(new ListCacheBinsPass()); + // Add the compiler pass that will process the tagged breadcrumb builder + // services. + $container->addCompilerPass(new RegisterBreadcrumbBuilderPass()); } /** diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterBreadcrumbBuilderPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterBreadcrumbBuilderPass.php new file mode 100644 index 0000000..2e26ed6 --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterBreadcrumbBuilderPass.php @@ -0,0 +1,30 @@ +hasDefinition('breadcrumb')) { + return; + } + $manager = $container->getDefinition('breadcrumb'); + foreach ($container->findTaggedServiceIds('breadcrumb_builder') as $id => $attributes) { + $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; + $manager->addMethodCall('addBuilder', array(new Reference($id), $priority)); + } + } + +} diff --git a/core/modules/forum/forum.services.yml b/core/modules/forum/forum.services.yml new file mode 100644 index 0000000..19033e8 --- /dev/null +++ b/core/modules/forum/forum.services.yml @@ -0,0 +1,6 @@ +services: + forum.breadcrumb: + class: Drupal\forum\ForumBreadcrumbBuilder + arguments: ['@plugin.manager.entity', '@config.factory'] + tags: + - { name: breadcrumb_builder, priority: 1001 } diff --git a/core/modules/forum/lib/Drupal/forum/ForumBreadcrumbBuilder.php b/core/modules/forum/lib/Drupal/forum/ForumBreadcrumbBuilder.php new file mode 100644 index 0000000..ee5267d --- /dev/null +++ b/core/modules/forum/lib/Drupal/forum/ForumBreadcrumbBuilder.php @@ -0,0 +1,119 @@ +entityManager = $entity_manager; + $this->config = $configFactory->get('forum.settings'); + } + + /** + * {@inheritdoc} + */ + public function build(array $attributes) { + + // @todo This only works for legacy routes. Once node/% and forum/% are + // converted to the new router this code will need to be updated. + if (isset($attributes['drupal_menu_item'])) { + $item = $attributes['drupal_menu_item']; + switch ($item['path']) { + + case 'node/%': + $node = $item['map'][1]; + // Load the object in case of missing wildcard loaders. + $node = is_object($node) ? $node : node_load($node); + if (_forum_node_check_node_type($node)) { + $breadcrumb = $this->forumPostBreadcrumb($node); + } + break; + + case 'forum/%': + $term = $item['map'][1]; + // Load the object in case of missing wildcard loaders. + $term = is_object($term) ? $term : forum_forum_load($term); + $breadcrumb = $this->forumTermBreadcrumb($term); + break; + } + } + + if (!empty($breadcrumb)) { + return $breadcrumb; + } + } + + /** + * Builds the breadcrumb for a forum post page. + */ + protected function forumPostBreadcrumb($node) { + $vocabularies = $this->entityManager->getStorageController('taxonomy_vocabulary')->load(array($this->config->get('vocabulary'))); + $vocabulary = current($vocabularies); + + $breadcrumb[] = l(t('Home'), NULL); + $breadcrumb[] = l($vocabulary->name, 'forum'); + if ($parents = taxonomy_term_load_parents_all($node->forum_tid)) { + $parents = array_reverse($parents); + foreach ($parents as $parent) { + $breadcrumb[] = l($parent->label(), 'forum/' . $parent->tid); + } + } + return $breadcrumb; + } + + /** + * Builds the breadcrumb for a forum term page. + */ + protected function forumTermBreadcrumb($term) { + $vocabularies = $this->entityManager->getStorageController('taxonomy_vocabulary')->load(array($this->config->get('vocabulary'))); + $vocabulary = current($vocabularies); + + $breadcrumb[] = l(t('Home'), NULL); + if ($term->tid) { + // Parent of all forums is the vocabulary name. + $breadcrumb[] = l($vocabulary->label(), 'forum'); + } + // Add all parent forums to breadcrumbs. + if ($term->parents) { + foreach (array_reverse($term->parents) as $parent) { + if ($parent->id() != $term->tid) { + $breadcrumb[] = l($parent->label(), 'forum/' . $parent->id()); + } + } + } + return $breadcrumb; + } + +} diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkBreadcrumbBuilder.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkBreadcrumbBuilder.php new file mode 100644 index 0000000..a56856a --- /dev/null +++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkBreadcrumbBuilder.php @@ -0,0 +1,34 @@ +build($request->attributes->all()); + if (!empty($breadcrumb)) { + // $breadcrumb is expected to be an array of rendered breadcrumb links. + return array( + '#theme' => 'breadcrumb', + '#breadcrumb' => $breadcrumb, + ); + } + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuTestBase.php index eb7fbaf..cc8ca91 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuTestBase.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuTestBase.php @@ -34,13 +34,30 @@ protected function assertBreadcrumb($goto, array $trail, $page_title = NULL, arr if (isset($goto)) { $this->drupalGet($goto); } + $this->assertBreadcrumbParts($trail); + + // Additionally assert page title, if given. + if (isset($page_title)) { + $this->assertTitle(strtr('@title | Drupal', array('@title' => $page_title))); + } + + // Additionally assert active trail in a menu tree output, if given. + if ($tree) { + $this->assertMenuActiveTrail($tree, $last_active); + } + } + + protected function assertBreadcrumbParts($trail) { // Compare paths with actual breadcrumb. - $parts = $this->getParts(); + $parts = $this->getBreadcrumbParts(); $pass = TRUE; - foreach ($trail as $path => $title) { - $url = url($path); - $part = array_shift($parts); - $pass = ($pass && $part['href'] === $url && $part['text'] === check_plain($title)); + // There may be more than one breadcrumb on the page. + while (!empty($parts)) { + foreach ($trail as $path => $title) { + $url = url($path); + $part = array_shift($parts); + $pass = ($pass && $part['href'] === $url && $part['text'] === check_plain($title)); + } } // No parts must be left, or an expected "Home" will always pass. $pass = ($pass && empty($parts)); @@ -49,14 +66,11 @@ protected function assertBreadcrumb($goto, array $trail, $page_title = NULL, arr '%parts' => implode(' ยป ', $trail), '@path' => $this->getUrl(), ))); + } - // Additionally assert page title, if given. - if (isset($page_title)) { - $this->assertTitle(strtr('@title | Drupal', array('@title' => $page_title))); - } - - // Additionally assert active trail in a menu tree output, if given. - if ($tree) { + protected function assertMenuActiveTrail($tree, $last_active) { + // Indentation to reduce git diff. + if (TRUE) { end($tree); $active_link_path = key($tree); $active_link_title = array_pop($tree); @@ -102,7 +116,7 @@ protected function assertBreadcrumb($goto, array $trail, $page_title = NULL, arr /** * Returns the breadcrumb contents of the current page in the internal browser. */ - protected function getParts() { + protected function getBreadcrumbParts() { $parts = array(); $elements = $this->xpath('//nav[@class="breadcrumb"]/ol/li/a'); if (!empty($elements)) { diff --git a/core/modules/system/system.services.yml b/core/modules/system/system.services.yml index c049b0a..7758db9 100644 --- a/core/modules/system/system.services.yml +++ b/core/modules/system/system.services.yml @@ -6,3 +6,7 @@ services: plugin.manager.system.plugin_ui: class: Drupal\system\Plugin\Type\PluginUIManager arguments: ['@container.namespaces'] + system.breadcrumb.legacy: + class: Drupal\system\LegacyBreadcrumbBuilder + tags: + - {name: breadcrumb_builder, priority: 500} diff --git a/core/profiles/standard/config/block.block.bartik.breadcrumbs.yml b/core/profiles/standard/config/block.block.bartik.breadcrumbs.yml new file mode 100644 index 0000000..268782a --- /dev/null +++ b/core/profiles/standard/config/block.block.bartik.breadcrumbs.yml @@ -0,0 +1,22 @@ +id: bartik.breadcrumbs +weight: '-5' +status: '1' +langcode: en +region: content +plugin: system_breadcrumb_block +settings: + label: Breadcrumbs + module: system + label_display: '0' + cache: '-1' +visibility: + path: + visibility: '0' + pages: '' + role: + roles: { } + node_type: + types: + article: '0' + page: '0' + visibility__active_tab: edit-visibility-path diff --git a/core/profiles/standard/config/block.block.seven.breadcrumbs.yml b/core/profiles/standard/config/block.block.seven.breadcrumbs.yml new file mode 100644 index 0000000..2520d77 --- /dev/null +++ b/core/profiles/standard/config/block.block.seven.breadcrumbs.yml @@ -0,0 +1,22 @@ +id: seven.breadcrumbs +weight: '-2' +status: '1' +langcode: en +region: content +plugin: system_breadcrumb_block +settings: + label: Breadcrumbs + module: system + label_display: '0' + cache: '-1' +visibility: + path: + visibility: '0' + pages: '' + role: + roles: { } + node_type: + types: + article: '0' + page: '0' + visibility__active_tab: edit-visibility-path