diff --git a/core/core.services.yml b/core/core.services.yml index 55d5b34..bb7b59e 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 84cfa94..ca70a3c 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -286,6 +286,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__); @@ -297,19 +302,6 @@ function drupal_set_breadcrumb($breadcrumb = NULL) { } /** - * Gets the breadcrumb trail for the current page. - */ -function drupal_get_breadcrumb() { - $breadcrumb = drupal_set_breadcrumb(); - - if (!isset($breadcrumb)) { - $breadcrumb = menu_get_active_breadcrumb(); - } - - return $breadcrumb; -} - -/** * Adds output to the HEAD tag of the HTML page. * * This function can be called as long as the headers aren't sent. Pass no diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 011db8d..317a72f 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -2887,7 +2887,7 @@ function template_process_page(&$variables) { // @see menu_tree_page_data() $variables['breadcrumb'] = array( '#theme' => 'breadcrumb', - '#breadcrumb' => drupal_get_breadcrumb(), + '#breadcrumb' => \Drupal::service('breadcrumb')->build(\Drupal::service('request')->attributes->all()), ); } if (!isset($variables['title'])) { diff --git a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php new file mode 100644 index 0000000..aa9f8cb --- /dev/null +++ b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php @@ -0,0 +1,26 @@ +builders[$priority][] = $builder; + } + + /** + * {@inheritdoc} + */ + public function build(array $attributes) { + // Call the build method of registered breadcrumb builders, + // until one of them returns array. + 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; + } + else { + throw new \UnexpectedValueException(format_string('Invalid breadcrumb returned by !class::build().', array('!class' => get_class($builder)))); + } + } + + // 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..1119989 --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterBreadcrumbBuilderPass.php @@ -0,0 +1,33 @@ +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.module b/core/modules/forum/forum.module index 42d7326..4e722ac 100644 --- a/core/modules/forum/forum.module +++ b/core/modules/forum/forum.module @@ -262,29 +262,6 @@ function _forum_node_check_node_type(EntityInterface $node) { } /** - * Implements hook_node_view(). - */ -function forum_node_view(EntityInterface $node, EntityDisplay $display, $view_mode) { - $vid = config('forum.settings')->get('vocabulary'); - $vocabulary = taxonomy_vocabulary_load($vid); - if (_forum_node_check_node_type($node)) { - if ($view_mode == 'full' && node_is_page($node)) { - // Breadcrumb navigation - $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->id()); - } - } - drupal_set_breadcrumb($breadcrumb); - - } - } -} - -/** * Implements hook_node_validate(). * * Checks in particular that the node is assigned only a "leaf" term in the diff --git a/core/modules/forum/forum.pages.inc b/core/modules/forum/forum.pages.inc index 422d92e..7b7d02d 100644 --- a/core/modules/forum/forum.pages.inc +++ b/core/modules/forum/forum.pages.inc @@ -28,22 +28,6 @@ function forum_page($forum_term = NULL) { drupal_set_title($vocabulary->label()); } - // Breadcrumb navigation. - $breadcrumb[] = l(t('Home'), NULL); - if ($forum_term->id()) { - // Parent of all forums is the vocabulary name. - $breadcrumb[] = l($vocabulary->label(), 'forum'); - } - // Add all parent forums to breadcrumbs. - if ($forum_term->parents) { - foreach (array_reverse($forum_term->parents) as $parent) { - if ($parent->id() != $forum_term->id()) { - $breadcrumb[] = l($parent->label(), 'forum/' . $parent->id()); - } - } - } - drupal_set_breadcrumb($breadcrumb); - if ($forum_term->id() && array_search($forum_term->id(), $config->get('containers')) === FALSE) { // Add RSS feed for forums. drupal_add_feed('taxonomy/term/' . $forum_term->id() . '/feed', 'RSS - ' . $forum_term->label()); 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..3bbb0df --- /dev/null +++ b/core/modules/forum/lib/Drupal/forum/ForumBreadcrumbBuilder.php @@ -0,0 +1,121 @@ +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 = reset($vocabularies); + + $breadcrumb[] = l(t('Home'), NULL); + $breadcrumb[] = l($vocabulary->label(), '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->id()); + } + } + 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->id()) { + $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..acece08 --- /dev/null +++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkBreadcrumbBuilder.php @@ -0,0 +1,26 @@ +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..7d23727 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuTestBase.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuTestBase.php @@ -10,6 +10,7 @@ use Drupal\simpletest\WebTestBase; abstract class MenuTestBase extends WebTestBase { + /** * Assert that a given path shows certain breadcrumb links. * @@ -34,13 +35,37 @@ 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); + } + } + + /** + * Assert that a trail exists in the internal browser. + * + * @param array $trail + * An associative array whose keys are expected breadcrumb link paths and + * whose values are expected breadcrumb link texts (not sanitized). + */ + 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,60 +74,65 @@ 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. + /** + * Assert that active trail exists in a menu tree output. + * + * @param array $tree + * An associative array whose keys are link paths and whose + * values are link titles (not sanitized) of an expected active trail in a + * menu tree output on the page. + * @param bool $last_active + * Whether the last link in $tree is expected to be active (TRUE) + * or just to be in the active trail (FALSE). + */ + protected function assertMenuActiveTrail($tree, $last_active) { + end($tree); + $active_link_path = key($tree); + $active_link_title = array_pop($tree); + $xpath = ''; if ($tree) { - end($tree); - $active_link_path = key($tree); - $active_link_title = array_pop($tree); - $xpath = ''; - if ($tree) { - $i = 0; - foreach ($tree as $link_path => $link_title) { - $part_xpath = (!$i ? '//' : '/following-sibling::ul/descendant::'); - $part_xpath .= 'li[contains(@class, :class)]/a[contains(@href, :href) and contains(text(), :title)]'; - $part_args = array( - ':class' => 'active-trail', - ':href' => url($link_path), - ':title' => $link_title, - ); - $xpath .= $this->buildXPathQuery($part_xpath, $part_args); - $i++; - } - $elements = $this->xpath($xpath); - $this->assertTrue(!empty($elements), 'Active trail to current page was found in menu tree.'); - - // Append prefix for active link asserted below. - $xpath .= '/following-sibling::ul/descendant::'; - } - else { - $xpath .= '//'; + $i = 0; + foreach ($tree as $link_path => $link_title) { + $part_xpath = (!$i ? '//' : '/following-sibling::ul/descendant::'); + $part_xpath .= 'li[contains(@class, :class)]/a[contains(@href, :href) and contains(text(), :title)]'; + $part_args = array( + ':class' => 'active-trail', + ':href' => url($link_path), + ':title' => $link_title, + ); + $xpath .= $this->buildXPathQuery($part_xpath, $part_args); + $i++; } - $xpath_last_active = ($last_active ? 'and contains(@class, :class-active)' : ''); - $xpath .= 'li[contains(@class, :class-trail)]/a[contains(@href, :href) ' . $xpath_last_active . 'and contains(text(), :title)]'; - $args = array( - ':class-trail' => 'active-trail', - ':class-active' => 'active', - ':href' => url($active_link_path), - ':title' => $active_link_title, - ); - $elements = $this->xpath($xpath, $args); - $this->assertTrue(!empty($elements), format_string('Active link %title was found in menu tree, including active trail links %tree.', array( - '%title' => $active_link_title, - '%tree' => implode(' » ', $tree), - ))); + $elements = $this->xpath($xpath); + $this->assertTrue(!empty($elements), 'Active trail to current page was found in menu tree.'); + + // Append prefix for active link asserted below. + $xpath .= '/following-sibling::ul/descendant::'; } + else { + $xpath .= '//'; + } + $xpath_last_active = ($last_active ? 'and contains(@class, :class-active)' : ''); + $xpath .= 'li[contains(@class, :class-trail)]/a[contains(@href, :href) ' . $xpath_last_active . 'and contains(text(), :title)]'; + $args = array( + ':class-trail' => 'active-trail', + ':class-active' => 'active', + ':href' => url($active_link_path), + ':title' => $active_link_title, + ); + $elements = $this->xpath($xpath, $args); + $this->assertTrue(!empty($elements), format_string('Active link %title was found in menu tree, including active trail links %tree.', array( + '%title' => $active_link_title, + '%tree' => implode(' » ', $tree), + ))); } /** * 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/modules/views/lib/Drupal/views/ViewExecutable.php b/core/modules/views/lib/Drupal/views/ViewExecutable.php index b7b606e..d3bd6b0 100644 --- a/core/modules/views/lib/Drupal/views/ViewExecutable.php +++ b/core/modules/views/lib/Drupal/views/ViewExecutable.php @@ -1666,8 +1666,8 @@ public function getBreadcrumb($set = FALSE) { } if ($set) { - if ($base) { - $breadcrumb = array_merge(drupal_get_breadcrumb(), $breadcrumb); + if ($base && $current_breadcrumbs = drupal_set_breadcrumb()) { + $breadcrumb = array_merge($current_breadcrumbs, $breadcrumb); } drupal_set_breadcrumb($breadcrumb); } 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..db92b96 --- /dev/null +++ b/core/profiles/standard/config/block.block.bartik.breadcrumbs.yml @@ -0,0 +1,22 @@ +id: bartik.breadcrumbs +weight: '-5' +status: '0' +langcode: en +region: '-1' +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..6ca8962 --- /dev/null +++ b/core/profiles/standard/config/block.block.seven.breadcrumbs.yml @@ -0,0 +1,22 @@ +id: seven.breadcrumbs +weight: '-2' +status: '0' +langcode: en +region: '-1' +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