Issue #1751194 by Cottser, mikl, effulgentsia, benjifisher: Introduce hook_theme_suggestions_HOOK() and hook_theme_suggestions_HOOK_alter().

8.0.x
Nathaniel Catchpole 2013-09-29 08:19:59 +01:00
parent 4bdcb12bf9
commit 7e163dbb19
29 changed files with 699 additions and 143 deletions

View File

@ -862,17 +862,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
@ -1002,11 +1001,41 @@ 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 later use. For example if '#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'])) {
$base_theme_hook = $info['base hook'];
}
else {
$base_theme_hook = $hook;
}
// Invoke hook_theme_suggestions_HOOK().
$suggestions = Drupal::moduleHandler()->invokeAll('theme_suggestions_' . $base_theme_hook, array($variables));
// If theme() was invoked with a direct theme suggestion like
// '#theme' => 'node__article', add it to the suggestions array before
// invoking suggestion alter hooks.
if (isset($info['base hook'])) {
$suggestions[] = $hook;
}
// Allow suggestions to be altered via hook_theme_suggestions_HOOK_alter().
Drupal::moduleHandler()->alter('theme_suggestions_' . $base_theme_hook, $suggestions, $variables);
// 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];
@ -1017,44 +1046,20 @@ function theme($hook, $variables = array()) {
include_once DRUPAL_ROOT . '/' . $include_file;
}
}
// Replace the preprocess functions with those from the base hook.
if (isset($base_hook_info['preprocess functions'])) {
$variables['theme_hook_suggestion'] = $hook;
$hook = $base_hook;
$info = $base_hook_info;
// Set a variable for the 'theme_hook_suggestion'. This is used to
// maintain backwards compatibility with template engines.
$theme_hook_suggestion = $hook;
$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.
@ -1117,6 +1122,16 @@ function theme($hook, $variables = array()) {
if (isset($info['path'])) {
$template_file = $info['path'] . '/' . $template_file;
}
// Add the theme suggestions to the variables array just before rendering
// the template for backwards compatibility with template engines.
$variables['theme_hook_suggestions'] = $suggestions;
// For backwards compatibility, pass 'theme_hook_suggestion' on to the
// template engine. This is only set when calling a direct suggestion like
// '#theme' => 'menu_tree__shortcut_default' when the template exists in the
// current theme.
if (isset($theme_hook_suggestion)) {
$variables['theme_hook_suggestion'] = $theme_hook_suggestion;
}
$output = $render_function($template_file, $variables);
}
@ -2593,11 +2608,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.
@ -2706,11 +2716,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(
@ -2731,9 +2736,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().
@ -2747,9 +2753,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 = '__') {
@ -2923,12 +2928,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
@ -2989,7 +2988,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'];
}
/**

View File

@ -24,6 +24,11 @@ class UpdateModuleHandler extends ModuleHandler {
if (substr($hook, -6) === '_alter') {
return array();
}
// theme() is called during updates and fires hooks, so whitelist the
// system module.
if (substr($hook, 0, 6) == 'theme_') {
return array('system');
}
switch ($hook) {
// hook_requirements is necessary for updates to work.
case 'requirements':

View File

@ -484,6 +484,39 @@ function block_rebuild() {
}
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function block_theme_suggestions_block(array $variables) {
$suggestions = array();
$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;
}
return $suggestions;
}
/**
* Prepares variables for block templates.
*
@ -527,29 +560,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;
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* @file
* Contains \Drupal\block\Tests\BlockPreprocessUnitTest.
*/
namespace Drupal\block\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Unit tests for template_preprocess_block().
*/
class BlockPreprocessUnitTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('block');
public static function getInfo() {
return array(
'name' => '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:admin',
'region' => 'footer',
'id' => \Drupal::config('system.theme')->get('default') . '.machinename',
));
$variables = array();
$variables['elements']['#block'] = $block;
$variables['elements']['#configuration'] = $block->getPlugin()->getConfiguration();
$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['content_attributes']['class'], array('test-class', 'content'), 'Default .content class added to block content_attributes');
}
}

View File

@ -10,7 +10,7 @@ namespace Drupal\block\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Unit tests for template_preprocess_block().
* Unit tests for block_theme_suggestions_block().
*/
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() function.',
'group' => 'Block',
);
}
/**
* Test if template_preprocess_block() handles the suggestions right.
* Tests template suggestions from block_theme_suggestions_block().
*/
function testBlockThemeHookSuggestions() {
// Define a block with a derivative to be preprocessed, which includes both
@ -48,11 +48,8 @@ class BlockTemplateSuggestionsUnitTest extends WebTestBase {
$variables['elements']['#configuration'] = $block->getPlugin()->getConfiguration();
$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__admin', 'block__machinename'));
$this->assertEqual($variables['content_attributes']['class'], array('test-class', 'content'), 'Default .content class added to block content_attributes');
$suggestions = block_theme_suggestions_block($variables);
$this->assertEqual($suggestions, array('block__system', 'block__system_menu_block', 'block__system_menu_block__admin', 'block__machinename'));
}
}

View File

@ -659,6 +659,22 @@ function field_page_build(&$page) {
$page['#attached']['css'][$path . '/css/field.module.css'] = array('every_page' => TRUE);
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function field_theme_suggestions_field(array $variables) {
$suggestions = array();
$element = $variables['element'];
$suggestions[] = 'field__' . $element['#field_type'];
$suggestions[] = 'field__' . $element['#field_name'];
$suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#bundle'];
$suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#field_name'];
$suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#field_name'] . '__' . $element['#bundle'];
return $suggestions;
}
/**
* Prepares variables for field templates.
*
@ -716,15 +732,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['#entity_type'] . '__' . $element['#bundle'],
'field__' . $element['#entity_type'] . '__' . $element['#field_name'],
'field__' . $element['#entity_type'] . '__' . $element['#field_name'] . '__' . $element['#bundle'],
);
static $default_attributes;
if (!isset($default_attributes)) {
$default_attributes = new Attribute;

View File

@ -588,6 +588,33 @@ function forum_preprocess_block(&$variables) {
}
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function forum_theme_suggestions_forums(array $variables) {
$suggestions = array();
$tid = $variables['term']->id();
// 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__' . $tid;
$suggestions[] = 'forums__containers__' . $tid;
}
elseif (!$variables['forums'] && $variables['topics']) {
$suggestions[] = 'forums__topics';
$suggestions[] = 'forums__' . $tid;
$suggestions[] = 'forums__topics__' . $tid;
}
else {
$suggestions[] = 'forums__' . $tid;
}
return $suggestions;
}
/**
* Prepares variables for forums templates.
*
@ -635,23 +662,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();

View File

@ -641,6 +641,19 @@ function node_preprocess_block(&$variables) {
}
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function node_theme_suggestions_node(array $variables) {
$suggestions = array();
$node = $variables['elements']['#node'];
$suggestions[] = 'node__' . $node->bundle();
$suggestions[] = 'node__' . $node->id();
return $suggestions;
}
/**
* Prepares variables for node templates.
*
@ -730,11 +743,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';
}

View File

@ -74,6 +74,13 @@ function search_view($plugin_id = NULL, $keys = '') {
return $build;
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function search_theme_suggestions_search_results(array $variables) {
return array('search_results__' . $variables['plugin_id']);
}
/**
* Prepares variables for search results templates.
*
@ -100,7 +107,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['plugin_id'];
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function search_theme_suggestions_search_result(array $variables) {
return array('search_result__' . $variables['plugin_id']);
}
/**
@ -148,7 +161,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['plugin_id'];
}
/**

View File

@ -0,0 +1,120 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Theme\ThemeSuggestionsAlterTest.
*/
namespace Drupal\system\Tests\Theme;
use Drupal\simpletest\WebTestBase;
/**
* Tests theme suggestion alter hooks.
*/
class ThemeSuggestionsAlterTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('theme_test');
public static function getInfo() {
return array(
'name' => 'Theme suggestions alter',
'description' => 'Test theme suggestion alter hooks.',
'group' => 'Theme',
);
}
function setUp() {
parent::setUp();
theme_enable(array('test_theme'));
}
/**
* Tests that hooks to provide theme suggestions work.
*/
function testTemplateSuggestions() {
$this->drupalGet('theme-test/suggestion-provided');
$this->assertText('Template for testing suggestions provided by the module declaring the theme hook.');
// Enable test_theme, it contains a template suggested by theme_test.module
// in theme_test_theme_suggestions_theme_test_suggestion_provided().
config('system.theme')
->set('default', 'test_theme')
->save();
$this->drupalGet('theme-test/suggestion-provided');
$this->assertText('Template overridden based on suggestion provided by the module declaring the theme hook.');
}
/**
* 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.
\Drupal::moduleHandler()->install(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() {
// Test that the default template is rendered.
$this->drupalGet('theme-test/specific-suggestion-alter');
$this->assertText('Template for testing specific theme calls.');
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 matching the specific theme call.');
$this->assertText('theme_test_specific_suggestions__variant', 'Specific theme call is added to the suggestions array.');
// Ensure that the base hook is used to determine the suggestion alter hook.
\Drupal::moduleHandler()->install(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.');
$this->assertTrue(strpos($this->drupalGetContent(), 'theme_test_specific_suggestions__variant') < strpos($this->drupalGetContent(), 'theme_test_specific_suggestions__variant__foo'), 'Specific theme call is added to the suggestions array before the suggestions alter 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.
\Drupal::moduleHandler()->install(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.');
}
}

View File

@ -909,6 +909,54 @@ function system_menu() {
return $items;
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function system_theme_suggestions_html(array $variables) {
return theme_get_suggestions(arg(), 'html');
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function system_theme_suggestions_page(array $variables) {
return theme_get_suggestions(arg(), 'page');
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function system_theme_suggestions_maintenance_page(array $variables) {
$suggestions = array();
// 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';
}
return $suggestions;
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function system_theme_suggestions_region(array $variables) {
$suggestions = array();
if (!empty($variables['elements']['#region'])) {
$suggestions[] = 'region__' . $variables['elements']['#region'];
}
return $suggestions;
}
/**
* Theme callback for the default batch page.
*/

View File

@ -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

View File

@ -0,0 +1,27 @@
<?php
/**
* @file
* Support module for testing theme suggestions.
*/
/**
* Implements hook_theme_suggestions_HOOK_alter().
*/
function theme_suggestions_test_theme_suggestions_theme_test_suggestions_alter(array &$suggestions, array $variables) {
$suggestions[] = 'theme_test_suggestions__' . 'module_override';
}
/**
* Implements hook_theme_suggestions_HOOK_alter().
*/
function theme_suggestions_test_theme_suggestions_theme_test_function_suggestions_alter(array &$suggestions, array $variables) {
$suggestions[] = 'theme_test_function_suggestions__' . 'module_override';
}
/**
* Implements hook_theme_suggestions_HOOK_alter().
*/
function theme_suggestions_test_theme_suggestions_theme_test_specific_suggestions_alter(array &$suggestions, array $variables) {
$suggestions[] = 'theme_test_specific_suggestions__' . 'variant__foo';
}

View File

@ -92,4 +92,32 @@ class ThemeTestController extends ControllerBase {
return $GLOBALS['theme_test_output'];
}
/**
* Menu callback for testing suggestion alter hooks with template files.
*/
function suggestionProvided() {
return array('#theme' => 'theme_test_suggestion_provided');
}
/**
* Menu callback for testing suggestion alter hooks with template files.
*/
function suggestionAlter() {
return array('#theme' => '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');
}
}

View File

@ -0,0 +1,2 @@
{# Output for Theme API test #}
Template for testing specific theme calls.

View File

@ -0,0 +1,2 @@
{# Output for Theme API test #}
Template for testing suggestions provided by the module declaring the theme hook.

View File

@ -0,0 +1,2 @@
{# Output for Theme API test #}
Original template.

View File

@ -14,6 +14,21 @@ function theme_test_theme($existing, $type, $theme, $path) {
$items['theme_test_template_test_2'] = array(
'template' => 'theme_test.template_test',
);
$items['theme_test_suggestion_provided'] = array(
'template' => 'theme-test-suggestion-provided',
'variables' => array(),
);
$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),
);
@ -131,3 +146,17 @@ 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.';
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function theme_test_theme_suggestions_theme_test_suggestion_provided(array $variables) {
return array('theme_test_suggestion_provided__' . 'foo');
}

View File

@ -41,3 +41,31 @@ theme_test.request_listener:
_content: '\Drupal\theme_test\ThemeTestController::testRequestListener'
requirements:
_access: 'TRUE'
suggestion_alter:
path: '/theme-test/suggestion-alter'
defaults:
_content: '\Drupal\theme_test\ThemeTestController::suggestionAlter'
requirements:
_permission: 'access content'
suggestion_provided:
path: '/theme-test/suggestion-provided'
defaults:
_content: '\Drupal\theme_test\ThemeTestController::suggestionProvided'
requirements:
_permission: 'access content'
specific_suggestion_alter:
path: '/theme-test/specific-suggestion-alter'
defaults:
_content: '\Drupal\theme_test\ThemeTestController::specificSuggestionAlter'
requirements:
_permission: 'access content'
function_suggestion_alter:
path: '/theme-test/function-suggestion-alter'
defaults:
_content: '\Drupal\theme_test\ThemeTestController::functionSuggestionAlter'
requirements:
_permission: 'access content'

View File

@ -0,0 +1,5 @@
{# Output for Theme API test #}
Template overridden based on suggestion alter hook determined by the base hook.
<p>Theme hook suggestions:
{{ theme_hook_suggestions|join("<br />") }}</p>

View File

@ -0,0 +1,5 @@
{# Output for Theme API test #}
Template matching the specific theme call.
<p>Theme hook suggestions:
{{ theme_hook_suggestions|join("<br />") }}</p>

View File

@ -0,0 +1,2 @@
{# Output for Theme API test #}
Template overridden based on suggestion provided by the module declaring the theme hook.

View File

@ -0,0 +1,2 @@
{# Output for Theme API test #}
Template overridden based on new theme suggestion provided by a module.

View File

@ -0,0 +1,2 @@
{# Output for Theme API test #}
Template overridden based on new theme suggestion provided by the test_theme theme.

View File

@ -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.';
}

View File

@ -157,6 +157,67 @@ function hook_preprocess_HOOK(&$variables) {
$variables['attributes']['typeof'] = array('foaf:Image');
}
/**
* Provides alternate named suggestions for a specific theme hook.
*
* This hook allows the module implementing hook_theme() for a theme hook to
* provide alternative theme function or template name suggestions. This hook is
* only invoked for the first module implementing hook_theme() for a theme hook.
*
* HOOK is the least-specific version of the hook being called. For example, if
* '#theme' => 'node__article' is called, then node_theme_suggestions_node()
* will be invoked, not node_theme_suggestions_node__article(). The specific
* hook called (in this case 'node__article') is available in
* $variables['theme_hook_original'].
*
* @todo Add @code sample.
*
* @param array $variables
* An array of variables passed to the theme hook. Note that this hook is
* invoked before any preprocessing.
*
* @return array
* An array of theme suggestions.
*
* @see hook_theme_suggestions_HOOK_alter()
*/
function hook_theme_suggestions_HOOK(array $variables) {
$suggestions = array();
$suggestions[] = 'node__' . $variables['elements']['#langcode'];
return $suggestions;
}
/**
* Alters named suggestions for a specific theme hook.
*
* This hook allows any module or theme to provide altenative theme function or
* template name suggestions and reorder or remove suggestions provided by
* hook_theme_suggestions_HOOK() or by earlier invocations of this hook.
*
* HOOK is the least-specific version of the hook being called. For example, if
* '#theme' => 'node__article' is called, then node_theme_suggestions_node()
* will be invoked, not node_theme_suggestions_node__article(). The specific
* hook called (in this case 'node__article') is available in
* $variables['theme_hook_original'].
*
* @todo Add @code sample.
*
* @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_HOOK()
*/
function hook_theme_suggestions_HOOK_alter(array &$suggestions, array $variables) {
if (empty($variables['header'])) {
$suggestions[] = 'hookname__' . 'no_header';
}
}
/**
* Respond to themes being enabled.
*

View File

@ -406,6 +406,20 @@ function taxonomy_term_view_multiple(array $terms, $view_mode = 'full', $langcod
return entity_view_multiple($terms, $view_mode, $langcode);
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function taxonomy_theme_suggestions_taxonomy_term(array $variables) {
$suggestions = array();
$term = $variables['elements']['#term'];
$suggestions[] = 'taxonomy_term__' . $term->bundle();
$suggestions[] = 'taxonomy_term__' . $term->id();
return $suggestions;
}
/**
* Prepares variables for taxonomy term templates.
*
@ -447,9 +461,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();
}
/**

View File

@ -261,15 +261,13 @@ function views_preprocess_node(&$variables) {
// \Drupal\views\Plugin\views\row\EntityRow::preRender().
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 (!empty($variables['view']->current_display)
&& $variables['page']
&& $variables['view_mode'] == 'full'
&& !$variables['view']->display_handler->hasPath()) {
$variables['page'] = FALSE;
}
}
@ -279,6 +277,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.
@ -288,9 +299,18 @@ function views_preprocess_comment(&$variables) {
// \Drupal\views\Plugin\views\row\EntityRow::preRender().
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;
}
}
}

View File

@ -483,5 +483,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().
*/
function views_ui_theme_suggestions_views_ui_view_preview_section(array $variables) {
return array('views_ui_view_preview_section__' . $variables['section']);
}