From f484299c2914bbda554369ab65ec155d5216b5e4 Mon Sep 17 00:00:00 2001 From: niklas Date: Sat, 5 May 2012 21:07:17 +0200 Subject: [PATCH 1/2] - #1445224 by Niklas Fiekas: Added new HTML5 FAPI element: color. --- core/includes/common.inc | 3 + core/includes/form.inc | 41 +++++ core/lib/Drupal/Core/Utility/Color.php | 179 ++++++++++++++++++++ core/modules/simpletest/drupal_web_test_case.php | 1 + core/modules/system/system.module | 8 + core/modules/system/tests/common.test | 61 +++++++ core/modules/system/tests/form.test | 37 ++++- .../tests/modules/form_test/form_test.module | 42 +++++ core/themes/bartik/css/style.css | 1 + core/themes/seven/style.css | 3 + 10 files changed, 375 insertions(+), 1 deletions(-) create mode 100644 core/lib/Drupal/Core/Utility/Color.php diff --git a/core/includes/common.inc b/core/includes/common.inc index 14154af..845b38b 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -7019,6 +7019,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 c387384..5bdaf90 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 * @{ @@ -4086,6 +4088,45 @@ function form_validate_url(&$element, &$form_state) { } /** + * Form element validation handler for #type 'color'. + */ +function form_validate_color(&$element, &$form_state) { + // Empty means black. + $value = trim($element['#value']); + if ($value === '') { + $value = '#000000'; + } + + // Try to parse the value. + if ($parsed = Color::parseHex($value)) { + // Set a normalized value. + form_set_value($element, $parsed->__toString(), $form_state); + } + else { + 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 '' . drupal_render_children($element); +} + +/** * Returns HTML for a form. * * @param $variables diff --git a/core/lib/Drupal/Core/Utility/Color.php b/core/lib/Drupal/Core/Utility/Color.php new file mode 100644 index 0000000..472d223 --- /dev/null +++ b/core/lib/Drupal/Core/Utility/Color.php @@ -0,0 +1,179 @@ + 255 || $green > 255 || $blue > 255) { + throw new \InvalidArgumentException('Color components must be between 0 and 255.'); + } + + if ($alpha > 127) { + throw new \InvalidArgumentException('Alpha component must be between 0 and 127.'); + } + + $this->red = (int) $red; + $this->green = (int) $green; + $this->blue = (int) $blue; + $this->alpha = (int) $alpha; + } + + /** + * Implements PHP magic __toString method to convert the color to a string. + * + * @return string + * A hexadecimal representation of the color like '#aabbcc' or '#aabbcc55'. + */ + public function __toString() { + $result = '#'; + + foreach (array('red', 'green', 'blue', 'alpha') as $component) { + if ($component != 'alpha' || $this->{$component}) { + $result .= str_pad(dechex($this->{$component}), 2, '0', STR_PAD_LEFT); + } + } + + return $result; + } + + /** + * Gets the red component of the color. + * + * @return int + * The red component of the color as an integer between 0 and 255. + */ + public function getRed() { + return $this->red; + } + + /** + * Gets the green component of the color. + * + * @return int + * The green component of the color as an integer between 0 and 255. + */ + public function getGreen() { + return $this->green; + } + + /** + * Gets the blue component of the color. + * + * @return int + * The blue component of the color as an integer between 0 and 255. + */ + public function getBlue() { + return $this->blue; + } + + /** + * Gets the alpha component of the color. + * + * @return int + * The alpha component of the color as an integer between 0 and 127, where + * 0 is opaque and 127 is fully transparent. + */ + public function getAlpha() { + return $this->alpha; + } + + /** + * Gets the decimal alpha component of the color. + * + * @return float + * The alpha component of the color as a float between 0 and 1, where 0 is + * fully transparent and 1 is opaque. + */ + public function getDecimalAlpha() { + return 1 - $alpha / 127.0; + } + + /** + * Parses a hexadecimal color string like '#abc' or '#aabbcc'. + * + * @param string $hex + * The hexadecimal colorstring to parse. + * @param bool $allow_alpha + * Optional. Whether or not to allow an alpha component. Defaults to FALSE. + * + * @return false|Drupal\Core\Utility\Color + * The color object representation of the string or FALSE, if the string is + * invalid. + */ + public static function parseHex($hex, $allow_alpha = FALSE) { + $hex = ltrim($hex, '#'); + + // Expand shorthands like 'abc' to 'aabbcc'. + if (strlen($hex) < 4) { + $hex = preg_replace('|([0-9a-f])|i', '\1\1', $hex); + } + + if (!preg_match('/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-7][0-9a-f])?$/i', $hex, $rgba)) { + return FALSE; + } + + if (!$allow_alpha && isset($rgba[4])) { + return FALSE; + } + + return new Color(hexdec($rgba[1]), hexdec($rgba[2]), hexdec($rgba[3]), isset($rgba[4]) ? hexdec($rgba[4]) : 0); + } +} diff --git a/core/modules/simpletest/drupal_web_test_case.php b/core/modules/simpletest/drupal_web_test_case.php index 23fa526..6d563f5 100644 --- a/core/modules/simpletest/drupal_web_test_case.php +++ b/core/modules/simpletest/drupal_web_test_case.php @@ -2321,6 +2321,7 @@ class DrupalWebTestCase extends DrupalTestCase { case 'url': case 'number': case 'range': + case 'color': case 'hidden': case 'password': case 'email': diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 69ee2af..8d5f09e 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -425,6 +425,14 @@ function system_element_info() { '#theme' => 'range', '#theme_wrappers' => array('form_element'), ); + $types['color'] = array( + '#input' => TRUE, + '#default_value' => '#000000', + '#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/common.test b/core/modules/system/tests/common.test index 46b379e..e20f874 100644 --- a/core/modules/system/tests/common.test +++ b/core/modules/system/tests/common.test @@ -2050,6 +2050,67 @@ class CommonValidNumberStepUnitTestCase extends DrupalUnitTestCase { } /** + * Tests color conversion functions. + */ +class CommonColorConversionTestCase extends DrupalUnitTestCase { + public static function getInfo() { + return array( + 'name' => 'Color conversion', + 'description' => 'Tests color conversion by drupal_hex_to_rgba() and drupal_rgba_to_hex()', + 'group' => 'Common', + ); + } + + /** + * Tests drupal_hex_to_rgba(). + */ + function testDrupalHexToRGBA() { + // Test shorthand conversion without alpha. + $this->assertEqual(drupal_hex_to_rgba('#000'), array('red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 0)); + $this->assertEqual(drupal_hex_to_rgba('#fff'), array('red' => 255, 'green' => 255, 'blue' => 255, 'alpha' => 0)); + $this->assertEqual(drupal_hex_to_rgba('#abc'), array('red' => 170, 'green' => 187, 'blue' => 204, 'alpha' => 0)); + $this->assertEqual(drupal_hex_to_rgba('cba'), array('red' => 204, 'green' => 187, 'blue' => 170, 'alpha' => 0)); + + // Test shorthand conversion with alpha. + $this->assertEqual(drupal_hex_to_rgba('#0007', TRUE), array('red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 119)); + $this->assertEqual(drupal_hex_to_rgba('#7001', TRUE), array('red' => 119, 'green' => 0, 'blue' => 0, 'alpha' => 17)); + $this->assertEqual(drupal_hex_to_rgba('2000', TRUE), array('red' => 34, 'green' => 0, 'blue' => 0, 'alpha' => 0)); + + // Test conversion without alpha. + $this->assertEqual(drupal_hex_to_rgba('#000000'), array('red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 0)); + $this->assertEqual(drupal_hex_to_rgba('#010203'), array('red' => 1, 'green' => 2, 'blue' => 3, 'alpha' => 0)); + + // Test too high alpha values. + $this->assertIdentical(drupal_hex_to_rgba('#0008', TRUE), FALSE); + $this->assertIdentical(drupal_hex_to_rgba('#a00f', TRUE), FALSE); + $this->assertIdentical(drupal_hex_to_rgba('#aa00aaf1', TRUE), FALSE); + + // Test alpha values are invalid if they are not allowed. + $this->assertIdentical(drupal_hex_to_rgba('#1111'), FALSE); + $this->assertIdentical(drupal_hex_to_rgba('#22334455'), FALSE); + $this->assertIdentical(drupal_hex_to_rgba('#0000'), FALSE); + + // Test bogus input. + $this->assertIdentical(drupal_hex_to_rgba('#foo'), FALSE); + $this->assertIdentical(drupal_hex_to_rgba('123456789'), FALSE); + } + + /** + * Tests drupal_rgba_to_hex(). + */ + function testDrupalRGBAToHex() { + // Test conversion without alpha. + $this->assertEqual(drupal_rgba_to_hex(array('red' => 7)), '#070000'); + $this->assertEqual(drupal_rgba_to_hex(array('green' => 255)), '#00ff00'); + $this->assertEqual(drupal_rgba_to_hex(array('red' => 1, 'green' => 2, 'blue' => 3)), '#010203'); + + // Test conversion with alpha. + $this->assertEqual(drupal_rgba_to_hex(array('alpha' => 10)), '#0000000a'); + $this->assertEqual(drupal_rgba_to_hex(array('green' => 15, 'alpha' => 3)), '#000f0003'); + } +} + +/** * Tests writing of data records with drupal_write_record(). */ class CommonDrupalWriteRecordTestCase extends DrupalWebTestCase { diff --git a/core/modules/system/tests/form.test b/core/modules/system/tests/form.test index b64fdd7..3e31dc7 100644 --- a/core/modules/system/tests/form.test +++ b/core/modules/system/tests/form.test @@ -366,6 +366,41 @@ class FormsTestCase extends DrupalWebTestCase { } /** + * 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. * * @see _form_test_disabled_elements() @@ -408,7 +443,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), 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/tests/modules/form_test/form_test.module b/core/modules/system/tests/modules/form_test/form_test.module index f42b5f5..141c6d0 100644 --- a/core/modules/system/tests/modules/form_test/form_test.module +++ b/core/modules/system/tests/modules/form_test/form_test.module @@ -145,6 +145,12 @@ function form_test_menu() { 'page arguments' => array('form_test_number', 'range'), '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', @@ -1277,6 +1283,32 @@ function form_test_number($form, &$form_state, $element = 'number') { } /** + * 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. */ function form_test_placeholder_test($form, &$form_state) { @@ -1496,6 +1528,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 7ab3d4e..3563b44 100644 --- a/core/themes/bartik/css/style.css +++ b/core/themes/bartik/css/style.css @@ -1194,6 +1194,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 a3d6773..9e807d5 100644 --- a/core/themes/seven/style.css +++ b/core/themes/seven/style.css @@ -612,6 +612,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 { @@ -703,6 +704,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 { @@ -721,6 +723,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 { -- 1.7.6.msysgit.0 From 9946d60e6965da8448ff8ae0c9d736669b4bd3bd Mon Sep 17 00:00:00 2001 From: sun Date: Sun, 6 May 2012 01:59:16 +0200 Subject: [PATCH 2/2] Refactored Color class methods; added massive test coverage. --- core/includes/form.inc | 7 +- core/lib/Drupal/Core/Utility/Color.php | 224 ++++++++++++-------------------- core/modules/system/system.module | 1 - core/modules/system/tests/common.test | 167 ++++++++++++++++++------ 4 files changed, 219 insertions(+), 180 deletions(-) diff --git a/core/includes/form.inc b/core/includes/form.inc index 5bdaf90..45bf8fe 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -4092,18 +4092,21 @@ function form_validate_url(&$element, &$form_state) { */ function form_validate_color(&$element, &$form_state) { // Empty means black. + // @todo This default looks suspicious/bogus. $value = trim($element['#value']); if ($value === '') { $value = '#000000'; } // Try to parse the value. + // @todo Catch exceptions. if ($parsed = Color::parseHex($value)) { + // @todo Leave to value consumers...? (this destroys the original value) // Set a normalized value. - form_set_value($element, $parsed->__toString(), $form_state); + form_set_value($element, $parsed, $form_state); } else { - form_error($element, t('%name must be a valid color.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title']))); + form_error($element, t('The %name color %color is not valid.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title'], '%color' => $value))); } } diff --git a/core/lib/Drupal/Core/Utility/Color.php b/core/lib/Drupal/Core/Utility/Color.php index 472d223..6e1b1a9 100644 --- a/core/lib/Drupal/Core/Utility/Color.php +++ b/core/lib/Drupal/Core/Utility/Color.php @@ -8,172 +8,120 @@ namespace Drupal\Core\Utility; /** - * Represents a color. + * Performs color conversions. */ class Color { /** - * The red component of the color as an integer between 0 and 255. + * Validates whether a hexadecimal color value is syntatically correct. * - * @var int - */ - protected $red; - - /** - * The green component of the color as an integer between 0 and 255. - * - * @var int - */ - protected $green; - - /** - * The blue component of the color as an integer between 0 and 255. + * @param $hex + * The hexadecimal string to validate. May contain a leading '#'. May use + * the shorthand notation (e.g., '123'). May contain a 4th value for the + * alpha channel (ranging from 00 to FF). * - * @var int + * @return bool + * TRUE if $hex is valid or FALSE if it is not. */ - protected $blue; + public static function validateHex($hex) { + // Must be a string. + $valid = is_string($hex); + // Hash prefix is optional. + $hex = ltrim($hex, '#'); + // Must be either RGB, RGBA, RRGGBB, or RRGGBBAA. + // RGBA and RRGGBBAA notations are not in the official HTML and CSS + // specifications, but supported by a range of implementations and only a + // pragmatic extension to the spec. + $length = drupal_strlen($hex); + $valid = $valid && ($length === 3 || $length === 4 || $length === 6 || $length === 8); + // Must be a valid hex value. + $valid = $valid && ctype_xdigit($hex); + return $valid; + } /** - * The alpha component of the color as an integer between 0 and 127, where - * 0 is opaque and 127 is fully transparent. + * Parses a hexadecimal color string like '#abc' or '#aabbcc'. * - * @var int - */ - protected $alpha; - - /** - * Constructs a Color object. + * @param string $hex + * The hexadecimal color string to parse. + * @param bool $php_alpha + * (optional) Whether to return the alpha channel value suitable for PHP + * image functions (0 being opaque, 127 being transparent). Defaults to + * FALSE, in which case alpha values range between 0.0 and 1.0. * - * @param int $red - * The red component of the color as an integer between 0 and 255. - * @param int $green - * The green component of the color as an integer between 0 and 255. - * @param int $blue - * The blue component of the color as an integer between 0 and 255. - * @param int $alpha - * Optional. The alpha component of the color as an integer between 0 and - * 127, where 0 is opaque and 127 is fully transparent. Defaults to 0. + * @return array|false + * An array containing the values for 'red', 'green', 'blue', and 'alpha'. * - * @throws \InvalidArgumentException - * When one of the components has an invalid value. + * @throws InvalidArgumentException */ - public function __construct($red, $green, $blue, $alpha = 0) { - if ($red < 0 || $green < 0 || $blue < 0 || $alpha < 0) { - throw new \InvalidArgumentException('Color components must be positive integers.'); - } - - if ($red > 255 || $green > 255 || $blue > 255) { - throw new \InvalidArgumentException('Color components must be between 0 and 255.'); + public static function hex2rgba($hex, $php_alpha = FALSE) { + if (!self::validateHex($hex)) { + throw new \InvalidArgumentException("'$hex' is not a valid hex value."); } + // Ignore '#' prefixes. + $hex = ltrim($hex, '#'); - if ($alpha > 127) { - throw new \InvalidArgumentException('Alpha component must be between 0 and 127.'); + // Convert shorhands like '#abc' to '#aabbcc'. + if (strlen($hex) <= 4) { + $hex = preg_replace('/([0-9a-z])/i', '\1\1', $hex); } - $this->red = (int) $red; - $this->green = (int) $green; - $this->blue = (int) $blue; - $this->alpha = (int) $alpha; - } + // Parse out the components. + preg_match('/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/i', $hex, $rgba); - /** - * Implements PHP magic __toString method to convert the color to a string. - * - * @return string - * A hexadecimal representation of the color like '#aabbcc' or '#aabbcc55'. - */ - public function __toString() { - $result = '#'; + // Fill in default alpha channel value (opaque). + $rgba += array(4 => 'ff'); - foreach (array('red', 'green', 'blue', 'alpha') as $component) { - if ($component != 'alpha' || $this->{$component}) { - $result .= str_pad(dechex($this->{$component}), 2, '0', STR_PAD_LEFT); - } + // Calculate alpha channel return value. + if ($php_alpha) { + // Hex ranges from 00 to FF, whereas 00 is transparent and FF is opaque. + // PHP ranges from 0 to 127, whereas 0 is opaque and 127 is transparent. + $rgba[4] = (int) round((255 - hexdec($rgba[4])) * 127 / 255); + } + else { + // Hex ranges from 00 to FF. HTML/CSS ranges from 0.0 to 1.0. + // Precision accounts for the minimum hex value #01 == 0.003921 == 0.004. + $rgba[4] = (float) round(hexdec($rgba[4]) / 255, 3); } - return $result; - } - - /** - * Gets the red component of the color. - * - * @return int - * The red component of the color as an integer between 0 and 255. - */ - public function getRed() { - return $this->red; - } - - /** - * Gets the green component of the color. - * - * @return int - * The green component of the color as an integer between 0 and 255. - */ - public function getGreen() { - return $this->green; - } - - /** - * Gets the blue component of the color. - * - * @return int - * The blue component of the color as an integer between 0 and 255. - */ - public function getBlue() { - return $this->blue; - } - - /** - * Gets the alpha component of the color. - * - * @return int - * The alpha component of the color as an integer between 0 and 127, where - * 0 is opaque and 127 is fully transparent. - */ - public function getAlpha() { - return $this->alpha; + return array( + 'red' => (int) hexdec($rgba[1]), + 'green' => (int) hexdec($rgba[2]), + 'blue' => (int) hexdec($rgba[3]), + 'alpha' => $rgba[4], + ); } - /** - * Gets the decimal alpha component of the color. - * - * @return float - * The alpha component of the color as a float between 0 and 1, where 0 is - * fully transparent and 1 is opaque. - */ - public function getDecimalAlpha() { - return 1 - $alpha / 127.0; - } - - /** - * Parses a hexadecimal color string like '#abc' or '#aabbcc'. - * - * @param string $hex - * The hexadecimal colorstring to parse. - * @param bool $allow_alpha - * Optional. Whether or not to allow an alpha component. Defaults to FALSE. - * - * @return false|Drupal\Core\Utility\Color - * The color object representation of the string or FALSE, if the string is - * invalid. - */ - public static function parseHex($hex, $allow_alpha = FALSE) { - $hex = ltrim($hex, '#'); - - // Expand shorthands like 'abc' to 'aabbcc'. - if (strlen($hex) < 4) { - $hex = preg_replace('|([0-9a-f])|i', '\1\1', $hex); + public static function rgba2hex($input, $php_alpha = FALSE) { + // Remove named array keys if input comes from Color::hex2rgba(). + if (is_array($input)) { + $rgba = array_values($input); + } + // Parse string input in CSS notation ('10, 20, 30, 1.0'). + elseif (is_string($input)) { + preg_match('/(\d+), ?(\d+), ?(\d+)(?:, ?([\d\.]+))?/', $input, $rgba); + array_shift($rgba); } - if (!preg_match('/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-7][0-9a-f])?$/i', $hex, $rgba)) { - return FALSE; + $result = '#'; + for ($i = 0; $i < 3; $i++) { + $result .= str_pad(dechex($rgba[$i]), 2, '0', STR_PAD_LEFT); } - if (!$allow_alpha && isset($rgba[4])) { - return FALSE; + // Only add an alpha channel value, if one was contained in the input. + if (isset($rgba[3])) { + if ($php_alpha) { + // PHP ranges from 0 to 127, whereas 0 is opaque and 127 is transparent. + // Hex ranges from 00 to FF, whereas 00 is transparent and FF is opaque. + $rgba[3] = dechex(255 - round($rgba[3] * 255 / 127)); + } + else { + // HTML/CSS ranges from 0.0 to 1.0, Hex from 00 to FF. + $rgba[3] = dechex($rgba[3] * 255); + } + $result .= str_pad($rgba[3], 2, '0', STR_PAD_LEFT); } - return new Color(hexdec($rgba[1]), hexdec($rgba[2]), hexdec($rgba[3]), isset($rgba[4]) ? hexdec($rgba[4]) : 0); + return $result; } } diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 8d5f09e..af5ab2d 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -427,7 +427,6 @@ function system_element_info() { ); $types['color'] = array( '#input' => TRUE, - '#default_value' => '#000000', '#process' => array('ajax_process_form'), '#element_validate' => array('form_validate_color'), '#theme' => 'color', diff --git a/core/modules/system/tests/common.test b/core/modules/system/tests/common.test index e20f874..de4caf6 100644 --- a/core/modules/system/tests/common.test +++ b/core/modules/system/tests/common.test @@ -5,6 +5,8 @@ * Tests for common.inc functionality. */ +use Drupal\Core\Utility\Color; + /** * Tests for URL generation functions. */ @@ -2056,57 +2058,144 @@ class CommonColorConversionTestCase extends DrupalUnitTestCase { public static function getInfo() { return array( 'name' => 'Color conversion', - 'description' => 'Tests color conversion by drupal_hex_to_rgba() and drupal_rgba_to_hex()', + 'description' => 'Tests Color utility class conversions.', 'group' => 'Common', ); } /** - * Tests drupal_hex_to_rgba(). + * Tests Color::hex2rgba(). */ - function testDrupalHexToRGBA() { - // Test shorthand conversion without alpha. - $this->assertEqual(drupal_hex_to_rgba('#000'), array('red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 0)); - $this->assertEqual(drupal_hex_to_rgba('#fff'), array('red' => 255, 'green' => 255, 'blue' => 255, 'alpha' => 0)); - $this->assertEqual(drupal_hex_to_rgba('#abc'), array('red' => 170, 'green' => 187, 'blue' => 204, 'alpha' => 0)); - $this->assertEqual(drupal_hex_to_rgba('cba'), array('red' => 204, 'green' => 187, 'blue' => 170, 'alpha' => 0)); - - // Test shorthand conversion with alpha. - $this->assertEqual(drupal_hex_to_rgba('#0007', TRUE), array('red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 119)); - $this->assertEqual(drupal_hex_to_rgba('#7001', TRUE), array('red' => 119, 'green' => 0, 'blue' => 0, 'alpha' => 17)); - $this->assertEqual(drupal_hex_to_rgba('2000', TRUE), array('red' => 34, 'green' => 0, 'blue' => 0, 'alpha' => 0)); - - // Test conversion without alpha. - $this->assertEqual(drupal_hex_to_rgba('#000000'), array('red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 0)); - $this->assertEqual(drupal_hex_to_rgba('#010203'), array('red' => 1, 'green' => 2, 'blue' => 3, 'alpha' => 0)); - - // Test too high alpha values. - $this->assertIdentical(drupal_hex_to_rgba('#0008', TRUE), FALSE); - $this->assertIdentical(drupal_hex_to_rgba('#a00f', TRUE), FALSE); - $this->assertIdentical(drupal_hex_to_rgba('#aa00aaf1', TRUE), FALSE); + function testHexToRGBA() { + // 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, + )); - // Test alpha values are invalid if they are not allowed. - $this->assertIdentical(drupal_hex_to_rgba('#1111'), FALSE); - $this->assertIdentical(drupal_hex_to_rgba('#22334455'), FALSE); - $this->assertIdentical(drupal_hex_to_rgba('#0000'), FALSE); + foreach ($values as $test) { + $this->assertFalse(Color::validateHex($test), var_export($test, TRUE) . ' is invalid.'); + try { + Color::hex2rgba($test); + $this->fail('Color::hex2rgba(' . var_export($test, TRUE) . ') did not throw an exception.'); + } + // @todo InvalidArgumentException should be sufficient here, but the + // exception handler tries to log the exception in the database, which + // triggers a registry lookup, but the {registry} table does not exist + // in unit tests. We therefore have to catch all exceptions. + // @see http://drupal.org/node/1563620 + catch (Exception $e) { + $this->pass('Color::hex2rgba(' . var_export($test, TRUE) . ') threw an exception.'); + } + } - // Test bogus input. - $this->assertIdentical(drupal_hex_to_rgba('#foo'), FALSE); - $this->assertIdentical(drupal_hex_to_rgba('123456789'), FALSE); + // 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', 'rgba' => array('red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 1.0, 'php_alpha' => 0)), + array('hex' => '#fff', 'rgba' => array('red' => 255, 'green' => 255, 'blue' => 255, 'alpha' => 1.0, 'php_alpha' => 0)), + array('hex' => '#abc', 'rgba' => array('red' => 170, 'green' => 187, 'blue' => 204, 'alpha' => 1.0, 'php_alpha' => 0)), + array('hex' => 'cba', 'rgba' => array('red' => 204, 'green' => 187, 'blue' => 170, 'alpha' => 1.0, 'php_alpha' => 0)), + // Shorthands with alpha. + array('hex' => '#0000', 'rgba' => array('red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 0.0, 'php_alpha' => 127)), + array('hex' => '#ffff', 'rgba' => array('red' => 255, 'green' => 255, 'blue' => 255, 'alpha' => 1.0, 'php_alpha' => 0)), + array('hex' => '#7777', 'rgba' => array('red' => 119, 'green' => 119, 'blue' => 119, 'alpha' => 0.467, 'php_alpha' => 68)), + array('hex' => '#1111', 'rgba' => array('red' => 17, 'green' => 17, 'blue' => 17, 'alpha' => 0.067, 'php_alpha' => 119)), + array('hex' => '2000', 'rgba' => array('red' => 34, 'green' => 0, 'blue' => 0, 'alpha' => 0.0, 'php_alpha' => 127)), + // Full without alpha. + array('hex' => '#000000', 'rgba' => array('red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 1.0, 'php_alpha' => 0)), + array('hex' => '#ffffff', 'rgba' => array('red' => 255, 'green' => 255, 'blue' => 255, 'alpha' => 1.0, 'php_alpha' => 0)), + array('hex' => '#010203', 'rgba' => array('red' => 1, 'green' => 2, 'blue' => 3, 'alpha' => 1.0, 'php_alpha' => 0)), + // Full with alpha. + array('hex' => '#00000000', 'rgba' => array('red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 0.0, 'php_alpha' => 127)), + array('hex' => '#ffffffff', 'rgba' => array('red' => 255, 'green' => 255, 'blue' => 255, 'alpha' => 1.0, 'php_alpha' => 0)), + array('hex' => '#77777777', 'rgba' => array('red' => 119, 'green' => 119, 'blue' => 119, 'alpha' => 0.467, 'php_alpha' => 68)), + array('hex' => '#01020304', 'rgba' => array('red' => 1, 'green' => 2, 'blue' => 3, 'alpha' => 0.016, 'php_alpha' => 125)), + ); + foreach ($tests as $test) { + $hex = $test['hex']; + $expected = $test['rgba']; + $expected_php_alpha = $expected['php_alpha']; + unset($expected['php_alpha']); + try { + $result = Color::hex2rgba($hex); + $this->assertIdentical($result, $expected); + // Assert the PHP specific alpha channel return value. + // 127 is transparent, 0 is opaque. + // @see imagecolorallocatealpha() + $result = Color::hex2rgba($hex, TRUE); + $expected['alpha'] = $expected_php_alpha; + $this->assertIdentical($result, $expected); + } + catch (Exception $e) { + $this->fail($e->getMessage(), 'Exception'); + } + } } /** - * Tests drupal_rgba_to_hex(). + * Tests Color::rgba2hex(). */ - function testDrupalRGBAToHex() { - // Test conversion without alpha. - $this->assertEqual(drupal_rgba_to_hex(array('red' => 7)), '#070000'); - $this->assertEqual(drupal_rgba_to_hex(array('green' => 255)), '#00ff00'); - $this->assertEqual(drupal_rgba_to_hex(array('red' => 1, 'green' => 2, 'blue' => 3)), '#010203'); - - // Test conversion with alpha. - $this->assertEqual(drupal_rgba_to_hex(array('alpha' => 10)), '#0000000a'); - $this->assertEqual(drupal_rgba_to_hex(array('green' => 15, 'alpha' => 3)), '#000f0003'); + function testRGBAToHex() { + $tests = array( + '#00000000' => array('red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 0.0, 'php_alpha' => 127), + '#ffffffff' => array('red' => 255, 'green' => 255, 'blue' => 255, 'alpha' => 1.0, 'php_alpha' => 0), + '#77777778' => array('red' => 119, 'green' => 119, 'blue' => 119, 'alpha' => 0.471, 'php_alpha' => 67), + '#01020304' => array('red' => 1, 'green' => 2, 'blue' => 3, 'alpha' => 0.016, 'php_alpha' => 125), + ); + // Input using named RGBA array (e.g., as returned by Color::hex2rgba()). + foreach ($tests as $expected => $rgba) { + unset($rgba['php_alpha']); + $this->assertIdentical(Color::rgba2hex($rgba), $expected); + } + // Input using named RGB array. + foreach ($tests as $expected => $rgba) { + unset($rgba['alpha'], $rgba['php_alpha']); + $expected = substr($expected, 0, 7); + $this->assertIdentical(Color::rgba2hex($rgba), $expected); + } + // Input using indexed RGBA array (e.g.: array(10, 10, 10, 0.0)). + foreach ($tests as $expected => $rgba) { + unset($rgba['php_alpha']); + $rgba = array_values($rgba); + $this->assertIdentical(Color::rgba2hex($rgba), $expected); + } + // Input using indexed RGB array. + foreach ($tests as $expected => $rgba) { + unset($rgba['alpha'], $rgba['php_alpha']); + $expected = substr($expected, 0, 7); + $rgba = array_values($rgba); + $this->assertIdentical(Color::rgba2hex($rgba), $expected); + } + // Input using CSS RGBA string notation (e.g.: 10, 10, 10, 0.0). + foreach ($tests as $expected => $rgba) { + unset($rgba['php_alpha']); + $rgba = implode(', ', $rgba); + $this->assertIdentical(Color::rgba2hex($rgba), $expected); + } + // Input using CSS RGB string notation (e.g.: 10, 10, 10). + foreach ($tests as $expected => $rgba) { + unset($rgba['alpha'], $rgba['php_alpha']); + $expected = substr($expected, 0, 7); + $rgba = implode(', ', $rgba); + $this->assertIdentical(Color::rgba2hex($rgba), $expected); + } + // Input using PHP alpha channel value. + foreach ($tests as $expected => $rgba) { + $rgba['alpha'] = $rgba['php_alpha']; + unset($rgba['php_alpha']); + $this->assertIdentical(Color::rgba2hex($rgba, TRUE), $expected); + } } } -- 1.7.6.msysgit.0