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..45bf8fe 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,48 @@ function form_validate_url(&$element, &$form_state) { } /** + * Form element validation handler for #type 'color'. + */ +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, $form_state); + } + else { + form_error($element, t('The %name color %color is not valid.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title'], '%color' => $value))); + } +} + +/** + * 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..6e1b1a9 --- /dev/null +++ b/core/lib/Drupal/Core/Utility/Color.php @@ -0,0 +1,127 @@ + 'ff'); + + // 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 array( + 'red' => (int) hexdec($rgba[1]), + 'green' => (int) hexdec($rgba[2]), + 'blue' => (int) hexdec($rgba[3]), + 'alpha' => $rgba[4], + ); + } + + 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); + } + + $result = '#'; + for ($i = 0; $i < 3; $i++) { + $result .= str_pad(dechex($rgba[$i]), 2, '0', STR_PAD_LEFT); + } + + // 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 $result; + } +} diff --git a/core/modules/simpletest/drupal_web_test_case.php b/core/modules/simpletest/drupal_web_test_case.php index 23fa526..2f2ca28 100644 --- a/core/modules/simpletest/drupal_web_test_case.php +++ b/core/modules/simpletest/drupal_web_test_case.php @@ -2321,6 +2330,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..af5ab2d 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/common.test b/core/modules/system/tests/common.test index 46b379e..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. */ @@ -2050,6 +2052,154 @@ class CommonValidNumberStepUnitTestCase extends DrupalUnitTestCase { } /** + * Tests color conversion functions. + */ +class CommonColorConversionTestCase extends DrupalUnitTestCase { + public static function getInfo() { + return array( + 'name' => 'Color conversion', + 'description' => 'Tests Color utility class conversions.', + 'group' => 'Common', + ); + } + + /** + * Tests Color::hex2rgba(). + */ + 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, + )); + + 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.'); + } + } + + // 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 Color::rgba2hex(). + */ + 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); + } + } +} + +/** * 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 {