From 8d86bbac41cb13fa373d29fb62894257b394fdca Fri, 20 Jan 2012 10:42:01 +0100 From: Bram Goffings Date: Fri, 20 Jan 2012 10:40:44 +0100 Subject: [PATCH] number element diff --git a/core/includes/common.inc b/core/includes/common.inc index eefc053..3c830d7 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -6875,6 +6875,9 @@ 'tel' => array( 'render element' => 'element', ), + 'number' => array( + 'render element' => 'element', + ), 'form' => array( 'render element' => 'element', ), diff --git a/core/includes/form.inc b/core/includes/form.inc index 5663ac6..8768bde 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -3758,6 +3758,125 @@ return $output . $extra; } +/* + * Returns HTML for a number form element. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #title, #value, #description, #size, #min, #max, + * #required, #attributes, #step. + * + * @ingroup themeable + */ +function theme_number($variables) { + $element = $variables['element']; + + if (isset($element['#step']) && !is_numeric($element['#step'])) { + trigger_error(t('Form element type #number only accepts numeric values for step.'), E_USER_ERROR); + } + elseif (isset($element['#min']) && !is_numeric($element['#min'])) { + trigger_error(t('Form element type #number only accepts numeric values for min.'), E_USER_ERROR); + } + elseif (isset($element['#max']) && !is_numeric($element['#max'])) { + trigger_error(t('Form element type #number only accepts numeric values for max.'), E_USER_ERROR); + } + + $element['#attributes']['type'] = 'number'; + element_set_attributes($element, array('id', 'name', 'value', 'step', 'min', 'max')); + _form_set_class($element, array('form-text', 'form-number')); + + $output = ''; + + return $output; +} + +/** + * Form element validation handler for #type 'number'. + * + * Note that #required is validated by _form_validate() already. + */ +function form_validate_number(&$element, &$form_state) { + $value = $element['#value']; + if(!$value) + return; + + // Ensure the input is numeric. + if (!is_numeric($value)) { + form_error($element, t('%name must be a number.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title']))); + } + + // Ensure that the input is greater than the #min property, if set. + if (isset($element['#min']) && $value < $element['#min']) { + form_error($element, t('%name must be higher or equal to %min.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title'], '%min' => $element['#min']))); + } + + // Ensure that the input is less than the #max property, if set. + if (isset($element['#max']) && $value > $element['#max']) { + form_error($element, t('%name must be below or equal to %max.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title'], '%max' => $element['#max']))); + } + + // Check that the input is an allowed multiple of #step (offset by #min if + // #min is set). + if (isset($element['#min'])) { + if (_element_number_validate_step_mismatch($value, $element['#step'], $element['#min'])) { + form_error($element, t('%name is not a valid number.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title']))); + } + } + elseif (_element_number_validate_step_mismatch($value, $element['#step'])) { + form_error($element, t('%name is not a valid number.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title']))); + } +} + +/** + * Checks if a step mismatch has occured. + * + * This is based on the number/range verification methods of webkit. + * + * @see http://codesearch.google.com/#OAMlx_jo-ck/src/third_party/WebKit/Source/WebCore/html/NumberInputType.cpp&type=cs&l=191 + * + * @param $value + * The value that needs to be checked. + * @param $step + * The step scale factor for the widget. + * @param $min + * The min value of the widget. + * + * @return + * TRUE if the input value is invalid + * FALSE if the input value is valid + */ +function _element_number_validate_step_mismatch($value, $step, $min = 0.0) { + $double_value = (double) $value; + $double_value = abs($double_value - $min); + + // The precision length. Floating point numbers have limited precision. + // Although it depends on the system, PHP typically uses the IEEE 754 double + // precision format, which will give a maximum relative error due to rounding + // in the order of 1.11e-16. + $digits = 16; + + // This is max workable length for 32bit representation of mantissa. + $mantissa = 30; + + // Double's fractional part size is $mantissa-bit. If the current value + // is greater than step*2^$mantissa, the following computation for + // remainder makes no sense. + if ($double_value / pow(2.0, $mantissa) > $step) { + return false; + } + + // The computation follows HTML5 4.10.7.2.10 `The step attribute' : + // ... that number subtracted from the step base is not an integral multiple + // of the allowed value step, the element is suffering from a step mismatch. + (double) $remainder = abs($double_value - $step * round($double_value / $step)); + // Accepts erros in lower fractional part which IEEE 754 single-precision + // can't represent. + $computed_acceptable_error = (double)($step / pow(2.0, $digits)); + + return $computed_acceptable_error < $remainder && $remainder < ($step - $computed_acceptable_error); +} + /** * Returns HTML for a form. * diff --git a/core/modules/simpletest/tests/form.test b/core/modules/simpletest/tests/form.test index 784da88..82c9862 100644 --- a/core/modules/simpletest/tests/form.test +++ b/core/modules/simpletest/tests/form.test @@ -219,6 +219,80 @@ } /** + * Tests validation of #type 'number' elements. + */ + function testNumber() { + variable_set('error_level', ERROR_REPORTING_DISPLAY_ALL); + $form = $form_state = array(); + $form = form_test_number($form, $form_state); + $this->drupalGet('form-test/number'); + + // Array with all the error messages we need to check. + $errorMessages = array( + '!name must be a number.', + '!name must be higher or equal to !min.', + '!name must be below or equal to !max.', + '!name is not a valid number.', + ); + + /** + * The $expected array contains a key for each form title. For each title + * there are 4 different booleans, each boolean tells us where we expect a + * particular error. Below is the list of the possible errors in the same + * order as they would appear in the $errorMessages array. + * + * 1) %name must be a number. + * 2) %name must be higher or equal to %min. + * 3) %name must be below or equal to %max. + * 4) %name is not a valid number. + */ + $expected = array( + 'integer_no_number' => array(true, false, false, false), + 'integer_no_step' => array(false, false, false, false), + 'integer_no_step_step_error' => array(false, false, false, true), + 'integer_step' => array(false, false, false, false), + 'integer_step_error' => array(false, false, false, true), + 'integer_step_min' => array(false, false, false, false), + 'integer_step_min_error' => array(false, true, false, false), + 'integer_step_max' => array(false, false, false, false), + 'integer_step_max_error' => array(false, false, true, false), + 'integer_step_min_border' => array(false, false, false, false), + 'integer_step_max_border' => array(false, false, false, false), + 'integer_step_based_on_min' => array(false, false, false, false), + 'integer_step_based_on_min_error' => array(false, false, false, true), + 'float_small_step' => array(false, false, false, false), + 'float_step_no_error' => array(false, false, false, false), + 'float_step_error' => array(false, false, false, true), + 'float_step_hard_no_error' => array(false, false, false, false), + 'float_step_hard_error' => array(false, false, false, true), + ); + + // Post form and show errors. + $this->drupalPost(NULL, array(), 'Submit'); + + foreach ($expected as $element => $errors) { + foreach ($errors as $id => $error) { + // Create placeholder array + $placeholders = array('!name' => $form[$element]['#title']); + if ($id == 1) { + $placeholders['!min'] = isset($form[$element]['#min']) ? $form[$element]['#min'] : '0'; + } + if ($id == 2) { + $placeholders['!max'] = isset($form[$element]['#max']) ? $form[$element]['#max'] : '0'; + } + + // Check if we can find the error on the page. + if($error) { + $this->assertText(t($errorMessages[$id], $placeholders)); + } + else { + $this->assertNoText(t($errorMessages[$id], $placeholders)); + } + } + } + } + + /** * Test handling of disabled elements. * * @see _form_test_disabled_elements() diff --git a/core/modules/simpletest/tests/form_test.module b/core/modules/simpletest/tests/form_test.module index e1e2435..807db8b 100644 --- a/core/modules/simpletest/tests/form_test.module +++ b/core/modules/simpletest/tests/form_test.module @@ -119,6 +119,12 @@ 'page arguments' => array('form_test_placeholder_test'), 'access callback' => TRUE, ); + $items['form-test/number'] = array( + 'title' => t('Number'), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_test_number'), + 'access callback' => TRUE, + ); $items['form-test/checkboxes-radios'] = array( 'title' => t('Checkboxes, Radios'), 'page callback' => 'drupal_get_form', @@ -1023,6 +1029,115 @@ exit(); } +/* + * Builds a form to test #type 'number' validation. + */ +function form_test_number($form, &$form_state) { + define('SIMPLETEST_COLLECT_ERRORS', FALSE); + + $base = array( + '#type' => 'number', + ); + + $form['integer_no_number'] = $base + array( + '#title' => 'Integer test, #no_error', + '#default_value' => '#no_number', + ); + $form['integer_no_step'] = $base + array( + '#title' => 'Integer test without step', + '#default_value' => 5, + ); + $form['integer_no_step_step_error'] = $base + array( + '#title' => 'Integer test without step, #step_error', + '#default_value' => 4.5, + ); + $form['integer_step'] = $base + array( + '#title' => 'Integer test with step', + '#default_value' => 5, + '#step' => 1, + ); + $form['integer_step_error'] = $base + array( + '#title' => 'Integer test, with step, #step_error', + '#default_value' => 5, + '#step' => 2, + ); + $form['integer_step_min'] = $base + array( + '#title' => 'Integer test with min value', + '#default_value' => 5, + '#min' => 0, + '#step' => 1, + ); + $form['integer_step_min_error'] = $base + array( + '#title' => 'Integer test with min value, #min_error', + '#default_value' => 5, + '#min' => 6, + '#step' => 1, + ); + $form['integer_step_max'] = $base + array( + '#title' => 'Integer test with max value', + '#default_value' => 5, + '#max' => 6, + '#step' => 1, + ); + $form['integer_step_max_error'] = $base + array( + '#title' => 'Integer test with max value, #max_error', + '#default_value' => 5, + '#max' => 4, + '#step' => 1, + ); + $form['integer_step_min_border'] = $base + array( + '#title' => 'Integer test with min border check', + '#default_value' => -1, + '#min' => -1, + '#step' => 1, + ); + $form['integer_step_max_border'] = $base + array( + '#title' => 'Integer test with max border check', + '#default_value' => 5, + '#max' => 5, + '#step' => 1, + ); + $form['integer_step_based_on_min'] = $base + array( + '#title' => 'Integer test with step based on min check', + '#default_value' => 3, + '#min' => -1, + '#step' => 2, + ); + $form['integer_step_based_on_min_error'] = $base + array( + '#title' => 'Integer test with step based on min check, #step_error', + '#default_value' => 4, + '#min' => -1, + '#step' => 2, + ); + $form['float_small_step'] = $base + array( + '#title' => 'Float test with a small step', + '#default_value' => 4, + '#step' => 0.0000000000001, + ); + $form['float_step_no_error'] = $base + array( + '#title' => 'Float test hard', + '#default_value' => 1.2, + '#step' => 0.3, + ); + $form['float_step_error'] = $base + array( + '#title' => 'Float test, #step_error', + '#default_value' => 1.3, + '#step' => 0.3, + ); + $form['float_step_hard_no_error'] = $base + array( + '#title' => 'Float test hard', + '#default_value' => 0.9411764712, + '#step' => 0.00392156863712, + ); + $form['float_step_hard_error'] = $base + array( + '#title' => 'Float test hard, #step_error', + '#default_value' => 0.9411764, + '#step' => 0.00392156863, + ); + $form['submit'] = array('#type' => 'submit', '#value' => 'Submit'); + return $form; +} + /** * Builds a form to test the placeholder attribute. */ diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 28768f9..8c498b9 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -377,6 +377,16 @@ '#theme' => 'tel', '#theme_wrappers' => array('form_element'), ); + $types['number'] = array( + '#input' => TRUE, + '#size' => 60, + '#step' => 1, + '#maxlength' => 128, + '#process' => array('ajax_process_form'), + '#element_validate' => array('form_validate_number'), + '#theme' => 'number', + '#theme_wrappers' => array('form_element'), + ); $types['machine_name'] = array( '#input' => TRUE, '#default_value' => NULL,