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 a555bf0..8bdab8b 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 * @{ @@ -4166,6 +4168,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 '' . 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..27ba440 --- /dev/null +++ b/core/lib/Drupal/Core/Utility/Color.php @@ -0,0 +1,101 @@ + $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 277e97b..cd57178 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -1584,6 +1584,7 @@ abstract class WebTestBase extends TestBase { 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 0000000..2c32b31 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Common/ColorTest.php @@ -0,0 +1,100 @@ + '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/system.module b/core/modules/system/system.module index f26bb07..ee00f5e 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -421,6 +421,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/form.test b/core/modules/system/tests/form.test index fe5f922..eb80173 100644 --- a/core/modules/system/tests/form.test +++ b/core/modules/system/tests/form.test @@ -382,6 +382,41 @@ class FormsTestCase extends WebTestBase { } /** + * 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() @@ -424,7 +459,7 @@ class FormsTestCase extends WebTestBase { // 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 f681c75..bca16dd 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', @@ -1404,6 +1410,32 @@ function form_test_range_invalid($form, &$form_state) { } /** + * 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) { @@ -1623,6 +1655,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 {