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 ' ' . $t('!title !required', array('!title' => filter_xss_admin($element['#title']), '!required' => $required)) . "\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;