From 532a5575962528913c00d6ecb735197ab784e7ff Mon, 13 Jun 2011 17:36:46 +0200
From: Bram Goffings <bramgoffings@gmail.com>
Date: Mon, 13 Jun 2011 17:34:29 +0200
Subject: [PATCH] Html 5 range + test



diff --git a/includes/common.inc b/includes/common.inc
index 4ec37dc..fafdde9 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -6558,6 +6558,9 @@
     'textfield' => array(
       'render element' => 'element',
     ),
+    'range' => array(
+      'render element' => 'element',
+    ),
     'form' => array(
       'render element' => 'element',
     ),
diff --git a/includes/form.inc b/includes/form.inc
index 8f2ee26..80e7c46 100644
--- a/includes/form.inc
+++ b/includes/form.inc
@@ -3655,6 +3655,107 @@
 }
 
 /**
+ * Returns HTML for a range 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_range($variables) {
+  $element = $variables['element'];
+  $element['#attributes']['type'] = 'range';
+  element_set_attributes($element, array('id', 'name', 'value', 'step', 'min', 'max'));
+  _form_set_class($element, array('form-text', 'form-range'));
+
+  $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;
+
+  // Validate the #number property.
+  if (!is_numeric($value)) {
+    form_error($element, t('%name must be a number.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title'])));
+  }
+
+  // Validate the #min property.
+  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'])));
+  }
+
+  // Validate the #max property.
+  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'])));
+  }
+
+  // Validate the #step property.
+  if (isset($element['#step'])) {
+    if (isset($element['#min'])) {
+      if(_element_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_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'])));
+    }
+  }
+}
+
+/**
+ * Helper function for _element_validate_step().
+ *
+ * Checks if value is a valid step.
+ * 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=129
+ *
+ * @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 value is invalid
+ *   FALSE if value is valid
+ */
+function _element_validate_step_mismatch($value, $step, $min = 0.0) {
+  $doubleValue = (double)$value;
+  $doubleValue = abs($doubleValue - $min);
+  $digits = 16; // DBL_MANT_DIG
+
+  // double's fractional part size is DBL_MANT_DIG-bit. If the current value
+  // is greater than step*2^DBL_MANT_DIG, the following computation for
+  // remainder makes no sense.
+  if ($doubleValue / pow(2.0, $digits) > $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($doubleValue - $step * round($doubleValue / $step));
+  // Accepts erros in lower fractional part which IEEE 754 single-precision
+  // can't represent.
+  $computedAcceptableError = (double)($step / pow(2.0, $digits));
+
+  return $computedAcceptableError < $remainder && $remainder < ($step - $computedAcceptableError);
+}
+
+/**
  * Returns HTML for a form.
  *
  * @param $variables
@@ -3835,7 +3936,7 @@
   if (!empty($element['#name'])) {
     $attributes['class'][] = 'form-item-' . strtr($element['#name'], array(' ' => '-', '_' => '-', '[' => '-', ']' => ''));
   }
-  // Add a class for disabled elements to facilitate cross-browser styling.
+  // Add a class for disabled ee to facilitate cross-browser styling.
   if (!empty($element['#attributes']['disabled'])) {
     $attributes['class'][] = 'form-disabled';
   }
diff --git a/modules/simpletest/tests/form.test b/modules/simpletest/tests/form.test
index e7ae9de..608f993 100644
--- a/modules/simpletest/tests/form.test
+++ b/modules/simpletest/tests/form.test
@@ -216,6 +216,74 @@
   }
 
   /**
+   * Tests validation of #type 'range' elements.
+   */
+  function testRange() {
+    $form = $form_state = array();
+    $form = form_test_number($form, $form_state, 'range');
+    $error = '!name field is required.';
+    $this->drupalGet('form-test/number/range');
+
+    // 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.',
+    );
+
+    /**
+     * An array keyed by the form title.
+     * Each value contains of 4 booleans, each boolean tells us if we suspect a
+     * partcular error. A list of the possible errors in the same order as they
+     * 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(
+      'range_integer_no_number' => array(true, false, false, false),
+      'range_integer_no_step' => array(false, false, false, false),
+      'range_integer_step' => array(false, false, false, false),
+      'range_integer_step_error' => array(false, false, false, true),
+      'range_integer_step_min' => array(false, false, false, false),
+      'range_integer_step_min_error' => array(false, true, false, false),
+      'range_integer_step_max' => array(false, false, false, false),
+      'range_integer_step_max_error' => array(false, false, true, false),
+      'range_integer_step_min_border' => array(false, false, false, false),
+      'range_integer_step_max_border' => array(false, false, false, false),
+      'range_integer_step_based_on_min' => array(false, false, false, false),
+      'range_integer_step_based_on_min_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/modules/simpletest/tests/form_test.module b/modules/simpletest/tests/form_test.module
index 00be7d2..801b8af 100644
--- a/modules/simpletest/tests/form_test.module
+++ b/modules/simpletest/tests/form_test.module
@@ -105,6 +105,18 @@
     'page arguments' => array('form_test_select'),
     'access callback' => TRUE,
   );
+  $items['form-test/number'] = array(
+    'title' => t('Select'),
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('form_test_select'),
+    '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',
@@ -967,6 +979,85 @@
     '#required' => TRUE,
     '#multiple' => TRUE,
   );
+
+  $form['submit'] = array('#type' => 'submit', '#value' => 'Submit');
+  return $form;
+}
+
+/**
+ * Builds a form to test #type 'range' validation.
+ */
+function form_test_number($form, &$form_state, $type = 'number') {
+  $base = array(
+    '#type' => $type,
+  );
+
+  $form['range_integer_no_number'] = $base + array(
+    '#title' => 'Integer test, #no_number_error',
+    '#default_value' => '#no_number',
+  );
+  $form['range_integer_no_step'] = $base + array(
+    '#title' => 'Integer test without step',
+    '#default_value' => 5,
+  );
+  $form['range_integer_step'] = $base + array(
+    '#title' => 'Integer test with step',
+    '#default_value' => 5,
+    '#step' => 1,
+  );
+  $form['range_integer_step_error'] = $base + array(
+    '#title' => 'Integer test, with step, #step_error',
+    '#default_value' => 5,
+    '#step' => 2,
+  );
+  $form['range_integer_step_min'] = $base + array(
+    '#title' => 'Integer test with min value',
+    '#default_value' => 5,
+    '#min' => 0,
+    '#step' => 1,
+  );
+  $form['range_integer_step_min_error'] = $base + array(
+    '#title' => 'Integer test with min value, #min_error',
+    '#default_value' => 5,
+    '#min' => 6,
+    '#step' => 1,
+  );
+  $form['range_integer_step_max'] = $base + array(
+    '#title' => 'Integer test with max value',
+    '#default_value' => 5,
+    '#max' => 6,
+    '#step' => 1,
+  );
+  $form['range_integer_step_max_error'] = $base + array(
+    '#title' => 'Integer test with max value, #max_error',
+    '#default_value' => 5,
+    '#max' => 4,
+    '#step' => 1,
+  );
+  $form['range_integer_step_min_border'] = $base + array(
+    '#title' => 'Integer test with min border check',
+    '#default_value' => -1,
+    '#min' => -1,
+    '#step' => 1,
+  );
+  $form['range_integer_step_max_border'] = $base + array(
+    '#title' => 'Integer test with max border check',
+    '#default_value' => 5,
+    '#max' => 5,
+    '#step' => 1,
+  );
+  $form['range_integer_step_based_on_min'] = $base + array(
+    '#title' => 'Integer test with step based on min check',
+    '#default_value' => 3,
+    '#min' => -1,
+    '#step' => 2,
+  );
+  $form['range_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['submit'] = array('#type' => 'submit', '#value' => 'Submit');
   return $form;
diff --git a/modules/system/system.module b/modules/system/system.module
index 3ebc657..a4debc2 100644
--- a/modules/system/system.module
+++ b/modules/system/system.module
@@ -355,6 +355,15 @@
     '#theme' => 'textfield',
     '#theme_wrappers' => array('form_element'),
   );
+  $types['range'] = array(
+    '#input' => TRUE,
+    '#size' => 60,
+    '#maxlength' => 128,
+    '#process' => array('ajax_process_form'),
+    '#element_validate' => array('form_validate_number'),
+    '#theme' => 'range',
+    '#theme_wrappers' => array('form_element'),
+  );
   $types['machine_name'] = array(
     '#input' => TRUE,
     '#default_value' => NULL,
