diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 84d4752..bae77f9 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -856,17 +856,16 @@ function drupal_find_base_themes($themes, $key, $used_keys = array()) { * a noticeable performance penalty. * * @subsection sub_alternate_suggestions Suggesting Alternate Hooks - * There are two special variables that these preprocess 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, in - * reverse order of priority. theme_hook_suggestion will always be a higher - * priority than items in theme_hook_suggestions. theme() will use the highest - * priority implementation that exists. If none exists, theme() will use the - * implementation for the theme hook it was called with. These suggestions are - * similar to, and are used for similar reasons as, calling theme() with an - * array as the $hook parameter (see below). The difference is whether the - * suggestions are determined by the code that calls theme() or by a preprocess - * function. + * Alternate hooks can be suggested by implementing the hook-specific + * hook_theme_suggestions_HOOK_alter() or the generic + * hook_theme_suggestions_alter(). These alter hooks are used to manipulate an + * array of suggested alternate theme hooks to use, in reverse order of + * priority. theme() will use the highest priority implementation that exists. + * If none exists, theme() will use the implementation for the theme hook it was + * called with. These suggestions are similar to and are used for similar + * reasons as calling theme() with an array as the $hook parameter (see below). + * The difference is whether the suggestions are determined by the code that + * calls theme() or by altering the suggestions via the suggestion alter hooks. * * @param $hook * The name of the theme hook to call. If the name contains a @@ -995,11 +994,39 @@ function theme($hook, $variables = array()) { 'theme_hook_original' => $original_hook, ); - // Invoke the variable preprocessors, if any. The preprocessors 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 preprocessors of - // the base hook, but retain the suggestion as a high priority suggestion to - // be used unless overridden by a variable preprocessor function. + // Set base hook for use with suggestions alter hooks. This way if e.g. + // '#theme' => 'node__article' is called, we run + // hook_theme_suggestions_node_alter() rather than + // hook_theme_suggestions_node__article_alter(), and also pass in the base + // hook as the last parameter to the suggestions alter hooks. + if (isset($info['base hook'])) { + $suggestions_base_hook = $info['base hook']; + } + else { + $suggestions_base_hook = $hook; + } + + // Invoke theme hook suggestion alter hooks. + $suggestions = array(); + $suggestion_hooks = array( + 'theme_suggestions', + 'theme_suggestions_' . $suggestions_base_hook, + ); + Drupal::moduleHandler()->alter($suggestion_hooks, $suggestions, $variables, $suggestions_base_hook); + + // 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 module can add + // 'node__article' as a suggestion via hook_theme_suggestions_HOOK_alter(), + // 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 preprocessors, if any. if (isset($info['base hook'])) { $base_hook = $info['base hook']; $base_hook_info = $hooks[$base_hook]; @@ -1010,44 +1037,19 @@ function theme($hook, $variables = array()) { include_once DRUPAL_ROOT . '/' . $include_file; } } + // Previously we just replaced the entire $info array with $base_hook_info + // if any preprocess functions were set, but now we determine the + // suggestions first and then fire the appropriate preprocess functions. if (isset($base_hook_info['preprocess functions'])) { - $variables['theme_hook_suggestion'] = $hook; - $hook = $base_hook; - $info = $base_hook_info; + $info['preprocess functions'] = $base_hook_info['preprocess functions']; } } if (isset($info['preprocess functions'])) { - $variables['theme_hook_suggestions'] = array(); foreach ($info['preprocess functions'] as $preprocessor_function) { if (function_exists($preprocessor_function)) { $preprocessor_function($variables, $hook, $info); } } - // If the preprocess 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 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. @@ -1110,6 +1112,7 @@ function theme($hook, $variables = array()) { if (isset($info['path'])) { $template_file = $info['path'] . '/' . $template_file; } + $variables['theme_hook_suggestions'] = $suggestions; $output = $render_function($template_file, $variables); } @@ -2631,11 +2634,6 @@ function template_preprocess_html(&$variables) { drupal_add_html_head($element, $name); } - // Populate the page template suggestions. - if ($suggestions = theme_get_suggestions(arg(), 'html')) { - $variables['theme_hook_suggestions'] = $suggestions; - } - drupal_add_library('system', 'html5shiv', TRUE); // Render page_top and page_bottom into top level variables. @@ -2746,11 +2744,6 @@ function template_preprocess_page(&$variables) { $variables['node'] = $node; } - // Populate the page template suggestions. - if ($suggestions = theme_get_suggestions(arg(), 'page')) { - $variables['theme_hook_suggestions'] = $suggestions; - } - // Prepare render array for messages. drupal_get_messages() is called later, // when this variable is rendered in a theme function or template file. $variables['messages'] = array( @@ -2771,9 +2764,10 @@ function template_preprocess_page(&$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 - * wanting to base the additional suggestions on the path of the current page. + * This is typically called for adding to the suggestions in + * hook_theme_suggestions_HOOK_alter() or adding to 'attributes' class key + * variables from within preprocess functions, when wanting to base the + * additional suggestions or classes on the path of the current page. * * @param $args * An array of path arguments, such as from function arg(). @@ -2787,9 +2781,8 @@ function template_preprocess_page(&$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 = '__') { @@ -2951,12 +2944,6 @@ function template_preprocess_maintenance_page(&$variables) { $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'; - } - $variables['head'] = drupal_get_html_head(); // While this code is used in the installer, the language module may not be @@ -3009,7 +2996,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 0f481ad..ad0c1b4 100644 --- a/core/modules/block/block.module +++ b/core/modules/block/block.module @@ -491,6 +491,35 @@ function block_rebuild() { } /** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function block_theme_suggestions_block_alter(array &$suggestions, array $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. @@ -533,29 +562,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/BlockPreprocessUnitTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockPreprocessUnitTest.php new file mode 100644 index 0000000..ddbc40c --- /dev/null +++ b/core/modules/block/lib/Drupal/block/Tests/BlockPreprocessUnitTest.php @@ -0,0 +1,56 @@ + 'Block preprocess', + 'description' => 'Test the template_preprocess_block() function.', + 'group' => 'Block', + ); + } + + /** + * Tests block classes with template_preprocess_block(). + */ + function testBlockClasses() { + // Define a block with a derivative to be preprocessed, which includes both + // an underscore (not transformed) and a hyphen (transformed to underscore), + // and generates possibilities for each level of derivative. + // @todo Clarify this comment. + $block = entity_create('block', array( + 'plugin' => 'system_menu_block:menu-admin', + 'region' => 'footer', + 'id' => config('system.theme')->get('default') . '.machinename', + )); + + $variables = array(); + $variables['elements']['#block'] = $block; + $variables['elements']['#configuration'] = $block->getPlugin()->getConfig(); + $variables['elements']['#plugin_id'] = $block->get('plugin'); + $variables['elements']['content'] = array(); + $variables['content_attributes']['class'][] = 'test-class'; + template_preprocess_block($variables); + $this->assertEqual($variables['content_attributes']['class'], array('test-class', 'content'), 'Default .content class added to block content_attributes'); + } + +} diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockTemplateSuggestionsUnitTest.php index 51630ba..67ce2ed 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,14 @@ function testBlockThemeHookSuggestions() { 'id' => config('system.theme')->get('default') . '.machinename', )); + $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 e929de6..bb0c112 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -915,6 +915,18 @@ function field_page_build(&$page) { } /** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function field_theme_suggestions_field_alter(array &$suggestions, array $variables) { + $element = $variables['element']; + + $suggestions[] = 'field__' . $element['#field_type']; + $suggestions[] = 'field__' . $element['#field_name']; + $suggestions[] = 'field__' . $element['#bundle']; + $suggestions[] = 'field__' . $element['#field_name'] . '__' . $element['#bundle']; +} + +/** * Prepares variables for field templates. * * Default template: field.html.twig. @@ -968,14 +980,6 @@ function template_preprocess_field(&$variables, $hook) { $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'], - ); - static $default_attributes; if (!isset($default_attributes)) { $default_attributes = new Attribute; diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module index d0d0ab8..f710bad 100644 --- a/core/modules/forum/forum.module +++ b/core/modules/forum/forum.module @@ -955,6 +955,28 @@ function forum_preprocess_block(&$variables) { } /** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function forum_theme_suggestions_forums_alter(array &$suggestions, array $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. @@ -1000,23 +1022,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 5a6d53a..1c0ffd0 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -667,6 +667,16 @@ function node_preprocess_block(&$variables) { } /** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function node_theme_suggestions_node_alter(array &$suggestions, array $variables) { + $node = $variables['elements']['#node']; + + $suggestions[] = 'node__' . $node->bundle(); + $suggestions[] = 'node__' . $node->id(); +} + +/** * Prepares variables for node templates. * * Default template: node.html.twig. @@ -755,11 +765,6 @@ function template_preprocess_node(&$variables) { if (isset($variables['preview'])) { $variables['attributes']['class'][] = 'preview'; } - - // Clean up name so there are no underscores. - $variables['theme_hook_suggestions'][] = 'node__' . $node->bundle(); - $variables['theme_hook_suggestions'][] = 'node__' . $node->id(); - $variables['content_attributes']['class'][] = 'content'; } diff --git a/core/modules/search/search.pages.inc b/core/modules/search/search.pages.inc index a39a815..0ca1d8f 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(array &$suggestions, array $variables) { + $suggestions[] = 'search_results__' . $variables['module']; +} + +/** * Prepares variables for search results templates. * * Default template: search-results.html.twig. @@ -101,7 +108,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(array &$suggestions, array $variables) { + $suggestions[] = 'search_result__' . $variables['module']; } /** @@ -150,7 +163,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/lib/Drupal/system/Tests/Theme/ThemeSuggestionsAlterTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeSuggestionsAlterTest.php new file mode 100644 index 0000000..7faa77c --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeSuggestionsAlterTest.php @@ -0,0 +1,97 @@ + 'Theme suggestions alter', + 'description' => 'Test theme suggestion alter hooks.', + 'group' => 'Theme', + ); + } + + function setUp() { + parent::setUp(); + theme_enable(array('test_theme')); + } + + /** + * Tests that theme suggestion alter hooks work for templates. + */ + function testTemplateSuggestionsAlter() { + $this->drupalGet('theme-test/suggestion-alter'); + $this->assertText('Original template.'); + + // Enable test_theme and test that themes can alter template suggestions. + config('system.theme') + ->set('default', 'test_theme') + ->save(); + $this->drupalGet('theme-test/suggestion-alter'); + $this->assertText('Template overridden based on new theme suggestion provided by the test_theme theme.'); + + // Enable the theme_suggestions_test module to test modules implementing + // suggestions alter hooks. + module_enable(array('theme_suggestions_test')); + $this->drupalGet('theme-test/suggestion-alter'); + $this->assertText('Template overridden based on new theme suggestion provided by a module.'); + } + + /** + * Tests that theme suggestion alter hooks work for specific theme calls. + */ + function testSpecificSuggestionsAlter() { + config('system.theme') + ->set('default', 'test_theme') + ->save(); + + // Test a specific theme call similar to '#theme' => 'node__article'. + $this->drupalGet('theme-test/specific-suggestion-alter'); + $this->assertText('Template for testing specific theme calls.'); + + // Ensure that the base hook is used to determine the suggestion alter hook. + module_enable(array('theme_suggestions_test')); + $this->drupalGet('theme-test/specific-suggestion-alter'); + $this->assertText('Template overridden based on suggestion alter hook determined by the base hook.'); + } + + /** + * Tests that theme suggestion alter hooks work for theme functions. + */ + function testThemeFunctionSuggestionsAlter() { + $this->drupalGet('theme-test/function-suggestion-alter'); + $this->assertText('Original theme function.'); + + // Enable test_theme and test that themes can alter theme suggestions. + config('system.theme') + ->set('default', 'test_theme') + ->save(); + $this->drupalGet('theme-test/function-suggestion-alter'); + $this->assertText('Theme function overridden based on new theme suggestion provided by the test_theme theme.'); + + // Enable the theme_suggestions_test module to test modules implementing + // suggestions alter hooks. + module_enable(array('theme_suggestions_test')); + $this->drupalGet('theme-test/function-suggestion-alter'); + $this->assertText('Theme function overridden based on new theme suggestion provided by a module.'); + } + +} diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 4a7cdf4..fcfe388 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1007,6 +1007,48 @@ function system_menu() { } /** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function system_theme_suggestions_html_alter(array &$suggestions, array $variables) { + $suggestions = array_merge($suggestions, theme_get_suggestions(arg(), 'html')); +} + +/** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function system_theme_suggestions_page_alter(array &$suggestions, array $variables) { + $suggestions = array_merge($suggestions, theme_get_suggestions(arg(), 'page')); +} + +/** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function system_theme_suggestions_maintenance_page_alter(array &$suggestions, array $variables) { + // Dead databases will show error messages so supplying this template will + // allow themers to override the page and the content completely. + $offline = defined('MAINTENANCE_MODE'); + try { + drupal_is_front_page(); + } + catch (Exception $e) { + // The database is not yet available. + $offline = TRUE; + } + if ($offline) { + $suggestions[] = 'maintenance_page__offline'; + } +} + +/** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function system_theme_suggestions_region_alter(array &$suggestions, array $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/system/tests/modules/theme_suggestions_test/theme_suggestions_test.info.yml b/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.info.yml new file mode 100644 index 0000000..95f429f --- /dev/null +++ b/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.info.yml @@ -0,0 +1,7 @@ +name: 'Theme suggestions test' +type: module +description: 'Support module for testing theme suggestions.' +package: Testing +version: VERSION +core: 8.x +hidden: true diff --git a/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module b/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module new file mode 100644 index 0000000..58f9cdb --- /dev/null +++ b/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module @@ -0,0 +1,27 @@ + 'theme_test_suggestions'); + } + + /** + * Menu callback for testing suggestion alter hooks with specific suggestions. + */ + function specificSuggestionAlter() { + return array('#theme' => 'theme_test_specific_suggestions__variant'); + } + + /** + * Menu callback for testing suggestion alter hooks with theme functions. + */ + function functionSuggestionAlter() { + return array('#theme' => 'theme_test_function_suggestions'); + } + } diff --git a/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestions--specific.html.twig b/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestions--specific.html.twig new file mode 100644 index 0000000..6e112fd --- /dev/null +++ b/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestions--specific.html.twig @@ -0,0 +1,2 @@ +{# Output for Theme API test #} +Template for testing specific theme calls. diff --git a/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestions.html.twig b/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestions.html.twig new file mode 100644 index 0000000..dfc848c --- /dev/null +++ b/core/modules/system/tests/modules/theme_test/templates/theme-test-suggestions.html.twig @@ -0,0 +1,2 @@ +{# Output for Theme API test #} +Original template. diff --git a/core/modules/system/tests/modules/theme_test/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module index 51cb6d3..19f1ff5 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -14,6 +14,17 @@ function theme_test_theme($existing, $type, $theme, $path) { $items['theme_test_template_test_2'] = array( 'template' => 'theme_test.template_test', ); + $items['theme_test_specific_suggestions'] = array( + 'template' => 'theme-test-specific-suggestions', + 'variables' => array(), + ); + $items['theme_test_suggestions'] = array( + 'template' => 'theme-test-suggestions', + 'variables' => array(), + ); + $items['theme_test_function_suggestions'] = array( + 'variables' => array(), + ); $items['theme_test_foo'] = array( 'variables' => array('foo' => NULL), ); @@ -52,6 +63,18 @@ function theme_test_menu() { 'theme callback' => '_theme_custom_theme', 'type' => MENU_CALLBACK, ); + $items['theme-test/suggestion-alter'] = array( + 'title' => 'Test suggestion alter hook for template files', + 'route_name' => 'suggestion_alter', + ); + $items['theme-test/specific-suggestion-alter'] = array( + 'title' => 'Test suggestion alter hook for specific theme suggestions', + 'route_name' => 'specific_suggestion_alter', + ); + $items['theme-test/function-suggestion-alter'] = array( + 'title' => 'Test suggestion alter hook for theme functions', + 'route_name' => 'function_suggestion_alter', + ); $items['theme-test/alter'] = array( 'title' => 'Suggestion', 'page callback' => '_theme_test_alter', @@ -197,3 +220,10 @@ function template_preprocess_theme_test_render_element(&$variables) { function theme_theme_test_render_element_children($variables) { return drupal_render($variables['element']); } + +/** + * Returns HTML for a theme function suggestion test. + */ +function theme_theme_test_function_suggestions($variables) { + return 'Original theme function.'; +} diff --git a/core/modules/system/tests/modules/theme_test/theme_test.routing.yml b/core/modules/system/tests/modules/theme_test/theme_test.routing.yml index 866171e..1f3d8af 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.routing.yml +++ b/core/modules/system/tests/modules/theme_test/theme_test.routing.yml @@ -4,3 +4,21 @@ function_template_override: _content: '\Drupal\theme_test\ThemeTestController::functionTemplateOverridden' requirements: _permission: 'access content' +suggestion_alter: + pattern: '/theme-test/suggestion-alter' + defaults: + _content: '\Drupal\theme_test\ThemeTestController::suggestionAlter' + requirements: + _permission: 'access content' +specific_suggestion_alter: + pattern: '/theme-test/specific-suggestion-alter' + defaults: + _content: '\Drupal\theme_test\ThemeTestController::specificSuggestionAlter' + requirements: + _permission: 'access content' +function_suggestion_alter: + pattern: '/theme-test/function-suggestion-alter' + defaults: + _content: '\Drupal\theme_test\ThemeTestController::functionSuggestionAlter' + requirements: + _permission: 'access content' diff --git a/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant--foo.html.twig b/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant--foo.html.twig new file mode 100644 index 0000000..f0de75d --- /dev/null +++ b/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant--foo.html.twig @@ -0,0 +1,2 @@ +{# Output for Theme API test #} +Template overridden based on suggestion alter hook determined by the base hook. diff --git a/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant.html.twig b/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant.html.twig new file mode 100644 index 0000000..6e112fd --- /dev/null +++ b/core/modules/system/tests/themes/test_theme/templates/theme-test-specific-suggestions--variant.html.twig @@ -0,0 +1,2 @@ +{# Output for Theme API test #} +Template for testing specific theme calls. diff --git a/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--module-override.html.twig b/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--module-override.html.twig new file mode 100644 index 0000000..26ce57b --- /dev/null +++ b/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--module-override.html.twig @@ -0,0 +1,2 @@ +{# Output for Theme API test #} +Template overridden based on new theme suggestion provided by a module. diff --git a/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--theme-override.html.twig b/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--theme-override.html.twig new file mode 100644 index 0000000..dee829f --- /dev/null +++ b/core/modules/system/tests/themes/test_theme/templates/theme-test-suggestions--theme-override.html.twig @@ -0,0 +1,2 @@ +{# Output for Theme API test #} +Template overridden based on new theme suggestion provided by the test_theme theme. diff --git a/core/modules/system/tests/themes/test_theme/test_theme.theme b/core/modules/system/tests/themes/test_theme/test_theme.theme index 62b5abf..9b10b2b 100644 --- a/core/modules/system/tests/themes/test_theme/test_theme.theme +++ b/core/modules/system/tests/themes/test_theme/test_theme.theme @@ -29,3 +29,43 @@ function test_theme_theme_test__suggestion($variables) { function test_theme_theme_test_alter_alter(&$data) { $data = 'test_theme_theme_test_alter_alter was invoked'; } + +/** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function test_theme_theme_suggestions_theme_test_suggestions_alter(array &$suggestions, array $variables) { + // Theme alter hooks run after module alter hooks, so add this theme + // suggestion to the beginning of the array so that the suggestion added by + // the theme_suggestions_test module can be picked up when that module is + // enabled. + array_unshift($suggestions, 'theme_test_suggestions__' . 'theme_override'); +} + +/** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function test_theme_theme_suggestions_theme_test_function_suggestions_alter(array &$suggestions, array $variables) { + // Theme alter hooks run after module alter hooks, so add this theme + // suggestion to the beginning of the array so that the suggestion added by + // the theme_suggestions_test module can be picked up when that module is + // enabled. + array_unshift($suggestions, 'theme_test_function_suggestions__' . 'theme_override'); +} + +/** + * Returns HTML for a theme function suggestion test. + * + * Implements the theme_test_function_suggestions__theme_override suggestion. + */ +function test_theme_theme_test_function_suggestions__theme_override($variables) { + return 'Theme function overridden based on new theme suggestion provided by the test_theme theme.'; +} + +/** + * Returns HTML for a theme function suggestion test. + * + * Implements the theme_test_function_suggestions__module_override suggestion. + */ +function test_theme_theme_test_function_suggestions__module_override($variables) { + return 'Theme function overridden based on new theme suggestion provided by a module.'; +} diff --git a/core/modules/system/theme.api.php b/core/modules/system/theme.api.php index ebda671..4ee2afb 100644 --- a/core/modules/system/theme.api.php +++ b/core/modules/system/theme.api.php @@ -158,6 +158,44 @@ function hook_preprocess_HOOK(&$variables) { } /** + * Alters theme suggestions. + * + * Provide alternative theme function or template suggestions for theme hooks. + * + * @param array $suggestions + * An array of theme suggestions. + * @param array $variables + * An array of variables passed to the theme hook. Note that this hook is + * invoked before any preprocessing. + * @param string $hook + * The theme hook being called. + * + * @see hook_theme_suggestions_HOOK_alter() + */ +function hook_theme_suggestions_alter(array &$suggestions, array $variables, $hook) { + if ($hook == 'node') { + $suggestions[] = 'node__' . $variables['elements']['#langcode']; + } +} + +/** + * Alters theme suggestions for a specific theme hook. + * + * @param array $suggestions + * An array of theme suggestions. + * @param array $variables + * An array of variables passed to the theme hook. Note that this hook is + * invoked before any preprocessing. + * + * @see hook_theme_suggestions_alter() + */ +function hook_theme_suggestions_HOOK_alter(array &$suggestions, array $variables) { + if (empty($variables['header'])) { + $suggestions[] = 'hookname__' . 'no_header'; + } +} + +/** * Respond to themes being enabled. * * @param array $theme_list diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index b654e0d..f30682a 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -435,6 +435,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(array &$suggestions, array $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. @@ -475,9 +485,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 2165f67..bd7c335 100644 --- a/core/modules/views/views.module +++ b/core/modules/views/views.module @@ -241,15 +241,10 @@ function views_preprocess_node(&$variables) { // The 'view' attribute of the node is added in views_preprocess_node() if (!empty($variables['node']->view) && $variables['node']->view->storage->id()) { $variables['view'] = $variables['node']->view; - $variables['theme_hook_suggestions'][] = 'node__view__' . $variables['node']->view->storage->id(); - if (!empty($variables['node']->view->current_display)) { - $variables['theme_hook_suggestions'][] = 'node__view__' . $variables['node']->view->storage->id() . '__' . $variables['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 ($variables['page'] && $variables['view_mode'] == 'full' && !$variables['view']->display_handler->hasPath()) { - $variables['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 ($variables['page'] && $variables['view_mode'] == 'full' && !$variables['view']->display_handler->hasPath()) { + $variables['page'] = FALSE; } } @@ -260,6 +255,19 @@ function views_preprocess_node(&$variables) { } /** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function views_theme_suggestions_node_alter(array &$suggestions, array $variables) { + $node = $variables['elements']['#node']; + if (!empty($node->view) && $node->view->storage->id()) { + $suggestions[] = 'node__view__' . $node->view->storage->id(); + if (!empty($node->view->current_display)) { + $suggestions[] = 'node__view__' . $node->view->storage->id() . '__' . $node->view->current_display; + } + } +} + +/** * A theme preprocess function to automatically allow view-based node * templates if called from a view. */ @@ -267,9 +275,18 @@ function views_preprocess_comment(&$variables) { // The 'view' attribute of the node is added in template_preprocess_views_view_row_comment() if (!empty($variables['comment']->view) && $variables['comment']->view->storage->id()) { $variables['view'] = &$variables['comment']->view; - $variables['theme_hook_suggestions'][] = 'comment__view__' . $variables['comment']->view->storage->id(); - if (!empty($variables['node']->view->current_display)) { - $variables['theme_hook_suggestions'][] = 'comment__view__' . $variables['comment']->view->storage->id() . '__' . $variables['comment']->view->current_display; + } +} + +/** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function views_theme_suggestions_comment_alter(array &$suggestions, array $variables) { + $comment = $variables['elements']['#comment']; + if (!empty($comment->view) && $comment->view->storage->id()) { + $suggestions[] = 'comment__view__' . $comment->view->storage->id(); + if (!empty($comment->view->current_display)) { + $suggestions[] = 'comment__view__' . $comment->view->storage->id() . '__' . $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 acd4573..d4f5dcb 100644 --- a/core/modules/views_ui/views_ui.theme.inc +++ b/core/modules/views_ui/views_ui.theme.inc @@ -473,5 +473,11 @@ function template_preprocess_views_ui_view_preview_section(&$variables) { ); $variables['links'] = $build; } - $variables['theme_hook_suggestions'][] = 'views_ui_view_preview_section__' . $variables['section']; +} + +/** + * Implements hook_theme_suggestions_HOOK_alter(). + */ +function views_ui_theme_suggestions_views_ui_view_preview_section_alter(array &$suggestions, array $variables) { + $suggestions[] = 'views_ui_view_preview_section__' . $variables['section']; }