Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.992
diff -u -p -r1.992 common.inc
--- includes/common.inc 18 Sep 2009 10:54:20 -0000 1.992
+++ includes/common.inc 18 Sep 2009 16:20:57 -0000
@@ -4704,6 +4704,9 @@ function drupal_common_theme() {
'form_element' => array(
'arguments' => array('element' => NULL),
),
+ 'form_element_label' => array(
+ 'arguments' => array('element' => NULL),
+ ),
'text_format_wrapper' => array(
'arguments' => array('element' => NULL),
),
Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.373
diff -u -p -r1.373 form.inc
--- includes/form.inc 18 Sep 2009 00:12:45 -0000 1.373
+++ includes/form.inc 18 Sep 2009 16:20:58 -0000
@@ -1106,6 +1106,17 @@ function form_builder($form_id, $element
$form_state['has_file_element'] = TRUE;
}
+ // Set the element's title attribute to the elements #title, if needed.
+ if (!empty($element['#title']) && isset($element['#show_title']) && $element['#show_title'] == FORM_ELEMENT_SHOW_TITLE_ATTRIBUTE) {
+ $element['#attributes']['title'] = $element['#title'];
+ if (!empty($element['#required'])) {
+ // Add an indication that this field is required.
+ $element['#attributes']['title'] .= ' (' . $t('This field is required.') . ')';
+ }
+ // Unset #show_title to simplify later theming, we have done all we need.
+ unset($element['#show_title']);
+ }
+
if (isset($element['#type']) && $element['#type'] == 'form') {
// We are on the top form.
// If there is a file element, we set the form encoding.
@@ -1522,8 +1533,8 @@ function form_options_flatten($array, $r
*
* @param $element
* An associative array containing the properties of the element.
- * Properties used: #title, #value, #options, #description, #extra, #multiple,
- * #required, #name, #attributes, #size.
+ * Properties used: #size, #multiple, #name, #id, #value, #options,
+ * #attributes.
* @return
* A themed HTML string representing the form element.
*
@@ -1534,7 +1545,6 @@ function form_options_flatten($array, $r
* values are associative arrays in the normal $options format.
*/
function theme_select($element) {
- $select = '';
$size = $element['#size'] ? ' size="' . $element['#size'] . '"' : '';
_form_set_class($element, array('form-select'));
$multiple = $element['#multiple'];
@@ -1669,8 +1679,7 @@ function theme_fieldset($element) {
*
* @param $element
* An associative array containing the properties of the element.
- * Properties used: #required, #return_value, #value, #attributes, #title,
- * #description
+ * Properties used: #id, #name, #value, #return_value, #attributes.
* @return
* A themed HTML string representing the form item group.
*
@@ -1684,9 +1693,6 @@ function theme_radio($element) {
$output .= 'value="' . $element['#return_value'] . '" ';
$output .= (check_plain($element['#value']) == $element['#return_value']) ? ' checked="checked" ' : ' ';
$output .= drupal_attributes($element['#attributes']) . ' />';
- if (!is_null($element['#title'])) {
- $output = '';
- }
return $output;
}
@@ -2005,8 +2011,7 @@ function theme_text_format_wrapper($elem
*
* @param $element
* An associative array containing the properties of the element.
- * Properties used: #title, #value, #return_value, #description, #required,
- * #attributes.
+ * Properties used: #name, #id, #value, #return_value, #attributes.
* @return
* A themed HTML string representing the checkbox.
*
@@ -2022,10 +2027,6 @@ function theme_checkbox($element) {
$checkbox .= $element['#value'] ? ' checked="checked" ' : ' ';
$checkbox .= drupal_attributes($element['#attributes']) . ' />';
- if (!is_null($element['#title'])) {
- $checkbox = '';
- }
-
return $checkbox;
}
@@ -2586,7 +2587,8 @@ function theme_file($element) {
*
* @param element
* An associative array containing the properties of the element.
- * Properties used: #title, #description, #id, #required, #children
+ * Properties used: #type, #name, #title, #show_title, #children,
+ * #description
* @return
* A string representing the form element.
*
@@ -2606,19 +2608,19 @@ function theme_form_element($element) {
}
$output = '
' . "\n";
- $required = !empty($element['#required']) ? '*' : '';
- if (!empty($element['#title']) && empty($element['#form_element_skip_title'])) {
- $title = $element['#title'];
- if (!empty($element['#id'])) {
- $output .= ' \n";
- }
- else {
- $output .= ' \n";
- }
+ // Place label & required mark in correct position, depending on #show_title.
+ if (isset($element['#show_title']) && $element['#show_title'] == FORM_ELEMENT_SHOW_TITLE_BEFORE) {
+ $output .= theme('form_element_label', $element) . ' ' . $element['#children'] . "\n";
+ }
+ else {
+ // In all other cases (#show_title is FORM_ELEMENT_SHOW_TITLE_BEFORE,
+ // FORM_ELEMENT_SHOW_TITLE_ATTRIBUTE or is not set) we add the label and
+ // required mark after the element. In the last 2 cases this ensures the
+ // required mark is still present to indicate the field is required even
+ // through there is no label added.
+ $output .= $element['#children'] . ' ' . theme('form_element_label', $element) . "\n";
}
-
- $output .= " " . $element['#children'] . "\n";
if (!empty($element['#description'])) {
$output .= '
' . $element['#description'] . "
\n";
@@ -2630,6 +2632,39 @@ function theme_form_element($element) {
}
/**
+ * Theme a form element label and required mark.
+ *
+ * @param element
+ * An associative array containing the properties of the element.
+ * Properties used: #required, #show_title, #title, #label_option, #id
+ * @return
+ * A string representing the form element label and/or required mark.
+ *
+ * @ingroup themeable
+ */
+function theme_form_element_label($element) {
+ // This is also used in the installer, pre-database setup.
+ $t = get_t();
+
+ $required = !empty($element['#required']) ? '*' : '';
+ // If there is no title, or the title is not set to display we simply
+ // return any required mark here.
+ if (empty($element['#title']) || empty($element['#show_title'])) {
+ return $required;
+ }
+
+ $attributes = array();
+ if (isset($element['#label_option']) && $element['#label_option']) {
+ // This class styles the label as an option, inline with the element.
+ $attributes['class'] = 'option';
+ }
+ if (!empty($element['#id'])) {
+ $attributes['for'] = $element['#id'];
+ }
+ return ' \n";
+}
+
+/**
* Sets a form element's class attribute.
*
* Adds 'required' and 'error' classes as needed.
Index: modules/simpletest/tests/form.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form.test,v
retrieving revision 1.15
diff -u -p -r1.15 form.test
--- modules/simpletest/tests/form.test 18 Sep 2009 00:12:48 -0000 1.15
+++ modules/simpletest/tests/form.test 18 Sep 2009 16:20:58 -0000
@@ -112,6 +112,80 @@ class FormsTestTypeCase extends DrupalUn
}
/**
+ * Test the form elements, labels and associated output for correctness.
+ */
+class FormsElementsLabelsTestCase extends DrupalWebTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Form element and label output test',
+ 'description' => 'Test the form elements, labels and associated output for correctness.',
+ 'group' => 'Form API',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('form_test');
+ }
+
+ /**
+ * Test form elements, labels, title attibutes and required marks output
+ * correctly and have the correct label option class if needed.
+ */
+ function testFormLabels() {
+ $this->drupalGet('form_test/form-labels');
+
+ // Check that the checkbox/radio processing is not interfering with
+ // basic placement.
+ $elements = $this->xpath('//input[@id="edit-form-checkboxes-test-thirdcheckbox"]/following-sibling::label[@for="edit-form-checkboxes-test-thirdcheckbox" and @class="option"]');
+ $this->assertTrue(isset($elements[0]), t("Label follows field and label option class correct for regular checkboxes."));
+
+ $elements = $this->xpath('//input[@id="edit-form-radios-test-secondradio"]/following-sibling::label[@for="edit-form-radios-test-secondradio" and @class="option"]');
+ $this->assertTrue(isset($elements[0]), t("Label follows field and label option class correct for regular radios."));
+
+ // Exercise various defaults for checkboxes and modifications to ensure
+ // appropriate override and correct behaviour.
+ $elements = $this->xpath('//input[@id="edit-form-checkbox-test"]/following-sibling::label[@for="edit-form-checkbox-test" and @class="option"]');
+ $this->assertTrue(isset($elements[0]), t("Label follows field and label option class correct for a checkbox."));
+
+ $elements = $this->xpath('//input[@id="edit-form-checkbox-test-attribute" and @title="Checkbox test with label as attribute"]');
+ $this->assertTrue(isset($elements[0]), t("Title as attribute for a checkbox is correct."));
+
+ $elements = $this->xpath('//input[@id="edit-form-checkbox-test-attribute"]/following-sibling::label');
+ $this->assertFalse(isset($elements[0]), t("Label does not follow title as attribute for a checkbox."));
+
+ $elements = $this->xpath('//input[@id="edit-form-checkbox-test-no-label-option"]/following-sibling::label[@for="edit-form-checkbox-test-no-label-option"]');
+ $this->assertTrue(isset($elements[0]), t("Label follows field for a checkbox without a label option class."));
+
+ $elements = $this->xpath('//input[@id="edit-form-checkbox-test-no-label-option"]/following-sibling::label[@for="edit-form-checkbox-test-no-label-option" and @class="option"]');
+ $this->assertFalse(isset($elements[0]), t("No label option class found for checkbox without a label option class."));
+
+ // Exercise various defaults for textboxes and modifications to ensure
+ // appropriate override and correct behaviour.
+ $elements = $this->xpath('//label[@for="edit-form-textfield-test-title-and-required"]/child::span[@class="form-required"]/parent::*/following-sibling::input[@id="edit-form-textfield-test-title-and-required"]');
+ $this->assertTrue(isset($elements[0]), t("Label preceeds textfield, with required marker inside label."));
+
+ $elements = $this->xpath('//input[@id="edit-form-textfield-test-no-title-required"]/preceding-sibling::span[@class="form-required"]');
+ $this->assertTrue(isset($elements[0]), t("Required marker when required, preceeds textfield."));
+
+ $elements = $this->xpath('//label[@for="edit-form-textfield-test-no-title-required"]');
+ $this->assertFalse(isset($elements[0]), t("No label tag when no title set on field."));
+
+ $elements = $this->xpath('//input[@id="edit-form-textfield-test-title"]/preceding-sibling::span[@class="form-required"]');
+ $this->assertFalse(isset($elements[0]), t("No required marker on non-required field."));
+
+ $elements = $this->xpath('//input[@id="edit-form-textfield-test-title-attribute" and @title="Textfield test for title as attribute"]');
+ $this->assertTrue(isset($elements[0]), t("Title as attribute for a textfield is correct."));
+
+ $elements = $this->xpath('//input[@id="edit-form-textfield-test-title-after-option"]/following-sibling::label[@for="edit-form-textfield-test-title-after-option" and @class="option"]');
+ $this->assertTrue(isset($elements[0]), t("Label preceeds field and label option class correct for text field when set."));
+
+ $elements = $this->xpath('//label[@for="edit-form-textfield-test-title-no-show"]');
+ $this->assertFalse(isset($elements[0]), t("No label tag when title set not to display."));
+ }
+}
+
+/**
* Test the tableselect form element for expected behavior.
*/
class FormsElementsTableSelectFunctionalTest extends DrupalWebTestCase {
Index: modules/simpletest/tests/form_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form_test.module,v
retrieving revision 1.9
diff -u -p -r1.9 form_test.module
--- modules/simpletest/tests/form_test.module 18 Sep 2009 00:12:48 -0000 1.9
+++ modules/simpletest/tests/form_test.module 18 Sep 2009 16:20:59 -0000
@@ -69,6 +69,14 @@ function form_test_menu() {
'type' => MENU_CALLBACK,
);
+ $items['form_test/form-labels'] = array(
+ 'title' => 'Form label test',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('form_label_test_form'),
+ 'access arguments' => array('access content'),
+ 'type' => MENU_CALLBACK,
+ );
+
return $items;
}
@@ -363,6 +371,77 @@ function form_storage_test_form_submit($
drupal_set_message("Form constructions: ". $_SESSION['constructions']);
}
+ /**
+ * A form for testing form labels and required marks.
+ */
+function form_label_test_form(&$form_state) {
+ $form['form_checkboxes_test'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Checkboxes test'),
+ '#options' => array(
+ 'firstcheckbox' => t('First Checkbox'),
+ 'secondcheckbox' => t('Second Checkbox'),
+ 'thirdcheckbox' => t('Third Checkbox'),
+ ),
+ );
+ $form['form_radios_test'] = array(
+ '#type' => 'radios',
+ '#title' => t('Radios test'),
+ '#options' => array(
+ 'firstradio' => t('First Radio'),
+ 'secondradio' => t('Second Radio'),
+ 'thirdradio' => t('Third Radio'),
+ ),
+ );
+ $form['form_checkbox_test'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Checkbox test'),
+ );
+ $form['form_checkbox_test_attribute'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Checkbox test with label as attribute'),
+ '#show_title' => FORM_ELEMENT_SHOW_TITLE_ATTRIBUTE,
+ );
+ $form['form_checkbox_test_no_label_option'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Checkbox test with label not styled as an option'),
+ '#label_option' => FALSE,
+ );
+ $form['form_textfield_test_title_and_required'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Textfield test for required with title'),
+ '#required' => TRUE,
+ );
+ $form['form_textfield_test_no_title_required'] = array(
+ '#type' => 'textfield',
+ // No title.
+ '#required' => TRUE,
+ );
+ $form['form_textfield_test_title'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Textfield test for title only'),
+ // Not required.
+ );
+ $form['form_textfield_test_title_attribute'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Textfield test for title as attribute'),
+ '#show_title' => FORM_ELEMENT_SHOW_TITLE_ATTRIBUTE,
+ );
+ $form['form_textfield_test_title_after_option'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Textfield test for title after element with label styled as an option'),
+ '#show_title' => FORM_ELEMENT_SHOW_TITLE_AFTER,
+ '#label_option' => TRUE,
+ );
+ $form['form_textfield_test_title_no_show'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Textfield test for title set not to display'),
+ '#show_title' => FALSE,
+ );
+
+ return $form;
+}
+
/**
* Menu callback; Invokes a form builder function with a wrapper callback.
*/
Index: modules/system/system.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v
retrieving revision 1.75
diff -u -p -r1.75 system.api.php
--- modules/system/system.api.php 18 Sep 2009 00:04:23 -0000 1.75
+++ modules/system/system.api.php 18 Sep 2009 16:21:00 -0000
@@ -168,6 +168,9 @@ function hook_cron() {
* - "#pre_render": array of callback functions taking $element and $form_state.
* - "#post_render": array of callback functions taking $element and $form_state.
* - "#submit": array of callback functions taking $form and $form_state.
+ * - "#show_title": optionally one of the FORM_ELEMENT_SHOW_TITLE_* constants
+ * indicating if and how the #title should be displayed.
+ * - "#label_option": add a class to style the element label as an option.
*
* @see hook_element_info_alter()
* @see system_element_info()
Index: modules/system/system.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.module,v
retrieving revision 1.791
diff -u -p -r1.791 system.module
--- modules/system/system.module 18 Sep 2009 00:12:48 -0000 1.791
+++ modules/system/system.module 18 Sep 2009 16:21:01 -0000
@@ -86,6 +86,27 @@ define('REGIONS_VISIBLE', 'visible');
*/
define('REGIONS_ALL', 'all');
+/**
+ *
+ * Output form element titles as labels before form elements.
+ * @see system_elements().
+ */
+define('FORM_ELEMENT_SHOW_TITLE_BEFORE', 'before');
+
+/**
+ *
+ * Output form element titles as labels after form elements.
+ * @see system_elements().
+ */
+define('FORM_ELEMENT_SHOW_TITLE_AFTER', 'after');
+
+/**
+ *
+ * Output form element titles as the title attribute of form elements.
+ * @see system_elements().
+ */
+define('FORM_ELEMENT_SHOW_TITLE_ATTRIBUTE', 'attribute');
+
/**
* Implement hook_help().
@@ -335,6 +356,7 @@ function system_element_info() {
'#process' => array('form_process_text_format', 'ajax_process_form'),
'#theme' => 'textfield',
'#theme_wrappers' => array('form_element'),
+ '#show_title' => FORM_ELEMENT_SHOW_TITLE_BEFORE,
);
$types['password'] = array(
'#input' => TRUE,
@@ -343,11 +365,13 @@ function system_element_info() {
'#process' => array('ajax_process_form'),
'#theme' => 'password',
'#theme_wrappers' => array('form_element'),
+ '#show_title' => FORM_ELEMENT_SHOW_TITLE_BEFORE,
);
$types['password_confirm'] = array(
'#input' => TRUE,
'#process' => array('form_process_password_confirm'),
'#theme_wrappers' => array('form_element'),
+ '#show_title' => FORM_ELEMENT_SHOW_TITLE_BEFORE,
);
$types['textarea'] = array(
'#input' => TRUE,
@@ -357,12 +381,14 @@ function system_element_info() {
'#process' => array('form_process_text_format', 'ajax_process_form'),
'#theme' => 'textarea',
'#theme_wrappers' => array('form_element'),
+ '#show_title' => FORM_ELEMENT_SHOW_TITLE_BEFORE,
);
$types['radios'] = array(
'#input' => TRUE,
'#process' => array('form_process_radios'),
'#theme_wrappers' => array('radios'),
'#pre_render' => array('form_pre_render_conditional_form_element'),
+ '#show_title' => FORM_ELEMENT_SHOW_TITLE_BEFORE,
);
$types['radio'] = array(
'#input' => TRUE,
@@ -370,7 +396,8 @@ function system_element_info() {
'#process' => array('ajax_process_form'),
'#theme' => 'radio',
'#theme_wrappers' => array('form_element'),
- '#form_element_skip_title' => TRUE,
+ '#show_title' => FORM_ELEMENT_SHOW_TITLE_AFTER,
+ '#label_option' => TRUE,
);
$types['checkboxes'] = array(
'#input' => TRUE,
@@ -378,6 +405,7 @@ function system_element_info() {
'#process' => array('form_process_checkboxes'),
'#theme_wrappers' => array('checkboxes'),
'#pre_render' => array('form_pre_render_conditional_form_element'),
+ '#show_title' => FORM_ELEMENT_SHOW_TITLE_BEFORE,
);
$types['checkbox'] = array(
'#input' => TRUE,
@@ -385,7 +413,8 @@ function system_element_info() {
'#process' => array('ajax_process_form'),
'#theme' => 'checkbox',
'#theme_wrappers' => array('form_element'),
- '#form_element_skip_title' => TRUE,
+ '#show_title' => FORM_ELEMENT_SHOW_TITLE_AFTER,
+ '#label_option' => TRUE,
);
$types['select'] = array(
'#input' => TRUE,
@@ -394,6 +423,7 @@ function system_element_info() {
'#process' => array('ajax_process_form'),
'#theme' => 'select',
'#theme_wrappers' => array('form_element'),
+ '#show_title' => FORM_ELEMENT_SHOW_TITLE_BEFORE,
);
$types['weight'] = array(
'#input' => TRUE,
@@ -407,12 +437,14 @@ function system_element_info() {
'#process' => array('form_process_date'),
'#theme' => 'date',
'#theme_wrappers' => array('form_element'),
+ '#show_title' => FORM_ELEMENT_SHOW_TITLE_BEFORE,
);
$types['file'] = array(
'#input' => TRUE,
'#size' => 60,
'#theme' => 'file',
'#theme_wrappers' => array('form_element'),
+ '#show_title' => FORM_ELEMENT_SHOW_TITLE_BEFORE,
);
$types['tableselect'] = array(
'#input' => TRUE,
@@ -429,6 +461,7 @@ function system_element_info() {
'#markup' => '',
'#theme' => 'markup',
'#theme_wrappers' => array('form_element'),
+ '#show_title' => FORM_ELEMENT_SHOW_TITLE_BEFORE,
);
$types['hidden'] = array(
'#input' => TRUE,
Index: modules/user/user.pages.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.pages.inc,v
retrieving revision 1.53
diff -u -p -r1.53 user.pages.inc
--- modules/user/user.pages.inc 18 Sep 2009 00:12:48 -0000 1.53
+++ modules/user/user.pages.inc 18 Sep 2009 16:21:01 -0000
@@ -459,7 +459,6 @@ function user_cancel_methods() {
'#return_value' => $name,
'#default_value' => $default_method,
'#parents' => array('user_cancel_method'),
- '#required' => TRUE,
);
}
return $form;