diff --git a/core/includes/common.inc b/core/includes/common.inc index 46caf3e..3da7fc0 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -1141,6 +1141,51 @@ function valid_url($url, $absolute = FALSE) { } /** + * Verifies that a number is a multiple of a given step. + * + * This is based on the number/range verification methods of webkit. + * + * @param $value + * The value that needs to be checked. + * @param $step + * The step scale factor. Must be positive. + * @param $offset + * (optional) An offset, to which the difference must be a multiple of the + * given step. + * + * @return + * TRUE if no step mismatch has occured + * FALSE if a step mismatch has occured. + * + * @see http://opensource.apple.com/source/WebCore/WebCore-1298/html/NumberInputType.cpp + */ +function valid_number_step($value, $step, $offset = 0.0) { + $double_value = (double) abs($value - $offset); + + // The number of digits a float has. + $digits = 24; + + // The number of digits a double has. + $mantissa = 53; + + // 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 TRUE; + } + + // 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. + $remainder = (double) abs($double_value - $step * round($double_value / $step)); + // Accepts erros in lower fractional part. + $computed_acceptable_error = (double)($step / pow(2.0, $mantissa)); + + return $computed_acceptable_error >= $remainder && $remainder >= ($step - $computed_acceptable_error); +} + +/** * @} End of "defgroup validation". */ @@ -6952,6 +6997,9 @@ function drupal_common_theme() { 'email' => 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 0124abc..3b96191 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -3836,6 +3836,66 @@ function theme_tel($variables) { 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, + * #placeholder, #required, #attributes, #step. + * + * @ingroup themeable + */ +function theme_number($variables) { + $element = $variables['element']; + + $element['#attributes']['type'] = 'number'; + element_set_attributes($element, array('id', 'name', 'value', 'size', 'step', 'min', 'max', 'maxlength', 'placeholder')); + _form_set_class($element, array('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']))); + } + + if (strtolower($element['#step']) != 'any') { + // Check that the input is an allowed multiple of #step (offset by #min if + // #min is set). + $min = isset($element['#min']) ? $element['#min'] : 0.0; + + if (!valid_number_step($value, $element['#step'], $min)) { + form_error($element, t('%name is not a valid number.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title']))); + } + } +} + /** * Returns HTML for a form. * diff --git a/core/modules/simpletest/drupal_web_test_case.php b/core/modules/simpletest/drupal_web_test_case.php index e8cbaad..72bec91 100644 --- a/core/modules/simpletest/drupal_web_test_case.php +++ b/core/modules/simpletest/drupal_web_test_case.php @@ -2262,6 +2262,7 @@ class DrupalWebTestCase extends DrupalTestCase { case 'text': case 'tel': case 'textarea': + case 'number': case 'hidden': case 'password': case 'email': diff --git a/core/modules/simpletest/tests/common.test b/core/modules/simpletest/tests/common.test index 353ab1c..4297a56 100644 --- a/core/modules/simpletest/tests/common.test +++ b/core/modules/simpletest/tests/common.test @@ -2002,6 +2002,53 @@ class CommonValidUrlUnitTestCase extends DrupalUnitTestCase { } /** + * Tests number step validation by valid_number_step(). + */ +class CommonValidNumberStepUnitTestCase extends DrupalUnitTestCase { + public static function getInfo() { + return array( + 'name' => 'Number step validation', + 'description' => 'Tests number step validation by valid_number_step()', + 'group' => 'Common', + ); + } + + /** + * Tests valid_number_step() without offset. + */ + function testNumberStep() { + $this->assertTrue(valid_number_step(10.3, 10.3)); + $this->assertTrue(valid_number_step(42, 21)); + $this->assertTrue(valid_number_step(42, 3)); + $this->assertTrue(valid_number_step(42, 10.5)); + $this->assertTrue(valid_number_step(1, 1/3)); + $this->assertTrue(valid_number_step(-100, 100/7)); + $this->assertTrue(valid_number_step(1000, -10)); + + $this->assertFalse(valid_number_step(6, 5/7)); + $this->assertFalse(valid_number_step(100, 30)); + $this->assertFalse(valid_number_step(-10, 4)); + $this->assertFalse(valid_number_step(10.3, 10.25)); + } + + /** + * Tests valid_number_step() with offset. + */ + function testNumberStepOffset() { + $this->assertTrue(valid_number_step(11.3, 10.3, 1)); + $this->assertTrue(valid_number_step(100, 10, 50)); + $this->assertTrue(valid_number_step(-100, 90/7, -10)); + $this->assertTrue(valid_number_step(2/7 + 5/9, 1/7, 5/9)); + + $this->assertFalse(valid_number_step(10.3, 10.3, 0.0001)); + $this->assertFalse(valid_number_step(1000, 10, -5)); + $this->assertFalse(valid_number_step(-10, 4, 0)); + $this->assertFalse(valid_number_step(-10, 4, -4)); + $this->assertFalse(valid_number_step(1/5, 1/7, 1/11)); + } +} + +/** * Tests writing of data records with drupal_write_record(). */ class CommonDrupalWriteRecordTestCase extends DrupalWebTestCase { diff --git a/core/modules/simpletest/tests/form.test b/core/modules/simpletest/tests/form.test index d3c5907..930c136 100644 --- a/core/modules/simpletest/tests/form.test +++ b/core/modules/simpletest/tests/form.test @@ -219,6 +219,80 @@ class FormsTestCase extends DrupalWebTestCase { } /** + * Tests validation of #type 'number' elements. + */ + function testNumber() { + $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), + 'float_step_any_no_error' => array(FALSE, FALSE ,FALSE, FALSE), + ); + + // 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->assertRaw(t($errorMessages[$id], $placeholders)); + } + else { + $this->assertNoRaw(t($errorMessages[$id], $placeholders)); + } + } + } + } + + /** * Test handling of disabled elements. * * @see _form_test_disabled_elements() @@ -261,7 +335,7 @@ class FormsTestCase extends DrupalWebTestCase { // All the elements should be marked as disabled, including the ones below // the disabled container. - $this->assertEqual(count($disabled_elements), 35, 'The correct elements have the disabled property in the HTML code.'); + $this->assertEqual(count($disabled_elements), 36, 'The correct elements have the disabled property in the HTML code.'); $this->drupalPost(NULL, $edit, t('Submit')); $returned_values['hijacked'] = drupal_json_decode($this->content); diff --git a/core/modules/simpletest/tests/form_test.module b/core/modules/simpletest/tests/form_test.module index 6e76bad..592dbb0 100644 --- a/core/modules/simpletest/tests/form_test.module +++ b/core/modules/simpletest/tests/form_test.module @@ -119,6 +119,12 @@ function form_test_menu() { 'page arguments' => array('form_test_placeholder_test'), 'access callback' => TRUE, ); + $items['form-test/number'] = array( + 'title' => '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', @@ -1017,7 +1023,11 @@ function form_test_select($form, &$form_state) { '#multiple' => TRUE, ); - $form['submit'] = array('#type' => 'submit', '#value' => 'Submit'); + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Submit', + ); + return $form; } @@ -1029,11 +1039,123 @@ function form_test_select_submit($form, &$form_state) { exit(); } +/* + * Builds a form to test #type 'number' validation. + */ +function form_test_number($form, &$form_state) { + $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', + '#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.9411764729088, + '#step' => 0.00392156863712, + ); + $form['float_step_hard_error'] = $base + array( + '#title' => 'Float test hard, #step_error', + '#default_value' => 0.9411764, + '#step' => 0.00392156863, + ); + $form['float_step_any_no_error'] = $base + array( + '#title' => 'Arbitrary float', + '#default_value' => 0.839562930284, + '#step' => 'aNy', + ); + $form['submit'] = array('#type' => 'submit', '#value' => 'Submit'); + return $form; +} + /** * Builds a form to test the placeholder attribute. */ function form_test_placeholder_test($form, &$form_state) { - foreach (array('textfield', 'textarea', 'password', 'tel', 'email') as $type) { + foreach (array('textfield', 'textarea', 'password', 'tel', 'email', 'number') as $type) { $form[$type] = array( '#type' => $type, '#title' => $type, @@ -1214,6 +1336,14 @@ function _form_test_disabled_elements($form, &$form_state) { '#disabled' => TRUE, ); + $form['number'] = array( + '#type' => 'number', + '#title' => 'number', + '#disabled' => TRUE, + '#default_value' => 1, + '#test_hijack_value' => 2, + ); + // Date. $form['date'] = array( '#type' => 'date', diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 86a5f7b..7efc21a 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -384,6 +384,16 @@ function system_element_info() { '#theme' => 'email', '#theme_wrappers' => array('form_element'), ); + $types['number'] = array( + '#input' => TRUE, + '#size' => 30, + '#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, diff --git a/core/themes/bartik/css/style.css b/core/themes/bartik/css/style.css index 739ae81..aa8bab7 100644 --- a/core/themes/bartik/css/style.css +++ b/core/themes/bartik/css/style.css @@ -1196,6 +1196,7 @@ select.form-select { input.form-text, input.form-tel, input.form-email, +input.form-number, textarea.form-textarea, select.form-select { border: 1px solid #ccc; diff --git a/core/themes/seven/style.css b/core/themes/seven/style.css index 4214453..db769bb 100644 --- a/core/themes/seven/style.css +++ b/core/themes/seven/style.css @@ -603,6 +603,7 @@ div.teaser-checkbox .form-item, .form-disabled input.form-text, .form-disabled input.form-tel, .form-disabled input.form-email, +.form-disabled input.form-number, .form-disabled input.form-file, .form-disabled textarea.form-textarea, .form-disabled select.form-select { @@ -691,6 +692,7 @@ input.form-autocomplete, input.form-text, input.form-tel, input.form-email, +input.form-number, input.form-file, textarea.form-textarea, select.form-select { @@ -706,6 +708,7 @@ select.form-select { input.form-text:focus, input.form-tel:focus, input.form-email:focus, +input.form-number:focus, input.form-file:focus, textarea.form-textarea:focus, select.form-select:focus {