Commit 29c8e40e authored by Dries's avatar Dries

- Patch #355236 by Frando: refactor drupal_render() theming.

parent 607e9626
......@@ -3217,7 +3217,7 @@ function drupal_alter($type, &$data) {
*/
function drupal_get_page($content = NULL) {
// Initialize page array with defaults. @see hook_elements() - 'page' element.
$page = _element_info('page');
$page = element_info('page');
$page['content'] = is_array($content) ? $content : array('main' => array('#markup' => $content));
return $page;
......@@ -3253,6 +3253,28 @@ function drupal_render_page($page) {
* Renders HTML given a structured array tree.
*
* Recursively iterates over each of the array elements, generating HTML code.
*
* HTML generation is controlled by two properties containing theme functions,
* #theme and #theme_wrapper.
*
* #theme is the theme function called first. If it is set and the element has any
* children, they have to be rendered there. For elements that are not allowed
* to have any children, e.g. buttons or textfields, it can be used to render
* the element itself. If #theme is not present and the element has children,
* they are rendered and concatenated into a string by drupal_render_children().
*
* The theme function in #theme_wrapper will be called after #theme has run. It
* can be used to add further markup around the rendered children, e.g. fieldsets
* add the required markup for a fieldset around their rendered child elements.
* A wrapper theme function always has to include the element's #children property
* in its output, as this contains the rendered children.
*
* For example, for the form element type, by default only the #theme_wrapper
* property is set, which adds the form markup around the rendered child elements
* of the form. This allows you to set the #theme property on a specific form to
* a custom theme function, giving you complete control over the placement of the
* form's children while not at all having to deal with the form markup itself.
*
* This function is usually called from within a another function, like
* drupal_get_form() or node_view(). Elements are sorted internally using
* uasort(). Since this is expensive, when passing already sorted elements to
......@@ -3267,15 +3289,27 @@ function drupal_render_page($page) {
function drupal_render(&$elements) {
// Early-return nothing if user does not have access.
if (!isset($elements) || (isset($elements['#access']) && !$elements['#access'])) {
return NULL;
return;
}
// Do not print elements twice.
if (isset($elements['#printed']) && $elements['#printed']) {
return;
}
// If the default values for this element haven't been loaded yet, populate
// If the default values for this element have not been loaded yet, populate
// them.
if (!isset($elements['#defaults_loaded']) || !$elements['#defaults_loaded']) {
if ((!empty($elements['#type'])) && ($info = _element_info($elements['#type']))) {
$elements += $info;
}
if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
$elements += element_info($elements['#type']);
}
else {
$elements += element_basic_defaults();
}
// If #markup is not empty and no theme function is set, use theme_markup.
// This allows to specify just #markup on an element without setting the #type.
if (!empty($elements['#markup']) && empty($elements['#theme'])) {
$elements['#theme'] = 'markup';
}
// Make any final changes to the element before it is rendered. This means
......@@ -3297,68 +3331,63 @@ function drupal_render(&$elements) {
$elements['#sorted'] = TRUE;
}
$content = '';
$elements += array('#title' => NULL, '#description' => NULL);
if (!isset($elements['#children'])) {
$children = element_children($elements);
// Render all the children that use a theme function.
if (isset($elements['#theme']) && empty($elements['#theme_used'])) {
$elements['#theme_used'] = TRUE;
$previous = array();
foreach (array('#type', '#prefix', '#suffix') as $key) {
$previous[$key] = isset($elements[$key]) ? $elements[$key] : NULL;
}
// If we rendered a single element, then we will skip the renderer.
if (empty($children)) {
$elements['#printed'] = TRUE;
}
else {
$elements['#markup'] = '';
}
unset($elements['#type'], $elements['#prefix'], $elements['#suffix']);
$content = theme($elements['#theme'], $elements);
foreach (array('#type', '#prefix', '#suffix') as $key) {
$elements[$key] = isset($previous[$key]) ? $previous[$key] : NULL;
}
}
// Render each of the children using drupal_render and concatenate them.
if (!isset($content) || $content === '') {
foreach ($children as $key) {
$content .= drupal_render($elements[$key]);
}
}
$elements['#children'] = '';
// Call the element's #theme function if it is set. Then any children of the
// element have to be rendered there.
if (isset($elements['#theme'])) {
$elements['#children'] = theme($elements['#theme'], $elements);
}
if (isset($content) && $content !== '') {
$elements['#children'] = $content;
// If #theme was not set and the element has children, render them now
// using drupal_render_children().
if ($elements['#children'] == '') {
$elements['#children'] = drupal_render_children($elements);
}
// Until now, we rendered the children, here we render the element itself
if (!isset($elements['#printed'])) {
$content = theme(!empty($elements['#type']) ? $elements['#type'] : 'markup', $elements);
$elements['#printed'] = TRUE;
// Let the theme function in #theme_wrapper add markup around the rendered
// children.
if (!empty($elements['#theme_wrapper'])) {
$elements['#children'] = theme($elements['#theme_wrapper'], $elements);
}
if (isset($content) && $content !== '') {
// Filter the outputted content and make any last changes before the
// content is sent to the browser. The changes are made on $content
// which allows the output'ed text to be filtered.
if (isset($elements['#post_render'])) {
foreach ($elements['#post_render'] as $function) {
if (drupal_function_exists($function)) {
$content = $function($content, $elements);
}
// Filter the outputted content and make any last changes before the
// content is sent to the browser. The changes are made on $content
// which allows the output'ed text to be filtered.
if (isset($elements['#post_render'])) {
foreach ($elements['#post_render'] as $function) {
if (drupal_function_exists($function)) {
$elements['#children'] = $function($elements['#children'], $elements);
}
}
$prefix = isset($elements['#prefix']) ? $elements['#prefix'] : '';
$suffix = isset($elements['#suffix']) ? $elements['#suffix'] : '';
$content = $prefix . $content . $suffix;
// Store the rendered content, so higher level elements can reuse it.
$elements['#content'] = $content;
return $content;
}
$prefix = isset($elements['#prefix']) ? $elements['#prefix'] : '';
$suffix = isset($elements['#suffix']) ? $elements['#suffix'] : '';
$elements['#printed'] = TRUE;
return $prefix . $elements['#children'] . $suffix;
}
/**
* Render children of an element and concatenate them.
*
* This renders all children of an element using drupal_render() and then
* joins them together into a single string.
*
* @param $element
* The structured array whose children shall be rendered.
* @param $children_keys
* If the keys of the element's children are already known, they can be passed
* in to save another run of element_children().
*/
function drupal_render_children($element, $children_keys = NULL) {
if ($children_keys === NULL) {
$children_keys = element_children($element);
}
$output = '';
foreach ($children_keys as $key) {
$output .= drupal_render($element[$key]);
}
return $output;
}
/**
......@@ -3373,6 +3402,44 @@ function element_sort($a, $b) {
return ($a_weight < $b_weight) ? -1 : 1;
}
/**
* Retrieve the default properties for the defined element type.
*/
function element_info($type, $refresh = NULL) {
static $cache;
if (!isset($cache) || $refresh) {
$basic_defaults = element_basic_defaults();
$cache = array();
foreach (module_implements('elements') as $module) {
$elements = module_invoke($module, 'elements');
if (isset($elements) && is_array($elements)) {
$cache = array_merge_recursive($cache, $elements);
}
}
if (!empty($cache)) {
foreach ($cache as $element_type => $info) {
$cache[$element_type] = array_merge_recursive($basic_defaults, $info);
$cache[$element_type]['#type'] = $element_type;
}
}
}
return $cache[$type];
}
/**
* Retrieve the basic default properties that are common to all elements.
*/
function element_basic_defaults() {
return array(
'#description' => '',
'#title' => '',
'#attributes' => array(),
'#required' => FALSE,
);
}
/**
* Function used by uasort to sort structured arrays by weight, without the property weight prefix.
*/
......@@ -3410,7 +3477,13 @@ function element_child($key) {
* Get keys of a structured array tree element that are not properties (i.e., do not begin with '#').
*/
function element_children($element) {
return array_filter(array_keys((array) $element), 'element_child');
$keys = array();
foreach(array_keys($element) as $key) {
if ($key[0] !== '#') {
$keys[] = $key;
}
}
return $keys;
}
/**
......@@ -3564,24 +3637,15 @@ function drupal_common_theme() {
'radios' => array(
'arguments' => array('element' => NULL),
),
'password_confirm' => array(
'arguments' => array('element' => NULL),
),
'date' => array(
'arguments' => array('element' => NULL),
),
'item' => array(
'arguments' => array('element' => NULL),
),
'checkbox' => array(
'arguments' => array('element' => NULL),
),
'checkboxes' => array(
'arguments' => array('element' => NULL),
),
'submit' => array(
'arguments' => array('element' => NULL),
),
'button' => array(
'arguments' => array('element' => NULL),
),
......@@ -3591,9 +3655,6 @@ function drupal_common_theme() {
'hidden' => array(
'arguments' => array('element' => NULL),
),
'token' => array(
'arguments' => array('element' => NULL),
),
'textfield' => array(
'arguments' => array('element' => NULL),
),
......@@ -3616,7 +3677,7 @@ function drupal_common_theme() {
'arguments' => array('element' => NULL),
),
'form_element' => array(
'arguments' => array('element' => NULL, 'value' => NULL),
'arguments' => array('element' => NULL),
),
);
}
......
......@@ -512,7 +512,7 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
$form['#id'] = form_clean_id($form_id);
}
$form += _element_info('form');
$form += element_info('form');
$form += array('#tree' => FALSE, '#parents' => array());
if (!isset($form['#validate'])) {
......@@ -861,7 +861,7 @@ function form_builder($form_id, $form, &$form_state) {
$form['#processed'] = FALSE;
// Use element defaults.
if ((!empty($form['#type'])) && ($info = _element_info($form['#type']))) {
if ((!empty($form['#type'])) && ($info = element_info($form['#type']))) {
// Overlay $info onto $form, retaining preexisting keys in $form.
$form += $info;
}
......@@ -1348,35 +1348,6 @@ function _form_set_value(&$form_values, $form_item, $parents, $value) {
}
}
/**
* Retrieve the default properties for the defined element type.
*/
function _element_info($type, $refresh = NULL) {
static $cache;
$basic_defaults = array(
'#description' => NULL,
'#attributes' => array(),
'#required' => FALSE,
);
if (!isset($cache) || $refresh) {
$cache = array();
foreach (module_implements('elements') as $module) {
$elements = module_invoke($module, 'elements');
if (isset($elements) && is_array($elements)) {
$cache = array_merge_recursive($cache, $elements);
}
}
if (sizeof($cache)) {
foreach ($cache as $element_type => $info) {
$cache[$element_type] = array_merge_recursive($basic_defaults, $info);
}
}
}
return $cache[$type];
}
function form_options_flatten($array, $reset = TRUE) {
static $return;
......@@ -1419,7 +1390,7 @@ function theme_select($element) {
$size = $element['#size'] ? ' size="' . $element['#size'] . '"' : '';
_form_set_class($element, array('form-select'));
$multiple = $element['#multiple'];
return theme('form_element', $element, '<select name="' . $element['#name'] . '' . ($multiple ? '[]' : '') . '"' . ($multiple ? ' multiple="multiple" ' : '') . drupal_attributes($element['#attributes']) . ' id="' . $element['#id'] . '" ' . $size . '>' . form_select_options($element) . '</select>');
return '<select name="' . $element['#name'] . '' . ($multiple ? '[]' : '') . '"' . ($multiple ? ' multiple="multiple" ' : '') . drupal_attributes($element['#attributes']) . ' id="' . $element['#id'] . '" ' . $size . '>' . form_select_options($element) . '</select>';
}
function form_select_options($element, $choices = NULL) {
......@@ -1555,8 +1526,7 @@ function theme_radio($element) {
$output = '<label class="option" for="' . $element['#id'] . '">' . $output . ' ' . $element['#title'] . '</label>';
}
unset($element['#title']);
return theme('form_element', $element, $output);
return $output;
}
/**
......@@ -1576,28 +1546,8 @@ function theme_radios($element) {
$class .= ' ' . $element['#attributes']['class'];
}
$element['#children'] = '<div class="' . $class . '">' . (!empty($element['#children']) ? $element['#children'] : '') . '</div>';
if ($element['#title'] || $element['#description']) {
unset($element['#id']);
return theme('form_element', $element, $element['#children']);
}
else {
return $element['#children'];
}
}
/**
* Format a password_confirm item.
*
* @param $element
* An associative array containing the properties of the element.
* Properties used: title, value, id, required, error.
* @return
* A themed HTML string representing the form item.
*
* @ingroup themeable
*/
function theme_password_confirm($element) {
return theme('form_element', $element, $element['#children']);
return $element['#children'];
}
/**
......@@ -1665,7 +1615,7 @@ function password_confirm_validate($form, &$form_state) {
* @ingroup themeable
*/
function theme_date($element) {
return theme('form_element', $element, '<div class="container-inline">' . $element['#children'] . '</div>');
return '<div class="container-inline">' . drupal_render_children($element) . '</div>';
}
/**
......@@ -1881,6 +1831,8 @@ function form_process_input_format($element) {
// We need to break references, otherwise form_builder recurses infinitely.
$element['value'] = (array)$element;
$element['#type'] = 'markup';
$element['#theme'] = NULL;
$element['#theme_wrapper'] = NULL;
$element['format'] = filter_form($element['#text_format'], 1, $element_parents);
// We need to clear the #text_format from the new child otherwise we
......@@ -1976,21 +1928,6 @@ function form_process_ahah($element) {
return $element;
}
/**
* Format a form item.
*
* @param $element
* An associative array containing the properties of the element.
* Properties used: title, value, description, required, error
* @return
* A themed HTML string representing the form item.
*
* @ingroup themeable
*/
function theme_item($element) {
return theme('form_element', $element, $element['#markup'] . (!empty($element['#children']) ? $element['#children'] : ''));
}
/**
* Format a checkbox.
*
......@@ -2016,8 +1953,7 @@ function theme_checkbox($element) {
$checkbox = '<label class="option" for="' . $element['#id'] . '">' . $checkbox . ' ' . $element['#title'] . '</label>';
}
unset($element['#title']);
return theme('form_element', $element, $checkbox);
return $checkbox;
}
/**
......@@ -2036,13 +1972,21 @@ function theme_checkboxes($element) {
$class .= ' ' . $element['#attributes']['class'];
}
$element['#children'] = '<div class="' . $class . '">' . (!empty($element['#children']) ? $element['#children'] : '') . '</div>';
return $element['#children'];
}
/**
* Add form_element theming to an element if title or desription is set.
*
* This is used as a pre render function for checkboxes and radios.
*/
function form_pre_render_conditional_form_element($element) {
if ($element['#title'] || $element['#description']) {
unset($element['#id']);
return theme('form_element', $element, $element['#children']);
}
else {
return $element['#children'];
$element['#theme_wrapper'] = 'form_element';
}
return $element;
}
function form_process_checkboxes($element) {
......@@ -2090,7 +2034,7 @@ function theme_tableselect($element) {
$row = array();
// Render the checkbox / radio element.
$row[] = $element[$key]['#content'];
$row[] = drupal_render($element[$key]);
// As theme_table only maps header and row columns by order, create the
// correct order by iterating over the header fields.
......@@ -2243,15 +2187,6 @@ function theme_hidden($element) {
return '<input type="hidden" name="' . $element['#name'] . '" id="' . $element['#id'] . '" value="' . check_plain($element['#value']) . "\" " . drupal_attributes($element['#attributes']) . " />\n";
}
/**
* Format a form token.
*
* @ingroup themeable
*/
function theme_token($element) {
return theme('hidden', $element);
}
/**
* Format a textfield.
*
......@@ -2286,8 +2221,7 @@ function theme_textfield($element) {
if (isset($element['#field_suffix'])) {
$output .= ' <span class="field-suffix">' . $element['#field_suffix'] . '</span>';
}
return theme('form_element', $element, $output) . $extra;
return $output . $extra;
}
/**
......@@ -2337,7 +2271,7 @@ function theme_textarea($element) {
}
_form_set_class($element, $class);
return theme('form_element', $element, '<textarea cols="' . $element['#cols'] . '" rows="' . $element['#rows'] . '" name="' . $element['#name'] . '" id="' . $element['#id'] . '" ' . drupal_attributes($element['#attributes']) . '>' . check_plain($element['#value']) . '</textarea>');
return '<textarea cols="' . $element['#cols'] . '" rows="' . $element['#rows'] . '" name="' . $element['#name'] . '" id="' . $element['#id'] . '" ' . drupal_attributes($element['#attributes']) . '>' . check_plain($element['#value']) . '</textarea>';
}
/**
......@@ -2355,7 +2289,7 @@ function theme_textarea($element) {
*/
function theme_markup($element) {
return (isset($element['#markup']) ? $element['#markup'] : '') . (isset($element['#children']) ? $element['#children'] : '');
return (!empty($element['#markup']) ? $element['#markup'] : '') . drupal_render_children($element);
}
/**
......@@ -2375,7 +2309,7 @@ function theme_password($element) {
_form_set_class($element, array('form-text'));
$output = '<input type="password" name="' . $element['#name'] . '" id="' . $element['#id'] . '" ' . $maxlength . $size . drupal_attributes($element['#attributes']) . ' />';
return theme('form_element', $element, $output);
return $output;
}
/**
......@@ -2388,7 +2322,7 @@ function form_process_weight($element) {
$element['#options'] = $weights;
$element['#type'] = 'select';
$element['#is_weight'] = TRUE;
$element += _element_info('select');
$element += element_info('select');
return $element;
}
......@@ -2415,7 +2349,7 @@ function form_process_weight($element) {
*/
function theme_file($element) {
_form_set_class($element, array('form-file'));
return theme('form_element', $element, '<input type="file" name="' . $element['#name'] . '"' . ($element['#attributes'] ? ' ' . drupal_attributes($element['#attributes']) : '') . ' id="' . $element['#id'] . '" size="' . $element['#size'] . "\" />\n");
return '<input type="file" name="' . $element['#name'] . '"' . ($element['#attributes'] ? ' ' . drupal_attributes($element['#attributes']) : '') . ' id="' . $element['#id'] . '" size="' . $element['#size'] . "\" />\n";
}
/**
......@@ -2423,15 +2357,13 @@ function theme_file($element) {
*
* @param element
* An associative array containing the properties of the element.
* Properties used: title, description, id, required
* @param $value
* The form element's data.
* Properties used: title, description, id, required, children
* @return
* A string representing the form element.
*
* @ingroup themeable
*/
function theme_form_element($element, $value) {
function theme_form_element($element) {
// This is also used in the installer, pre-database setup.
$t = get_t();
......@@ -2442,7 +2374,7 @@ function theme_form_element($element, $value) {
$output .= ">\n";
$required = !empty($element['#required']) ? '<span class="form-required" title="' . $t('This field is required.') . '">*</span>' : '';
if (!empty($element['#title'])) {
if (!empty($element['#title']) && empty($element['#form_element_skip_title'])) {
$title = $element['#title'];
if (!empty($element['#id'])) {
$output .= ' <label for="' . $element['#id'] . '">' . $t('!title: !required', array('!title' => filter_xss_admin($title), '!required' => $required)) . "</label>\n";
......@@ -2452,7 +2384,7 @@ function theme_form_element($element, $value) {
}
}
$output .= " $value\n";
$output .= " " . $element['#children'] . "\n";
if (!empty($element['#description'])) {
$output .= ' <div class="description">' . $element['#description'] . "</div>\n";
......
......@@ -96,7 +96,7 @@ function theme_locale_languages_overview_form($form) {
}
$header = array(array('data' => t('English name')), array('data' => t('Native name')), array('data' => t('Code')), array('data' => t('Direction')), array('data' => t('Enabled')), array('data' => t('Default')), array('data' => t('Weight')), array('data' => t('Operations')));
$output = theme('table', $header, $rows, array('id' => 'language-order'));
$output .= drupal_render($form);
$output .= drupal_render_children($form);
drupal_add_tabledrag('language-order', 'order', 'sibling', 'language-order-weight');
......
......@@ -1584,7 +1584,7 @@ function theme_item_list($items = array(), $title = NULL, $type = 'ul', $attribu
*/
function theme_list($elements) {
// Populate any missing array elements with their defaults.
$elements += _element_info('list');
$elements += element_info('list');
return theme('item_list', $elements['#items'], $elements['#title'], $elements['#list_type'], $elements['#attributes']);
}
......
......@@ -250,7 +250,7 @@ function theme_aggregator_categorize_items($form) {
}
$output .= theme('table', array('', t('Categorize')), $rows);
$output .= drupal_render($form['submit']);
$output .= drupal_render($form);
$output .= drupal_render_children($form);