diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 45737c5..19e3bab 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -894,6 +894,7 @@ function drupal_find_base_themes($themes, $key, $used_keys = array()) { * process functions for them would incur a noticeable performance penalty. * * @subsection sub_alternate_suggestions Suggesting Alternate Hooks + * @todo Update the below section. * There are two special variables that these preprocess and process functions * can set: 'theme_hook_suggestion' and 'theme_hook_suggestions'. These will be * merged together to form a list of 'suggested' alternate theme hooks to use, @@ -1031,11 +1032,31 @@ function theme($hook, $variables = array()) { 'theme_hook_original' => $original_hook, ); - // Invoke the variable processors, if any. The processors may specify - // alternate suggestions for which hook's template/function to use. If the - // hook is a suggestion of a base hook, invoke the variable processors of - // the base hook, but retain the suggestion as a high priority suggestion to - // be used unless overridden by a variable processor function. + // Invoke template suggestion alter hooks. + $suggestions = array(); + $suggestion_hooks = array( + 'theme_suggestions', + 'theme_suggestions_' . $original_hook, + ); + // @todo Consider passing $original_hook as last argument. + Drupal::moduleHandler()->alter($suggestion_hooks, $suggestions, $variables); + + // Store array of suggestions in variables array. + $variables['theme_hook_suggestions'] = $suggestions; + + // Check if each suggestion exists in the theme registry, and if so, + // use it instead of the hook that theme() was called with. For example, a + // function may call theme('node', ...), but a a module can add + // 'node__article' as a suggestion via hook_theme_suggestions_HOOK, + // enabling a theme to have an alternate template file for article nodes. + foreach (array_reverse($suggestions) as $suggestion) { + if (isset($hooks[$suggestion])) { + $info = $hooks[$suggestion]; + break; + } + } + + // Invoke the variable processors, if any. if (isset($info['base hook'])) { $base_hook = $info['base hook']; $base_hook_info = $hooks[$base_hook]; @@ -1046,14 +1067,15 @@ function theme($hook, $variables = array()) { include_once DRUPAL_ROOT . '/' . $include_file; } } - if (isset($base_hook_info['preprocess functions']) || isset($base_hook_info['process functions'])) { - $variables['theme_hook_suggestion'] = $hook; - $hook = $base_hook; - $info = $base_hook_info; + // Add preprocess and process functions from the base hook to the $info + // array. + foreach (array('preprocess functions', 'process functions') as $processor) { + if (isset($base_hook_info[$processor])) { + $info = array_merge($info, array($processor => $base_hook_info[$processor])); + } } } if (isset($info['preprocess functions']) || isset($info['process functions'])) { - $variables['theme_hook_suggestions'] = array(); foreach (array('preprocess functions', 'process functions') as $phase) { if (!empty($info[$phase])) { foreach ($info[$phase] as $processor_function) { @@ -1063,31 +1085,6 @@ function theme($hook, $variables = array()) { } } } - // If the preprocess/process functions specified hook suggestions, and the - // suggestion exists in the theme registry, use it instead of the hook that - // theme() was called with. This allows the preprocess/process step to - // route to a more specific theme hook. For example, a function may call - // theme('node', ...), but a preprocess function can add 'node__article' as - // a suggestion, enabling a theme to have an alternate template file for - // article nodes. Suggestions are checked in the following order: - // - The 'theme_hook_suggestion' variable is checked first. It overrides - // all others. - // - The 'theme_hook_suggestions' variable is checked in FILO order, so the - // last suggestion added to the array takes precedence over suggestions - // added earlier. - $suggestions = array(); - if (!empty($variables['theme_hook_suggestions'])) { - $suggestions = $variables['theme_hook_suggestions']; - } - if (!empty($variables['theme_hook_suggestion'])) { - $suggestions[] = $variables['theme_hook_suggestion']; - } - foreach (array_reverse($suggestions) as $suggestion) { - if (isset($hooks[$suggestion])) { - $info = $hooks[$suggestion]; - break; - } - } } // Generate the output using either a function or a template. @@ -2694,11 +2691,6 @@ function template_preprocess_html(&$variables) { foreach ($elements as $name => $element) { drupal_add_html_head($element, $name); } - - // Populate the page template suggestions. - if ($suggestions = theme_get_suggestions(arg(), 'html')) { - $variables['theme_hook_suggestions'] = $suggestions; - } } /** @@ -2787,11 +2779,6 @@ function template_preprocess_page(&$variables) { if ($node = menu_get_object()) { $variables['node'] = $node; } - - // Populate the page template suggestions. - if ($suggestions = theme_get_suggestions(arg(), 'page')) { - $variables['theme_hook_suggestions'] = $suggestions; - } } /** @@ -2851,8 +2838,8 @@ function template_process_html(&$variables) { /** * Generate an array of suggestions from path arguments. * - * This is typically called for adding to the 'theme_hook_suggestions' or - * 'attributes' class key variables from within preprocess functions, when + * This is typically called for adding to the 'hook_theme_suggestions_HOOK' + * or 'attributes' class key variables from within preprocess functions, when * wanting to base the additional suggestions on the path of the current page. * * @param $args @@ -2867,9 +2854,8 @@ function template_process_html(&$variables) { * * @return * An array of suggestions, suitable for adding to - * $variables['theme_hook_suggestions'] within a preprocess function or to - * $variables['attributes']['class'] if the suggestions represent extra CSS - * classes. + * hook_theme_suggestions_HOOK_alter() or to $variables['attributes']['class'] + * if the suggestions represent extra CSS classes. */ function theme_get_suggestions($args, $base, $delimiter = '__') { @@ -3015,12 +3001,6 @@ function template_preprocess_maintenance_page(&$variables) { $variables['attributes']['class'][] = 'one-sidebar'; $variables['attributes']['class'][] = 'sidebar-' . $variables['layout']; } - - // Dead databases will show error messages so supplying this template will - // allow themers to override the page and the content completely. - if (isset($variables['db_is_active']) && !$variables['db_is_active']) { - $variables['theme_hook_suggestion'] = 'maintenance_page__offline'; - } } /** @@ -3062,7 +3042,6 @@ function template_preprocess_region(&$variables) { $variables['attributes']['class'][] = 'region'; $variables['attributes']['class'][] = drupal_html_class('region-' . $variables['region']); - $variables['theme_hook_suggestions'][] = 'region__' . $variables['region']; } /** diff --git a/core/modules/block/block.module b/core/modules/block/block.module index ca13d32..e4811d4 100644 --- a/core/modules/block/block.module +++ b/core/modules/block/block.module @@ -505,6 +505,35 @@ function block_rebuild() { } /** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function block_theme_suggestions_block_alter(&$suggestions, $variables) { + $suggestions[] = 'block__' . $variables['elements']['#configuration']['module']; + // Hyphens (-) and underscores (_) play a special role in theme suggestions. + // Theme suggestions should only contain underscores, because within + // drupal_find_theme_templates(), underscores are converted to hyphens to + // match template file names, and then converted back to underscores to match + // pre-processing and other function names. So if your theme suggestion + // contains a hyphen, it will end up as an underscore after this conversion, + // and your function names won't be recognized. So, we need to convert + // hyphens to underscores in block deltas for the theme suggestions. + + // We can safely explode on : because we know the Block plugin type manager + // enforces that delimiter for all derivatives. + $parts = explode(':', $variables['elements']['#plugin_id']); + $suggestion = 'block'; + while ($part = array_shift($parts)) { + $suggestions[] = $suggestion .= '__' . strtr($part, '-', '_'); + } + + if ($id = $variables['elements']['#block']->id()) { + $config_id = explode('.', $id); + $machine_name = array_pop($config_id); + $suggestions[] = 'block__' . $machine_name; + } +} + +/** * Prepares variables for block templates. * * Default template: block.html.twig. @@ -547,29 +576,11 @@ function template_preprocess_block(&$variables) { // Add default class for block content. $variables['content_attributes']['class'][] = 'content'; - $variables['theme_hook_suggestions'][] = 'block__' . $variables['configuration']['module']; - // Hyphens (-) and underscores (_) play a special role in theme suggestions. - // Theme suggestions should only contain underscores, because within - // drupal_find_theme_templates(), underscores are converted to hyphens to - // match template file names, and then converted back to underscores to match - // pre-processing and other function names. So if your theme suggestion - // contains a hyphen, it will end up as an underscore after this conversion, - // and your function names won't be recognized. So, we need to convert - // hyphens to underscores in block deltas for the theme suggestions. - - // We can safely explode on : because we know the Block plugin type manager - // enforces that delimiter for all derivatives. - $parts = explode(':', $variables['plugin_id']); - $suggestion = 'block'; - while ($part = array_shift($parts)) { - $variables['theme_hook_suggestions'][] = $suggestion .= '__' . strtr($part, '-', '_'); - } // Create a valid HTML ID and make sure it is unique. if ($id = $variables['elements']['#block']->id()) { $config_id = explode('.', $id); $machine_name = array_pop($config_id); $variables['attributes']['id'] = drupal_html_id('block-' . $machine_name); - $variables['theme_hook_suggestions'][] = 'block__' . $machine_name; } } diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php index 51630ba..bc4ab59 100644 --- a/core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php +++ b/core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php @@ -10,7 +10,7 @@ use Drupal\simpletest\WebTestBase; /** - * Unit tests for template_preprocess_block(). + * Unit tests for block_theme_suggestions_block_alter(). */ class BlockTemplateSuggestionsUnitTest extends WebTestBase { @@ -24,13 +24,13 @@ class BlockTemplateSuggestionsUnitTest extends WebTestBase { public static function getInfo() { return array( 'name' => 'Block template suggestions', - 'description' => 'Test the template_preprocess_block() function.', + 'description' => 'Test the block_theme_suggestions_block_alter() function.', 'group' => 'Block', ); } /** - * Test if template_preprocess_block() handles the suggestions right. + * Tests template suggestions from block_theme_suggestions_block_alter(). */ function testBlockThemeHookSuggestions() { // Define a block with a derivative to be preprocessed, which includes both @@ -43,16 +43,15 @@ function testBlockThemeHookSuggestions() { 'id' => config('system.theme')->get('default') . '.machinename', )); + // @todo Cast to array in suggestions alter hook signature? + $suggestions = array(); $variables = array(); $variables['elements']['#block'] = $block; $variables['elements']['#configuration'] = $block->getPlugin()->getConfig(); $variables['elements']['#plugin_id'] = $block->get('plugin'); $variables['elements']['content'] = array(); - // Test adding a class to the block content. - $variables['content_attributes']['class'][] = 'test-class'; - template_preprocess_block($variables); - $this->assertEqual($variables['theme_hook_suggestions'], array('block__system', 'block__system_menu_block', 'block__system_menu_block__menu_admin', 'block__machinename')); - $this->assertEqual($variables['content_attributes']['class'], array('test-class', 'content'), 'Default .content class added to block content_attributes'); + block_theme_suggestions_block_alter($suggestions, $variables); + $this->assertEqual($suggestions, array('block__system', 'block__system_menu_block', 'block__system_menu_block__menu_admin', 'block__machinename')); } } diff --git a/core/modules/field/field.module b/core/modules/field/field.module index 403ace2..c20f9cf 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -952,6 +952,20 @@ function field_page_build(&$page) { } /** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function field_theme_suggestions_field_alter(&$suggestions, $variables) { + $element = $variables['element']; + + $suggestions = array_merge($suggestions, array( + 'field__' . $element['#field_type'], + 'field__' . $element['#field_name'], + 'field__' . $element['#bundle'], + 'field__' . $element['#field_name'] . '__' . $element['#bundle'], + )); +} + +/** * Prepares variables for field templates. * * Default template: field.html.twig. @@ -1004,14 +1018,6 @@ function template_preprocess_field(&$variables, $hook) { if ($element['#label_display'] == 'inline') { $variables['attributes']['class'][] = 'clearfix'; } - - // Add specific suggestions that can override the default implementation. - $variables['theme_hook_suggestions'] = array( - 'field__' . $element['#field_type'], - 'field__' . $element['#field_name'], - 'field__' . $element['#bundle'], - 'field__' . $element['#field_name'] . '__' . $element['#bundle'], - ); } /** diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module index c06edde..ba405df 100644 --- a/core/modules/forum/forum.module +++ b/core/modules/forum/forum.module @@ -962,6 +962,28 @@ function forum_preprocess_block(&$variables) { } /** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function forum_theme_suggestions_forums_alter(&$suggestions, $variables) { + // Provide separate template suggestions based on what's being output. Topic + // ID is also accounted for. Check both variables to be safe then the inverse. + // Forums with topic IDs take precedence. + if ($variables['forums'] && !$variables['topics']) { + $suggestions[] = 'forums__containers'; + $suggestions[] = 'forums__' . $variables['tid']; + $suggestions[] = 'forums__containers__' . $variables['tid']; + } + elseif (!$variables['forums'] && $variables['topics']) { + $suggestions[] = 'forums__topics'; + $suggestions[] = 'forums__' . $variables['tid']; + $suggestions[] = 'forums__topics__' . $variables['tid']; + } + else { + $suggestions[] = 'forums__' . $variables['tid']; + } +} + +/** * Prepares variables for forums templates. * * Default template: forums.html.twig. @@ -1007,23 +1029,6 @@ function template_preprocess_forums(&$variables) { else { $variables['topics'] = array(); } - - // Provide separate template suggestions based on what's being output. Topic id is also accounted for. - // Check both variables to be safe then the inverse. Forums with topic ID's take precedence. - if ($variables['forums'] && !$variables['topics']) { - $variables['theme_hook_suggestions'][] = 'forums__containers'; - $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid']; - $variables['theme_hook_suggestions'][] = 'forums__containers__' . $variables['tid']; - } - elseif (!$variables['forums'] && $variables['topics']) { - $variables['theme_hook_suggestions'][] = 'forums__topics'; - $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid']; - $variables['theme_hook_suggestions'][] = 'forums__topics__' . $variables['tid']; - } - else { - $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid']; - } - } else { $variables['forums'] = array(); diff --git a/core/modules/node/node.module b/core/modules/node/node.module index fbdf07d..71a1435 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -1020,6 +1020,18 @@ function node_preprocess_block(&$variables) { } /** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function node_theme_suggestions_node_alter(&$suggestions, $variables) { + $node = $variables['elements']['#node']; + + $suggestions = array_merge($suggestions, array( + 'node__' . $node->type, + 'node__' . $node->nid, + )); +} + +/** * Prepares variables for node templates. * * Default template: node.html.twig. @@ -1107,10 +1119,6 @@ function template_preprocess_node(&$variables) { $variables['attributes']['class'][] = 'preview'; } - // Clean up name so there are no underscores. - $variables['theme_hook_suggestions'][] = 'node__' . $node->type; - $variables['theme_hook_suggestions'][] = 'node__' . $node->nid; - $variables['content_attributes']['class'][] = 'content'; } diff --git a/core/modules/search/search.pages.inc b/core/modules/search/search.pages.inc index 646028a..fc3705d 100644 --- a/core/modules/search/search.pages.inc +++ b/core/modules/search/search.pages.inc @@ -75,6 +75,13 @@ function search_view($module = NULL, $keys = '') { } /** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function search_theme_suggestions_search_results_alter(&$suggestions, $variables) { + $suggestions[] = 'search_results__' . $variables['module']; +} + +/** * Prepares variables for search results templates. * * Default template: search-results.html.twig. @@ -104,7 +111,13 @@ function template_preprocess_search_results(&$variables) { // @todo Revisit where this help text is added, see also // http://drupal.org/node/1918856. $variables['help'] = search_help('search#noresults', drupal_help_arg()); - $variables['theme_hook_suggestions'][] = 'search_results__' . $variables['module']; +} + +/** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function search_theme_suggestions_search_result_alter(&$suggestions, $variables) { + $suggestions[] = 'search_result__' . $variables['module']; } /** @@ -153,7 +166,6 @@ function template_preprocess_search_result(&$variables) { // Provide separated and grouped meta information.. $variables['info_split'] = $info; $variables['info'] = implode(' - ', $info); - $variables['theme_hook_suggestions'][] = 'search_result__' . $variables['module']; } /** diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 125f807..f004902 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1046,6 +1046,40 @@ function system_menu() { } /** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function system_theme_suggestions_html_alter(&$suggestions, $variables) { + $suggestions = array_merge($suggestions, theme_get_suggestions(arg(), 'html')); +} + +/** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function system_theme_suggestions_page_alter(&$suggestions, $variables) { + $suggestions = array_merge($suggestions, theme_get_suggestions(arg(), 'page')); +} + +/** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function system_theme_suggestions_maintenance_page_alter(&$suggestions, $variables) { + // Dead databases will show error messages so supplying this template will + // allow themers to override the page and the content completely. + if (defined('MAINTENANCE_MODE')) { + $suggestions[] = 'maintenance_page__offline'; + } +} + +/** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function system_theme_suggestions_region_alter(&$suggestions, $variables) { + if (!empty($variables['elements']['#region'])) { + $suggestions[] = 'region__' . $variables['elements']['#region']; + } +} + +/** * Proxies to the plugin class' form method. * * @todo This needs more explanation, an @see or two, and parameter diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index b2c6b83..88c9eae 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -450,6 +450,16 @@ function taxonomy_term_view_multiple(array $terms, $view_mode = 'full', $langcod } /** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function taxonomy_theme_suggestions_taxonomy_term_alter(&$suggestions, $variables) { + $term = $variables['elements']['#term']; + + $suggestions[] = 'taxonomy_term__' . $term->bundle(); + $suggestions[] = 'taxonomy_term__' . $term->id(); +} + +/** * Prepares variables for taxonomy term templates. * * Default template: taxonomy-term.html.twig. @@ -490,9 +500,6 @@ function template_preprocess_taxonomy_term(&$variables) { $variables['attributes']['class'][] = 'taxonomy-term'; $vocabulary_name_css = str_replace('_', '-', $term->bundle()); $variables['attributes']['class'][] = 'vocabulary-' . $vocabulary_name_css; - - $variables['theme_hook_suggestions'][] = 'taxonomy_term__' . $term->bundle(); - $variables['theme_hook_suggestions'][] = 'taxonomy_term__' . $term->id(); } /** diff --git a/core/modules/views/views.module b/core/modules/views/views.module index c7077e9..e0da079 100644 --- a/core/modules/views/views.module +++ b/core/modules/views/views.module @@ -238,17 +238,13 @@ function views_plugin_list() { function views_preprocess_node(&$vars) { Drupal::moduleHandler()->loadInclude('node', 'views.inc'); // The 'view' attribute of the node is added in views_preprocess_node() + // @todo The comment above doesn't make any sense to me - it's saying the + // attribute is added in *this* function? if (!empty($vars['node']->view) && $vars['node']->view->storage->id()) { - $vars['view'] = $vars['node']->view; - $vars['theme_hook_suggestions'][] = 'node__view__' . $vars['node']->view->storage->id(); - if (!empty($vars['node']->view->current_display)) { - $vars['theme_hook_suggestions'][] = 'node__view__' . $vars['node']->view->storage->id() . '__' . $vars['node']->view->current_display; - - // If a node is being rendered in a view, and the view does not have a path, - // prevent drupal from accidentally setting the $page variable: - if ($vars['page'] && $vars['view_mode'] == 'full' && !$vars['view']->display_handler->hasPath()) { - $vars['page'] = FALSE; - } + // If a node is being rendered in a view, and the view does not have a path, + // prevent drupal from accidentally setting the $page variable: + if ($vars['page'] && $vars['view_mode'] == 'full' && !$vars['view']->display_handler->hasPath()) { + $vars['page'] = FALSE; } } @@ -259,16 +255,40 @@ function views_preprocess_node(&$vars) { } /** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function views_theme_suggestions_node_alter(&$suggestions, $variables) { + if (!empty($variables['node']->view) && $variables['node']->view->storage->id()) { + $variables['view'] = $variables['node']->view; + $suggestions[] = 'node__view__' . $variables['node']->view->storage->id(); + if (!empty($variables['node']->view->current_display)) { + $suggestions[] = 'node__view__' . $variables['node']->view->storage->id() . '__' . $variables['node']->view->current_display; + } + } +} + +/** * A theme preprocess function to automatically allow view-based node * templates if called from a view. */ function views_preprocess_comment(&$vars) { // The 'view' attribute of the node is added in template_preprocess_views_view_row_comment() + // @todo I can't find the function mentioned in the above comment. if (!empty($vars['comment']->view) && $vars['comment']->view->storage->id()) { $vars['view'] = &$vars['comment']->view; - $vars['theme_hook_suggestions'][] = 'comment__view__' . $vars['comment']->view->storage->id(); - if (!empty($vars['node']->view->current_display)) { - $vars['theme_hook_suggestions'][] = 'comment__view__' . $vars['comment']->view->storage->id() . '__' . $vars['comment']->view->current_display; + } +} + +/** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function views_theme_suggestions_comment_alter(&$suggestions, $variables) { + if (!empty($variables['comment']->view) && $variables['comment']->view->storage->id()) { + $suggestions[] = 'comment__view__' . $variables['comment']->view->storage->id(); + // @todo Shouldn't the below be checking $variables['comment'] instead of + // $variables['node']? + if (!empty($variables['node']->view->current_display)) { + $suggestions[] = 'comment__view__' . $variables['comment']->view->storage->id() . '__' . $variables['comment']->view->current_display; } } } diff --git a/core/modules/views_ui/views_ui.theme.inc b/core/modules/views_ui/views_ui.theme.inc index 8e503fc..48eb6d1 100644 --- a/core/modules/views_ui/views_ui.theme.inc +++ b/core/modules/views_ui/views_ui.theme.inc @@ -450,7 +450,13 @@ function template_preprocess_views_ui_view_preview_section(&$vars) { ); $vars['links'] = drupal_render($build); } - $vars['theme_hook_suggestions'][] = 'views_ui_view_preview_section__' . $vars['section']; +} + +/** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function views_ui_theme_suggestions_views_ui_view_preview_section_alter(&$suggestions, $variables) { + $suggestions[] = 'views_ui_view_preview_section__' . $variables['section']; } /**