Commit 33d4ef88 authored by Dries's avatar Dries

- Patch #1290694 by hefox, tlattimore, c4rl, chx, tim.plunkett, tstoeckler,...

- Patch #1290694 by hefox, tlattimore, c4rl, chx, tim.plunkett, tstoeckler, msonnabaum: provide consistency for attributes and classes arrays provided by template_preprocess().
parent 68926b7e
......@@ -4,6 +4,7 @@
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Drupal\Core\Database\Database;
use Drupal\Core\Template\Attribute;
/**
* @file
......@@ -2271,11 +2272,7 @@ function drupal_http_header_attributes(array $attributes = array()) {
* @ingroup sanitization
*/
function drupal_attributes(array $attributes = array()) {
foreach ($attributes as $attribute => &$data) {
$data = implode(' ', (array) $data);
$data = $attribute . '="' . check_plain($data) . '"';
}
return $attributes ? ' ' . implode(' ', $attributes) : '';
return new Attribute($attributes);
}
/**
......
......@@ -2312,7 +2312,7 @@ function _theme_table_cell($cell, $header = FALSE) {
*/
function template_preprocess(&$variables, $hook) {
global $user;
static $count = array();
static $count = array(), $default_attributes;
// Track run count for each hook to provide zebra striping.
// See "template_preprocess_block()" which provides the same feature specific to blocks.
......@@ -2323,9 +2323,6 @@ function template_preprocess(&$variables, $hook) {
// Tell all templates where they are located.
$variables['directory'] = path_to_theme();
// Initialize html class attribute for the current hook.
$variables['classes_array'] = array(drupal_html_class($hook));
// Merge in variables that don't depend on hook and don't change during a
// single page request.
// Use the advanced drupal_static() pattern, since this is called very often.
......@@ -2340,7 +2337,17 @@ function template_preprocess(&$variables, $hook) {
if (!isset($default_variables) || ($user !== $default_variables['user'])) {
$default_variables = _template_preprocess_default_variables();
}
$variables += $default_variables;
if (!isset($default_attributes)) {
$default_attributes = drupal_attributes(array('class' => array()));
}
$variables += $default_variables + array(
'attributes' => clone $default_attributes,
'title_attributes' => clone $default_attributes,
'content_attributes' => clone $default_attributes,
);
// Initialize html class attribute for the current hook.
$variables['attributes']['class'][] = drupal_html_class($hook);
}
/**
......@@ -2351,9 +2358,6 @@ function _template_preprocess_default_variables() {
// Variables that don't depend on a database connection.
$variables = array(
'attributes_array' => array(),
'title_attributes_array' => array(),
'content_attributes_array' => array(),
'title_prefix' => array(),
'title_suffix' => array(),
'user' => $user,
......@@ -2384,34 +2388,6 @@ function _template_preprocess_default_variables() {
return $variables;
}
/**
* Adds helper variables derived from variables defined during preprocessing.
*
* When preparing variables for a theme hook implementation, all 'preprocess'
* functions run first, then all 'process' functions (see theme() for details
* about the full sequence).
*
* This function serializes array variables manipulated during the preprocessing
* phase into strings for convenient use by templates. As with
* template_preprocess(), this function does not get called for theme hooks
* implemented as functions.
*
* @see theme()
* @see template_preprocess()
*/
function template_process(&$variables, $hook) {
// Flatten out classes.
$variables['classes'] = implode(' ', $variables['classes_array']);
// Flatten out attributes, title_attributes, and content_attributes.
// Because this function can be called very often, and often with empty
// attributes, optimize performance by only calling drupal_attributes() if
// necessary.
$variables['attributes'] = $variables['attributes_array'] ? drupal_attributes($variables['attributes_array']) : '';
$variables['title_attributes'] = $variables['title_attributes_array'] ? drupal_attributes($variables['title_attributes_array']) : '';
$variables['content_attributes'] = $variables['content_attributes_array'] ? drupal_attributes($variables['content_attributes_array']) : '';
}
/**
* Preprocess variables for html.tpl.php
*
......@@ -2424,22 +2400,24 @@ function template_preprocess_html(&$variables) {
// Compile a list of classes that are going to be applied to the body element.
// This allows advanced theming based on context (home page, node of certain type, etc.).
// Add a class that tells us whether we're on the front page or not.
$variables['classes_array'][] = $variables['is_front'] ? 'front' : 'not-front';
$variables['attributes']['class'][] = $variables['is_front'] ? 'front' : 'not-front';
// Add a class that tells us whether the page is viewed by an authenticated user or not.
$variables['classes_array'][] = $variables['logged_in'] ? 'logged-in' : 'not-logged-in';
$variables['attributes']['class'][] = $variables['logged_in'] ? 'logged-in' : 'not-logged-in';
// Add information about the number of sidebars.
if (!empty($variables['page']['sidebar_first']) && !empty($variables['page']['sidebar_second'])) {
$variables['classes_array'][] = 'two-sidebars';
$variables['attributes']['class'][] = 'two-sidebars';
}
elseif (!empty($variables['page']['sidebar_first'])) {
$variables['classes_array'][] = 'one-sidebar sidebar-first';
$variables['attributes']['class'][] = 'one-sidebar';
$variables['attributes']['class'][] = 'sidebar-first';
}
elseif (!empty($variables['page']['sidebar_second'])) {
$variables['classes_array'][] = 'one-sidebar sidebar-second';
$variables['attributes']['class'][] = 'one-sidebar';
$variables['attributes']['class'][] = 'sidebar-second';
}
else {
$variables['classes_array'][] = 'no-sidebars';
$variables['attributes']['class'][] = 'no-sidebars';
}
// Populate the body classes.
......@@ -2449,23 +2427,22 @@ function template_preprocess_html(&$variables) {
// Add current suggestion to page classes to make it possible to theme
// the page depending on the current page type (e.g. node, admin, user,
// etc.) as well as more specific data like node-12 or node-edit.
$variables['classes_array'][] = drupal_html_class($suggestion);
$variables['attributes']['class'][] = drupal_html_class($suggestion);
}
}
}
// If on an individual node page, add the node type to body classes.
if ($node = menu_get_object()) {
$variables['classes_array'][] = drupal_html_class('node-type-' . $node->type);
$variables['attributes']['class'][] = drupal_html_class('node-type-' . $node->type);
}
// Initializes attributes which are specific to the html and body elements.
$variables['html_attributes_array'] = array();
$variables['body_attributes_array'] = array();
$variables['html_attributes'] = drupal_attributes();
// HTML element attributes.
$variables['html_attributes_array']['lang'] = $language_interface->langcode;
$variables['html_attributes_array']['dir'] = $language_interface->direction ? 'rtl' : 'ltr';
$variables['html_attributes']['lang'] = $language_interface->langcode;
$variables['html_attributes']['dir'] = $language_interface->direction ? 'rtl' : 'ltr';
// Add favicon.
if (theme_get_setting('toggle_favicon')) {
......@@ -2633,10 +2610,6 @@ function template_process_page(&$variables) {
* @see html.tpl.php
*/
function template_process_html(&$variables) {
// Flatten out html_attributes and body_attributes.
$variables['html_attributes'] = drupal_attributes($variables['html_attributes_array']);
$variables['body_attributes'] = drupal_attributes($variables['body_attributes_array']);
// Render page_top and page_bottom into top level variables.
$variables['page_top'] = drupal_render($variables['page']['page_top']);
$variables['page_bottom'] = drupal_render($variables['page']['page_bottom']);
......@@ -2654,8 +2627,8 @@ function template_process_html(&$variables) {
* Generate an array of suggestions from path arguments.
*
* This is typically called for adding to the 'theme_hook_suggestions' or
* 'classes_array' variables from within preprocess functions, when wanting to
* base the additional suggestions on the path of the current page.
* 'attributes' class key variables from within preprocess functions, when
* wanting to base the additional suggestions on the path of the current page.
*
* @param $args
* An array of path arguments, such as from function arg().
......@@ -2670,7 +2643,7 @@ function template_process_html(&$variables) {
* @return
* An array of suggestions, suitable for adding to
* $variables['theme_hook_suggestions'] within a preprocess function or to
* $variables['classes_array'] if the suggestions represent extra CSS classes.
* $variables['attributes']['class'] if the suggestions represent extra CSS classes.
*/
function theme_get_suggestions($args, $base, $delimiter = '__') {
......@@ -2800,18 +2773,19 @@ function template_preprocess_maintenance_page(&$variables) {
$variables['title'] = drupal_get_title();
// Compile a list of classes that are going to be applied to the body element.
$variables['classes_array'][] = 'in-maintenance';
$variables['attributes']['class'][] = 'in-maintenance';
if (isset($variables['db_is_active']) && !$variables['db_is_active']) {
$variables['classes_array'][] = 'db-offline';
$variables['attributes']['class'][] = 'db-offline';
}
if ($variables['layout'] == 'both') {
$variables['classes_array'][] = 'two-sidebars';
$variables['attributes']['class'][] = 'two-sidebars';
}
elseif ($variables['layout'] == 'none') {
$variables['classes_array'][] = 'no-sidebars';
$variables['attributes']['class'][] = 'no-sidebars';
}
else {
$variables['classes_array'][] = 'one-sidebar sidebar-' . $variables['layout'];
$variables['attributes']['class'][] = 'one-sidebar';
$variables['attributes']['class'][] = 'sidebar-' . $variables['layout'];
}
// Dead databases will show error messages so supplying this template will
......@@ -2849,6 +2823,6 @@ function template_preprocess_region(&$variables) {
$variables['content'] = $variables['elements']['#children'];
$variables['region'] = $variables['elements']['#region'];
$variables['classes_array'][] = drupal_region_class($variables['region']);
$variables['attributes']['class'][] = drupal_region_class($variables['region']);
$variables['theme_hook_suggestions'][] = 'region__' . $variables['region'];
}
<?php
/**
* @file
* Definition of Drupal\Core\Template\Attribute.
*/
namespace Drupal\Core\Template;
use ArrayAccess;
use IteratorAggregate;
/**
* A class that can be used for collecting then rendering HTML attributtes.
*
* To use, one may both pass in an array of already defined attributes and
* add attributes to it like using array syntax.
* @code
* $attributes = new Attribute(array('id' => 'socks'));
* $attributes['class'] = array('black-cat', 'white-cat');
* $attributes['class'][] = 'black-white-cat';
* echo '<cat ' . $attributes . '>';
* // Produces <cat id="socks" class="black-cat white-cat black-white-cat">
* @endcode
*
* individual parts of the attribute may be printed first.
* @code
* $attributes = new Attribute(array('id' => 'socks'));
* $attributes['class'] = array('black-cat', 'white-cat');
* $attributes['class'][] = 'black-white-cat';
* echo '<cat class="cat ' . $attributes['class'] . '" ' . $attributes . '>';
* // Produces <cat class="cat black-cat white-cat black-white-cat" id="socks">
* @endcode
*
* Most of the time this object is not created directly, but
* instantiated by drupal_attributes().
*
* @see drupal_attributes()
*/
class Attribute implements ArrayAccess, IteratorAggregate {
/**
* Stores the attribute data.
*
* @var array
*/
protected $storage = array();
/**
* Constructs a \Drupal\Core\Template\Attribute object.
*
* @param array $attributes
* An associative array of key-value pairs to be converted to attributes.
*/
public function __construct($attributes = array()) {
foreach ($attributes as $name => $value) {
$this->offsetSet($name, $value);
}
}
/**
* Implements ArrayAccess::offsetGet().
*/
public function offsetGet($name) {
if (isset($this->storage[$name])) {
return $this->storage[$name];
}
}
/**
* Implements ArrayAccess::offsetSet().
*/
public function offsetSet($name, $value) {
if (is_array($value)) {
$value = new AttributeArray($name, $value);
}
elseif (is_bool($value)) {
$value = new AttributeBoolean($name, $value);
}
elseif (!is_object($value)) {
$value = new AttributeString($name, $value);
}
// The $name could be NULL.
if (isset($name)) {
$this->storage[$name] = $value;
}
else {
$this->storage[] = $value;
}
}
/**
* Implements ArrayAccess::offsetUnset().
*/
public function offsetUnset($name) {
unset($this->storage[$name]);
}
/**
* Implements ArrayAccess::offsetExists().
*/
public function offsetExists($name) {
return isset($this->storage[$name]);
}
/**
* Implements the magic __toString() method.
*/
public function __toString() {
$return = '';
foreach ($this->storage as $name => $value) {
if (!$value->printed()) {
$rendered = is_object($value) ? $value->render() : (check_plain($name) . ' = "' . check_plain($value) . '"');
if ($rendered) {
$return .= " $rendered";
}
}
}
return $return;
}
/**
* Implements the magic __clone() method.
*/
public function __clone() {
foreach ($this->storage as $name => $value) {
if (is_object($value)) {
$this->storage[$name] = clone $value;
}
}
}
/**
* Implements IteratorAggregate::getIterator().
*/
public function getIterator() {
return new ArrayIterator($this->storage);
}
/**
* Returns the whole array.
*/
public function value() {
return $this->value;
}
}
<?php
/**
* @file
* Definition of Drupal\Core\Template\AttributeArray.
*/
namespace Drupal\Core\Template;
use ArrayAccess;
use IteratorAggregate;
/**
* A class that defines a type of Attribute that can be added to as an array.
*
* To use with Attribute, the array must be specified.
* Correct:
* @code
* $attributes = new Attribute(array());
* $attributes['class'] = array();
* $attributes['class'][] = 'cat';
* @endcode
* Incorrect:
* @code
* $attributes = new Attribute(array());
* $attributes['class'][] = 'cat';
* @endcode
*
* @see Drupal\Core\Template\Attribute
*/
class AttributeArray extends AttributeValueBase implements ArrayAccess, IteratorAggregate {
/**
* Implements ArrayAccess::offsetGet().
*/
public function offsetGet($offset) {
return $this->value[$offset];
}
/**
* Implements ArrayAccess::offsetSet().
*/
public function offsetSet($offset, $value) {
if (isset($offset)) {
$this->value[$offset] = $value;
}
else {
$this->value[] = $value;
}
}
/**
* Implements ArrayAccess::offsetUnset().
*/
public function offsetUnset($offset) {
unset($this->value[$offset]);
}
/**
* Implements ArrayAccess::offsetExists().
*/
public function offsetExists($offset) {
return isset($this->value[$offset]);
}
/**
* Implements the magic __toString() method.
*/
public function __toString() {
$this->printed = TRUE;
return implode(' ', array_map('check_plain', $this->value));
}
/**
* Implements IteratorAggregate::getIterator().
*/
public function getIterator() {
return new ArrayIterator($this->value);
}
/**
* Returns the whole array.
*/
public function value() {
return $this->value;
}
}
<?php
/**
* @file
* Definition of Drupal\Core\Template\AttributeBoolean.
*/
namespace Drupal\Core\Template;
/**
* A class that defines a type of boolean HTML attribute.
*
* Boolean HTML attributes are not attributes with values of TRUE/FALSE.
* They are attributes that if they exist in the tag, they are TRUE.
* Examples include selected, disabled, checked, readonly.
*
* To set a boolean attribute on the Attribute class, set it to TRUE.
* @code
* $attributes = new Attribute(array());
* $attributes['disabled'] = TRUE;
* echo '<select ' . $attributes . '/>';
* // produces <select disabled>;
* $attributes['disabled'] = FALSE;
* echo '<select ' . $attributes . '/>';
* // produces <select>;
* @endcode
*
* @see Drupal\Core\Template\Attribute
*/
class AttributeBoolean extends AttributeValueBase {
/**
* Overrides AttributeValueBase::render().
*/
public function render() {
return $this->__toString();
}
/**
* Implements the magic __toString() method.
*/
public function __toString() {
$this->printed = TRUE;
return $this->value === FALSE ? '' : check_plain($this->name);
}
}
<?php
/**
* @file
* Definition of Drupal\Core\Template\AttributeString.
*/
namespace Drupal\Core\Template;
/**
* A class that represents most standard HTML attributes.
*
* To use with the Attribute class, set the key to be the attribute name
* and the value the attribute value.
* @code
* $attributes = new Attribute(array());
* $attributes['id'] = 'socks';
* $attributes['style'] = 'background-color:white';
* echo '<cat ' . $attributes . '>';
* // Produces: <cat id="socks" style="background-color:white">.
* @endcode
*
* @see Drupal\Core\Template\Attribute
*/
class AttributeString extends AttributeValueBase {
/**
* Implements the magic __toString() method.
*/
public function __toString() {
$this->printed = TRUE;
return check_plain($this->value);
}
}
<?php
/**
* @file
* Definition of Drupal\Core\Template\AttributeValueBase.
*/
namespace Drupal\Core\Template;
/**
* Defines the base class for an attribute type.
*
* @see Drupal\Core\Template\Attribute
*/
abstract class AttributeValueBase {
/**
* Whether this attribute hsa been printed already.
*
* @var bool
*/
protected $printed = FALSE;
/**
* The value itself.
*
* @var mixed
*/
protected $value;
/**
* The name of the value.
*
* @var mixed
*/
protected $name;
/**
* Constructs a \Drupal\Core\Template\AttributeValueBase object.
*/
public function __construct($name, $value) {
$this->name = $name;
$this->value = $value;
}
/**
* Returns a string representation of the attribute.
*
* While __toString only returns the value in a string form, render()
* contains the name of the attribute as well.
*
* @return string
* The string representation of the attribute.
*/
public function render() {
return $this->name . '="' . $this . '"';
}
/**
* Whether this attribute hsa been printed already.
*
* @return bool
* TRUE if this attribute has been printed, FALSE otherwise.
*/
public function printed() {
return $this->printed;
}
/**
* Implements the magic __toString() method.
*/
abstract function __toString();
}
......@@ -14,7 +14,7 @@
* @ingroup themeable
*/
?>
<div class="<?php print $classes; ?>">
<div class="<?php print $attributes['class']; ?>">
<?php print $content; ?>
<?php print $pager; ?>
</div>
......@@ -818,6 +818,6 @@ function aggregator_aggregator_fetch_info() {
*/
function aggregator_preprocess_block(&$variables) {
if ($variables['block']->module == 'aggregator') {
$variables['attributes_array']['role'] = 'complementary';
$variables['attributes']['role'] = 'complementary';
}
}
......@@ -998,10 +998,10 @@ function template_preprocess_block(&$variables) {
// Create the $content variable that templates expect.
$variables['content'] = $variables['elements']['#children'];
$variables['classes_array'][] = drupal_html_class('block-' . $variables['block']->module);
$variables['attributes']['class'][] = drupal_html_class('block-' . $variables['block']->module);
// Add default class for block content.
$variables['content_attributes_array']['class'][] = 'content';
$variables['content_attributes']['class'][] = 'content';
$variables['theme_hook_suggestions'][] = 'block__' . $variables['block']->region;
$variables['theme_hook_suggestions'][] = 'block__' . $variables['block']->module;
......
......@@ -10,10 +10,9 @@
* - $block->module: Module that generated the block.
* - $block->delta: An ID for the block, unique within each module.
* - $block->region: The block region embedding the current block.
* - $classes: String of classes that can be used to style contextually through
* CSS. It can be manipulated through the variable $classes_array from
* preprocess functions. The default values can be one or more of the
* following:
* - $attributes: An instance of Attributes class that can be manipulated as an
* array and printed as a string.
* It includes the 'class' information, which includes:
* - block: The current template type, i.e., "theming hook".
* - block-[module]: The module generating the block. For example, the user
* module is responsible for handling the default user navigation block. In
......@@ -26,8 +25,6 @@
* the template.
*
* Helper variables:
* - $classes_array: Array of html class attribute values. It is flattened
* into a string within the variable $classes.
* - $block_zebra: Outputs 'odd' and 'even' dependent on each block region.
* - $zebra: Same output as $block_zebra but independent of any block region.
* - $block_id: Counter dependent on each block region.
......@@ -44,7 +41,7 @@
* @ingroup themeable
*/
?>
<div id="<?php print $block_html_id; ?>" class="<?php print $classes; ?>"<?php print $attributes; ?>>
<div id="<?php print $block_html_id; ?>" <?php print $attributes; ?>>
<?php print render($title_prefix); ?>
<?php if ($block->subject): ?>
......
......@@ -47,10 +47,10 @@ function testBlockThemeHookSuggestions() {
$variables2['elements']['#block'] = $block2;
$variables2['elements']['#children'] = '';
// Test adding a class to the block content.
$variables2['content_attributes_array']['class'][] = 'test-class';
$variables2['content_attributes']['class'][] = 'test-class';
template_preprocess_block($variables2);
$this->assertEqual($variables2['theme_hook_suggestions'], array('block__footer', 'block__block', 'block__block__hyphen_test'), t('Hyphens (-) in block delta were replaced by underscore (_)'));
// Test that the default class and added class are available.
$this->assertEqual($variables2['content_attributes_array']['class'], array('test-class', 'content'), t('Default .content class added to block content_attributes_array'));
$this->assertEqual($variables2['content_attributes']['class'], array('test-class', 'content'), t('Default .content class added to block content_attributes_array'));
}
}
......@@ -1068,7 +1068,7 @@ function _book_link_defaults($nid) {
*/
function book_preprocess_block(&$variables) {
if ($variables['block']-> module == 'book') {
$variables['attributes_array']['role'] = 'navigation';
$variables['attributes']['role'] = 'navigation';
}
}
......@@ -1233,8 +1233,8 @@ function template_preprocess_book_export_html(&$variables) {
$variables['head'] = drupal_get_html_head();
// HTML element attributes.
$variables['html_attributes_array']['lang'] = $language_interface->langcode;
$variables['html_attributes_array']['dir'] = $language_interface->direction ? 'rtl' : 'ltr';