Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.984 diff -u -u -p -r1.984 common.inc --- includes/common.inc 5 Sep 2009 15:05:01 -0000 1.984 +++ includes/common.inc 10 Sep 2009 06:07:59 -0000 @@ -4709,6 +4709,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.370 diff -u -u -p -r1.370 form.inc --- includes/form.inc 5 Sep 2009 15:05:01 -0000 1.370 +++ includes/form.inc 10 Sep 2009 06:07:59 -0000 @@ -1090,6 +1090,13 @@ 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']; + // 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. @@ -1518,7 +1525,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']; @@ -1668,9 +1674,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; } @@ -2006,10 +2009,6 @@ function theme_checkbox($element) { $checkbox .= $element['#value'] ? ' checked="checked" ' : ' '; $checkbox .= drupal_attributes($element['#attributes']) . ' />'; - if (!is_null($element['#title'])) { - $checkbox = ''; - } - return $checkbox; } @@ -2570,7 +2569,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. * @@ -2590,19 +2590,15 @@ 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 we follow the form element. + $output .= $element['#children'] . ' ' . theme('form_element_label', $element) . "\n"; } - - $output .= " " . $element['#children'] . "\n"; if (!empty($element['#description'])) { $output .= '
' . $element['#description'] . "
\n"; @@ -2614,6 +2610,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.14 diff -u -u -p -r1.14 form.test --- modules/simpletest/tests/form.test 13 Jul 2009 21:51:41 -0000 1.14 +++ modules/simpletest/tests/form.test 10 Sep 2009 06:07:59 -0000 @@ -112,6 +112,77 @@ 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-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.8 diff -u -u -p -r1.8 form_test.module --- modules/simpletest/tests/form_test.module 17 Aug 2009 07:12:16 -0000 1.8 +++ modules/simpletest/tests/form_test.module 10 Sep 2009 06:07:59 -0000 @@ -65,6 +65,15 @@ function form_test_menu() { 'access arguments' => array('access content'), '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; } @@ -365,3 +374,74 @@ function form_storage_test_form_submit($ $form_state['storage']['step']++; 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; +} \ No newline at end of file Index: modules/system/system.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v retrieving revision 1.72 diff -u -u -p -r1.72 system.api.php --- modules/system/system.api.php 5 Sep 2009 13:05:31 -0000 1.72 +++ modules/system/system.api.php 10 Sep 2009 06:07:59 -0000 @@ -215,6 +215,9 @@ function hook_db_rewrite_sql($query, $pr * - "#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. */ function hook_elements() { $type['filter_format'] = array('#input' => TRUE); Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.785 diff -u -u -p -r1.785 system.module --- modules/system/system.module 5 Sep 2009 15:05:04 -0000 1.785 +++ modules/system/system.module 10 Sep 2009 06:07:59 -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(). @@ -341,6 +362,7 @@ function system_elements() { '#process' => array('form_process_text_format', 'ajax_process_form'), '#theme' => 'textfield', '#theme_wrappers' => array('form_element'), + '#show_title' => FORM_ELEMENT_SHOW_TITLE_BEFORE, ); $type['password'] = array( @@ -350,12 +372,14 @@ function system_elements() { '#process' => array('ajax_process_form'), '#theme' => 'password', '#theme_wrappers' => array('form_element'), + '#show_title' => FORM_ELEMENT_SHOW_TITLE_BEFORE, ); $type['password_confirm'] = array( '#input' => TRUE, '#process' => array('form_process_password_confirm'), '#theme_wrappers' => array('form_element'), + '#show_title' => FORM_ELEMENT_SHOW_TITLE_BEFORE, ); $type['textarea'] = array( @@ -366,6 +390,7 @@ function system_elements() { '#process' => array('form_process_text_format', 'ajax_process_form'), '#theme' => 'textarea', '#theme_wrappers' => array('form_element'), + '#show_title' => FORM_ELEMENT_SHOW_TITLE_BEFORE, ); $type['radios'] = array( @@ -373,6 +398,7 @@ function system_elements() { '#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, ); $type['radio'] = array( @@ -381,7 +407,8 @@ function system_elements() { '#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, ); $type['checkboxes'] = array( @@ -390,6 +417,7 @@ function system_elements() { '#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, ); $type['checkbox'] = array( @@ -398,7 +426,8 @@ function system_elements() { '#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, ); $type['select'] = array( @@ -408,6 +437,7 @@ function system_elements() { '#process' => array('ajax_process_form'), '#theme' => 'select', '#theme_wrappers' => array('form_element'), + '#show_title' => FORM_ELEMENT_SHOW_TITLE_BEFORE, ); $type['weight'] = array( @@ -423,6 +453,7 @@ function system_elements() { '#process' => array('form_process_date'), '#theme' => 'date', '#theme_wrappers' => array('form_element'), + '#show_title' => FORM_ELEMENT_SHOW_TITLE_BEFORE, ); $type['file'] = array( @@ -430,6 +461,7 @@ function system_elements() { '#size' => 60, '#theme' => 'file', '#theme_wrappers' => array('form_element'), + '#show_title' => FORM_ELEMENT_SHOW_TITLE_BEFORE, ); $type['tableselect'] = array(