Commit 4193c4ce authored by catch's avatar catch

Issue #1174640 by Niklas Fiekas, aspilicious, ericduran, cosmicdreams,...

Issue #1174640 by Niklas Fiekas, aspilicious, ericduran, cosmicdreams, amateescu, tim.plunkett: Added new HTML5 FAPI element: number.
parent 68eecef4
......@@ -1149,6 +1149,52 @@ function valid_url($url, $absolute = FALSE) {
}
}
/**
* Verifies that a number is a multiple of a given step.
*
* The implementation assumes it is dealing with IEEE 754 double precision
* floating point numbers that are used by PHP on most systems.
*
* 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 bool
* TRUE if no step mismatch has occured, or FALSE otherwise.
*
* @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 fractional part of a double has 53 bits. The greatest number that could
// be represented with that is 2^53. If the given value is even bigger than
// $step * 2^53, then dividing by $step will result in a very small remainder.
// Since that remainder can't even be represented with a single precision
// float the following computation of the remainder makes no sense and we can
// safely ignore it instead.
if ($double_value / pow(2.0, 53) > $step) {
return TRUE;
}
// Now compute that remainder of a division by $step.
$remainder = (double) abs($double_value - $step * round($double_value / $step));
// $remainder is a double precision floating point number. Remainders that
// can't be represented with single precision floats are acceptable. The
// fractional part of a float has 24 bits. That means remainders smaller than
// $step * 2^-24 are acceptable.
$computed_acceptable_error = (double)($step / pow(2.0, 24));
return $computed_acceptable_error >= $remainder || $remainder >= ($step - $computed_acceptable_error);
}
/**
* @} End of "defgroup validation".
*/
......@@ -6978,6 +7024,9 @@ function drupal_common_theme() {
'url' => array(
'render element' => 'element',
),
'number' => array(
'render element' => 'element',
),
'form' => array(
'render element' => 'element',
),
......
......@@ -3909,6 +3909,69 @@ function theme_tel($variables) {
return '<input' . drupal_attributes($element['#attributes']) . ' />' . drupal_render_children($element);
}
/**
* 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 = '<input' . drupal_attributes($element['#attributes']) . ' />';
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;
}
$name = empty($element['#title']) ? $element['#parents'][0] : $element['#title'];
// Ensure the input is numeric.
if (!is_numeric($value)) {
form_error($element, t('%name must be a number.', array('%name' => $name)));
return;
}
// 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' => $name, '%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' => $name, '%max' => $element['#max'])));
}
if (isset($element['#step']) && strtolower($element['#step']) != 'any') {
// Check that the input is an allowed multiple of #step (offset by #min if
// #min is set).
$offset = isset($element['#min']) ? $element['#min'] : 0.0;
if (!valid_number_step($value, $element['#step'], $offset)) {
form_error($element, t('%name is not a valid number.', array('%name' => $name)));
}
}
}
/**
* Returns HTML for a url form element.
*
......@@ -4345,16 +4408,6 @@ function element_validate_integer_positive($element, &$form_state) {
}
}
/**
* Form element validation handler for number elements.
*/
function element_validate_number($element, &$form_state) {
$value = $element['#value'];
if ($value != '' && !is_numeric($value)) {
form_error($element, t('%name must be a number.', array('%name' => $element['#title'])));
}
}
/**
* @} End of "defgroup form_api".
*/
......
......@@ -98,14 +98,14 @@ function number_field_instance_settings_form($field, $instance) {
'#title' => t('Minimum'),
'#default_value' => $settings['min'],
'#description' => t('The minimum value that should be allowed in this field. Leave blank for no minimum.'),
'#element_validate' => array('element_validate_number'),
'#element_validate' => array('form_validate_number'),
);
$form['max'] = array(
'#type' => 'textfield',
'#title' => t('Maximum'),
'#default_value' => $settings['max'],
'#description' => t('The maximum value that should be allowed in this field. Leave blank for no maximum.'),
'#element_validate' => array('element_validate_number'),
'#element_validate' => array('form_validate_number'),
);
$form['prefix'] = array(
'#type' => 'textfield',
......
......@@ -2263,6 +2263,7 @@ protected function handleForm(&$post, &$edit, &$upload, $submit, $form) {
case 'tel':
case 'textarea':
case 'url':
case 'number':
case 'hidden':
case 'password':
case 'email':
......
......@@ -2006,6 +2006,73 @@ 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() {
// Value and step equal.
$this->assertTrue(valid_number_step(10.3, 10.3));
// Valid integer steps.
$this->assertTrue(valid_number_step(42, 21));
$this->assertTrue(valid_number_step(42, 3));
// Valid float steps.
$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));
// Valid and very small float steps.
$this->assertTrue(valid_number_step(1000.12345, 1e-10));
$this->assertTrue(valid_number_step(3.9999999999999, 1e-13));
// Invalid integer steps.
$this->assertFalse(valid_number_step(100, 30));
$this->assertFalse(valid_number_step(-10, 4));
// Invalid float steps.
$this->assertFalse(valid_number_step(6, 5/7));
$this->assertFalse(valid_number_step(10.3, 10.25));
// Step mismatches very close to beeing valid.
$this->assertFalse(valid_number_step(70 + 9e-7, 10 + 9e-7));
$this->assertFalse(valid_number_step(1936.5, 3e-8));
}
/**
* Tests valid_number_step() with offset.
*/
function testNumberStepOffset() {
// Try obvious fits.
$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));
// Ensure a small offset is still invalid.
$this->assertFalse(valid_number_step(10.3, 10.3, 0.0001));
$this->assertFalse(valid_number_step(1/5, 1/7, 1/11));
// Try negative values and offsets.
$this->assertFalse(valid_number_step(1000, 10, -5));
$this->assertFalse(valid_number_step(-10, 4, 0));
$this->assertFalse(valid_number_step(-10, 4, -4));
}
}
/**
* Tests writing of data records with drupal_write_record().
*/
......
......@@ -300,6 +300,69 @@ 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 to be checked.
$error_messages = array(
'no_number' => '%name must be a number.',
'too_low' => '%name must be higher or equal to %min.',
'too_high' => '%name must be below or equal to %max.',
'step_mismatch' => '%name is not a valid number.',
);
// The expected errors.
$expected = array(
'integer_no_number' => 'no_number',
'integer_no_step' => 0,
'integer_no_step_step_error' => 'step_mismatch',
'integer_step' => 0,
'integer_step_error' => 'step_mismatch',
'integer_step_min' => 0,
'integer_step_min_error' => 'too_low',
'integer_step_max' => 0,
'integer_step_max_error' => 'too_high',
'integer_step_min_border' => 0,
'integer_step_max_border' => 0,
'integer_step_based_on_min' => 0,
'integer_step_based_on_min_error' => 'step_mismatch',
'float_small_step' => 0,
'float_step_no_error' => 0,
'float_step_error' => 'step_mismatch',
'float_step_hard_no_error' => 0,
'float_step_hard_error' => 'step_mismatch',
'float_step_any_no_error' => 0,
);
// Post form and show errors.
$this->drupalPost(NULL, array(), 'Submit');
foreach ($expected as $element => $error) {
// Create placeholder array.
$placeholders = array(
'%name' => $form[$element]['#title'],
'%min' => isset($form[$element]['#min']) ? $form[$element]['#min'] : '0',
'%max' => isset($form[$element]['#max']) ? $form[$element]['#max'] : '0',
);
foreach ($error_messages as $id => $message) {
// Check if the error exists on the page, if the current message ID is
// expected. Otherwise ensure that the error message is not present.
if ($id === $error) {
$this->assertRaw(format_string($message, $placeholders));
}
else {
$this->assertNoRaw(format_string($message, $placeholders));
}
}
}
}
/**
* Test handling of disabled elements.
*
......@@ -343,7 +406,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), 37, 'The correct elements have the disabled property in the HTML code.');
$this->assertEqual(count($disabled_elements), 38, '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);
......
......@@ -133,6 +133,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',
......@@ -1125,11 +1131,126 @@ 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', 'url', 'password', 'search', 'tel', 'email') as $type) {
foreach (array('textfield', 'textarea', 'url', 'password', 'search', 'tel', 'email', 'number') as $type) {
$form[$type] = array(
'#type' => $type,
'#title' => $type,
......@@ -1343,6 +1464,15 @@ function _form_test_disabled_elements($form, &$form_state) {
'#disabled' => TRUE,
);
// Number.
$form['number'] = array(
'#type' => 'number',
'#title' => 'number',
'#disabled' => TRUE,
'#default_value' => 1,
'#test_hijack_value' => 2,
);
// Date.
$form['date'] = array(
'#type' => 'date',
......
......@@ -403,6 +403,16 @@ function system_element_info() {
'#theme' => 'search',
'#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,
......
/* ---------- Overall Specifications ---------- */
body {
......@@ -1192,6 +1193,7 @@ input.form-tel,
input.form-email,
input.form-url,
input.form-search,
input.form-number,
textarea.form-textarea,
select.form-select {
border: 1px solid #ccc;
......
/**
* Generic elements.
*/
......@@ -604,6 +605,7 @@ div.teaser-checkbox .form-item,
.form-disabled input.form-email,
.form-disabled input.form-url,
.form-disabled input.form-search,
.form-disabled input.form-number,
.form-disabled input.form-file,
.form-disabled textarea.form-textarea,
.form-disabled select.form-select {
......@@ -694,6 +696,7 @@ input.form-tel,
input.form-email,
input.form-url,
input.form-search,
input.form-number,
input.form-file,
textarea.form-textarea,
select.form-select {
......@@ -711,6 +714,7 @@ input.form-tel:focus,
input.form-email:focus,
input.form-url:focus,
input.form-search:focus,
input.form-number:focus,
input.form-file:focus,
textarea.form-textarea:focus,
select.form-select:focus {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment