Commit 7e163dbb authored by catch's avatar catch

Issue #1751194 by Cottser, mikl, effulgentsia, benjifisher: Introduce...

Issue #1751194 by Cottser, mikl, effulgentsia, benjifisher: Introduce hook_theme_suggestions_HOOK() and hook_theme_suggestions_HOOK_alter().
parent 4bdcb12b
This diff is collapsed.
......@@ -24,6 +24,11 @@ public function getImplementations($hook) {
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':
......
......@@ -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;
}
}
......
<?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');
}
}
......@@ -10,7 +10,7 @@
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 @@ function testBlockThemeHookSuggestions() {
$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'));
}
}
......@@ -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;
......
......@@ -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();
......
......@@ -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';
}
......
......@@ -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'];
}
/**
......
<?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.');
}
}
......@@ -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.
*/
......
name: 'Theme suggestions test'
type: module
description: 'Support module for testing theme suggestions.'
package: Testing
version: VERSION
core: 8.x
hidden: true
<?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';
}
......@@ -92,4 +92,32 @@ public function testRequestListener() {
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');
}
}
{# Output for Theme API test #}
Template for testing specific theme calls.
{# Output for Theme API test #}
Template for testing suggestions provided by the module declaring the theme hook.
......@@ -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(