From 5789200b3e8ed6719457c45c359a315a48979b52 Mon Sep 17 00:00:00 2001
From: webchick <webchick@24967.no-reply.drupal.org>
Date: Sat, 7 Jul 2012 13:21:18 -0700
Subject: [PATCH] Issue #1445224 by Niklas Fiekas, sun, Tor Arne Thune: Add new
 HTML5 FAPI element: color.

---
 core/includes/common.inc                      |   3 +
 core/includes/form.inc                        |  43 ++++++++
 core/lib/Drupal/Core/Utility/Color.php        | 101 ++++++++++++++++++
 .../lib/Drupal/simpletest/WebTestBase.php     |   1 +
 .../Drupal/system/Tests/Common/ColorTest.php  | 100 +++++++++++++++++
 .../lib/Drupal/system/Tests/Form/FormTest.php |  37 ++++++-
 core/modules/system/system.module             |   7 ++
 .../tests/modules/form_test/form_test.module  |  42 ++++++++
 core/themes/bartik/css/style.css              |   1 +
 core/themes/seven/style.css                   |   3 +
 10 files changed, 337 insertions(+), 1 deletion(-)
 create mode 100644 core/lib/Drupal/Core/Utility/Color.php
 create mode 100644 core/modules/system/lib/Drupal/system/Tests/Common/ColorTest.php

diff --git a/core/includes/common.inc b/core/includes/common.inc
index 8b53cc601c68..5c5777a6abdd 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -6949,6 +6949,9 @@ function drupal_common_theme() {
     'range' => array(
       'render element' => 'element',
     ),
+    'color' => array(
+      'render element' => 'element',
+    ),
     'form' => array(
       'render element' => 'element',
     ),
diff --git a/core/includes/form.inc b/core/includes/form.inc
index 571a43921638..395cca75beef 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -5,6 +5,8 @@
  * Functions for form and batch generation and processing.
  */
 
+use Drupal\Core\Utility\Color;
+
 /**
  * @defgroup forms Form builder functions
  * @{
@@ -4180,6 +4182,47 @@ function form_validate_url(&$element, &$form_state) {
   }
 }
 
+/**
+ * Form element validation handler for #type 'color'.
+ */
+function form_validate_color(&$element, &$form_state) {
+  $value = trim($element['#value']);
+
+  // Default to black if no value is given.
+  // @see http://www.w3.org/TR/html5/number-state.html#color-state
+  if ($value === '') {
+    form_set_value($element, '#000000', $form_state);
+  }
+  else {
+    // Try to parse the value and normalize it.
+    try {
+      form_set_value($element, Color::rgbToHex(Color::hexToRgb($value)), $form_state);
+    }
+    catch (InvalidArgumentException $e) {
+      form_error($element, t('%name must be a valid color.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title'])));
+    }
+  }
+}
+
+/**
+ * Returns HTML for a color form element.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #title, #value, #description, #attributes.
+ *
+ * @ingroup themeable
+ */
+function theme_color($variables) {
+  $element = $variables['element'];
+  $element['#attributes']['type'] = 'color';
+  element_set_attributes($element, array('id', 'name', 'value'));
+  _form_set_class($element, array('form-color'));
+
+  return '<input' . drupal_attributes($element['#attributes']) . ' />' . drupal_render_children($element);
+}
+
 /**
  * Returns HTML for a form.
  *
diff --git a/core/lib/Drupal/Core/Utility/Color.php b/core/lib/Drupal/Core/Utility/Color.php
new file mode 100644
index 000000000000..27ba440f30f1
--- /dev/null
+++ b/core/lib/Drupal/Core/Utility/Color.php
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Utility\Color.
+ */
+
+namespace Drupal\Core\Utility;
+
+/**
+ * Performs color conversions.
+ */
+class Color {
+
+  /**
+   * Validates whether a hexadecimal color value is syntatically correct.
+   *
+   * @param $hex
+   *   The hexadecimal string to validate. May contain a leading '#'. May use
+   *   the shorthand notation (e.g., '123' for '112233').
+   *
+   * @return bool
+   *   TRUE if $hex is valid or FALSE if it is not.
+   */
+  public static function validateHex($hex) {
+    // Must be a string.
+    $valid = is_string($hex);
+    // Hash prefix is optional.
+    $hex = ltrim($hex, '#');
+    // Must be either RGB or RRGGBB.
+    $length = drupal_strlen($hex);
+    $valid = $valid && ($length === 3 || $length === 6);
+    // Must be a valid hex value.
+    $valid = $valid && ctype_xdigit($hex);
+    return $valid;
+  }
+
+  /**
+   * Parses a hexadecimal color string like '#abc' or '#aabbcc'.
+   *
+   * @param string $hex
+   *   The hexadecimal color string to parse.
+   *
+   * @return array
+   *   An array containing the values for 'red', 'green', 'blue'.
+   *
+   * @throws \InvalidArgumentException
+   */
+  public static function hexToRgb($hex) {
+    if (!self::validateHex($hex)) {
+      throw new \InvalidArgumentException("'$hex' is not a valid hex value.");
+    }
+
+    // Ignore '#' prefixes.
+    $hex = ltrim($hex, '#');
+
+    // Convert shorhands like '#abc' to '#aabbcc'.
+    if (strlen($hex) == 3) {
+      $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
+    }
+
+    $c = hexdec($hex);
+
+    return array(
+      'red' => $c >> 16 & 0xFF,
+      'green' => $c >> 8 & 0xFF,
+      'blue' => $c & 0xFF,
+    );
+  }
+
+  /**
+   * Converts RGB color arrays and RGB strings in CSS notation to lowercase
+   * simple colors like '#aabbcc'.
+   *
+   * @param array|string $input
+   *   The value to convert. If the value is an array the first three elements
+   *   will be used as the red, green and blue components. String values in CSS
+   *   notation like '10, 20, 30' are also supported.
+   *
+   * @return string
+   *   The lowercase simple color representation of the given color.
+   */
+  public static function rgbToHex($input) {
+    // Remove named array keys if input comes from Color::hex2rgb().
+    if (is_array($input)) {
+      $rgb = array_values($input);
+    }
+    // Parse string input in CSS notation ('10, 20, 30').
+    elseif (is_string($input)) {
+      preg_match('/(\d+), ?(\d+), ?(\d+)/', $input, $rgb);
+      array_shift($rgb);
+    }
+
+    $out = 0;
+    foreach ($rgb as $k => $v) {
+      $out |= $v << (16 - $k * 8);
+    }
+
+    return '#' . str_pad(dechex($out), 6, 0, STR_PAD_LEFT);
+  }
+}
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
index 486dbcb74146..592e9d94aed4 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
@@ -1463,6 +1463,7 @@ protected function handleForm(&$post, &$edit, &$upload, $submit, $form) {
           case 'url':
           case 'number':
           case 'range':
+          case 'color':
           case 'hidden':
           case 'password':
           case 'email':
diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/ColorTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/ColorTest.php
new file mode 100644
index 000000000000..2c32b31e8895
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Common/ColorTest.php
@@ -0,0 +1,100 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Common\ColorTest.
+ */
+
+namespace Drupal\system\Tests\Common;
+
+use Drupal\Core\Utility\Color;
+use Drupal\simpletest\UnitTestBase;
+
+/**
+ * Tests color conversion functions.
+ */
+class ColorTest extends UnitTestBase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Color conversion',
+      'description' => 'Tests Color utility class conversions.',
+      'group' => 'Common',
+    );
+  }
+
+  /**
+   * Tests Color::hexToRgb().
+   */
+  function testHexToRgb() {
+    // Any invalid arguments should throw an exception.
+    $values = array('', '-1', '1', '12', '12345', '1234567', '123456789', '123456789a', 'foo');
+    // Duplicate all invalid value tests with additional '#' prefix.
+    // The '#' prefix inherently turns the data type into a string.
+    foreach ($values as $value) {
+      $values[] = '#' . $value;
+    }
+    // Add invalid data types (hex value must be a string).
+    $values = array_merge($values, array(
+      1, 12, 1234, 12345, 123456, 1234567, 12345678, 123456789, 123456789,
+      -1, PHP_INT_MAX, PHP_INT_MAX + 1, -PHP_INT_MAX,
+      0x0, 0x010,
+    ));
+
+    foreach ($values as $test) {
+      $this->assertFalse(Color::validateHex($test), var_export($test, TRUE) . ' is invalid.');
+      try {
+        Color::hexToRgb($test);
+        $this->fail('Color::hexToRgb(' . var_export($test, TRUE) . ') did not throw an exception.');
+      }
+      catch (\InvalidArgumentException $e) {
+        $this->pass('Color::hexToRgb(' . var_export($test, TRUE) . ') threw an exception.');
+      }
+    }
+
+    // PHP automatically casts a numeric array key into an integer.
+    // Since hex values may consist of 0-9 only, they need to be defined as
+    // array values.
+    $tests = array(
+      // Shorthands without alpha.
+      array('hex' => '#000', 'rgb' => array('red' => 0, 'green' => 0, 'blue' => 0)),
+      array('hex' => '#fff', 'rgb' => array('red' => 255, 'green' => 255, 'blue' => 255)),
+      array('hex' => '#abc', 'rgb' => array('red' => 170, 'green' => 187, 'blue' => 204)),
+      array('hex' => 'cba', 'rgb' => array('red' => 204, 'green' => 187, 'blue' => 170)),
+      // Full without alpha.
+      array('hex' => '#000000', 'rgb' => array('red' => 0, 'green' => 0, 'blue' => 0)),
+      array('hex' => '#ffffff', 'rgb' => array('red' => 255, 'green' => 255, 'blue' => 255)),
+      array('hex' => '#010203', 'rgb' => array('red' => 1, 'green' => 2, 'blue' => 3)),
+    );
+    foreach ($tests as $test) {
+      $result = Color::hexToRgb($test['hex']);
+      $this->assertIdentical($result, $test['rgb']);
+    }
+  }
+
+  /**
+   * Tests Color::rgbToHex().
+   */
+  function testRgbToHex() {
+    $tests = array(
+      '#000000' => array('red' => 0, 'green' => 0, 'blue' => 0),
+      '#ffffff' => array('red' => 255, 'green' => 255, 'blue' => 255),
+      '#777777' => array('red' => 119, 'green' => 119, 'blue' => 119),
+      '#010203' => array('red' => 1, 'green' => 2, 'blue' => 3),
+    );
+    // Input using named RGB array (e.g., as returned by Color::hexToRgb()).
+    foreach ($tests as $expected => $rgb) {
+      $this->assertIdentical(Color::rgbToHex($rgb), $expected);
+    }
+    // Input using indexed RGB array (e.g.: array(10, 10, 10)).
+    foreach ($tests as $expected => $rgb) {
+      $rgb = array_values($rgb);
+      $this->assertIdentical(Color::rgbToHex($rgb), $expected);
+    }
+    // Input using CSS RGB string notation (e.g.: 10, 10, 10).
+    foreach ($tests as $expected => $rgb) {
+      $rgb = implode(', ', $rgb);
+      $this->assertIdentical(Color::rgbToHex($rgb), $expected);
+    }
+  }
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php b/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php
index 578ac286618c..6225bb9d5f0f 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php
@@ -383,6 +383,41 @@ function testRange() {
     $this->assertFieldByXPath('//input[@type="range" and contains(@class, "error")]', NULL, 'Range element has the error class.');
   }
 
+  /**
+   * Tests validation of #type 'color' elements.
+   */
+  function testColorValidation() {
+    // Keys are inputs, values are expected results.
+    $values = array(
+      '' => '#000000',
+      '#000' => '#000000',
+      'AAA' => '#aaaaaa',
+      '#af0DEE' => '#af0dee',
+      '#99ccBc' => '#99ccbc',
+      '#aabbcc' => '#aabbcc',
+      '123456' => '#123456',
+    );
+
+    // Tests that valid values are properly normalized.
+    foreach ($values as $input => $expected) {
+      $edit = array(
+        'color' => $input,
+      );
+      $result = json_decode($this->drupalPost('form-test/color', $edit, 'Submit'));
+      $this->assertEqual($result->color, $expected);
+    }
+
+    // Tests invalid values are rejected.
+    $values = array('#0008', '#1234', '#fffffg', '#abcdef22', '17', '#uaa');
+    foreach ($values as $input) {
+      $edit = array(
+        'color' => $input,
+      );
+      $this->drupalPost('form-test/color', $edit, 'Submit');
+      $this->assertRaw(t('%name must be a valid color.', array('%name' => 'Color')));
+    }
+  }
+
   /**
    * Test handling of disabled elements.
    *
@@ -426,7 +461,7 @@ function testDisabledElements() {
 
     // All the elements should be marked as disabled, including the ones below
     // the disabled container.
-    $this->assertEqual(count($disabled_elements), 39, 'The correct elements have the disabled property in the HTML code.');
+    $this->assertEqual(count($disabled_elements), 40, '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/system/system.module b/core/modules/system/system.module
index 75b1fa25061a..ae875c27b7df 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -425,6 +425,13 @@ function system_element_info() {
     '#theme' => 'range',
     '#theme_wrappers' => array('form_element'),
   );
+  $types['color'] = array(
+    '#input' => TRUE,
+    '#process' => array('ajax_process_form'),
+    '#element_validate' => array('form_validate_color'),
+    '#theme' => 'color',
+    '#theme_wrappers' => array('form_element'),
+  );
   $types['machine_name'] = array(
     '#input' => TRUE,
     '#default_value' => NULL,
diff --git a/core/modules/system/tests/modules/form_test/form_test.module b/core/modules/system/tests/modules/form_test/form_test.module
index 1e9e8820a649..e25a16e86a61 100644
--- a/core/modules/system/tests/modules/form_test/form_test.module
+++ b/core/modules/system/tests/modules/form_test/form_test.module
@@ -163,6 +163,12 @@ function form_test_menu() {
     'page arguments' => array('form_test_range_invalid'),
     'access callback' => TRUE,
   );
+  $items['form-test/color'] = array(
+    'title' => 'Color',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('form_test_color'),
+    'access callback' => TRUE,
+  );
   $items['form-test/checkboxes-radios'] = array(
     'title' => t('Checkboxes, Radios'),
     'page callback' => 'drupal_get_form',
@@ -1413,6 +1419,32 @@ function form_test_range_invalid($form, &$form_state) {
   return $form;
 }
 
+/**
+ * Form constructor for testing #type 'color' elements.
+ *
+ * @see form_test_color_submit()
+ * @ingroup forms
+ */
+function form_test_color($form, &$form_state) {
+  $form['color'] = array(
+    '#type' => 'color',
+    '#title' => 'Color',
+  );
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => 'Submit',
+  );
+  return $form;
+}
+
+/**
+ * Form submission handler for form_test_color().
+ */
+function form_test_color_submit($form, &$form_state) {
+  drupal_json_output($form_state['values']);
+  exit;
+}
+
 /**
  * Builds a form to test the placeholder attribute.
  */
@@ -1637,6 +1669,16 @@ function _form_test_disabled_elements($form, &$form_state) {
     );
   }
 
+  // Color.
+  $form['color'] = array(
+    '#type' => 'color',
+    '#title' => 'color',
+    '#disabled' => TRUE,
+    '#default_value' => '#0000ff',
+    '#test_hijack_value' => '#ff0000',
+    '#disabled' => TRUE,
+  );
+
   // Date.
   $form['date'] = array(
     '#type' => 'date',
diff --git a/core/themes/bartik/css/style.css b/core/themes/bartik/css/style.css
index 72f776de4a3d..7773eda86034 100644
--- a/core/themes/bartik/css/style.css
+++ b/core/themes/bartik/css/style.css
@@ -1200,6 +1200,7 @@ input.form-email,
 input.form-url,
 input.form-search,
 input.form-number,
+input.form-color,
 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 380aec821faf..ae443a449c0e 100644
--- a/core/themes/seven/style.css
+++ b/core/themes/seven/style.css
@@ -610,6 +610,7 @@ div.teaser-checkbox .form-item,
 .form-disabled input.form-url,
 .form-disabled input.form-search,
 .form-disabled input.form-number,
+.form-disabled input.form-color,
 .form-disabled input.form-file,
 .form-disabled textarea.form-textarea,
 .form-disabled select.form-select {
@@ -701,6 +702,7 @@ input.form-email,
 input.form-url,
 input.form-search,
 input.form-number,
+input.form-color,
 input.form-file,
 textarea.form-textarea,
 select.form-select {
@@ -719,6 +721,7 @@ input.form-email:focus,
 input.form-url:focus,
 input.form-search:focus,
 input.form-number:focus,
+input.form-color:focus,
 input.form-file:focus,
 textarea.form-textarea:focus,
 select.form-select:focus {
-- 
GitLab